mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-26 22:26:08 +02:00
Restructure frontend svelte file src folder
- Goal: Dependency structure mirrored in file structure
This commit is contained in:
228
web/frontend/src/generic/helper/JobFootprint.svelte
Normal file
228
web/frontend/src/generic/helper/JobFootprint.svelte
Normal file
@@ -0,0 +1,228 @@
|
||||
<!--
|
||||
@component Footprint component; Displays job.footprint data as bars in relation to thresholds
|
||||
|
||||
Properties:
|
||||
- `job Object`: The GQL job object
|
||||
- `displayTitle Bool?`: If to display cardHeader with title [Default: true]
|
||||
- `width String?`: Width of the card [Default: 'auto']
|
||||
- `height String?`: Height of the card [Default: '310px']
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
function findJobThresholds(job, metricConfig) {
|
||||
if (!job || !metricConfig) {
|
||||
console.warn("Argument missing for findJobThresholds!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// metricConfig is on subCluster-Level
|
||||
const defaultThresholds = {
|
||||
peak: metricConfig.peak,
|
||||
normal: metricConfig.normal,
|
||||
caution: metricConfig.caution,
|
||||
alert: metricConfig.alert
|
||||
};
|
||||
|
||||
// Job_Exclusivity does not matter, only aggregation
|
||||
if (metricConfig.aggregation === "avg") {
|
||||
return defaultThresholds;
|
||||
} else if (metricConfig.aggregation === "sum") {
|
||||
const topol = getContext("getHardwareTopology")(job.cluster, job.subCluster)
|
||||
const jobFraction = job.numHWThreads / topol.node.length;
|
||||
|
||||
return {
|
||||
peak: round(defaultThresholds.peak * jobFraction, 0),
|
||||
normal: round(defaultThresholds.normal * jobFraction, 0),
|
||||
caution: round(defaultThresholds.caution * jobFraction, 0),
|
||||
alert: round(defaultThresholds.alert * jobFraction, 0),
|
||||
};
|
||||
} else {
|
||||
console.warn(
|
||||
"Missing or unkown aggregation mode (sum/avg) for metric:",
|
||||
metricConfig,
|
||||
);
|
||||
return defaultThresholds;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
Progress,
|
||||
Icon,
|
||||
Tooltip,
|
||||
Row,
|
||||
Col
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { round } from "mathjs";
|
||||
|
||||
export let job;
|
||||
export let displayTitle = true;
|
||||
export let width = "auto";
|
||||
export let height = "310px";
|
||||
|
||||
const footprintData = job?.footprint?.map((jf) => {
|
||||
// Unit
|
||||
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
|
||||
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
|
||||
|
||||
// Threshold / -Differences
|
||||
const fmt = findJobThresholds(job, fmc);
|
||||
if (jf.name === "flops_any") fmt.peak = round(fmt.peak * 0.85, 0);
|
||||
|
||||
// Define basic data -> Value: Use as Provided
|
||||
const fmBase = {
|
||||
name: jf.name + ' (' + jf.stat + ')',
|
||||
avg: jf.value,
|
||||
unit: unit,
|
||||
max: fmt.peak,
|
||||
dir: fmc.lowerIsBetter
|
||||
};
|
||||
|
||||
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "danger",
|
||||
message: `Metric average way ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||
impact: 3
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "warning",
|
||||
message: `Metric average ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||
impact: 2,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "success",
|
||||
message: "Metric average within expected thresholds.",
|
||||
impact: 1,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "info",
|
||||
message:
|
||||
"Metric average above expected normal thresholds: Check for artifacts recommended.",
|
||||
impact: 0,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "secondary",
|
||||
message:
|
||||
"Metric average above expected peak threshold: Check for artifacts!",
|
||||
impact: -1,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function evalFootprint(mean, thresholds, lowerIsBetter, level) {
|
||||
// Handle Metrics in which less value is better
|
||||
switch (level) {
|
||||
case "peak":
|
||||
if (lowerIsBetter)
|
||||
return false; // metric over peak -> return false to trigger impact -1
|
||||
else return mean <= thresholds.peak && mean > thresholds.normal;
|
||||
case "alert":
|
||||
if (lowerIsBetter)
|
||||
return mean <= thresholds.peak && mean >= thresholds.alert;
|
||||
else return mean <= thresholds.alert && mean >= 0;
|
||||
case "caution":
|
||||
if (lowerIsBetter)
|
||||
return mean < thresholds.alert && mean >= thresholds.caution;
|
||||
else return mean <= thresholds.caution && mean > thresholds.alert;
|
||||
case "normal":
|
||||
if (lowerIsBetter)
|
||||
return mean < thresholds.caution && mean >= 0;
|
||||
else return mean <= thresholds.normal && mean > thresholds.caution;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="mt-1 overflow-auto" style="width: {width}; height: {height}">
|
||||
{#if displayTitle}
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-0 d-flex justify-content-center">
|
||||
Core Metrics Footprint
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{/if}
|
||||
<CardBody>
|
||||
{#each footprintData as fpd, index}
|
||||
<div class="mb-1 d-flex justify-content-between">
|
||||
<div> <b>{fpd.name}</b></div>
|
||||
<!-- For symmetry, see below ...-->
|
||||
<div
|
||||
class="cursor-help d-inline-flex"
|
||||
id={`footprint-${job.jobId}-${index}`}
|
||||
>
|
||||
<div class="mx-1">
|
||||
<!-- Alerts Only -->
|
||||
{#if fpd.impact === 3 || fpd.impact === -1}
|
||||
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="exclamation-triangle" class="text-warning" />
|
||||
{/if}
|
||||
<!-- Emoji for all states-->
|
||||
{#if fpd.impact === 3}
|
||||
<Icon name="emoji-frown" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="emoji-neutral" class="text-warning" />
|
||||
{:else if fpd.impact === 1}
|
||||
<Icon name="emoji-smile" class="text-success" />
|
||||
{:else if fpd.impact === 0}
|
||||
<Icon name="emoji-laughing" class="text-info" />
|
||||
{:else if fpd.impact === -1}
|
||||
<Icon name="emoji-dizzy" class="text-danger" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<!-- Print Values -->
|
||||
{fpd.avg} / {fpd.max}
|
||||
{fpd.unit} <!-- To increase margin to tooltip: No other way manageable ... -->
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
target={`footprint-${job.jobId}-${index}`}
|
||||
placement="right"
|
||||
offset={[0, 20]}>{fpd.message}</Tooltip
|
||||
>
|
||||
</div>
|
||||
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
|
||||
{#if fpd.dir}
|
||||
<Col xs="1">
|
||||
<Icon name="caret-left-fill" />
|
||||
</Col>
|
||||
{/if}
|
||||
<Col xs="11" class="align-content-center">
|
||||
<Progress value={fpd.avg} max={fpd.max} color={fpd.color} />
|
||||
</Col>
|
||||
{#if !fpd.dir}
|
||||
<Col xs="1">
|
||||
<Icon name="caret-right-fill" />
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
{/each}
|
||||
{#if job?.metaData?.message}
|
||||
<hr class="mt-1 mb-2" />
|
||||
{@html job.metaData.message}
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.cursor-help {
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
54
web/frontend/src/generic/helper/Refresher.svelte
Normal file
54
web/frontend/src/generic/helper/Refresher.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
@component Triggers upstream data refresh in selectable intervals
|
||||
|
||||
Properties:
|
||||
- `initially Number?`: Initial refresh interval on component mount, in seconds [Default: null]
|
||||
|
||||
Events:
|
||||
- `refresh`: When fired, the upstream component refreshes its contents
|
||||
-->
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { Button, Icon, InputGroup } from "@sveltestrap/sveltestrap";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let refreshInterval = null;
|
||||
let refreshIntervalId = null;
|
||||
function refreshIntervalChanged() {
|
||||
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
|
||||
|
||||
if (refreshInterval == null) return;
|
||||
|
||||
refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval);
|
||||
}
|
||||
|
||||
export let initially = null;
|
||||
|
||||
if (initially != null) {
|
||||
refreshInterval = initially * 1000;
|
||||
refreshIntervalChanged();
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputGroup>
|
||||
<Button
|
||||
outline
|
||||
on:click={() => dispatch("refresh")}
|
||||
disabled={refreshInterval != null}
|
||||
>
|
||||
<Icon name="arrow-clockwise" /> Refresh
|
||||
</Button>
|
||||
<select
|
||||
class="form-select"
|
||||
bind:value={refreshInterval}
|
||||
on:change={refreshIntervalChanged}
|
||||
>
|
||||
<option value={null}>No periodic refresh</option>
|
||||
<option value={30 * 1000}>Update every 30 seconds</option>
|
||||
<option value={60 * 1000}>Update every minute</option>
|
||||
<option value={2 * 60 * 1000}>Update every two minutes</option>
|
||||
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
|
44
web/frontend/src/generic/helper/Tag.svelte
Normal file
44
web/frontend/src/generic/helper/Tag.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
@component Single tag pill component
|
||||
|
||||
Properties:
|
||||
- id: ID! (if the tag-id is known but not the tag type/name, this can be used)
|
||||
- tag: { id: ID!, type: String, name: String }
|
||||
- clickable: Boolean (default is true)
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
const allTags = getContext('tags'),
|
||||
initialized = getContext('initialized')
|
||||
|
||||
export let id = null
|
||||
export let tag = null
|
||||
export let clickable = true
|
||||
|
||||
if (tag != null && id == null)
|
||||
id = tag.id
|
||||
|
||||
$: {
|
||||
if ($initialized && tag == null)
|
||||
tag = allTags.find(tag => tag.id == id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
a {
|
||||
margin-left: 0.5rem;
|
||||
line-height: 2;
|
||||
}
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a target={clickable ? "_blank" : null} href={clickable ? `/monitoring/jobs/?tag=${id}` : null}>
|
||||
{#if tag}
|
||||
<span class="badge bg-warning text-dark">{tag.type}: {tag.name}</span>
|
||||
{:else}
|
||||
Loading...
|
||||
{/if}
|
||||
</a>
|
113
web/frontend/src/generic/helper/TextFilter.svelte
Normal file
113
web/frontend/src/generic/helper/TextFilter.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
@component Search Field for Job-Lists with separate mode if project filter is active
|
||||
|
||||
Properties:
|
||||
- `presetProject String?`: Currently active project filter [Default: '']
|
||||
- `authlevel Number?`: The current users authentication level [Default: null]
|
||||
- `roles [Number]?`: Enum containing available roles [Default: null]
|
||||
|
||||
Events:
|
||||
- `set-filter, {String?, String?, String?}`: Set 'user, project, jobName' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { scramble, scrambleNames } from "../utils.js";
|
||||
|
||||
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("set-filter", {
|
||||
user,
|
||||
project,
|
||||
jobName
|
||||
});
|
||||
}, sleep);
|
||||
} else {
|
||||
if (mode == "project") project = term;
|
||||
else jobName = term;
|
||||
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch("set-filter", {
|
||||
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>
|
||||
|
Reference in New Issue
Block a user