mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 01:25:06 +01:00 
			
		
		
		
	@@ -76,6 +76,9 @@ const configString = `
 | 
				
			|||||||
        "kind": "file",
 | 
					        "kind": "file",
 | 
				
			||||||
        "path": "./var/job-archive"
 | 
					        "path": "./var/job-archive"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "jwts": {
 | 
				
			||||||
 | 
					        "max-age": "2000h"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "clusters": [
 | 
					    "clusters": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            "name": "name",
 | 
					            "name": "name",
 | 
				
			||||||
@@ -115,15 +118,15 @@ func initEnv() {
 | 
				
			|||||||
		os.Exit(0)
 | 
							os.Exit(0)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := os.WriteFile("config.json", []byte(configString), 0666); err != nil {
 | 
						if err := os.WriteFile("config.json", []byte(configString), 0o666); err != nil {
 | 
				
			||||||
		log.Fatalf("Writing config.json failed: %s", err.Error())
 | 
							log.Fatalf("Writing config.json failed: %s", err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := os.WriteFile(".env", []byte(envString), 0666); err != nil {
 | 
						if err := os.WriteFile(".env", []byte(envString), 0o666); err != nil {
 | 
				
			||||||
		log.Fatalf("Writing .env failed: %s", err.Error())
 | 
							log.Fatalf("Writing .env failed: %s", err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := os.Mkdir("var", 0777); err != nil {
 | 
						if err := os.Mkdir("var", 0o777); err != nil {
 | 
				
			||||||
		log.Fatalf("Mkdir var failed: %s", err.Error())
 | 
							log.Fatalf("Mkdir var failed: %s", err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -109,8 +109,8 @@ func LoadData(job *schema.Job,
 | 
				
			|||||||
			jd, err = repo.LoadData(job, metrics, scopes, ctx)
 | 
								jd, err = repo.LoadData(job, metrics, scopes, ctx)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				if len(jd) != 0 {
 | 
									if len(jd) != 0 {
 | 
				
			||||||
					log.Errorf("partial error: %s", err.Error())
 | 
										log.Warnf("partial error: %s", err.Error())
 | 
				
			||||||
					return err, 0, 0
 | 
										// return err, 0, 0 // Reactivating will block archiving on one partial error
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					log.Error("Error while loading job data from metric repository")
 | 
										log.Error("Error while loading job data from metric repository")
 | 
				
			||||||
					return err, 0, 0
 | 
										return err, 0, 0
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -520,7 +520,7 @@ func (r *JobRepository) archivingWorker() {
 | 
				
			|||||||
			// not using meta data, called to load JobMeta into Cache?
 | 
								// not using meta data, called to load JobMeta into Cache?
 | 
				
			||||||
			// will fail if job meta not in repository
 | 
								// will fail if job meta not in repository
 | 
				
			||||||
			if _, err := r.FetchMetadata(job); err != nil {
 | 
								if _, err := r.FetchMetadata(job); err != nil {
 | 
				
			||||||
				log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error())
 | 
									log.Errorf("archiving job (dbid: %d) failed at check metadata step: %s", job.ID, err.Error())
 | 
				
			||||||
				r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
 | 
									r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -529,14 +529,14 @@ func (r *JobRepository) archivingWorker() {
 | 
				
			|||||||
			// TODO: Maybe use context with cancel/timeout here
 | 
								// TODO: Maybe use context with cancel/timeout here
 | 
				
			||||||
			jobMeta, err := metricdata.ArchiveJob(job, context.Background())
 | 
								jobMeta, err := metricdata.ArchiveJob(job, context.Background())
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error())
 | 
									log.Errorf("archiving job (dbid: %d) failed at archiving job step: %s", job.ID, err.Error())
 | 
				
			||||||
				r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
 | 
									r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Update the jobs database entry one last time:
 | 
								// Update the jobs database entry one last time:
 | 
				
			||||||
			if err := r.MarkArchived(job.ID, schema.MonitoringStatusArchivingSuccessful, jobMeta.Statistics); err != nil {
 | 
								if err := r.MarkArchived(job.ID, schema.MonitoringStatusArchivingSuccessful, jobMeta.Statistics); err != nil {
 | 
				
			||||||
				log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error())
 | 
									log.Errorf("archiving job (dbid: %d) failed at marking archived step: %s", job.ID, err.Error())
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			log.Debugf("archiving job %d took %s", job.JobID, time.Since(start))
 | 
								log.Debugf("archiving job %d took %s", job.JobID, time.Since(start))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -236,6 +236,9 @@ func buildStringCondition(field string, cond *model.StringInput, query sq.Select
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func buildMetaJsonCondition(jsonField string, cond *model.StringInput, query sq.SelectBuilder) sq.SelectBuilder {
 | 
					func buildMetaJsonCondition(jsonField string, cond *model.StringInput, query sq.SelectBuilder) sq.SelectBuilder {
 | 
				
			||||||
 | 
						// Verify and Search Only in Valid Jsons
 | 
				
			||||||
 | 
						query = query.Where("JSON_VALID(meta_data)")
 | 
				
			||||||
 | 
						// add "AND" Sql query Block for field match
 | 
				
			||||||
	if cond.Eq != nil {
 | 
						if cond.Eq != nil {
 | 
				
			||||||
		return query.Where("JSON_EXTRACT(meta_data, \"$."+jsonField+"\") = ?", *cond.Eq)
 | 
							return query.Where("JSON_EXTRACT(meta_data, \"$."+jsonField+"\") = ?", *cond.Eq)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -302,11 +302,19 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
 | 
				
			|||||||
			case "jobId":
 | 
								case "jobId":
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
				
			||||||
			case "jobName":
 | 
								case "jobName":
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
									// Add Last 30 Days to migitate timeouts
 | 
				
			||||||
 | 
									untilTime := strconv.FormatInt(time.Now().Unix(), 10)
 | 
				
			||||||
 | 
									fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
				
			||||||
			case "projectId":
 | 
								case "projectId":
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
				
			||||||
			case "arrayJobId":
 | 
								case "arrayJobId":
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
									// Add Last 30 Days to migitate timeouts
 | 
				
			||||||
 | 
									untilTime := strconv.FormatInt(time.Now().Unix(), 10)
 | 
				
			||||||
 | 
									fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
				
			||||||
			case "username":
 | 
								case "username":
 | 
				
			||||||
				if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
									if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
				
			||||||
					http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
 | 
										http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
 | 
				
			||||||
@@ -339,7 +347,11 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
 | 
				
			|||||||
			} else if project != "" {
 | 
								} else if project != "" {
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(project), http.StatusFound) // projectId (equal)
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(project), http.StatusFound) // projectId (equal)
 | 
				
			||||||
			} else if jobname != "" {
 | 
								} else if jobname != "" {
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains)
 | 
									// Add Last 30 Days to migitate timeouts
 | 
				
			||||||
 | 
									untilTime := strconv.FormatInt(time.Now().Unix(), 10)
 | 
				
			||||||
 | 
									fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&jobName="+url.QueryEscape(jobname), http.StatusFound) // 30D Fitler + JobName (contains)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
									web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -165,10 +165,11 @@
 | 
				
			|||||||
        .find((c) => c.name == job.cluster)
 | 
					        .find((c) => c.name == job.cluster)
 | 
				
			||||||
        .metricConfig.map((mc) => mc.name);
 | 
					        .metricConfig.map((mc) => mc.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
 | 
					    // Metric not found in JobMetrics && Metric not explicitly disabled in config or deselected: Was expected, but is Missing
 | 
				
			||||||
    missingMetrics = metricNames.filter(
 | 
					    missingMetrics = metricNames.filter(
 | 
				
			||||||
      (metric) =>
 | 
					      (metric) =>
 | 
				
			||||||
        !metrics.some((jm) => jm.name == metric) &&
 | 
					        !metrics.some((jm) => jm.name == metric) &&
 | 
				
			||||||
 | 
					        selectedMetrics.includes(metric) && 
 | 
				
			||||||
        !checkMetricDisabled(
 | 
					        !checkMetricDisabled(
 | 
				
			||||||
          metric,
 | 
					          metric,
 | 
				
			||||||
          $initq.data.job.cluster,
 | 
					          $initq.data.job.cluster,
 | 
				
			||||||
@@ -306,9 +307,6 @@
 | 
				
			|||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
  </Col>
 | 
					  </Col>
 | 
				
			||||||
  <!--     <Col xs="auto">
 | 
					 | 
				
			||||||
        <Zoom timeseriesPlots={plots} />
 | 
					 | 
				
			||||||
    </Col> -->
 | 
					 | 
				
			||||||
</Row>
 | 
					</Row>
 | 
				
			||||||
<Row>
 | 
					<Row>
 | 
				
			||||||
  <Col>
 | 
					  <Col>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
  import Refresher from "./joblist/Refresher.svelte";
 | 
					  import Refresher from "./joblist/Refresher.svelte";
 | 
				
			||||||
  import Sorting from "./joblist/SortSelection.svelte";
 | 
					  import Sorting from "./joblist/SortSelection.svelte";
 | 
				
			||||||
  import MetricSelection from "./MetricSelection.svelte";
 | 
					  import MetricSelection from "./MetricSelection.svelte";
 | 
				
			||||||
  import UserOrProject from "./filters/UserOrProject.svelte";
 | 
					  import TextFilter from "./filters/TextFilter.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { query: initq } = init();
 | 
					  const { query: initq } = init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,6 +38,7 @@
 | 
				
			|||||||
    ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
 | 
					    ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
 | 
				
			||||||
    : !!ccconfig.plot_list_showFootprint;
 | 
					    : !!ccconfig.plot_list_showFootprint;
 | 
				
			||||||
  let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
 | 
					  let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
 | 
				
			||||||
 | 
					  let presetProject = filterPresets?.project ? filterPresets.project : ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // The filterPresets are handled by the Filters component,
 | 
					  // The filterPresets are handled by the Filters component,
 | 
				
			||||||
  // so we need to wait for it to be ready before we can start a query.
 | 
					  // so we need to wait for it to be ready before we can start a query.
 | 
				
			||||||
@@ -86,7 +87,8 @@
 | 
				
			|||||||
  </Col>
 | 
					  </Col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <Col xs="3" style="margin-left: auto;">
 | 
					  <Col xs="3" style="margin-left: auto;">
 | 
				
			||||||
    <UserOrProject
 | 
					    <TextFilter
 | 
				
			||||||
 | 
					      {presetProject}
 | 
				
			||||||
      bind:authlevel
 | 
					      bind:authlevel
 | 
				
			||||||
      bind:roles
 | 
					      bind:roles
 | 
				
			||||||
      on:update={({ detail }) => filterComponent.update(detail)}
 | 
					      on:update={({ detail }) => filterComponent.update(detail)}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,8 +33,17 @@
 | 
				
			|||||||
    error = null;
 | 
					    error = null;
 | 
				
			||||||
  let selectedScope = minScope(scopes);
 | 
					  let selectedScope = minScope(scopes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let statsPattern = /(.*)-stats$/
 | 
				
			||||||
 | 
					  let statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null)
 | 
				
			||||||
 | 
					  let selectedScopeIndex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: availableScopes = scopes;
 | 
					  $: availableScopes = scopes;
 | 
				
			||||||
  $: selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
 | 
					  $: patternMatches = statsPattern.exec(selectedScope)
 | 
				
			||||||
 | 
					  $: if (!patternMatches) {
 | 
				
			||||||
 | 
					      selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      selectedScopeIndex = scopes.findIndex((s) => s == patternMatches[1]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  $: data = rawData[selectedScopeIndex];
 | 
					  $: data = rawData[selectedScopeIndex];
 | 
				
			||||||
  $: series = data?.series.filter(
 | 
					  $: series = data?.series.filter(
 | 
				
			||||||
    (series) => selectedHost == null || series.hostname == selectedHost,
 | 
					    (series) => selectedHost == null || series.hostname == selectedHost,
 | 
				
			||||||
@@ -62,6 +71,7 @@
 | 
				
			|||||||
      if (jm.scope != "node") {
 | 
					      if (jm.scope != "node") {
 | 
				
			||||||
        scopes = [...scopes, jm.scope];
 | 
					        scopes = [...scopes, jm.scope];
 | 
				
			||||||
        rawData.push(jm.metric);
 | 
					        rawData.push(jm.metric);
 | 
				
			||||||
 | 
					        statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null)
 | 
				
			||||||
        selectedScope = jm.scope;
 | 
					        selectedScope = jm.scope;
 | 
				
			||||||
        selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
 | 
					        selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
 | 
				
			||||||
        dispatch("more-loaded", jm);
 | 
					        dispatch("more-loaded", jm);
 | 
				
			||||||
@@ -79,15 +89,18 @@
 | 
				
			|||||||
      : "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
 | 
					      : "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
 | 
				
			||||||
  </InputGroupText>
 | 
					  </InputGroupText>
 | 
				
			||||||
  <select class="form-select" bind:value={selectedScope}>
 | 
					  <select class="form-select" bind:value={selectedScope}>
 | 
				
			||||||
    {#each availableScopes as scope}
 | 
					    {#each availableScopes as scope, index}
 | 
				
			||||||
      <option value={scope}>{scope}</option>
 | 
					      <option value={scope}>{scope}</option>
 | 
				
			||||||
 | 
					      {#if statsSeries[index]}
 | 
				
			||||||
 | 
					        <option value={scope + '-stats'}>stats series ({scope})</option>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
    {/each}
 | 
					    {/each}
 | 
				
			||||||
    {#if availableScopes.length == 1 && metricConfig?.scope != "node"}
 | 
					    {#if availableScopes.length == 1 && metricConfig?.scope != "node"}
 | 
				
			||||||
      <option value={"load-more"}>Load more...</option>
 | 
					      <option value={"load-more"}>Load more...</option>
 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
  </select>
 | 
					  </select>
 | 
				
			||||||
  {#if job.resources.length > 1}
 | 
					  {#if job.resources.length > 1}
 | 
				
			||||||
    <select class="form-select" bind:value={selectedHost}>
 | 
					    <select class="form-select" bind:value={selectedHost} disabled={patternMatches}>
 | 
				
			||||||
      <option value={null}>All Hosts</option>
 | 
					      <option value={null}>All Hosts</option>
 | 
				
			||||||
      {#each job.resources as { hostname }}
 | 
					      {#each job.resources as { hostname }}
 | 
				
			||||||
        <option value={hostname}>{hostname}</option>
 | 
					        <option value={hostname}>{hostname}</option>
 | 
				
			||||||
@@ -100,7 +113,7 @@
 | 
				
			|||||||
    <Spinner />
 | 
					    <Spinner />
 | 
				
			||||||
  {:else if error != null}
 | 
					  {:else if error != null}
 | 
				
			||||||
    <Card body color="danger">{error.message}</Card>
 | 
					    <Card body color="danger">{error.message}</Card>
 | 
				
			||||||
  {:else if series != null}
 | 
					  {:else if series != null && !patternMatches}
 | 
				
			||||||
    <Timeseries
 | 
					    <Timeseries
 | 
				
			||||||
      bind:this={plot}
 | 
					      bind:this={plot}
 | 
				
			||||||
      {width}
 | 
					      {width}
 | 
				
			||||||
@@ -114,5 +127,21 @@
 | 
				
			|||||||
      {isShared}
 | 
					      {isShared}
 | 
				
			||||||
      resources={job.resources}
 | 
					      resources={job.resources}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
 | 
					  {:else if statsSeries[selectedScopeIndex] != null && patternMatches}
 | 
				
			||||||
 | 
					    <Timeseries
 | 
				
			||||||
 | 
					      bind:this={plot}
 | 
				
			||||||
 | 
					      {width}
 | 
				
			||||||
 | 
					      height={300}
 | 
				
			||||||
 | 
					      {cluster}
 | 
				
			||||||
 | 
					      {subCluster}
 | 
				
			||||||
 | 
					      timestep={data.timestep}
 | 
				
			||||||
 | 
					      scope={selectedScope}
 | 
				
			||||||
 | 
					      metric={metricName}
 | 
				
			||||||
 | 
					      {series}
 | 
				
			||||||
 | 
					      {isShared}
 | 
				
			||||||
 | 
					      resources={job.resources}
 | 
				
			||||||
 | 
					      statisticsSeries={statsSeries[selectedScopeIndex]}
 | 
				
			||||||
 | 
					      useStatsSeries={!!statsSeries[selectedScopeIndex]}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
{/key}
 | 
					{/key}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -315,20 +315,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<!-- Loading indicator & Refresh -->
 | 
					<!-- Loading indicator & Refresh -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Row cols={3}>
 | 
					<Row cols={{ lg: 3, md: 3, sm: 1 }}>
 | 
				
			||||||
  <Col xs="auto" style="align-self: flex-end;">
 | 
					  <Col style="">
 | 
				
			||||||
    <h4 class="mb-0">Current utilization of cluster "{cluster}"</h4>
 | 
					    <h4 class="mb-0">Current utilization of cluster "{cluster}"</h4>
 | 
				
			||||||
  </Col>
 | 
					  </Col>
 | 
				
			||||||
  <Col xs="auto" style="margin-left: 0.25rem;">
 | 
					  <Col class="mt-2 mt-md-0 text-md-end">
 | 
				
			||||||
    {#if $initq.fetching || $mainQuery.fetching}
 | 
					 | 
				
			||||||
      <Spinner />
 | 
					 | 
				
			||||||
    {:else if $initq.error}
 | 
					 | 
				
			||||||
      <Card body color="danger">{$initq.error.message}</Card>
 | 
					 | 
				
			||||||
    {:else}
 | 
					 | 
				
			||||||
      <!-- ... -->
 | 
					 | 
				
			||||||
    {/if}
 | 
					 | 
				
			||||||
  </Col>
 | 
					 | 
				
			||||||
  <Col xs="auto" style="margin-left: auto;">
 | 
					 | 
				
			||||||
    <Button
 | 
					    <Button
 | 
				
			||||||
      outline
 | 
					      outline
 | 
				
			||||||
      color="secondary"
 | 
					      color="secondary"
 | 
				
			||||||
@@ -337,7 +328,7 @@
 | 
				
			|||||||
      <Icon name="bar-chart-line" /> Select Histograms
 | 
					      <Icon name="bar-chart-line" /> Select Histograms
 | 
				
			||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  </Col>
 | 
					  </Col>
 | 
				
			||||||
  <Col xs="auto" style="margin-left: 0.25rem;">
 | 
					  <Col class="mt-2 mt-md-0">
 | 
				
			||||||
    <Refresher
 | 
					    <Refresher
 | 
				
			||||||
      initially={120}
 | 
					      initially={120}
 | 
				
			||||||
      on:reload={() => {
 | 
					      on:reload={() => {
 | 
				
			||||||
@@ -347,6 +338,17 @@
 | 
				
			|||||||
    />
 | 
					    />
 | 
				
			||||||
  </Col>
 | 
					  </Col>
 | 
				
			||||||
</Row>
 | 
					</Row>
 | 
				
			||||||
 | 
					<Row cols={1} class="text-center mt-3">
 | 
				
			||||||
 | 
					  <Col>
 | 
				
			||||||
 | 
					    {#if $initq.fetching || $mainQuery.fetching}
 | 
				
			||||||
 | 
					      <Spinner />
 | 
				
			||||||
 | 
					    {:else if $initq.error}
 | 
				
			||||||
 | 
					      <Card body color="danger">{$initq.error.message}</Card>
 | 
				
			||||||
 | 
					    {:else}
 | 
				
			||||||
 | 
					      <!-- ... -->
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					  </Col>
 | 
				
			||||||
 | 
					</Row>
 | 
				
			||||||
{#if $mainQuery.error}
 | 
					{#if $mainQuery.error}
 | 
				
			||||||
  <Row cols={1}>
 | 
					  <Row cols={1}>
 | 
				
			||||||
    <Col>
 | 
					    <Col>
 | 
				
			||||||
@@ -361,8 +363,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{#if $initq.data && $mainQuery.data}
 | 
					{#if $initq.data && $mainQuery.data}
 | 
				
			||||||
  {#each $initq.data.clusters.find((c) => c.name == cluster).subClusters as subCluster, i}
 | 
					  {#each $initq.data.clusters.find((c) => c.name == cluster).subClusters as subCluster, i}
 | 
				
			||||||
    <Row cols={2} class="mb-3 justify-content-center">
 | 
					    <Row cols={{ lg: 2, md: 1 , sm: 1}} class="mb-3 justify-content-center">
 | 
				
			||||||
      <Col md="4" class="px-3">
 | 
					      <Col class="px-3">
 | 
				
			||||||
        <Card class="h-auto mt-1">
 | 
					        <Card class="h-auto mt-1">
 | 
				
			||||||
          <CardHeader>
 | 
					          <CardHeader>
 | 
				
			||||||
            <CardTitle class="mb-0">SubCluster "{subCluster.name}"</CardTitle>
 | 
					            <CardTitle class="mb-0">SubCluster "{subCluster.name}"</CardTitle>
 | 
				
			||||||
@@ -433,7 +435,7 @@
 | 
				
			|||||||
          </CardBody>
 | 
					          </CardBody>
 | 
				
			||||||
        </Card>
 | 
					        </Card>
 | 
				
			||||||
      </Col>
 | 
					      </Col>
 | 
				
			||||||
      <Col class="px-3">
 | 
					      <Col class="px-3 mt-2 mt-lg-0">
 | 
				
			||||||
        <div bind:clientWidth={plotWidths[i]}>
 | 
					        <div bind:clientWidth={plotWidths[i]}>
 | 
				
			||||||
          {#key $mainQuery.data.nodeMetrics}
 | 
					          {#key $mainQuery.data.nodeMetrics}
 | 
				
			||||||
            <Roofline
 | 
					            <Roofline
 | 
				
			||||||
@@ -457,7 +459,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <!-- Usage Stats as Histograms -->
 | 
					  <!-- Usage Stats as Histograms -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <Row cols={4}>
 | 
					  <Row cols={{ lg: 4, md: 2, sm: 1 }}>
 | 
				
			||||||
    <Col class="p-2">
 | 
					    <Col class="p-2">
 | 
				
			||||||
      <div bind:clientWidth={colWidth1}>
 | 
					      <div bind:clientWidth={colWidth1}>
 | 
				
			||||||
        <h4 class="text-center">
 | 
					        <h4 class="text-center">
 | 
				
			||||||
@@ -580,7 +582,7 @@
 | 
				
			|||||||
    </Col>
 | 
					    </Col>
 | 
				
			||||||
  </Row>
 | 
					  </Row>
 | 
				
			||||||
  <hr class="my-2" />
 | 
					  <hr class="my-2" />
 | 
				
			||||||
  <Row cols={2}>
 | 
					  <Row cols={{ lg: 2, md: 1 }}>
 | 
				
			||||||
    <Col class="p-2">
 | 
					    <Col class="p-2">
 | 
				
			||||||
      <div bind:clientWidth={colWidth2}>
 | 
					      <div bind:clientWidth={colWidth2}>
 | 
				
			||||||
        {#key $mainQuery.data.stats}
 | 
					        {#key $mainQuery.data.stats}
 | 
				
			||||||
@@ -610,7 +612,7 @@
 | 
				
			|||||||
      {/key}
 | 
					      {/key}
 | 
				
			||||||
    </Col>
 | 
					    </Col>
 | 
				
			||||||
  </Row>
 | 
					  </Row>
 | 
				
			||||||
  <Row cols={2}>
 | 
					  <Row cols={{ lg: 2, md: 1 }}>
 | 
				
			||||||
    <Col class="p-2">
 | 
					    <Col class="p-2">
 | 
				
			||||||
      <div bind:clientWidth={colWidth2}>
 | 
					      <div bind:clientWidth={colWidth2}>
 | 
				
			||||||
        {#key $mainQuery.data.stats}
 | 
					        {#key $mainQuery.data.stats}
 | 
				
			||||||
@@ -642,7 +644,7 @@
 | 
				
			|||||||
  </Row>
 | 
					  </Row>
 | 
				
			||||||
  <hr class="my-2" />
 | 
					  <hr class="my-2" />
 | 
				
			||||||
  {#if metricsInHistograms}
 | 
					  {#if metricsInHistograms}
 | 
				
			||||||
    <Row>
 | 
					    <Row cols={1}>
 | 
				
			||||||
      <Col>
 | 
					      <Col>
 | 
				
			||||||
        {#key $mainQuery.data.stats[0].histMetrics}
 | 
					        {#key $mainQuery.data.stats[0].histMetrics}
 | 
				
			||||||
          <PlotTable
 | 
					          <PlotTable
 | 
				
			||||||
@@ -650,7 +652,7 @@
 | 
				
			|||||||
            let:width
 | 
					            let:width
 | 
				
			||||||
            renderFor="user"
 | 
					            renderFor="user"
 | 
				
			||||||
            items={$mainQuery.data.stats[0].histMetrics}
 | 
					            items={$mainQuery.data.stats[0].histMetrics}
 | 
				
			||||||
            itemsPerRow={3}
 | 
					            itemsPerRow={2}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Histogram
 | 
					            <Histogram
 | 
				
			||||||
              data={convert2uplot(item.data)}
 | 
					              data={convert2uplot(item.data)}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@
 | 
				
			|||||||
  } from "@sveltestrap/sveltestrap";
 | 
					  } from "@sveltestrap/sveltestrap";
 | 
				
			||||||
  import { queryStore, gql, getContextClient } from "@urql/svelte";
 | 
					  import { queryStore, gql, getContextClient } from "@urql/svelte";
 | 
				
			||||||
  import Filters from "./filters/Filters.svelte";
 | 
					  import Filters from "./filters/Filters.svelte";
 | 
				
			||||||
 | 
					  import TextFilter from "./filters/TextFilter.svelte"
 | 
				
			||||||
  import JobList from "./joblist/JobList.svelte";
 | 
					  import JobList from "./joblist/JobList.svelte";
 | 
				
			||||||
  import Sorting from "./joblist/SortSelection.svelte";
 | 
					  import Sorting from "./joblist/SortSelection.svelte";
 | 
				
			||||||
  import Refresher from "./joblist/Refresher.svelte";
 | 
					  import Refresher from "./joblist/Refresher.svelte";
 | 
				
			||||||
@@ -132,6 +133,11 @@
 | 
				
			|||||||
    />
 | 
					    />
 | 
				
			||||||
  </Col>
 | 
					  </Col>
 | 
				
			||||||
  <Col xs="auto" style="margin-left: auto;">
 | 
					  <Col xs="auto" style="margin-left: auto;">
 | 
				
			||||||
 | 
					    <TextFilter
 | 
				
			||||||
 | 
					      on:update={({ detail }) => filterComponent.update(detail)}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </Col>
 | 
				
			||||||
 | 
					  <Col xs="auto">
 | 
				
			||||||
    <Refresher on:reload={() => jobList.refresh()} />
 | 
					    <Refresher on:reload={() => jobList.refresh()} />
 | 
				
			||||||
  </Col>
 | 
					  </Col>
 | 
				
			||||||
</Row>
 | 
					</Row>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<td>{user.username}</td>
 | 
					<td>{user.username}</td>
 | 
				
			||||||
<td>{user.name}</td>
 | 
					<td>{user.name}</td>
 | 
				
			||||||
<td>{user.projects}</td>
 | 
					<td style="max-width: 200px;">{user.projects}</td>
 | 
				
			||||||
<td>{user.email}</td>
 | 
					<td>{user.email}</td>
 | 
				
			||||||
<td><code>{user?.roles ? user.roles.join(", ") : "No Roles"}</code></td>
 | 
					<td><code>{user?.roles ? user.roles.join(", ") : "No Roles"}</code></td>
 | 
				
			||||||
<td>
 | 
					<td>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -193,7 +193,8 @@
 | 
				
			|||||||
      opts.push(`userMatch=${filters.userMatch}`);
 | 
					      opts.push(`userMatch=${filters.userMatch}`);
 | 
				
			||||||
    if (filters.project) opts.push(`project=${filters.project}`);
 | 
					    if (filters.project) opts.push(`project=${filters.project}`);
 | 
				
			||||||
    if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
 | 
					    if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
 | 
				
			||||||
    if (filters.projectMatch != "contains")
 | 
					    if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
 | 
				
			||||||
 | 
					    if (filters.project && filters.projectMatch != "contains")
 | 
				
			||||||
      opts.push(`projectMatch=${filters.projectMatch}`);
 | 
					      opts.push(`projectMatch=${filters.projectMatch}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (opts.length == 0 && window.location.search.length <= 1) return;
 | 
					    if (opts.length == 0 && window.location.search.length <= 1) return;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										101
									
								
								web/frontend/src/filters/TextFilter.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								web/frontend/src/filters/TextFilter.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
 | 
				
			||||||
 | 
					  import { createEventDispatcher } from "svelte";
 | 
				
			||||||
 | 
					  import { scramble, scrambleNames } from "../joblist/JobInfo.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  export let presetProject = ""; // If page with this component has project preset, keep preset until reset
 | 
				
			||||||
 | 
					  export let authlevel = null;
 | 
				
			||||||
 | 
					  export let roles = null;
 | 
				
			||||||
 | 
					  let mode = presetProject ? "jobName" : "project";
 | 
				
			||||||
 | 
					  let term = "";
 | 
				
			||||||
 | 
					  let user = "";
 | 
				
			||||||
 | 
					  let project = presetProject ? presetProject : "";
 | 
				
			||||||
 | 
					  let jobName = "";
 | 
				
			||||||
 | 
					  const throttle = 500;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function modeChanged() {
 | 
				
			||||||
 | 
					    if (mode == "user") {
 | 
				
			||||||
 | 
					      project = presetProject ? presetProject : "";
 | 
				
			||||||
 | 
					      jobName = "";
 | 
				
			||||||
 | 
					    } else if (mode == "project") {
 | 
				
			||||||
 | 
					      user = "";
 | 
				
			||||||
 | 
					      jobName = "";
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      project = presetProject ? presetProject : "";
 | 
				
			||||||
 | 
					      user = "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    termChanged(0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let timeoutId = null;
 | 
				
			||||||
 | 
					  // Compatibility: Handle "user role" and "no role" identically
 | 
				
			||||||
 | 
					  function termChanged(sleep = throttle) {
 | 
				
			||||||
 | 
					    if (roles && authlevel >= roles.manager) {
 | 
				
			||||||
 | 
					      if (mode == "user") user = term;
 | 
				
			||||||
 | 
					      else if (mode == "project") project = term;
 | 
				
			||||||
 | 
					      else jobName = term;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (timeoutId != null) clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      timeoutId = setTimeout(() => {
 | 
				
			||||||
 | 
					        dispatch("update", {
 | 
				
			||||||
 | 
					          user,
 | 
				
			||||||
 | 
					          project,
 | 
				
			||||||
 | 
					          jobName
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }, sleep);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      if (mode == "project") project = term;
 | 
				
			||||||
 | 
					      else jobName = term;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (timeoutId != null) clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      timeoutId = setTimeout(() => {
 | 
				
			||||||
 | 
					        dispatch("update", {
 | 
				
			||||||
 | 
					          project,
 | 
				
			||||||
 | 
					          jobName
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }, sleep);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function resetProject () {
 | 
				
			||||||
 | 
					    mode = "project"
 | 
				
			||||||
 | 
					    term = ""
 | 
				
			||||||
 | 
					    presetProject = ""
 | 
				
			||||||
 | 
					    project = ""
 | 
				
			||||||
 | 
					    termChanged(0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<InputGroup>
 | 
				
			||||||
 | 
					  <select
 | 
				
			||||||
 | 
					    style="max-width: 175px;"
 | 
				
			||||||
 | 
					    class="form-select"
 | 
				
			||||||
 | 
					    bind:value={mode}
 | 
				
			||||||
 | 
					    on:change={modeChanged}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    {#if !presetProject}
 | 
				
			||||||
 | 
					      <option value={"project"}>Search Project</option>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					    {#if roles && authlevel >= roles.manager}
 | 
				
			||||||
 | 
					      <option value={"user"}>Search User</option>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					    <option value={"jobName"}>Search Jobname</option>
 | 
				
			||||||
 | 
					  </select>
 | 
				
			||||||
 | 
					  <Input
 | 
				
			||||||
 | 
					    type="text"
 | 
				
			||||||
 | 
					    bind:value={term}
 | 
				
			||||||
 | 
					    on:change={() => termChanged()}
 | 
				
			||||||
 | 
					    on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
 | 
				
			||||||
 | 
					    placeholder={presetProject ? `Filter ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Filter ${mode} ...`}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					  {#if presetProject}
 | 
				
			||||||
 | 
					  <Button title="Reset Project" on:click={resetProject}
 | 
				
			||||||
 | 
					    ><Icon name="arrow-counterclockwise" /></Button
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					  {/if}
 | 
				
			||||||
 | 
					</InputGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,84 +0,0 @@
 | 
				
			|||||||
<script>
 | 
					 | 
				
			||||||
  import { InputGroup, Input } from "@sveltestrap/sveltestrap";
 | 
					 | 
				
			||||||
  import { createEventDispatcher } from "svelte";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const dispatch = createEventDispatcher();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  export let user = "";
 | 
					 | 
				
			||||||
  export let project = "";
 | 
					 | 
				
			||||||
  export let authlevel;
 | 
					 | 
				
			||||||
  export let roles;
 | 
					 | 
				
			||||||
  let mode = "user",
 | 
					 | 
				
			||||||
    term = "";
 | 
					 | 
				
			||||||
  const throttle = 500;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function modeChanged() {
 | 
					 | 
				
			||||||
    if (mode == "user") {
 | 
					 | 
				
			||||||
      project = term;
 | 
					 | 
				
			||||||
      term = user;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      user = term;
 | 
					 | 
				
			||||||
      term = project;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    termChanged(0);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let timeoutId = null;
 | 
					 | 
				
			||||||
  // Compatibility: Handle "user role" and "no role" identically
 | 
					 | 
				
			||||||
  function termChanged(sleep = throttle) {
 | 
					 | 
				
			||||||
    if (authlevel >= roles.manager) {
 | 
					 | 
				
			||||||
      if (mode == "user") user = term;
 | 
					 | 
				
			||||||
      else project = term;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (timeoutId != null) clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      timeoutId = setTimeout(() => {
 | 
					 | 
				
			||||||
        dispatch("update", {
 | 
					 | 
				
			||||||
          user,
 | 
					 | 
				
			||||||
          project,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }, sleep);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      project = term;
 | 
					 | 
				
			||||||
      if (timeoutId != null) clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      timeoutId = setTimeout(() => {
 | 
					 | 
				
			||||||
        dispatch("update", {
 | 
					 | 
				
			||||||
          project,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }, sleep);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#if authlevel >= roles.manager}
 | 
					 | 
				
			||||||
  <InputGroup>
 | 
					 | 
				
			||||||
    <select
 | 
					 | 
				
			||||||
      style="max-width: 175px;"
 | 
					 | 
				
			||||||
      class="form-select"
 | 
					 | 
				
			||||||
      bind:value={mode}
 | 
					 | 
				
			||||||
      on:change={modeChanged}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <option value={"user"}>Search User</option>
 | 
					 | 
				
			||||||
      <option value={"project"}>Search Project</option>
 | 
					 | 
				
			||||||
    </select>
 | 
					 | 
				
			||||||
    <Input
 | 
					 | 
				
			||||||
      type="text"
 | 
					 | 
				
			||||||
      bind:value={term}
 | 
					 | 
				
			||||||
      on:change={() => termChanged()}
 | 
					 | 
				
			||||||
      on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
 | 
					 | 
				
			||||||
      placeholder={mode == "user" ? "filter username..." : "filter project..."}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  </InputGroup>
 | 
					 | 
				
			||||||
{:else}
 | 
					 | 
				
			||||||
  <!-- Compatibility: Handle "user role" and "no role" identically-->
 | 
					 | 
				
			||||||
  <InputGroup>
 | 
					 | 
				
			||||||
    <Input
 | 
					 | 
				
			||||||
      type="text"
 | 
					 | 
				
			||||||
      bind:value={term}
 | 
					 | 
				
			||||||
      on:change={() => termChanged()}
 | 
					 | 
				
			||||||
      on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
 | 
					 | 
				
			||||||
      placeholder="filter project..."
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  </InputGroup>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user