Merge branch 'dev' of github.com:ClusterCockpit/cc-backend into dev

This commit is contained in:
2025-07-07 13:09:16 +02:00
77 changed files with 2082 additions and 1988 deletions
@@ -21,3 +21,15 @@ CREATE TABLE "node" (
meta_data TEXT, -- JSON meta_data TEXT, -- JSON
UNIQUE (hostname, cluster) UNIQUE (hostname, cluster)
); );
-- Add Indices For New Node Table VARCHAR Fields
CREATE INDEX IF NOT EXISTS nodes_cluster ON node (cluster);
CREATE INDEX IF NOT EXISTS nodes_cluster_subcluster ON node (cluster, subcluster);
CREATE INDEX IF NOT EXISTS nodes_state ON node (node_state);
CREATE INDEX IF NOT EXISTS nodes_cluster_state ON node (cluster, node_state);
CREATE INDEX IF NOT EXISTS nodes_health ON node (health_state);
CREATE INDEX IF NOT EXISTS nodes_cluster_health ON node (cluster, health_state);
-- Add Indices For Increased Amounts of Tags
CREATE INDEX IF NOT EXISTS tags_jobid ON jobtag (job_id);
CREATE INDEX IF NOT EXISTS tags_tagid ON jobtag (tag_id);
+11 -7
View File
@@ -1,9 +1,9 @@
<!-- <!--
@component Main analysis view component @component Main analysis view component
Properties: Properties:
- `filterPresets Object`: Optional predefined filter values - `filterPresets Object`: Optional predefined filter values
--> -->
<script> <script>
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
@@ -38,7 +38,9 @@
import RooflineHeatmap from "./generic/plots/RooflineHeatmap.svelte"; import RooflineHeatmap from "./generic/plots/RooflineHeatmap.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { filterPresets } = $props(); let {
filterPresets
} = $props();
// By default, look at the jobs of the last 6 hours: // By default, look at the jobs of the last 6 hours:
if (filterPresets?.startTime == null) { if (filterPresets?.startTime == null) {
@@ -346,8 +348,10 @@
{:else if cluster} {:else if cluster}
<PlotSelection <PlotSelection
availableMetrics={availableMetrics.map((av) => av.name)} availableMetrics={availableMetrics.map((av) => av.name)}
bind:metricsInHistograms presetMetricsInHistograms={metricsInHistograms}
bind:metricsInScatterplots presetMetricsInScatterplots={metricsInScatterplots}
applyHistograms={(metrics) => metricsInHistograms = [...metrics]}
applyScatter={(metrics) => metricsInScatterplots = [...metrics]}
/> />
{/if} {/if}
</Col> </Col>
+8 -7
View File
@@ -1,12 +1,13 @@
<!-- <!--
@component Main Config Option Component, Wrapper for admin and user sub-components @component Main Config Option Component, Wrapper for admin and user sub-components
Properties: Properties:
- `ìsAdmin Bool!`: Is currently logged in user admin authority - `ìsAdmin Bool!`: Is currently logged in user admin authority
- `isSupport Bool!`: Is currently logged in user support authority - `isSupport Bool!`: Is currently logged in user support authority
- `isApi Bool!`: Is currently logged in user api authority - `isApi Bool!`: Is currently logged in user api authority
- `username String!`: Empty string if auth. is disabled, otherwise the username as string - `username String!`: Empty string if auth. is disabled, otherwise the username as string
--> - `ncontent String!`: The currently displayed message on the homescreen
-->
<script> <script>
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap"; import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
+20 -14
View File
@@ -1,13 +1,13 @@
<!-- <!--
@component Main navbar component; handles view display based on user roles @component Main navbar component; handles view display based on user roles
Properties: Properties:
- `username String`: Empty string if auth. is disabled, otherwise the username as string - `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level - `authlevel Number`: The current users authentication level
- `clusters [String]`: List of cluster names - `clusters [String]`: List of cluster names
- `subClusters [String]`: List of subCluster names - `subClusters [String]`: List of subCluster names
- `roles [Number]`: Enum containing available roles - `roles [Number]`: Enum containing available roles
--> -->
<script> <script>
import { import {
@@ -25,7 +25,13 @@
import NavbarTools from "./header/NavbarTools.svelte"; import NavbarTools from "./header/NavbarTools.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { username, authlevel, clusters, subClusters, roles } = $props(); let {
username,
authlevel,
clusters,
subClusters,
roles
} = $props();
/* Const Init */ /* Const Init */
const jobsTitle = new Map(); const jobsTitle = new Map();
@@ -123,11 +129,11 @@
let isOpen = $state(false); let isOpen = $state(false);
let screenSize = $state(0); let screenSize = $state(0);
/* Derived Vars */ /* Derived */
let showMax = $derived(screenSize >= 1500); const showMax = $derived(screenSize >= 1500);
let showMid = $derived(screenSize < 1500 && screenSize >= 1300); const showMid = $derived(screenSize < 1500 && screenSize >= 1300);
let showSml = $derived(screenSize < 1300 && screenSize >= 768); const showSml = $derived(screenSize < 1300 && screenSize >= 768);
let showBrg = $derived(screenSize < 768); const showBrg = $derived(screenSize < 768);
</script> </script>
<svelte:window bind:innerWidth={screenSize} /> <svelte:window bind:innerWidth={screenSize} />
+9 -9
View File
@@ -1,12 +1,12 @@
<!-- <!--
@component Main single job display component; displays plots for every metric as well as various information @component Main single job display component; displays plots for every metric as well as various information
Properties: Properties:
- `dbid Number`: The jobs DB ID - `dbid Number`: The jobs DB ID
- `username String`: Empty string if auth. is disabled, otherwise the username as string - `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level - `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles - `roles [Number]`: Enum containing available roles
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -156,9 +156,9 @@
/* Effects */ /* Effects */
$effect(() => { $effect(() => {
document.title = $initq.fetching document.title = $initq?.fetching
? "Loading..." ? "Loading..."
: $initq.error : $initq?.error
? "Error" ? "Error"
: `Job ${$initq.data.job.jobId} - ClusterCockpit`; : `Job ${$initq.data.job.jobId} - ClusterCockpit`;
}); });
+12 -8
View File
@@ -1,12 +1,12 @@
<!-- <!--
@component Main job list component @component Main job list component
Properties:
- `filterPresets Object`: Optional predefined filter values
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
-->
Properties:
- `filterPresets Object?`: Optional predefined filter values [Default: {}]
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
-->
<script> <script>
import { untrack, onMount, getContext } from "svelte"; import { untrack, onMount, getContext } from "svelte";
import { import {
@@ -28,7 +28,11 @@
import MetricSelection from "./generic/select/MetricSelection.svelte"; import MetricSelection from "./generic/select/MetricSelection.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { filterPresets, authlevel, roles } = $props(); let {
filterPresets,
authlevel,
roles
} = $props();
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
+33 -27
View File
@@ -1,10 +1,10 @@
<!-- <!--
@component Main component for listing users or projects @component Main component for listing users or projects
Properties: Properties:
- `type String?`: The type of list ['USER' || 'PROJECT'] - `type String?`: The type of list ['USER' || 'PROJECT']
- `filterPresets Object?`: Optional predefined filter values [Default: {}] - `filterPresets Object?`: Optional predefined filter values [Default: {}]
--> -->
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
@@ -32,18 +32,12 @@
import Filters from "./generic/Filters.svelte"; import Filters from "./generic/Filters.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { type, filterPresets } = $props(); let {
type,
// By default, look at the jobs of the last 30 days: filterPresets
if (filterPresets?.startTime == null) { } = $props();
if (filterPresets == null) filterPresets = {};
filterPresets.startTime = {
range: "last30d",
text: "Last 30 Days",
};
}
/* Validate Type */
console.assert( console.assert(
type == "USER" || type == "PROJECT", type == "USER" || type == "PROJECT",
"Invalid list type provided!", "Invalid list type provided!",
@@ -64,16 +58,16 @@
queryStore({ queryStore({
client: client, client: client,
query: gql` query: gql`
query($jobFilters: [JobFilter!]!) { query($jobFilters: [JobFilter!]!) {
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) { rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
id id
name name
totalJobs totalJobs
totalWalltime totalWalltime
totalCoreHours totalCoreHours
totalAccHours totalAccHours
} }
}`, }`,
variables: { jobFilters }, variables: { jobFilters },
}) })
); );
@@ -107,7 +101,19 @@
} }
/* On Mount */ /* On Mount */
onMount(() => filterComponent.updateFilters()); onMount(() => {
// By default, look at the jobs of the last 30 days:
if (filterPresets?.startTime == null) {
if (filterPresets == null) filterPresets = {};
filterPresets.startTime = {
range: "last30d",
text: "Last 30 Days",
};
};
// Init Filter
filterComponent.updateFilters();
});
</script> </script>
<Row cols={{ xs: 1, md: 2}}> <Row cols={{ xs: 1, md: 2}}>
+7 -7
View File
@@ -1,12 +1,12 @@
<!-- <!--
@component System-View subcomponent; renders all current metrics for specified node @component System-View subcomponent; renders all current metrics for specified node
Properties: Properties:
- `cluster String`: Currently selected cluster - `cluster String`: Currently selected cluster
- `hostname String`: Currently selected host (== node) - `hostname String`: Currently selected host (== node)
- `from Date?`: Custom Time Range selection 'from' [Default: null] - `presetFrom Date?`: Custom Time Range selection 'from' [Default: null]
- `to Date?`: Custom Time Range selection 'to' [Default: null] - `presetTo Date?`: Custom Time Range selection 'to' [Default: null]
--> -->
<script> <script>
import { import {
+14 -13
View File
@@ -1,10 +1,10 @@
<!-- <!--
@component Main cluster status view component; renders current system-usage information @component Main cluster status view component; renders current system-usage information
Properties:
- `cluster String`: The cluster to show status information for
-->
Properties:
- `cluster String`: The cluster to show status information for
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { import {
@@ -43,7 +43,9 @@
import HistogramSelection from "./generic/select/HistogramSelection.svelte"; import HistogramSelection from "./generic/select/HistogramSelection.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { cluster } = $props(); let {
cluster
} = $props();
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
@@ -60,9 +62,13 @@
/* State Init */ /* State Init */
let from = $state(new Date(Date.now() - 5 * 60 * 1000)); let from = $state(new Date(Date.now() - 5 * 60 * 1000));
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let isHistogramSelectionOpen = $state(false);
let colWidth = $state(0); let colWidth = $state(0);
let plotWidths = $state([]); let plotWidths = $state([]);
// Histrogram
let isHistogramSelectionOpen = $state(false);
let selectedHistograms = $state(cluster
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || []);
// Bar Gauges // Bar Gauges
let allocatedNodes = $state({}); let allocatedNodes = $state({});
let flopRate = $state({}); let flopRate = $state({});
@@ -71,11 +77,7 @@
let memBwRate = $state({}); let memBwRate = $state({});
let memBwRateUnitPrefix = $state({}); let memBwRateUnitPrefix = $state({});
let memBwRateUnitBase = $state({}); let memBwRateUnitBase = $state({});
// Pie Charts
let selectedHistograms = $state(cluster
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || []);
let topProjectSelection = $state( let topProjectSelection = $state(
topOptions.find( topOptions.find(
(option) => (option) =>
@@ -86,7 +88,6 @@
(option) => option.key == ccconfig.status_view_selectedTopProjectCategory, (option) => option.key == ccconfig.status_view_selectedTopProjectCategory,
) )
); );
let topUserSelection = $state( let topUserSelection = $state(
topOptions.find( topOptions.find(
(option) => (option) =>
+15 -12
View File
@@ -1,12 +1,13 @@
<!-- <!--
@component Main cluster node status view component; renders overview or list depending on type @component Main cluster node status view component; renders overview or list depending on type
Properties: Properties:
- `displayType String?`: The type of node display ['OVERVIEW' || 'LIST'] - `displayType String?`: The type of node display ['OVERVIEW' || 'LIST']
- `cluster String`: The cluster to show status information for - `cluster String`: The cluster to show status information for [Default: null]
- `from Date?`: Custom Time Range selection 'from' [Default: null] - `subCluster String`: The subCluster to show status information for [Default: null]
- `to Date?`: Custom Time Range selection 'to' [Default: null] - `presetFrom Date?`: Custom Time Range selection 'from' [Default: null]
--> - `presetTo Date?`: Custom Time Range selection 'to' [Default: null]
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -38,8 +39,8 @@
displayType, displayType,
cluster = null, cluster = null,
subCluster = null, subCluster = null,
fromPreset = null, presetFrom = null,
toPreset = null, presetTo = null,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -54,9 +55,12 @@
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
const nowDate = new Date(Date.now()); const nowDate = new Date(Date.now());
/* Var Init */
let timeoutId = null;
/* State Init */ /* State Init */
let to = $state(toPreset || new Date(Date.now())); let to = $state(presetTo || new Date(Date.now()));
let from = $state(fromPreset || new Date(nowDate.setHours(nowDate.getHours() - 4))); let from = $state(presetFrom || new Date(nowDate.setHours(nowDate.getHours() - 4)));
let selectedResolution = $state(resampleConfig ? resampleDefault : 0); let selectedResolution = $state(resampleConfig ? resampleDefault : 0);
let hostnameFilter = $state(""); let hostnameFilter = $state("");
let pendingHostnameFilter = $state(""); let pendingHostnameFilter = $state("");
@@ -89,7 +93,6 @@
}; };
// Wait after input for some time to prevent too many requests // Wait after input for some time to prevent too many requests
let timeoutId = null;
function updateHostnameFilter() { function updateHostnameFilter() {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(function () { timeoutId = setTimeout(function () {
+9 -9
View File
@@ -1,11 +1,11 @@
<!-- <!--
@component Tag List Svelte Component. Displays All Tags, Allows deletion. @component Tag List Svelte Component. Displays All Tags, Allows deletion.
Properties: Properties:
- `username String!`: Users username. - `username String!`: Users username.
- `isAdmin Bool!`: User has Admin Auth. - `isAdmin Bool!`: User has Admin Auth.
- `tagmap Object!`: Map of accessible, appwide tags. Prefiltered in backend. - `presetTagmap Object!`: Map of accessible, appwide tags. Prefiltered in backend.
--> -->
<script> <script>
import { import {
@@ -37,7 +37,7 @@
/* State Init */ /* State Init */
let pendingChange = $state("none"); let pendingChange = $state("none");
let tagmap = $state(presetTagmap) let tagmap = $state(presetTagmap);
/* Functions */ /* Functions */
const removeTagMutation = ({ tagIds }) => { const removeTagMutation = ({ tagIds }) => {
@@ -68,8 +68,8 @@
} }
}, },
); );
} };
} };
</script> </script>
<div class="container"> <div class="container">
+9 -6
View File
@@ -1,10 +1,10 @@
<!-- <!--
@component Main user jobs list display component; displays job list and additional information for a given user @component Main user jobs list display component; displays job list and additional information for a given user
Properties: Properties:
- `user Object`: The GraphQL user object - `user Object`: The GraphQL user object
- `filterPresets Object`: Optional predefined filter values - `filterPresets Object`: Optional predefined filter values
--> -->
<script> <script>
import { onMount, getContext } from "svelte"; import { onMount, getContext } from "svelte";
@@ -43,7 +43,10 @@
import Refresher from "./generic/helper/Refresher.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { user, filterPresets } = $props(); let {
user,
filterPresets
} = $props();
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
+20 -10
View File
@@ -1,11 +1,13 @@
<!-- <!--
@component Analysis-View subcomponent; allows selection for normalized histograms and scatterplots @component Analysis-View subcomponent; allows selection for normalized histograms and scatterplots
Properties: Properties:
- `availableMetrics [String]`: Available metrics in selected cluster - `availableMetrics [String]`: Available metrics in selected cluster
- `metricsInHistograms [String]`: The currently selected metrics to display as histogram - `presetMetricsInHistograms [String]`: The latest selected metrics to display as histogram
- `metricsInScatterplots [[String, String]]`: The currently selected metrics to display as scatterplot - `presetMetricsInScatterplots [[String, String]]`: The latest selected metrics to display as scatterplot
--> - `applyHistograms Func`: The callback function to apply current histogramMetrics selection
- `applyScatter Func`: The callback function to apply current scatterMetrics selection
-->
<script> <script>
import { import {
@@ -24,8 +26,10 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
availableMetrics, availableMetrics,
metricsInHistograms = $bindable(), presetMetricsInHistograms,
metricsInScatterplots = $bindable(), presetMetricsInScatterplots,
applyHistograms,
applyScatter
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -45,6 +49,8 @@
/* State Init */ /* State Init */
let isHistogramConfigOpen = $state(false); let isHistogramConfigOpen = $state(false);
let isScatterPlotConfigOpen = $state(false); let isScatterPlotConfigOpen = $state(false);
let metricsInHistograms = $state(presetMetricsInHistograms);
let metricsInScatterplots = $state(presetMetricsInScatterplots);
let selectedMetric1 = $state(null); let selectedMetric1 = $state(null);
let selectedMetric2 = $state(null); let selectedMetric2 = $state(null);
@@ -84,11 +90,13 @@
type="checkbox" type="checkbox"
bind:group={metricsInHistograms} bind:group={metricsInHistograms}
value={metric} value={metric}
onchange={() => onchange={() => {
updateConfiguration({ updateConfiguration({
name: "analysis_view_histogramMetrics", name: "analysis_view_histogramMetrics",
value: metricsInHistograms, value: metricsInHistograms,
})} });
applyHistograms(metricsInHistograms);
}}
/> />
{metric} {metric}
@@ -126,6 +134,7 @@
name: "analysis_view_scatterPlotMetrics", name: "analysis_view_scatterPlotMetrics",
value: metricsInScatterplots, value: metricsInScatterplots,
}); });
applyScatter(metricsInScatterplots);
}} }}
> >
<Icon name="x" /> <Icon name="x" />
@@ -163,6 +172,7 @@
name: "analysis_view_scatterPlotMetrics", name: "analysis_view_scatterPlotMetrics",
value: metricsInScatterplots, value: metricsInScatterplots,
}); });
applyScatter(metricsInScatterplots);
}} }}
> >
Add Plot Add Plot
+8 -3
View File
@@ -1,6 +1,9 @@
<!-- <!--
@component Admin settings wrapper @component Admin settings wrapper
-->
Properties:
- `ncontent String`: The homepage notice content
-->
<script> <script>
import { Row, Col } from "@sveltestrap/sveltestrap"; import { Row, Col } from "@sveltestrap/sveltestrap";
@@ -13,7 +16,9 @@
import NoticeEdit from "./admin/NoticeEdit.svelte"; import NoticeEdit from "./admin/NoticeEdit.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { ncontent } = $props(); let {
ncontent
} = $props();
/* Const Init*/ /* Const Init*/
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
@@ -1,12 +1,12 @@
<!-- <!--
@component Support settings wrapper @component Support settings wrapper
Properties: None -->
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import SupportOptions from "./support/SupportOptions.svelte"; import SupportOptions from "./support/SupportOptions.svelte";
/* Const Init */
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
</script> </script>
+5 -5
View File
@@ -1,10 +1,10 @@
<!-- <!--
@component User settings wrapper @component User settings wrapper
Properties: Properties:
- `username String!`: Empty string if auth. is disabled, otherwise the username as string - `username String!`: Empty string if auth. is disabled, otherwise the username as string
- `isApi Bool!`: Is currently logged in user api authority - `isApi Bool!`: Is currently logged in user api authority
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
+9 -8
View File
@@ -1,19 +1,20 @@
<!-- <!--
@component User creation form card @component User creation form card
Properties: Properties:
- `roles [String]!`: List of roles used in app as strings - `roles [String]!`: List of roles used in app as strings
- `reloadUser Func`: The callback function to reload the user list
-->
Events:
- `reload`: Trigger upstream reload of user list after user creation
-->
<script> <script>
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap"; import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { roles, reloadUser } = $props(); let {
roles,
reloadUser
} = $props();
/* State Init */ /* State Init */
let message = $state({ msg: "", color: "#d63384" }); let message = $state({ msg: "", color: "#d63384" });
@@ -1,16 +1,18 @@
<!-- <!--
@component User managed project edit form card @component User managed project edit form card
Events: Properties:
- `reload`: Trigger upstream reload of user list after project update - `reloadUser Func`: The callback function to reload the user list
--> -->
<script> <script>
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { reloadUser } = $props(); let {
reloadUser
} = $props();
/* State Init */ /* State Init */
let message = $state({ msg: "", color: "#d63384" }); let message = $state({ msg: "", color: "#d63384" });
@@ -1,19 +1,20 @@
<!-- <!--
@component User role edit form card @component User role edit form card
Properties: Properties:
- `roles [String]!`: List of roles used in app as strings - `roles [String]!`: List of roles used in app as strings
- `reloadUser Func`: The callback function to reload the user list
Events: -->
- `reload`: Trigger upstream reload of user list after role edit
-->
<script> <script>
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
/* SVelte 5 Props */ /* SVelte 5 Props */
let {roles, reloadUser } = $props(); let {
roles,
reloadUser
} = $props();
/* State Init */ /* State Init */
let message = $state({ msg: "", color: "#d63384" }); let message = $state({ msg: "", color: "#d63384" });
@@ -1,13 +1,18 @@
<!-- <!--
@component Admin edit notice.txt content card @component Admin edit notice content card
-->
Properties:
- `ncontent String`: The homepage notice content
-->
<script> <script>
import { Col, Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Col, Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { ncontent } = $props(); let {
ncontent
} = $props();
/* State Init */ /* State Init */
let message = $state({ msg: "", color: "#d63384" }); let message = $state({ msg: "", color: "#d63384" });
+2 -2
View File
@@ -1,6 +1,6 @@
<!-- <!--
@component Admin option select card @component Admin option select card
--> -->
<script> <script>
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
@@ -1,12 +1,10 @@
<!-- <!--
@component User management table @component User management table
Properties: Properties:
- `users [Object]?`: List of users - `users [Object]?`: List of users [Bindable, Default: []]
- `reloadUser Func`: The callback function to reload the user list
Events: -->
- `reload`: Trigger upstream reload of user list
-->
<script> <script>
import { import {
@@ -19,7 +17,10 @@
import ShowUsersRow from "./ShowUsersRow.svelte"; import ShowUsersRow from "./ShowUsersRow.svelte";
/*Svelte 5 Props */ /*Svelte 5 Props */
let { users = $bindable([]), reloadUser } = $props(); let {
users = $bindable([]),
reloadUser
} = $props();
/* Functions */ /* Functions */
function deleteUser(username) { function deleteUser(username) {
@@ -1,17 +1,19 @@
<!-- <!--
@component User data row for table @component User data row for table
Properties: Properties:
- `user Object!`: User Object - `user Object!`: User Object
- {username: String, name: String, roles: [String], projects: String, email: String} - {username: String, name: String, roles: [String], projects: String, email: String}
--> -->
<script> <script>
import { Button } from "@sveltestrap/sveltestrap"; import { Button } from "@sveltestrap/sveltestrap";
import { fetchJwt } from "../../generic/utils.js" import { fetchJwt } from "../../generic/utils.js"
/* Svelte 5 Props */ /* Svelte 5 Props */
let { user } = $props(); let {
user
} = $props();
/* State Init */ /* State Init */
let jwt = $state(""); let jwt = $state("");
@@ -1,13 +1,18 @@
<!-- <!--
@component Support option select card @component Support option select card
-->
Properties:
- `config Object`: Config includes latest option states
-->
<script> <script>
import { Row, Col, Card, CardTitle, CardBody, Button} from "@sveltestrap/sveltestrap"; import { Row, Col, Card, CardTitle, CardBody, Button} from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { config } = $props(); let {
config
} = $props();
/* State Init */ /* State Init */
let message = $state(""); let message = $state("");
@@ -1,394 +1,392 @@
<!-- <!--
@component Plot color scheme selection for users @component Plot color scheme selection for users
Properties: Properties:
- `config Object`: Current cc-config - `config Object`: Current cc-config
- `message Object`: Message to display on success or error - `message Object`: Message to display on success or error [Bindable]
- `displayMessage Bool`: If to display message content - `displayMessage Bool`: If to display message content [Bindable]
- `cbmode Bool?`: Current colorblindness mode state [Bindable, Default: false]
Events: - `updateSetting Func`: The callback function to apply current option selection
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option -->
-->
<script> <script>
import { import {
Table, Table,
Row, Row,
Col, Col,
Card, Card,
CardTitle, CardTitle,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
config, config,
message = $bindable(), message = $bindable(),
displayMessage = $bindable(), displayMessage = $bindable(),
cbmode = $bindable(false), cbmode = $bindable(false),
updateSetting updateSetting
} = $props(); } = $props();
/* State Init */ /* State Init */
let activeRow = $state(JSON.stringify(config?.plot_general_colorscheme)); let activeRow = $state(JSON.stringify(config?.plot_general_colorscheme));
/* Const Init */ /* Const Init */
const colorschemes = { const colorschemes = {
Default: [ Default: [
"#00bfff", "#00bfff",
"#0000ff", "#0000ff",
"#ff00ff", "#ff00ff",
"#ff0000", "#ff0000",
"#ff8000", "#ff8000",
"#ffff00", "#ffff00",
"#80ff00", "#80ff00",
], ],
Autumn: [ Autumn: [
"rgb(255,0,0)", "rgb(255,0,0)",
"rgb(255,11,0)", "rgb(255,11,0)",
"rgb(255,20,0)", "rgb(255,20,0)",
"rgb(255,30,0)", "rgb(255,30,0)",
"rgb(255,41,0)", "rgb(255,41,0)",
"rgb(255,50,0)", "rgb(255,50,0)",
"rgb(255,60,0)", "rgb(255,60,0)",
"rgb(255,71,0)", "rgb(255,71,0)",
"rgb(255,80,0)", "rgb(255,80,0)",
"rgb(255,90,0)", "rgb(255,90,0)",
"rgb(255,101,0)", "rgb(255,101,0)",
"rgb(255,111,0)", "rgb(255,111,0)",
"rgb(255,120,0)", "rgb(255,120,0)",
"rgb(255,131,0)", "rgb(255,131,0)",
"rgb(255,141,0)", "rgb(255,141,0)",
"rgb(255,150,0)", "rgb(255,150,0)",
"rgb(255,161,0)", "rgb(255,161,0)",
"rgb(255,171,0)", "rgb(255,171,0)",
"rgb(255,180,0)", "rgb(255,180,0)",
"rgb(255,190,0)", "rgb(255,190,0)",
"rgb(255,201,0)", "rgb(255,201,0)",
"rgb(255,210,0)", "rgb(255,210,0)",
"rgb(255,220,0)", "rgb(255,220,0)",
"rgb(255,231,0)", "rgb(255,231,0)",
"rgb(255,240,0)", "rgb(255,240,0)",
"rgb(255,250,0)", "rgb(255,250,0)",
], ],
Beach: [ Beach: [
"rgb(0,252,0)", "rgb(0,252,0)",
"rgb(0,233,0)", "rgb(0,233,0)",
"rgb(0,212,0)", "rgb(0,212,0)",
"rgb(0,189,0)", "rgb(0,189,0)",
"rgb(0,169,0)", "rgb(0,169,0)",
"rgb(0,148,0)", "rgb(0,148,0)",
"rgb(0,129,4)", "rgb(0,129,4)",
"rgb(0,145,46)", "rgb(0,145,46)",
"rgb(0,162,90)", "rgb(0,162,90)",
"rgb(0,180,132)", "rgb(0,180,132)",
"rgb(29,143,136)", "rgb(29,143,136)",
"rgb(73,88,136)", "rgb(73,88,136)",
"rgb(115,32,136)", "rgb(115,32,136)",
"rgb(81,9,64)", "rgb(81,9,64)",
"rgb(124,51,23)", "rgb(124,51,23)",
"rgb(162,90,0)", "rgb(162,90,0)",
"rgb(194,132,0)", "rgb(194,132,0)",
"rgb(220,171,0)", "rgb(220,171,0)",
"rgb(231,213,0)", "rgb(231,213,0)",
"rgb(0,0,13)", "rgb(0,0,13)",
"rgb(0,0,55)", "rgb(0,0,55)",
"rgb(0,0,92)", "rgb(0,0,92)",
"rgb(0,0,127)", "rgb(0,0,127)",
"rgb(0,0,159)", "rgb(0,0,159)",
"rgb(0,0,196)", "rgb(0,0,196)",
"rgb(0,0,233)", "rgb(0,0,233)",
], ],
BlueRed: [ BlueRed: [
"rgb(0,0,131)", "rgb(0,0,131)",
"rgb(0,0,168)", "rgb(0,0,168)",
"rgb(0,0,208)", "rgb(0,0,208)",
"rgb(0,0,247)", "rgb(0,0,247)",
"rgb(0,27,255)", "rgb(0,27,255)",
"rgb(0,67,255)", "rgb(0,67,255)",
"rgb(0,108,255)", "rgb(0,108,255)",
"rgb(0,148,255)", "rgb(0,148,255)",
"rgb(0,187,255)", "rgb(0,187,255)",
"rgb(0,227,255)", "rgb(0,227,255)",
"rgb(8,255,247)", "rgb(8,255,247)",
"rgb(48,255,208)", "rgb(48,255,208)",
"rgb(87,255,168)", "rgb(87,255,168)",
"rgb(127,255,127)", "rgb(127,255,127)",
"rgb(168,255,87)", "rgb(168,255,87)",
"rgb(208,255,48)", "rgb(208,255,48)",
"rgb(247,255,8)", "rgb(247,255,8)",
"rgb(255,224,0)", "rgb(255,224,0)",
"rgb(255,183,0)", "rgb(255,183,0)",
"rgb(255,143,0)", "rgb(255,143,0)",
"rgb(255,104,0)", "rgb(255,104,0)",
"rgb(255,64,0)", "rgb(255,64,0)",
"rgb(255,23,0)", "rgb(255,23,0)",
"rgb(238,0,0)", "rgb(238,0,0)",
"rgb(194,0,0)", "rgb(194,0,0)",
"rgb(150,0,0)", "rgb(150,0,0)",
], ],
Rainbow: [ Rainbow: [
"rgb(125,0,255)", "rgb(125,0,255)",
"rgb(85,0,255)", "rgb(85,0,255)",
"rgb(39,0,255)", "rgb(39,0,255)",
"rgb(0,6,255)", "rgb(0,6,255)",
"rgb(0,51,255)", "rgb(0,51,255)",
"rgb(0,97,255)", "rgb(0,97,255)",
"rgb(0,141,255)", "rgb(0,141,255)",
"rgb(0,187,255)", "rgb(0,187,255)",
"rgb(0,231,255)", "rgb(0,231,255)",
"rgb(0,255,233)", "rgb(0,255,233)",
"rgb(0,255,189)", "rgb(0,255,189)",
"rgb(0,255,143)", "rgb(0,255,143)",
"rgb(0,255,99)", "rgb(0,255,99)",
"rgb(0,255,53)", "rgb(0,255,53)",
"rgb(0,255,9)", "rgb(0,255,9)",
"rgb(37,255,0)", "rgb(37,255,0)",
"rgb(83,255,0)", "rgb(83,255,0)",
"rgb(127,255,0)", "rgb(127,255,0)",
"rgb(173,255,0)", "rgb(173,255,0)",
"rgb(217,255,0)", "rgb(217,255,0)",
"rgb(255,248,0)", "rgb(255,248,0)",
"rgb(255,203,0)", "rgb(255,203,0)",
"rgb(255,159,0)", "rgb(255,159,0)",
"rgb(255,113,0)", "rgb(255,113,0)",
"rgb(255,69,0)", "rgb(255,69,0)",
"rgb(255,23,0)", "rgb(255,23,0)",
], ],
Binary: [ Binary: [
"rgb(215,215,215)", "rgb(215,215,215)",
"rgb(206,206,206)", "rgb(206,206,206)",
"rgb(196,196,196)", "rgb(196,196,196)",
"rgb(185,185,185)", "rgb(185,185,185)",
"rgb(176,176,176)", "rgb(176,176,176)",
"rgb(166,166,166)", "rgb(166,166,166)",
"rgb(155,155,155)", "rgb(155,155,155)",
"rgb(145,145,145)", "rgb(145,145,145)",
"rgb(136,136,136)", "rgb(136,136,136)",
"rgb(125,125,125)", "rgb(125,125,125)",
"rgb(115,115,115)", "rgb(115,115,115)",
"rgb(106,106,106)", "rgb(106,106,106)",
"rgb(95,95,95)", "rgb(95,95,95)",
"rgb(85,85,85)", "rgb(85,85,85)",
"rgb(76,76,76)", "rgb(76,76,76)",
"rgb(66,66,66)", "rgb(66,66,66)",
"rgb(55,55,55)", "rgb(55,55,55)",
"rgb(46,46,46)", "rgb(46,46,46)",
"rgb(36,36,36)", "rgb(36,36,36)",
"rgb(25,25,25)", "rgb(25,25,25)",
"rgb(16,16,16)", "rgb(16,16,16)",
"rgb(6,6,6)", "rgb(6,6,6)",
], ],
GistEarth: [ GistEarth: [
"rgb(0,0,0)", "rgb(0,0,0)",
"rgb(2,7,117)", "rgb(2,7,117)",
"rgb(9,30,118)", "rgb(9,30,118)",
"rgb(16,53,120)", "rgb(16,53,120)",
"rgb(23,73,122)", "rgb(23,73,122)",
"rgb(31,93,124)", "rgb(31,93,124)",
"rgb(39,110,125)", "rgb(39,110,125)",
"rgb(47,126,127)", "rgb(47,126,127)",
"rgb(51,133,119)", "rgb(51,133,119)",
"rgb(57,138,106)", "rgb(57,138,106)",
"rgb(62,145,94)", "rgb(62,145,94)",
"rgb(66,150,82)", "rgb(66,150,82)",
"rgb(74,157,71)", "rgb(74,157,71)",
"rgb(97,162,77)", "rgb(97,162,77)",
"rgb(121,168,83)", "rgb(121,168,83)",
"rgb(136,173,85)", "rgb(136,173,85)",
"rgb(153,176,88)", "rgb(153,176,88)",
"rgb(170,180,92)", "rgb(170,180,92)",
"rgb(185,182,94)", "rgb(185,182,94)",
"rgb(189,173,99)", "rgb(189,173,99)",
"rgb(192,164,101)", "rgb(192,164,101)",
"rgb(203,169,124)", "rgb(203,169,124)",
"rgb(215,178,149)", "rgb(215,178,149)",
"rgb(226,192,176)", "rgb(226,192,176)",
"rgb(238,212,204)", "rgb(238,212,204)",
"rgb(248,236,236)", "rgb(248,236,236)",
], ],
BlueWaves: [ BlueWaves: [
"rgb(83,0,215)", "rgb(83,0,215)",
"rgb(43,6,108)", "rgb(43,6,108)",
"rgb(9,16,16)", "rgb(9,16,16)",
"rgb(8,32,25)", "rgb(8,32,25)",
"rgb(0,50,8)", "rgb(0,50,8)",
"rgb(27,64,66)", "rgb(27,64,66)",
"rgb(69,67,178)", "rgb(69,67,178)",
"rgb(115,62,210)", "rgb(115,62,210)",
"rgb(155,50,104)", "rgb(155,50,104)",
"rgb(178,43,41)", "rgb(178,43,41)",
"rgb(180,51,34)", "rgb(180,51,34)",
"rgb(161,78,87)", "rgb(161,78,87)",
"rgb(124,117,187)", "rgb(124,117,187)",
"rgb(78,155,203)", "rgb(78,155,203)",
"rgb(34,178,85)", "rgb(34,178,85)",
"rgb(4,176,2)", "rgb(4,176,2)",
"rgb(9,152,27)", "rgb(9,152,27)",
"rgb(4,118,2)", "rgb(4,118,2)",
"rgb(34,92,85)", "rgb(34,92,85)",
"rgb(78,92,203)", "rgb(78,92,203)",
"rgb(124,127,187)", "rgb(124,127,187)",
"rgb(161,187,87)", "rgb(161,187,87)",
"rgb(180,248,34)", "rgb(180,248,34)",
"rgb(178,220,41)", "rgb(178,220,41)",
"rgb(155,217,104)", "rgb(155,217,104)",
"rgb(115,254,210)", "rgb(115,254,210)",
], ],
BlueGreenRedYellow: [ BlueGreenRedYellow: [
"rgb(0,0,0)", "rgb(0,0,0)",
"rgb(0,0,20)", "rgb(0,0,20)",
"rgb(0,0,41)", "rgb(0,0,41)",
"rgb(0,0,62)", "rgb(0,0,62)",
"rgb(0,25,83)", "rgb(0,25,83)",
"rgb(0,57,101)", "rgb(0,57,101)",
"rgb(0,87,101)", "rgb(0,87,101)",
"rgb(0,118,101)", "rgb(0,118,101)",
"rgb(0,150,101)", "rgb(0,150,101)",
"rgb(0,150,69)", "rgb(0,150,69)",
"rgb(0,148,37)", "rgb(0,148,37)",
"rgb(0,141,6)", "rgb(0,141,6)",
"rgb(60,120,0)", "rgb(60,120,0)",
"rgb(131,87,0)", "rgb(131,87,0)",
"rgb(180,25,0)", "rgb(180,25,0)",
"rgb(203,13,0)", "rgb(203,13,0)",
"rgb(208,36,0)", "rgb(208,36,0)",
"rgb(213,60,0)", "rgb(213,60,0)",
"rgb(219,83,0)", "rgb(219,83,0)",
"rgb(224,106,0)", "rgb(224,106,0)",
"rgb(229,129,0)", "rgb(229,129,0)",
"rgb(233,152,0)", "rgb(233,152,0)",
"rgb(238,176,0)", "rgb(238,176,0)",
"rgb(243,199,0)", "rgb(243,199,0)",
"rgb(248,222,0)", "rgb(248,222,0)",
"rgb(254,245,0)", "rgb(254,245,0)",
], ],
}; };
// https://personal.sron.nl/~pault/ // https://personal.sron.nl/~pault/
// https://tsitsul.in/blog/coloropt/ // https://tsitsul.in/blog/coloropt/
const cvdschemes = { const cvdschemes = {
HighContrast: [ HighContrast: [
"rgb(221,170,51)", "rgb(221,170,51)",
"rgb(187,85,102)", "rgb(187,85,102)",
"rgb(0,68,136)", "rgb(0,68,136)",
"rgb(0,0,0)", "rgb(0,0,0)",
], ],
Bright: [ Bright: [
"rgb(68,119,170)", "rgb(68,119,170)",
"rgb(102,204,238)", "rgb(102,204,238)",
"rgb(34,136,51)", "rgb(34,136,51)",
"rgb(204,187,68)", "rgb(204,187,68)",
"rgb(238,102,119)", "rgb(238,102,119)",
"rgb(170,51,119)", "rgb(170,51,119)",
"rgb(187,187,187)", "rgb(187,187,187)",
], ],
Muted: [ Muted: [
"rgb(51,34,136)", "rgb(51,34,136)",
"rgb(136,204,238)", "rgb(136,204,238)",
"rgb(68,170,153)", "rgb(68,170,153)",
"rgb(17,119,51)", "rgb(17,119,51)",
"rgb(153,153,51)", "rgb(153,153,51)",
"rgb(221,204,119)", "rgb(221,204,119)",
"rgb(204,102,119)", "rgb(204,102,119)",
"rgb(136,34,85)", "rgb(136,34,85)",
"rgb(170,68,153)", "rgb(170,68,153)",
"rgb(221,221,221)", "rgb(221,221,221)",
], ],
NormalSixColor: [ NormalSixColor: [
"rgb(64,83,211)", "rgb(64,83,211)",
"rgb(221,179,16)", "rgb(221,179,16)",
"rgb(181,29,20)", "rgb(181,29,20)",
"rgb(0,190,255)", "rgb(0,190,255)",
"rgb(251,73,176)", "rgb(251,73,176)",
"rgb(0,178,93)", "rgb(0,178,93)",
"rgb(202,202,202)", "rgb(202,202,202)",
], ],
NormalTwelveColor: [ NormalTwelveColor: [
"rgb(235,172,35)", "rgb(235,172,35)",
"rgb(184,0,88)", "rgb(184,0,88)",
"rgb(0,140,249)", "rgb(0,140,249)",
"rgb(0,110,0)", "rgb(0,110,0)",
"rgb(0,187,173)", "rgb(0,187,173)",
"rgb(209,99,230)", "rgb(209,99,230)",
"rgb(178,69,2)", "rgb(178,69,2)",
"rgb(255,146,135)", "rgb(255,146,135)",
"rgb(89,84,214)", "rgb(89,84,214)",
"rgb(0,198,248)", "rgb(0,198,248)",
"rgb(135,133,0)", "rgb(135,133,0)",
"rgb(0,167,108)", "rgb(0,167,108)",
"rgb(189,189,189)", "rgb(189,189,189)",
] ]
} }
</script> </script>
<Row cols={1} class="p-2 g-2"> <Row cols={1} class="p-2 g-2">
<!-- COLORSCHEME --> <!-- COLORSCHEME -->
<Col <Col>
><Card> <Card>
<form <form
id="colorscheme-form" id="colorscheme-form"
method="post" method="post"
action="/frontend/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <div>Color Scheme for Timeseries Plots {cbmode ? `(Color Blind Friendly Palettes)` : ``}</div>
<CardTitle {#if displayMessage && message.target == "cs"}
style="margin-bottom: 1em; display: flex; align-items: center;" <div style="margin-left: auto; font-size: 0.9em;">
> <code style="color: {message.color};" out:fade>
<div>Color Scheme for Timeseries Plots {cbmode ? `(Color Blind Friendly Palettes)` : ``}</div> Update: {message.msg}
{#if displayMessage && message.target == "cs"}<div </code>
style="margin-left: auto; font-size: 0.9em;" </div>
> {/if}
<code style="color: {message.color};" out:fade </CardTitle>
>Update: {message.msg}</code <input type="hidden" name="key" value="plot_general_colorscheme" />
> <Table hover>
</div>{/if} <tbody>
</CardTitle> {#key activeRow}
<input type="hidden" name="key" value="plot_general_colorscheme" /> {#each Object.entries(cbmode ? cvdschemes : colorschemes) as [name, rgbrow]}
<Table hover> <tr>
<tbody> <th scope="col">{name}</th>
{#key activeRow} <td>
{#each Object.entries(cbmode ? cvdschemes : colorschemes) as [name, rgbrow]} <input
<tr> type="radio"
<th scope="col">{name}</th> name="value"
<td> value={JSON.stringify(rgbrow)}
<input checked={activeRow == JSON.stringify(rgbrow)}
type="radio" onclick={(e) => {
name="value" activeRow = JSON.stringify(rgbrow)
value={JSON.stringify(rgbrow)} updateSetting(e, {
checked={activeRow == JSON.stringify(rgbrow)} selector: "#colorscheme-form",
onclick={(e) => { target: "cs",
activeRow = JSON.stringify(rgbrow) });
updateSetting(e, { }}
selector: "#colorscheme-form", />
target: "cs", </td>
}); <td>
}} {#each rgbrow as rgb}
/> <span class="color-dot" style="background-color: {rgb};"></span>
</td> {/each}
<td> </td>
{#each rgbrow as rgb} </tr>
<span class="color-dot" style="background-color: {rgb};" {/each}
></span> {/key}
{/each} </tbody>
</td> </Table>
</tr> </form>
{/each} </Card>
{/key} </Col>
</tbody>
</Table>
</form>
</Card></Col
>
</Row> </Row>
<style> <style>
.color-dot { .color-dot {
margin-left: 1px; margin-left: 1px;
height: 12px; height: 12px;
width: 12px; width: 12px;
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
} }
</style> </style>
@@ -1,224 +1,222 @@
<!-- <!--
@component Plot render option selection for users @component Plot render option selection for users
Properties: Properties:
- `config Object`: Current cc-config - `config Object`: Current cc-config
- `message Object`: Message to display on success or error - `message Object`: Message to display on success or error [Bindable]
- `displayMessage Bool`: If to display message content - `displayMessage Bool`: If to display message content [Bindable]
- `updateSetting Func`: The callback function to apply current option selection
-->
Events: <script>
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option import {
--> Button,
Row,
<script> Col,
import { Card,
Button, CardTitle,
Row, } from "@sveltestrap/sveltestrap";
Col, import { fade } from "svelte/transition";
Card,
CardTitle,
} from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
config, config,
message = $bindable(), message = $bindable(),
displayMessage = $bindable(), displayMessage = $bindable(),
updateSetting updateSetting
} = $props(); } = $props();
</script> </script>
<Row cols={3} class="p-2 g-2"> <Row cols={3} class="p-2 g-2">
<!-- LINE WIDTH --> <!-- LINE WIDTH -->
<Col <Col>
><Card class="h-100"> <Card class="h-100">
<form <form
id="line-width-form" id="line-width-form"
method="post" method="post"
action="/frontend/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
onsubmit={(e) => updateSetting(e, { onsubmit={(e) => updateSetting(e, {
selector: "#line-width-form", selector: "#line-width-form",
target: "lw", target: "lw",
})} })}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <div>Line Width</div>
<CardTitle <!-- Expand If-Clause for clarity once -->
style="margin-bottom: 1em; display: flex; align-items: center;" {#if displayMessage && message.target == "lw"}
> <div style="margin-left: auto; font-size: 0.9em;">
<div>Line Width</div> <code style="color: {message.color};" out:fade>
<!-- Expand If-Clause for clarity once --> Update: {message.msg}
{#if displayMessage && message.target == "lw"} </code>
<div style="margin-left: auto; font-size: 0.9em;"> </div>
<code style="color: {message.color};" out:fade> {/if}
Update: {message.msg} </CardTitle>
</code> <input type="hidden" name="key" value="plot_general_lineWidth" />
</div> <div class="mb-3">
<label for="value" class="form-label">Line Width</label>
<input
type="number"
class="form-control"
id="lwvalue"
name="value"
aria-describedby="lineWidthHelp"
value={config.plot_general_lineWidth}
min="1"
/>
<div id="lineWidthHelp" class="form-text">
Width of the lines in the timeseries plots.
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card>
</Col>
<!-- PLOTS PER ROW -->
<Col>
<Card class="h-100">
<form
id="plots-per-row-form"
method="post"
action="/frontend/configuration/"
class="card-body"
onsubmit={(e) => updateSetting(e, {
selector: "#plots-per-row-form",
target: "ppr",
})}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Plots per Row</div>
{#if displayMessage && message.target == "ppr"}
<div style="margin-left: auto; font-size: 0.9em;">
<code style="color: {message.color};" out:fade>
Update: {message.msg}
</code>
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
<div class="mb-3">
<label for="value" class="form-label">Plots per Row</label>
<input
type="number"
class="form-control"
id="pprvalue"
name="value"
aria-describedby="plotsperrowHelp"
value={config.plot_view_plotsPerRow}
min="1"
/>
<div id="plotsperrowHelp" class="form-text">
How many plots to show next to each other on pages such as
/monitoring/job/, /monitoring/system/...
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card>
</Col>
<!-- BACKGROUND -->
<Col class="d-flex justify-content-between">
<Card class="h-100" style="width: 49%;">
<form
id="backgrounds-form"
method="post"
action="/frontend/configuration/"
class="card-body"
onsubmit={(e) => updateSetting(e, {
selector: "#backgrounds-form",
target: "bg",
})}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Colored Backgrounds</div>
{#if displayMessage && message.target == "bg"}
<div style="margin-left: auto; font-size: 0.9em;">
<code style="color: {message.color};" out:fade>
Update: {message.msg}
</code>
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorBackground" />
<div class="mb-3">
<div>
{#if config.plot_general_colorBackground}
<input type="radio" id="colb-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="colb-true" name="value" value="true" />
{/if} {/if}
</CardTitle> <label for="true">Yes</label>
<input type="hidden" name="key" value="plot_general_lineWidth" />
<div class="mb-3">
<label for="value" class="form-label">Line Width</label>
<input
type="number"
class="form-control"
id="lwvalue"
name="value"
aria-describedby="lineWidthHelp"
value={config.plot_general_lineWidth}
min="1"
/>
<div id="lineWidthHelp" class="form-text">
Width of the lines in the timeseries plots.
</div>
</div> </div>
<Button color="primary" type="submit">Submit</Button> <div>
</form> {#if config.plot_general_colorBackground}
</Card></Col <input type="radio" id="colb-false" name="value" value="false" />
> {:else}
<input type="radio" id="colb-false-checked" name="value" value="false" checked />
<!-- PLOTS PER ROW --> {/if}
<Col <label for="false">No</label>
><Card class="h-100"> </div>
<form </div>
id="plots-per-row-form" <Button color="primary" type="submit">Submit</Button>
method="post" </form>
action="/frontend/configuration/" </Card>
class="card-body" <Card class="h-100" style="width: 49%;">
onsubmit={(e) => updateSetting(e, { <form
selector: "#plots-per-row-form", id="colorblindmode-form"
target: "ppr", method="post"
})} action="/frontend/configuration/"
class="card-body"
onsubmit={(e) => updateSetting(e, {
selector: "#colorblindmode-form",
target: "cbm",
})}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <div>Color Blind Mode</div>
<CardTitle {#if displayMessage && message.target == "cbm"}
style="margin-bottom: 1em; display: flex; align-items: center;" <div style="margin-left: auto; font-size: 0.9em;">
> <code style="color: {message.color};" out:fade>
<div>Plots per Row</div> Update: {message.msg}
{#if displayMessage && message.target == "ppr"}<div </code>
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
<div class="mb-3">
<label for="value" class="form-label">Plots per Row</label>
<input
type="number"
class="form-control"
id="pprvalue"
name="value"
aria-describedby="plotsperrowHelp"
value={config.plot_view_plotsPerRow}
min="1"
/>
<div id="plotsperrowHelp" class="form-text">
How many plots to show next to each other on pages such as
/monitoring/job/, /monitoring/system/...
</div> </div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorblindMode" />
<div class="mb-3">
<div>
{#if config?.plot_general_colorblindMode}
<input type="radio" id="cbm-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="cbm-true" name="value" value="true" />
{/if}
<label for="true">Yes</label>
</div> </div>
<Button color="primary" type="submit">Submit</Button> <div>
</form> {#if config?.plot_general_colorblindMode}
</Card></Col <input type="radio" id="cbm-false" name="value" value="false" />
> {:else}
<input type="radio" id="cbm-false-checked" name="value" value="false" checked />
<!-- BACKGROUND --> {/if}
<Col class="d-flex justify-content-between" <label for="false">No</label>
><Card class="h-100" style="width: 49%;">
<form
id="backgrounds-form"
method="post"
action="/frontend/configuration/"
class="card-body"
onsubmit={(e) => updateSetting(e, {
selector: "#backgrounds-form",
target: "bg",
})}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Colored Backgrounds</div>
{#if displayMessage && message.target == "bg"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorBackground" />
<div class="mb-3">
<div>
{#if config.plot_general_colorBackground}
<input type="radio" id="colb-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="colb-true" name="value" value="true" />
{/if}
<label for="true">Yes</label>
</div>
<div>
{#if config.plot_general_colorBackground}
<input type="radio" id="colb-false" name="value" value="false" />
{:else}
<input type="radio" id="colb-false-checked" name="value" value="false" checked />
{/if}
<label for="false">No</label>
</div>
</div> </div>
<Button color="primary" type="submit">Submit</Button> </div>
</form> <Button color="primary" type="submit">Submit</Button>
</Card> </form>
<Card class="h-100" style="width: 49%;"> </Card>
<form </Col>
id="colorblindmode-form"
method="post"
action="/frontend/configuration/"
class="card-body"
onsubmit={(e) => updateSetting(e, {
selector: "#colorblindmode-form",
target: "cbm",
})}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Color Blind Mode</div>
{#if displayMessage && message.target == "cbm"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorblindMode" />
<div class="mb-3">
<div>
{#if config?.plot_general_colorblindMode}
<input type="radio" id="cbm-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="cbm-true" name="value" value="true" />
{/if}
<label for="true">Yes</label>
</div>
<div>
{#if config?.plot_general_colorblindMode}
<input type="radio" id="cbm-false" name="value" value="false" />
{:else}
<input type="radio" id="cbm-false-checked" name="value" value="false" checked />
{/if}
<label for="false">No</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card>
</Col>
</Row> </Row>
+97 -99
View File
@@ -1,82 +1,80 @@
<!-- <!--
@component General option selection for users @component General option selection for users
Properties: Properties:
- `config Object`: Current cc-config - `config Object`: Current cc-config
- `message Object`: Message to display on success or error - `message Object`: Message to display on success or error [Bindable]
- `displayMessage Bool`: If to display message content - `displayMessage Bool`: If to display message content [Bindable]
- `username String!`: Empty string if auth. is disabled, otherwise the username as string - `username String!`: Empty string if auth. is disabled, otherwise the username as string
- `isApi Bool!`: Is currently logged in user api authority - `isApi Bool!`: Is currently logged in user api authority
- `updateSetting Func`: The callback function to apply current option selection
-->
Events:
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option
-->
<script> <script>
import { import {
Button, Button,
Row, Row,
Col, Col,
Card, Card,
CardTitle, CardTitle,
CardBody CardBody
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { fetchJwt } from "../../generic/utils.js"; import { fetchJwt } from "../../generic/utils.js";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
config, config,
message = $bindable(), message = $bindable(),
displayMessage = $bindable(), displayMessage = $bindable(),
username, username,
isApi, isApi,
updateSetting updateSetting
} = $props(); } = $props();
/* State Init */ /* State Init */
let jwt = $state(""); let jwt = $state("");
let displayCheck = $state(false); let displayCheck = $state(false);
/* Functions */ /* Functions */
function getUserJwt(username) { function getUserJwt(username) {
if (username) { if (username) {
const p = fetchJwt(username); const p = fetchJwt(username);
p.then((content) => { p.then((content) => {
jwt = content jwt = content
}).catch((error) => { }).catch((error) => {
console.error(`Could not get JWT: ${error}`); console.error(`Could not get JWT: ${error}`);
}); });
}
} }
}
function clipJwt() { function clipJwt() {
displayCheck = true; displayCheck = true;
// Navigator clipboard api needs a secure context (https) // Navigator clipboard api needs a secure context (https)
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard navigator.clipboard
.writeText(jwt) .writeText(jwt)
.catch((reason) => console.error(reason)); .catch((reason) => console.error(reason));
} else { } else {
// Workaround: Create, Fill, And Copy Content of Textarea // Workaround: Create, Fill, And Copy Content of Textarea
const textArea = document.createElement("textarea"); const textArea = document.createElement("textarea");
textArea.value = jwt; textArea.value = jwt;
textArea.style.position = "absolute"; textArea.style.position = "absolute";
textArea.style.left = "-999999px"; textArea.style.left = "-999999px";
document.body.prepend(textArea); document.body.prepend(textArea);
textArea.select(); textArea.select();
try { try {
document.execCommand('copy'); document.execCommand('copy');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
textArea.remove(); textArea.remove();
}
} }
setTimeout(function () {
displayCheck = false;
}, 1000);
} }
setTimeout(function () {
displayCheck = false;
}, 1000);
}
</script> </script>
<Row cols={isApi ? 3 : 1} class="p-2 g-2"> <Row cols={isApi ? 3 : 1} class="p-2 g-2">
@@ -133,41 +131,41 @@
{#if isApi} {#if isApi}
<!-- USER-JWT BTN --> <!-- USER-JWT BTN -->
<Col> <Col>
<Card class="h-100"> <Card class="h-100">
<CardBody> <CardBody>
<CardTitle>Generate JWT</CardTitle> <CardTitle>Generate JWT</CardTitle>
{#if jwt} {#if jwt}
<Button color="secondary" onclick={() => clipJwt()}> <Button color="secondary" onclick={() => clipJwt()}>
Copy JWT to Clipboard Copy JWT to Clipboard
</Button> </Button>
<p class="mt-2"> <p class="mt-2">
Your token is displayed on the right. Press this button to copy it to the clipboard. Your token is displayed on the right. Press this button to copy it to the clipboard.
</p> </p>
{#if displayCheck} {#if displayCheck}
<p class="mt-2"> <p class="mt-2">
<span class="text-success">Copied!</span> <span class="text-success">Copied!</span>
</p> </p>
{/if} {/if}
{:else} {:else}
<Button color="success" onclick={() => getUserJwt(username)}> <Button color="success" onclick={() => getUserJwt(username)}>
Generate JWT for '{username}' Generate JWT for '{username}'
</Button> </Button>
<p class="mt-2"> <p class="mt-2">
Generate a JSON Web Token for use with the ClusterCockpit REST-API endpoints. Generate a JSON Web Token for use with the ClusterCockpit REST-API endpoints.
</p> </p>
{/if} {/if}
</CardBody> </CardBody>
</Card> </Card>
</Col> </Col>
<!-- USER-JWT RES --> <!-- USER-JWT RES -->
<Col> <Col>
<Card class="h-100"> <Card class="h-100">
<CardBody> <CardBody>
<CardTitle>Display JWT</CardTitle> <CardTitle>Display JWT</CardTitle>
<textarea cols="32" rows="5" readonly>{jwt ? jwt : 'Press "Gen. JWT" to request token ...'}</textarea> <textarea cols="32" rows="5" readonly>{jwt ? jwt : 'Press "Gen. JWT" to request token ...'}</textarea>
</CardBody> </CardBody>
</Card> </Card>
</Col> </Col>
{/if} {/if}
</Row> </Row>
+15 -16
View File
@@ -1,19 +1,20 @@
<!-- <!--
@component Main filter component; handles filter object on sub-component changes before dispatching it @component Main filter component; handles filter object on sub-component changes before dispatching it
Properties: Properties:
- `menuText String?`: Optional text to show in the dropdown menu [Default: null] - `menuText String?`: Optional text to show in the dropdown menu [Default: null]
- `filterPresets Object?`: Optional predefined filter values [Default: {}] - `filterPresets Object?`: Optional predefined filter values [Default: {}]
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false] - `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
- `startTimeQuickSelect Bool?`: Render startTime quick selections [Default: false] - `startTimeQuickSelect Bool?`: Render startTime quick selections [Default: false]
- `matchedJobs Number?`: Number of jobs matching the filter [Default: -2] - `matchedJobs Number?`: Number of jobs matching the filter [Default: -2]
- `showFilter Func`: If the filter component should be rendered in addition to total count info [Default: true]
Events: - `applyFilters Func`: The callback function to apply current filter selection
- `update-filters, {filters: [Object]?}`: The detail's 'filters' prop are new filter items to be applied
Functions:
Functions: - `void updateFilters (additionalFilters: Object, force: Bool)`:
- `void updateFilters (additionalFilters: Object?)`: Handles new filters from nested components, triggers upstream update event Handles new filters from nested components, triggers upstream update event.
--> 'additionalFilters' usually added to existing selection, but can be forced to overwrite instead.
-->
<script> <script>
import { import {
@@ -510,8 +511,6 @@
setFilter={(filter) => updateFilters(filter)} setFilter={(filter) => updateFilters(filter)}
/> />
<style> <style>
:global(.cc-dropdown-on-hover:hover .dropdown-menu) { :global(.cc-dropdown-on-hover:hover .dropdown-menu) {
display: block; display: block;
+8 -7
View File
@@ -1,13 +1,13 @@
<!-- <!--
@component jobCompare component; compares jobs according to set filters or job selection @component jobCompare component; compares jobs according to set filters or job selection
Properties: Properties:
- `matchedCompareJobs Number?`: Number of matched jobs for selected filters [Default: 0] - `matchedCompareJobs Number?`: Number of matched jobs for selected filters [Bindable, Default: 0]
- `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection] - `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection]
- `showFootprint Bool`: If to display the jobFootprint component - `filterBuffer [Object]?`: Latest selected filters to keep for view switch to job list [Default: []]
Functions: Functions:
- `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1 - `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1
--> -->
<script> <script>
@@ -104,6 +104,7 @@
/* Effect */ /* Effect */
$effect(() => { $effect(() => {
// Update bound property
matchedCompareJobs = $compareData?.data != null ? $compareData.data.jobsMetricStats.length : -1; matchedCompareJobs = $compareData?.data != null ? $compareData.data.jobsMetricStats.length : -1;
}); });
+13 -11
View File
@@ -1,17 +1,19 @@
<!-- <!--
@component Main jobList component; lists jobs according to set filters @component Main jobList component; lists jobs according to set filters
Properties: Properties:
- `sorting Object?`: Currently active sorting [Default: {field: "startTime", type: "col", order: "DESC"}] - `sorting Object?`: Currently active sorting [Default: {field: "startTime", type: "col", order: "DESC"}]
- `matchedListJobs Number?`: Number of matched jobs for selected filters [Default: 0] - `matchedListJobs Number?`: Number of matched jobs for selected filters [Bindable, Default: 0]
- `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection] - `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection]
- `showFootprint Bool`: If to display the jobFootprint component - `showFootprint Bool?`: If to display the jobFootprint component [Default: false]
- `selectedJobs [Number]?`: IDs of jobs selected for job comparison [Bindable, Default: []]
- `filterBuffer [Object]?`: Latest selected filters to keep for view switch to job compare [Default: []]
Functions: Functions:
- `refreshJobs()`: Load jobs data with unchanged parameters and 'network-only' keyword - `refreshJobs()`: Load jobs data with unchanged parameters and 'network-only' keyword
- `refreshAllMetrics()`: Trigger downstream refresh of all running jobs' metric data - `refreshAllMetrics()`: Trigger downstream refresh of all running jobs' metric data
- `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1 - `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1
--> -->
<script> <script>
import { getContext, untrack } from "svelte"; import { getContext, untrack } from "svelte";
+5 -4
View File
@@ -1,9 +1,10 @@
<!-- <!--
@component Organized display of plots as bootstrap (sveltestrap) grid @component Organized display of svelte 5 snippets as bootstrap (sveltestrap) grid
Properties: Properties:
- `itemsPerRow Number`: Elements to render per row - `items [Any]`: Array of information required for gridContent SV5 snippet
- `items [Any]`: List of plot components to render - `itemsPerRow Number`: Elements to render per row
- `gridContent Func`: Svelte 5 Snippet from Upstream; Defines what and how to render $item data
--> -->
<script> <script>
+12 -16
View File
@@ -1,16 +1,13 @@
<!-- <!--
@component Filter sub-component for selecting cluster and subCluster @component Filter sub-component for selecting cluster and subCluster
Properties: Properties:
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `isModified Bool?`: Is this filter component modified [Default: false] - `presetCluster String?`: The latest selected cluster [Default: ""]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `presetPartition String?`: The latest selected partition [Default: ""]
- `cluster String?`: The currently selected cluster [Default: null] - `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
- `partition String?`: The currently selected partition (i.e. subCluster) [Default: null] - `setFilter Func`: The callback function to apply current filter selection
-->
Events:
- `set-filter, {String?, String?}`: Set 'cluster, subCluster' filter in upstream component
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -33,14 +30,13 @@
setFilter setFilter
} = $props(); } = $props();
/* Const Init */
const clusters = getContext("clusters");
const initialized = getContext("initialized");
/* State Init */ /* State Init */
let pendingCluster = $state(presetCluster); let pendingCluster = $state(presetCluster);
let pendingPartition = $state(presetPartition); let pendingPartition = $state(presetPartition);
/* Derived Vars */
const clusters = $derived(getContext("clusters"));
const initialized = $derived(getContext("initialized"));
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -1,16 +1,12 @@
<!-- <!--
@component Filter sub-component for selecting job duration @component Filter sub-component for selecting job duration
Properties: Properties:
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `lessThan Number?`: Amount of seconds [Default: null] - `presetDuration Object?`: Object containing the latest duration filter parameters
- `moreThan Number?`: Amount of seconds [Default: null] - Default: { lessThan: null, moreThan: null, from: null, to: null }
- `from Number?`: Epoch time in seconds [Default: null] - `setFilter Func`: The callback function to apply current filter selection
- `to Number?`: Epoch time in seconds [Default: null] -->
Events:
- `set-filter, {Number, Number, Number, Number}`: Set 'lessThan, moreThan, from, to' filter in upstream component
-->
<script> <script>
import { import {
@@ -26,7 +22,12 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
isOpen = $bindable(false), isOpen = $bindable(false),
presetDuration ={lessThan: null, moreThan: null, from: null, to: null}, presetDuration = {
lessThan: null,
moreThan: null,
from: null,
to: null
},
setFilter setFilter
} = $props(); } = $props();
+11 -10
View File
@@ -1,13 +1,12 @@
<!-- <!--
@component Filter sub-component for selecting job energies @component Filter sub-component for selecting job energies
Properties: Properties:
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, efault: false]
- `energy Object?`: The currently selected total energy filter [Default: {from:null, to:null}] - `presetEnergy Object?`: Object containing the latest energy filter parameters
- Default: { from: null, to: null }
Events: - `setFilter Func`: The callback function to apply current filter selection
- `set-filter, {Object}`: Set 'energy' filter in upstream component -->
-->
<script> <script>
import { import {
@@ -22,13 +21,15 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
isOpen = $bindable(false), isOpen = $bindable(false),
presetEnergy= {from: null, to: null}, presetEnergy = {
from: null,
to: null
},
setFilter, setFilter,
} = $props(); } = $props();
/* State Init */ /* State Init */
let energyState = $state(presetEnergy); let energyState = $state(presetEnergy);
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -1,17 +1,23 @@
<!-- <!--
@component Info pill displayed for active filters @component Info pill displayed for active filters
Properties: Properties:
- `icon String`: Sveltestrap icon name - `icon String`: Sveltestrap icon name
- `modified Bool?`: Optional if filter is modified [Default: false] - `modified Bool?`: Optional if filter is modified
- `onclick Fn()`: Opens Modal on click - `onclick Func`: Opens Modal on click
- `children Fn()?`: Internal prop, Svelte 5 version of <slot/> - `children Func`: Internal prop, Svelte 5 version of <slot/>
--> -->
<script> <script>
import { Button, Icon } from "@sveltestrap/sveltestrap"; import { Button, Icon } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { icon, modified, onclick, children } = $props(); let {
icon,
modified,
onclick,
children
} = $props();
</script> </script>
<Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} {onclick}> <Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} {onclick}>
@@ -1,17 +1,14 @@
<!-- <!--
@component Filter sub-component for selecting job states @component Filter sub-component for selecting job states
Properties: Properties:
- `isModified Bool?`: Is this filter component modified [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `presetStates [String]?`: The latest selected filter state [Default: [...allJobStates]]
- `states [String]?`: The currently selected states [Default: [...allJobStates]] - `setFilter Func`: The callback function to apply current filter selection
Events: Exported:
- `set-filter, {[String]}`: Set 'states' filter in upstream component - `const allJobStates [String]`: List of all available job states used in cc-backend
-->
Exported:
- `const allJobStates [String]`: List of all available job states used in cc-backend
-->
<script module> <script module>
export const allJobStates = [ export const allJobStates = [
@@ -1,17 +1,16 @@
<!-- <!--
@component Filter sub-component for selecting job resources @component Filter sub-component for selecting job resources
Properties: Properties:
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `activeCluster String?`: The currently selected cluster name [Default: null] - `activeCluster String?`: The currently selected cluster name [Default: null]
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}] - `presetNumNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
- `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}] - `presetNumHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}]
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}] - `presetNumAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null] - `presetNamedNode String?`: The currently selected single named node (= hostname) [Default: null]
- `presetNodeMatch String?`: The currently selected single named node (= hostname) [Default: "eq"]
Events: - `setFilter Func`: The callback function to apply current filter selection
- `set-filter, {Object, Object, Object, String}`: Set 'numNodes, numHWThreads, numAccelerators, namedNode' filter in upstream component -->
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -1,16 +1,15 @@
<!-- <!--
@component Filter sub-component for selecting job starttime @component Filter sub-component for selecting job starttime
Properties: Properties:
- `isModified Bool?`: Is this filter component modified [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `presetStartTime Object?`: Object containing the latest duration filter parameters
- `from Object?`: The currently selected from startime [Default: null] - Default: { from: null, to: null, range: "" }
- `to Object?`: The currently selected to starttime (i.e. subCluster) [Default: null] - `setFilter Func`: The callback function to apply current filter selection
- `range String?`: The currently selected starttime range as string [Default: ""]
Events: Exported:
- `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component - `const startTimeSelectOptions [Object]`: List of available fixed startTimes used in cc-backend
--> -->
<script module> <script module>
export const startTimeSelectOptions = [ export const startTimeSelectOptions = [
@@ -24,7 +23,6 @@
<script> <script>
/* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */ /* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */
import { parse, format, sub } from "date-fns"; import { parse, format, sub } from "date-fns";
import { import {
Row, Row,
@@ -1,13 +1,11 @@
<!-- <!--
@component Filter sub-component for selecting job statistics @component Filter sub-component for selecting job statistics
Properties: Properties:
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `stats [Object]?`: The currently selected statistics filter [Default: []] - `presetStats [Object]?`: The latest selected statistics filter
- `setFilter Func`: The callback function to apply current filter selection
Events: -->
- `set-filter, {[Object]}`: Set 'stats' filter in upstream component
-->
<script> <script>
import { getStatsItems } from "../utils.js"; import { getStatsItems } from "../utils.js";
+6 -9
View File
@@ -1,14 +1,11 @@
<!-- <!--
@component Filter sub-component for selecting tags @component Filter sub-component for selecting tags
Properties: Properties:
- `isModified Bool?`: Is this filter component modified [Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `presetTags [Number]?`: The currently selected tags (as IDs) [Default: []]
- `tags [Number]?`: The currently selected tags (as IDs) [Default: []] - `setFilter Func`: The callback function to apply current filter selection
-->
Events:
- `set-filter, {[Number]}`: Set 'tag' filter in upstream component
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -1,13 +1,13 @@
<!-- <!--
@component Concurrent Jobs Component; Lists all concurrent jobs in one scrollable card. @component Concurrent Jobs Component; Lists all concurrent jobs in one scrollable card.
Properties: Properties:
- `cJobs JobLinkResultList`: List of concurrent Jobs - `cJobs JobLinkResultList`: List of concurrent Jobs
- `showLinks Bool?`: Show list as clickable links [Default: false] - `showLinks Bool?`: Show list as clickable links [Default: false]
- `renderCard Bool?`: If to render component as content only or with card wrapping [Default: true] - `renderCard Bool?`: If to render component as content only or with card wrapping [Default: false]
- `width String?`: Width of the card [Default: 'auto'] - `width String?`: Width of the card [Default: 'auto']
- `height String?`: Height of the card [Default: '310px'] - `height String?`: Height of the card [Default: '400px']
--> -->
<script> <script>
import { import {
@@ -30,12 +30,12 @@
{#if renderCard} {#if renderCard}
<Card class="overflow-auto" style="width: {width}; height: {height}"> <Card class="overflow-auto" style="width: {width}; height: {height}">
<CardHeader class="mb-0 d-flex justify-content-center"> <CardHeader class="mb-0 d-flex justify-content-center">
{cJobs.items.length} Concurrent Jobs {cJobs.items.length} Concurrent Jobs
<Icon <Icon
style="cursor:help; margin-left:0.5rem;" style="cursor:help; margin-left:0.5rem;"
name="info-circle" name="info-circle"
title="Jobs running on the same node with overlapping runtimes using shared resources" title="Jobs running on the same node with overlapping runtimes using shared resources"
/> />
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{#if showLinks} {#if showLinks}
@@ -1,12 +1,12 @@
<!-- <!--
@component Footprint component; Displays job.footprint data as bars in relation to thresholds @component Footprint component; Displays job.footprint data as bars in relation to thresholds
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
- `displayTitle Bool?`: If to display cardHeader with title [Default: true] - `displayTitle Bool?`: If to display cardHeader with title [Default: true]
- `width String?`: Width of the card [Default: 'auto'] - `width String?`: Width of the card [Default: 'auto']
- `height String?`: Height of the card [Default: '310px'] - `height String?`: Height of the card [Default: '310px']
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -1,12 +1,12 @@
<!-- <!--
@component Triggers upstream data refresh in selectable intervals @component Triggers upstream data refresh in selectable intervals
Properties: Properties:
- `initially Number?`: Initial refresh interval on component mount, in seconds [Default: null] - `initially Number?`: Initial refresh interval on component mount, in seconds [Default: null]
- `presetClass String?`: Custom class to apply to main <InputGroup>
- `onRefresh Func`: The callback function to perform at refresh times
-->
Events:
- `refresh`: When fired, the upstream component refreshes its contents
-->
<script> <script>
import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap"; import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
+50 -50
View File
@@ -1,63 +1,63 @@
<!-- <!--
@component Single tag pill component @component Single tag pill component
Properties: Properties:
- id: ID! (if the tag-id is known but not the tag type/name, this can be used) - `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 } - `tag Object`: The tag Object
- clickable: Boolean (default is true) - `clickable Bool`: If tag should be click reactive [Default: true]
--> -->
<script> <script>
import { getContext } from 'svelte' import { getContext } from 'svelte'
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
id = null, id = null,
tag = null, tag = null,
clickable = true clickable = true
} = $props(); } = $props();
/* Derived */ /* Derived */
const allTags = $derived(getContext('tags')); const allTags = $derived(getContext('tags'));
const initialized = $derived(getContext('initialized')); const initialized = $derived(getContext('initialized'));
/* Effects */ /* Effects */
$effect(() => { $effect(() => {
if (tag != null && id == null) if (tag != null && id == null)
id = tag.id id = tag.id
}); });
$effect(() => { $effect(() => {
if ($initialized && tag == null) if ($initialized && tag == null)
tag = allTags.find(tag => tag.id == id) tag = allTags.find(tag => tag.id == id)
}); });
/* Function*/ /* Function*/
function getScopeColor(scope) { function getScopeColor(scope) {
switch (scope) { switch (scope) {
case "admin": case "admin":
return "#19e5e6"; return "#19e5e6";
case "global": case "global":
return "#c85fc8"; return "#c85fc8";
default: default:
return "#ffc107"; return "#ffc107";
}
} }
}
</script> </script>
<style>
a {
margin-right: 0.5rem;
}
span {
font-size: 0.9rem;
}
</style>
<a target={clickable ? "_blank" : null} href={clickable ? `/monitoring/jobs/?tag=${id}` : null}> <a target={clickable ? "_blank" : null} href={clickable ? `/monitoring/jobs/?tag=${id}` : null}>
{#if tag} {#if tag}
<span style="background-color:{getScopeColor(tag?.scope)};" class="my-1 badge text-dark">{tag.type}: {tag.name}</span> <span style="background-color:{getScopeColor(tag?.scope)};" class="my-1 badge text-dark">{tag.type}: {tag.name}</span>
{:else} {:else}
Loading... Loading...
{/if} {/if}
</a> </a>
<style>
a {
margin-right: 0.5rem;
}
span {
font-size: 0.9rem;
}
</style>
@@ -1,14 +1,15 @@
<!-- <!--
@component Job Info Subcomponent; allows management of job tags by deletion or new entries @component Job Info Subcomponent; allows management of job tags by deletion or new entries
Properties:
- `jobTags [Number]`: The array of currently designated tags [Bindable]
- `job Object`: The job object
- `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
- `renderModal Bool?`: If component is rendered as bootstrap modal button [Default: true]
-->
Properties:
- `job Object`: The job object
- `jobTags [Number]`: The array of currently designated tags
- `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
- `renderModal Bool?`: If component is rendered as bootstrap modal button [Default: true]
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
@@ -1,14 +1,12 @@
<!-- <!--
@component Search Field for Job-Lists with separate mode if project filter is active @component Search Field for Job-Lists with separate mode if project filter is active
Properties: Properties:
- `presetProject String?`: Currently active project filter [Default: ''] - `presetProject String?`: Currently active project filter [Default: '']
- `authlevel Number?`: The current users authentication level [Default: null] - `authlevel Number?`: The current users authentication level [Default: null]
- `roles [Number]?`: Enum containing available roles [Default: null] - `roles [Number]?`: Enum containing available roles [Default: null]
- `setFilter Func`: The callback function to apply current filter selection
Events: -->
- `set-filter, {String?, String?, String?}`: Set 'user, project, jobName' filter in upstream component
-->
<script> <script>
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap"; import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
+17 -12
View File
@@ -1,10 +1,16 @@
<!-- <!--
@component Displays job metaData, serves links to detail pages @component Displays job metaData, serves links to detail pages
Properties: Properties:
- `job Object`: The Job Object (GraphQL.Job) - `job Object`: The Job Object (GraphQL.Job)
- `jobTags [Number]?`: The jobs tags as IDs, default useful for dynamically updating the tags [Default: job.tags] - `jobTags [Number]?`: The jobs tags as IDs, default useful for dynamically updating the tags [Default: job.tags]
--> - `showJobSelect Bool?`: Show job selection interface for job comparison [Default: false]
- `showTagEdit Bool?`: Show tag editing interface [Default: false]
- `username String?`: The current username
- `authlevel Number`: The current user authentication level
- `roles [String]`: Available roles
- `isSelected Bool`: Whether job is selected for comparison [Bindable, Default: false]
-->
<script> <script>
import { Badge, Button, Icon, Tooltip } from "@sveltestrap/sveltestrap"; import { Badge, Button, Icon, Tooltip } from "@sveltestrap/sveltestrap";
@@ -16,12 +22,12 @@
let { let {
job, job,
jobTags = job.tags, jobTags = job.tags,
showTagedit = false, showJobSelect = false,
showTagEdit = false,
username = null, username = null,
authlevel= null, authlevel = null,
roles = null, roles = null,
isSelected = $bindable(), isSelected = $bindable(false),
showSelect = false,
} = $props(); } = $props();
/* State Init */ /* State Init */
@@ -49,7 +55,6 @@
} }
function clipJobId(jid) { function clipJobId(jid) {
// Navigator clipboard api needs a secure context (https) // Navigator clipboard api needs a secure context (https)
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard navigator.clipboard
@@ -82,7 +87,7 @@
({job.cluster}) ({job.cluster})
</span> </span>
<span> <span>
{#if showSelect} {#if showJobSelect}
<Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2" <Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2"
onclick={() => { onclick={() => {
isSelected = !isSelected isSelected = !isSelected
@@ -193,7 +198,7 @@
{/if} {/if}
</p> </p>
{#if showTagedit} {#if showTagEdit}
<hr class="mt-0 mb-2"/> <hr class="mt-0 mb-2"/>
<p class="mb-1"> <p class="mb-1">
<TagManagement bind:jobTags {job} {username} {authlevel} {roles} renderModal/> : <TagManagement bind:jobTags {job} {username} {authlevel} {roles} renderModal/> :
@@ -1,14 +1,17 @@
<!-- <!--
@component Data row for a single job displaying metric plots @component Data row for a single job displaying metric plots
Properties: Properties:
- `job Object`: The job object (GraphQL.Job) - `job Object`: The job object (GraphQL.Job)
- `metrics [String]`: Currently selected metrics - `metrics [String]`: Currently selected metrics
- `plotWidth Number`: Width of the sub-components - `plotWidth Number`: Width of the sub-components
- `plotHeight Number?`: Height of the sub-components [Default: 275] - `plotHeight Number?`: Height of the sub-components [Default: 275]
- `showFootprint Bool`: Display of footprint component for job - `showFootprint Bool`: Display of footprint component for job [Default: false]
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query - `previousSelect Bool`: The latest job select state for job comparison [Default: false]
--> - `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query [Default: false]
- `selectJob Func`: The callback function to select a job for comparison
- `unselectJob Func`: The callback function to unselect a job from comparison
-->
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
@@ -21,15 +24,15 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
triggerMetricRefresh = false,
job, job,
metrics, metrics,
plotWidth, plotWidth,
plotHeight = 275, plotHeight = 275,
showFootprint, showFootprint = false,
previousSelect = false, previousSelect = false,
triggerMetricRefresh = false,
selectJob, selectJob,
unselectJob unselectJob,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
+202 -203
View File
@@ -1,240 +1,239 @@
<!-- <!--
@component Pagination selection component @component Pagination selection component
Properties: Properties:
- page: Number (changes from inside) - `page Number?`: Current page [Default: 1]
- itemsPerPage: Number (changes from inside) - `itemsPerPage Number?`: Current items displayed per page [Default: 10]
- totalItems: Number (only displayed) - `totalItems Number?`: Total count of items [Default: 0]
- `itemText String?`: Name of paged items, e.g. "Jobs" [Default: "items"]
Events: - `pageSizes [Number!]?`: Options available for page sizes [Default: [10, 25, 50]]
- "update-paging": { page: Number, itemsPerPage: Number } - `updatePaging Func`: The callback function to apply current paging selection
- Dispatched once immediately and then each time page or itemsPerPage changes -->
-->
<script> <script>
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
page = 1, page = 1,
itemsPerPage = 10, itemsPerPage = 10,
totalItems = 0, totalItems = 0,
itemText = "items", itemText = "items",
pageSizes = [10,25,50], pageSizes = [10, 25, 50],
updatePaging updatePaging
} = $props(); } = $props();
/* Derived */ /* Derived */
const backButtonDisabled = $derived((page === 1)); const backButtonDisabled = $derived((page === 1));
const nextButtonDisabled = $derived((page >= (totalItems / itemsPerPage))); const nextButtonDisabled = $derived((page >= (totalItems / itemsPerPage)));
/* Functions */ /* Functions */
function pageUp ( event ) { function pageUp ( event ) {
event.preventDefault(); event.preventDefault();
page += 1; page += 1;
updatePaging({ itemsPerPage, page }); updatePaging({ itemsPerPage, page });
} }
function pageBack ( event ) { function pageBack ( event ) {
event.preventDefault(); event.preventDefault();
page -= 1; page -= 1;
updatePaging({ itemsPerPage, page }); updatePaging({ itemsPerPage, page });
} }
function pageReset ( event ) { function pageReset ( event ) {
event.preventDefault(); event.preventDefault();
page = 1; page = 1;
updatePaging({ itemsPerPage, page }); updatePaging({ itemsPerPage, page });
} }
function updateItems ( event ) { function updateItems ( event ) {
event.preventDefault(); event.preventDefault();
updatePaging({ itemsPerPage, page }); updatePaging({ itemsPerPage, page });
} }
</script> </script>
<div class="cc-pagination" > <div class="cc-pagination" >
<div class="cc-pagination-left"> <div class="cc-pagination-left">
<label for="cc-pagination-select">{ itemText } per page:</label> <label for="cc-pagination-select">{ itemText } per page:</label>
<div class="cc-pagination-select-wrapper"> <div class="cc-pagination-select-wrapper">
<select onblur={(e) => pageReset(e)} onchange={(e) => updateItems(e)} bind:value={itemsPerPage} id="cc-pagination-select" class="cc-pagination-select"> <select onblur={(e) => pageReset(e)} onchange={(e) => updateItems(e)} bind:value={itemsPerPage} id="cc-pagination-select" class="cc-pagination-select">
{#each pageSizes as size} {#each pageSizes as size}
<option value="{size}">{size}</option> <option value="{size}">{size}</option>
{/each} {/each}
</select> </select>
<span class="focus"></span> <span class="focus"></span>
</div>
<span class="cc-pagination-text">
{ ((page - 1) * itemsPerPage) + 1 } - { Math.min((page - 1) * itemsPerPage + itemsPerPage, totalItems) } of { totalItems } { itemText }
</span>
</div>
<div class="cc-pagination-right">
{#if !backButtonDisabled}
<button aria-label="page-reset" class="reset nav" type="button"
onclick={(e) => pageReset(e)}></button>
<button aria-label="page-back" class="left nav" type="button"
onclick={(e) => pageBack(e)}></button>
{/if}
{#if !nextButtonDisabled}
<button aria-label="page-up" class="right nav" type="button"
onclick={(e) => pageUp(e)}></button>
{/if}
</div> </div>
<span class="cc-pagination-text">
{ ((page - 1) * itemsPerPage) + 1 } - { Math.min((page - 1) * itemsPerPage + itemsPerPage, totalItems) } of { totalItems } { itemText }
</span>
</div>
<div class="cc-pagination-right">
{#if !backButtonDisabled}
<button aria-label="page-reset" class="reset nav" type="button"
onclick={(e) => pageReset(e)}></button>
<button aria-label="page-back" class="left nav" type="button"
onclick={(e) => pageBack(e)}></button>
{/if}
{#if !nextButtonDisabled}
<button aria-label="page-up" class="right nav" type="button"
onclick={(e) => pageUp(e)}></button>
{/if}
</div>
</div> </div>
<style> <style>
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
} }
div { div {
display: flex; display: flex;
align-items: center; align-items: center;
vertical-align: baseline; vertical-align: baseline;
box-sizing: border-box; box-sizing: border-box;
} }
label, select, button { label, select, button {
margin: 0; margin: 0;
padding: 0; padding: 0;
vertical-align: baseline; vertical-align: baseline;
color: #525252; color: #525252;
} }
button { button {
position: relative; position: relative;
border: none; border: none;
border-left: 1px solid #e0e0e0; border-left: 1px solid #e0e0e0;
height: 3rem; height: 3rem;
width: 3rem; width: 3rem;
background: 0 0; background: 0 0;
transition: all 70ms; transition: all 70ms;
} }
button:hover { button:hover {
background-color: #dde1e6; background-color: #dde1e6;
} }
button:focus { button:focus {
top: -1px; top: -1px;
left: -1px; left: -1px;
right: -1px; right: -1px;
bottom: -1px; bottom: -1px;
border: 1px solid blue; border: 1px solid blue;
border-radius: inherit; border-radius: inherit;
} }
.nav::after { .nav::after {
content: ""; content: "";
width: 0.9em; width: 0.9em;
height: 0.8em; height: 0.8em;
background-color: #777; background-color: #777;
z-index: 1; z-index: 1;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
} }
.nav:disabled { .nav:disabled {
background-color: #fff; background-color: #fff;
cursor: no-drop; cursor: no-drop;
} }
.reset::after { .reset::after {
clip-path: polygon(100% 0%, 75% 50%, 100% 100%, 25% 100%, 0% 50%, 25% 0%); clip-path: polygon(100% 0%, 75% 50%, 100% 100%, 25% 100%, 0% 50%, 25% 0%);
margin-top: -0.3em; margin-top: -0.3em;
margin-left: -0.5em; margin-left: -0.5em;
} }
.right::after { .right::after {
clip-path: polygon(100% 50%, 50% 0, 50% 100%); clip-path: polygon(100% 50%, 50% 0, 50% 100%);
margin-top: -0.3em; margin-top: -0.3em;
margin-left: -0.5em; margin-left: -0.5em;
} }
.left::after { .left::after {
clip-path: polygon(50% 0, 0 50%, 50% 100%); clip-path: polygon(50% 0, 0 50%, 50% 100%);
margin-top: -0.3em; margin-top: -0.3em;
margin-left: -0.3em; margin-left: -0.3em;
} }
.cc-pagination-select-wrapper::after { .cc-pagination-select-wrapper::after {
content: ""; content: "";
width: 0.8em; width: 0.8em;
height: 0.5em; height: 0.5em;
background-color: #777; background-color: #777;
clip-path: polygon(100% 0%, 0 0%, 50% 100%); clip-path: polygon(100% 0%, 0 0%, 50% 100%);
justify-self: end; justify-self: end;
} }
.cc-pagination { .cc-pagination {
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
} }
.cc-pagination-text { .cc-pagination-text {
color: #525252; color: #525252;
margin-left: 1rem; margin-left: 1rem;
} }
.cc-pagination-text { .cc-pagination-text {
color: #525252; color: #525252;
margin-right: 1rem; margin-right: 1rem;
} }
.cc-pagination-left { .cc-pagination-left {
padding: 0 1rem; padding: 0 1rem;
height: 3rem; height: 3rem;
} }
.cc-pagination-select-wrapper { .cc-pagination-select-wrapper {
display: grid; display: grid;
grid-template-areas: "select"; grid-template-areas: "select";
align-items: center; align-items: center;
position: relative; position: relative;
padding: 0 0.5em; padding: 0 0.5em;
min-width: 3em; min-width: 3em;
max-width: 6em; max-width: 6em;
border-right: 1px solid #e0e0e0; border-right: 1px solid #e0e0e0;
cursor: pointer; cursor: pointer;
transition: all 70ms; transition: all 70ms;
} }
.cc-pagination-select-wrapper:hover { .cc-pagination-select-wrapper:hover {
background-color: #dde1e6; background-color: #dde1e6;
} }
select, select,
.cc-pagination-select-wrapper::after { .cc-pagination-select-wrapper::after {
grid-area: select; grid-area: select;
} }
.cc-pagination-select { .cc-pagination-select {
height: 3rem; height: 3rem;
appearance: none; appearance: none;
background-color: transparent; background-color: transparent;
padding: 0 1em 0 0; padding: 0 1em 0 0;
margin: 0; margin: 0;
border: none; border: none;
width: 100%; width: 100%;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
cursor: inherit; cursor: inherit;
line-height: inherit; line-height: inherit;
z-index: 1; z-index: 1;
outline: none; outline: none;
} }
select:focus + .focus { select:focus + .focus {
position: absolute; position: absolute;
top: -1px; top: -1px;
left: -1px; left: -1px;
right: -1px; right: -1px;
bottom: -1px; bottom: -1px;
border: 1px solid blue; border: 1px solid blue;
border-radius: inherit; border-radius: inherit;
} }
.cc-pagination-right { .cc-pagination-right {
height: 3rem; height: 3rem;
} }
</style> </style>
@@ -1,16 +1,22 @@
<!-- <!--
@component Main plot component, based on uPlot; metricdata values by time @component Job Data Compare Plot Component, based on uPlot; metricData values by jobId/startTime
Only width/height should change reactively. Only width/height should change reactively.
Properties: Properties:
- `metric String`: The metric name - `metric String?`: The metric name [Default: ""]
- `width Number?`: The plot width [Default: 0] - `width Number?`: The plot width [Default: 0]
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `data [Array]`: The metric data object - `data [Array]`: The data object [Default: null]
- `cluster String`: Cluster name of the parent job / data - `title String?`: Plot title [Default: ""]
- `subCluster String`: Name of the subCluster of the parent job - `xlabel String?`: Plot X axis label [Default: ""]
--> - `ylabel String?`: Plot Y axis label [Default: ""]
- `yunit String?`: Plot Y axis unit [Default: ""]
- `xticks Array`: Array containing jobIDs [Default: []]
- `xinfo Array`: Array containing job information [Default: []]
- `forResources Bool?`: Render this plot for allocated jobResources [Default: false]
- `plot Sync Object!`: uPlot cursor synchronization key
-->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
+15 -12
View File
@@ -1,17 +1,20 @@
<!-- <!--
@component Histogram Plot based on uPlot Bars @component Histogram Plot based on uPlot Bars
Properties: Only width/height should change reactively.
- `data [[],[]]`: uPlot data structure array ( [[],[]] == [X, Y] )
- `usesBins Bool?`: If X-Axis labels are bins ("XX-YY") [Default: false] Properties:
- `width Number?`: Plot width (reactively adaptive) [Default: 500] - `data [[],[]]`: uPlot data structure array ( [[],[]] == [X, Y] )
- `height Number?`: Plot height (reactively adaptive) [Default: 300] - `usesBins Bool?`: If X-Axis labels are bins ("XX-YY") [Default: false]
- `title String?`: Plot title [Default: ""] - `width Number?`: Plot width (reactively adaptive) [Default: null]
- `xlabel String?`: Plot X axis label [Default: ""] - `height Number?`: Plot height (reactively adaptive) [Default: 250]
- `xunit String?`: Plot X axis unit [Default: ""] - `title String?`: Plot title [Default: ""]
- `ylabel String?`: Plot Y axis label [Default: ""] - `xlabel String?`: Plot X axis label [Default: ""]
- `yunit String?`: Plot Y axis unit [Default: ""] - `xunit String?`: Plot X axis unit [Default: ""]
--> - `xtime Bool?`: If X-Axis is based on time information [Default: false]
- `ylabel String?`: Plot Y axis label [Default: ""]
- `yunit String?`: Plot Y axis unit [Default: ""]
-->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
@@ -1,24 +1,27 @@
<!-- <!--
@component Main plot component, based on uPlot; metricdata values by time @component Main plot component, based on uPlot; metricdata values by time
Only width/height should change reactively. Only width/height should change reactively.
Properties: Properties:
- `metric String`: The metric name - `metric String`: The metric name
- `scope String?`: Scope of the displayed data [Default: node] - `scope String?`: Scope of the displayed data [Default: node]
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `timestep Number`: The timestep used for X-axis rendering - `timestep Number`: The timestep used for X-axis rendering
- `series [GraphQL.Series]`: The metric data object - `series [GraphQL.Series]`: The metric data object
- `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: false] - `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: false]
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null] - `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
- `cluster String`: Cluster name of the parent job / data - `cluster String?`: Cluster name of the parent job / data [Default: ""]
- `subCluster String`: Name of the subCluster of the parent job - `subCluster String`: Name of the subCluster of the parent job
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false] - `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false] - `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
- `numhwthreads Number?`: Number of job HWThreads [Default: 0] - `numhwthreads Number?`: Number of job HWThreads [Default: 0]
- `numaccs Number?`: Number of job Accelerators [Default: 0] - `numaccs Number?`: Number of job Accelerators [Default: 0]
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null] - `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
--> - `thersholdState Object?`: The last threshold state to preserve on user zoom [Default: null]
- `extendedLegendData Object?`: Additional information to be rendered in an extended legend [Default: null]
- `onZoom Func`: Callback function to handle zoom-in event
-->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
+11 -10
View File
@@ -1,16 +1,17 @@
<!-- <!--
@component Pie Plot based on chart.js Pie @component Pie Plot based on chart.js Pie
Properties: Properties:
- `size Number`: X and Y size of the plot, for square shape - `canvasId String?`: Unique ID for correct parallel chart.js rendering [Default: "pie-default"]
- `sliceLabel String`: Label used in segment legends - `size Number`: X and Y size of the plot, for square shape
- `quantities [Number]`: Data values - `sliceLabel String`: Label used in segment legends
- `entities [String]`: Data identifiers - `quantities [Number]`: Data values
- `displayLegend?`: Display uPlot legend [Default: false] - `entities [String]`: Data identifiers
- `displayLegend?`: Display uPlot legend [Default: false]
Exported: Exported:
- `colors ['rgb(x,y,z)', ...]`: Color range used for segments; upstream used for legend - `colors ['rgb(x,y,z)', ...]`: Color range used for segments; upstream used for legend
--> -->
<script module> <script module>
// http://tsitsul.in/blog/coloropt/ : 12 colors normal // http://tsitsul.in/blog/coloropt/ : 12 colors normal
+7 -6
View File
@@ -1,11 +1,12 @@
<!-- <!--
@component Polar Plot based on chart.js Radar @component Polar Plot based on chart.js Radar
Properties: Properties:
- `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ] - `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ]
- `polarData [GraphQL.JobMetricStatWithName]?`: Metric data [Default: null] - `polarData [GraphQL.JobMetricStatWithName]?`: Metric data [Default: null]
- `height Number?`: Plot height [Default: 365] - `canvasId String?`: Unique ID for correct parallel chart.js rendering [Default: "polar-default"]
--> - `height Number?`: Plot height [Default: 365]
-->
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
+19 -19
View File
@@ -1,25 +1,25 @@
<!-- <!--
@component Roofline Model Plot based on uPlot @component Roofline Model Plot based on uPlot
Properties:
- `data [null, [], []]`: Roofline Data Structure, see below for details [Default: null]
- `renderTime Bool?`: If time information should be rendered as colored dots [Default: false]
- `allowSizeChange Bool?`: If dimensions of rendered plot can change [Default: false]
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
- `width Number?`: Plot width (reactively adaptive) [Default: 600]
- `height Number?`: Plot height (reactively adaptive) [Default: 380]
Properties:
- `data [null, [], []]`: Roofline Data Structure, see below for details [Default: null]
- `renderTime Bool?`: If time information should be rendered as colored dots [Default: false]
- `allowSizeChange Bool?`: If dimensions of rendered plot can change [Default: false]
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
- `width Number?`: Plot width (reactively adaptive) [Default: 600]
- `height Number?`: Plot height (reactively adaptive) [Default: 380]
Data Format: Data Format:
- `data = [null, [], []]` - `data = [null, [], []]`
- Index 0: null-axis required for scatter - Index 0: null-axis required for scatter
- Index 1: Array of XY-Arrays for Scatter - Index 1: Array of XY-Arrays for Scatter
- Index 2: Optional Time Info - Index 2: Optional Time Info
- `data[1][0] = [100, 200, 500, ...]` - `data[1][0] = [100, 200, 500, ...]`
- X Axis: Intensity (Vals up to clusters' flopRateScalar value) - X Axis: Intensity (Vals up to clusters' flopRateScalar value)
- `data[1][1] = [1000, 2000, 1500, ...]` - `data[1][1] = [1000, 2000, 1500, ...]`
- Y Axis: Performance (Vals up to clusters' flopRateSimd value) - Y Axis: Performance (Vals up to clusters' flopRateSimd value)
- `data[2] = [0.1, 0.15, 0.2, ...]` - `data[2] = [0.1, 0.15, 0.2, ...]`
- Color Code: Time Information (Floats from 0 to 1) (Optional) - Color Code: Time Information (Floats from 0 to 1) (Optional)
--> -->
<script> <script>
@@ -5,12 +5,13 @@
- `X [Number]`: Data from first selected metric as X-values - `X [Number]`: Data from first selected metric as X-values
- `Y [Number]`: Data from second selected metric as Y-values - `Y [Number]`: Data from second selected metric as Y-values
- `S GraphQl.TimeWeights.X?`: Float to scale the data with [Default: null] - `S GraphQl.TimeWeights.X?`: Float to scale the data with [Default: null]
- `color String`: Color of the drawn scatter circles - `color String?`: Color of the drawn scatter circles [Default: '#0066cc']
- `width Number`: - `width Number?`: Width of the plot [Default: 250]
- `height Number`: - `height Number?`: Height of the plot [Default: 300]
- `xLabel String`: - `xLabel String?`: X-Axis Label [Ðefault: ""]
- `yLabel String`: - `yLabel String?`: Y-Axis Label [Default: ""]
--> -->
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { formatNumber } from '../units.js' import { formatNumber } from '../units.js'
@@ -23,8 +24,8 @@
color = '#0066cc', color = '#0066cc',
width = 250, width = 250,
height = 300, height = 300,
xLabel, xLabel = "",
yLabel, yLabel = "",
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -1,259 +1,257 @@
<!-- <!--
Copyright (c) 2021 Michael Keller Copyright (c) 2021 Michael Keller
Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider) Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider)
Changes: remove dependency, text inputs, configurable value ranges, on:change event Changes: remove dependency, text inputs, configurable value ranges, on:change event
Changes #2: Rewritten for Svelte 5, removed bodyHandler Changes #2: Rewritten for Svelte 5, removed bodyHandler
--> -->
<!--
@component Selector component to display range selections via min and max double-sliders
Properties: <!--
- min: Number @component Selector component to display range selections via min and max double-sliders
- max: Number
- sliderHandleFrom: Number (Starting position of slider #1) Properties:
- sliderHandleTo: Number (Starting position of slider #2) - `sliderMin Number!`: Minimum possible value for slider
- `sliderMax Number!`: Maximum possible value for slider
Events: - `fromPreset Number?`: Latest "from" value selection [Default: 1]
- `change`: [Number, Number] (Positions of the two sliders) - `toPreset Number?`: Latest "to" value selection [Default: 100]
- `changeRange Func`: The callback function to apply current range selections
--> -->
<script> <script>
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
sliderMin, sliderMin,
sliderMax, sliderMax,
fromPreset = 1, fromPreset = 1,
toPreset = 100, toPreset = 100,
changeRange changeRange
} = $props(); } = $props();
/* State Init */ /* State Init */
let pendingValues = $state([fromPreset, toPreset]); let pendingValues = $state([fromPreset, toPreset]);
let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.)); let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.));
let sliderTo = $state(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.)); let sliderTo = $state(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.));
let inputFieldFrom = $state(fromPreset.toString()); let inputFieldFrom = $state(fromPreset.toString());
let inputFieldTo = $state(toPreset.toString()); let inputFieldTo = $state(toPreset.toString());
let leftHandle = $state(); let leftHandle = $state();
let sliderMain = $state(); let sliderMain = $state();
/* Var Init */ /* Var Init */
let timeoutId = null; let timeoutId = null;
/* Functions */ /* Functions */
function queueChangeEvent() { function queueChangeEvent() {
if (timeoutId !== null) { if (timeoutId !== null) {
clearTimeout(timeoutId) clearTimeout(timeoutId)
} }
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null timeoutId = null
changeRange(pendingValues); changeRange(pendingValues);
}, 100); }, 100);
} }
function updateStates(newValue, newPosition, target) { function updateStates(newValue, newPosition, target) {
if (target === 'from') { if (target === 'from') {
pendingValues[0] = isNaN(newValue) ? null : newValue; pendingValues[0] = isNaN(newValue) ? null : newValue;
inputFieldFrom = isNaN(newValue) ? null : newValue.toString(); inputFieldFrom = isNaN(newValue) ? null : newValue.toString();
sliderFrom = newPosition; sliderFrom = newPosition;
} else if (target === 'to') { } else if (target === 'to') {
pendingValues[1] = isNaN(newValue) ? null : newValue; pendingValues[1] = isNaN(newValue) ? null : newValue;
inputFieldTo = isNaN(newValue) ? null : newValue.toString(); inputFieldTo = isNaN(newValue) ? null : newValue.toString();
sliderTo = newPosition; sliderTo = newPosition;
} }
queueChangeEvent(); queueChangeEvent();
} }
function rangeChanged (evt, target) { function rangeChanged (evt, target) {
evt.preventDefault() evt.preventDefault()
evt.stopPropagation() evt.stopPropagation()
const { left, right } = sliderMain.getBoundingClientRect(); const { left, right } = sliderMain.getBoundingClientRect();
const parentWidth = right - left; const parentWidth = right - left;
const newP = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1); const newP = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
const newV = Math.floor(sliderMin + newP * (sliderMax - sliderMin)); const newV = Math.floor(sliderMin + newP * (sliderMax - sliderMin));
updateStates(newV, newP, target); updateStates(newV, newP, target);
} }
function inputChanged(evt, target) { function inputChanged(evt, target) {
evt.preventDefault() evt.preventDefault()
evt.stopPropagation() evt.stopPropagation()
const newV = Number.parseInt(evt.target.value); const newV = Number.parseInt(evt.target.value);
const newP = clamp((newV - sliderMin) / (sliderMax - sliderMin), 0., 1.) const newP = clamp((newV - sliderMin) / (sliderMax - sliderMin), 0., 1.)
updateStates(newV, newP, target); updateStates(newV, newP, target);
} }
function clamp(x, testMin, testMax) { function clamp(x, testMin, testMax) {
return x < testMin return x < testMin
? testMin ? testMin
: (x > testMax : (x > testMax
? testMax ? testMax
: x : x
); );
} }
function draggable(node) { function draggable(node) {
let x; let x;
let y; let y;
function handleMousedown(event) { function handleMousedown(event) {
if (event.type === 'touchstart') { if (event.type === 'touchstart') {
event = event.touches[0]; event = event.touches[0];
} }
x = event.clientX; x = event.clientX;
y = event.clientY; y = event.clientY;
node.dispatchEvent(new CustomEvent('dragstart', { node.dispatchEvent(new CustomEvent('dragstart', {
detail: { x, y } detail: { x, y }
})); }));
window.addEventListener('mousemove', handleMousemove); window.addEventListener('mousemove', handleMousemove);
window.addEventListener('mouseup', handleMouseup); window.addEventListener('mouseup', handleMouseup);
window.addEventListener('touchmove', handleMousemove); window.addEventListener('touchmove', handleMousemove);
window.addEventListener('touchend', handleMouseup); window.addEventListener('touchend', handleMouseup);
} }
function handleMousemove(event) { function handleMousemove(event) {
if (event.type === 'touchmove') { if (event.type === 'touchmove') {
event = event.changedTouches[0]; event = event.changedTouches[0];
} }
const dx = event.clientX - x; const dx = event.clientX - x;
const dy = event.clientY - y; const dy = event.clientY - y;
x = event.clientX; x = event.clientX;
y = event.clientY; y = event.clientY;
node.dispatchEvent(new CustomEvent('dragmove', { node.dispatchEvent(new CustomEvent('dragmove', {
detail: { x, y, dx, dy } detail: { x, y, dx, dy }
})); }));
} }
function handleMouseup(event) { function handleMouseup(event) {
x = event.clientX; x = event.clientX;
y = event.clientY; y = event.clientY;
node.dispatchEvent(new CustomEvent('dragend', { node.dispatchEvent(new CustomEvent('dragend', {
detail: { x, y } detail: { x, y }
})); }));
window.removeEventListener('mousemove', handleMousemove); window.removeEventListener('mousemove', handleMousemove);
window.removeEventListener('mouseup', handleMouseup); window.removeEventListener('mouseup', handleMouseup);
window.removeEventListener('touchmove', handleMousemove); window.removeEventListener('touchmove', handleMousemove);
window.removeEventListener('touchend', handleMouseup); window.removeEventListener('touchend', handleMouseup);
} }
node.addEventListener('mousedown', handleMousedown); node.addEventListener('mousedown', handleMousedown);
node.addEventListener('touchstart', handleMousedown); node.addEventListener('touchstart', handleMousedown);
return {
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('touchstart', handleMousedown);
}
};
}
return {
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('touchstart', handleMousedown);
}
};
}
</script> </script>
<div class="double-range-container"> <div class="double-range-container">
<div class="header"> <div class="header">
<input class="form-control" type="text" placeholder="from..." value={inputFieldFrom} <input class="form-control" type="text" placeholder="from..." value={inputFieldFrom}
oninput={(e) => inputChanged(e, 'from')} /> oninput={(e) => inputChanged(e, 'from')} />
<span>Full Range: <b> {sliderMin} </b> - <b> {sliderMax} </b></span> <span>Full Range: <b> {sliderMin} </b> - <b> {sliderMax} </b></span>
<input class="form-control" type="text" placeholder="to..." value={inputFieldTo} <input class="form-control" type="text" placeholder="to..." value={inputFieldTo}
oninput={(e) => inputChanged(e, 'to')} /> oninput={(e) => inputChanged(e, 'to')} />
</div> </div>
<div id="slider-active" class="slider" bind:this={sliderMain}> <div id="slider-active" class="slider" bind:this={sliderMain}>
<div <div
class="slider-body" class="slider-body"
style="left: {100 * sliderFrom}%;right: {100 * (1 - sliderTo)}%;" style="left: {100 * sliderFrom}%;right: {100 * (1 - sliderTo)}%;"
></div> ></div>
<div <div
class="slider-handle" class="slider-handle"
bind:this={leftHandle} bind:this={leftHandle}
data-which="from" data-which="from"
use:draggable use:draggable
ondragmove={(e) => rangeChanged(e, 'from')} ondragmove={(e) => rangeChanged(e, 'from')}
style="left: {100 * sliderFrom}%" style="left: {100 * sliderFrom}%"
></div> ></div>
<div <div
class="slider-handle" class="slider-handle"
data-which="to" data-which="to"
use:draggable use:draggable
ondragmove={(e) => rangeChanged(e, 'to')} ondragmove={(e) => rangeChanged(e, 'to')}
style="left: {100 * sliderTo}%" style="left: {100 * sliderTo}%"
></div> ></div>
</div> </div>
</div> </div>
<style> <style>
.header { .header {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-end; align-items: flex-end;
margin-bottom: 5px; margin-bottom: 5px;
} }
.header :nth-child(2) { .header :nth-child(2) {
padding-top: 10px; padding-top: 10px;
} }
.header input { .header input {
height: 25px; height: 25px;
border-radius: 5px; border-radius: 5px;
width: 100px; width: 100px;
} }
.double-range-container { .double-range-container {
width: 100%; width: 100%;
height: 50px; height: 50px;
user-select: none; user-select: none;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap; white-space: nowrap;
margin-top: -4px; margin-top: -4px;
} }
.slider { .slider {
position: relative; position: relative;
width: 100%; width: 100%;
height: 6px; height: 6px;
top: 10px; top: 10px;
transform: translate(0, -50%); transform: translate(0, -50%);
background-color: #e2e2e2; background-color: #e2e2e2;
box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c; box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c;
border-radius: 6px; border-radius: 6px;
} }
.slider-handle { .slider-handle {
position: absolute; position: absolute;
top: 50%; top: 50%;
width: 0; width: 0;
height: 0; height: 0;
} }
.slider-handle:after { .slider-handle:after {
content: ' '; content: ' ';
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
width: 16px; width: 16px;
height: 16px; height: 16px;
background-color: #fdfdfd; background-color: #fdfdfd;
border: 1px solid #7b7b7b; border: 1px solid #7b7b7b;
transform: translate(-50%, -50%) transform: translate(-50%, -50%)
} }
/* .handle[data-which="end"]:after{ /* .handle[data-which="end"]:after{
transform: translate(-100%, -50%); transform: translate(-100%, -50%);
} */ } */
.slider-handle:active:after { .slider-handle:active:after {
background-color: #ddd; background-color: #ddd;
z-index: 9; z-index: 9;
} }
.slider-body { .slider-body {
top: 0; top: 0;
position: absolute; position: absolute;
background-color: #34a1ff; background-color: #34a1ff;
bottom: 0; bottom: 0;
} }
</style> </style>
@@ -1,11 +1,12 @@
<!-- <!--
@component Selector component for (footprint) metrics to be displayed as histogram @component Selector component for (footprint) metrics to be displayed as histogram
Properties: Properties:
- `cluster String`: Currently selected cluster - `cluster String`: Currently selected cluster
- `selectedHistograms [String]`: The currently selected metrics to display as histogram - `selectedHistograms [String]`: The currently selected metrics to display as histogram
- ìsOpen Bool`: Is selection opened - `ìsOpen Bool`: Is selection opened [Bindable]
--> - `applyChange Func`: The callback function to apply current selection
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -1,15 +1,18 @@
<!-- <!--
@component Metric selector component; allows reorder via drag and drop @component Metric selector component; allows reorder via drag and drop
Properties: Properties:
- `metrics [String]`: (changes from inside, needs to be initialised, list of selected metrics) - `isOpen Bool`: Is selection modal opened [Bindable, Default: false]
- `isOpen Bool`: (can change from inside and outside) - `showFootprint Bool?`: Upstream state of whether to render footprint card [Bindable, Default: false]
- `configName String`: The config key for the last saved selection (constant) - `totalMetrics Number?`: Total available metrics [Bindable, Default: 0]
- `allMetrics [String]?`: List of all available metrics [Default: null] - `presetMetrics [String]`: Latest selection of metrics [Default: []]
- `cluster String?`: The currently selected cluster [Default: null] - `cluster String?`: The currently selected cluster [Default: null]
- `showFootprint Bool?`: Upstream state of wether to render footpritn card [Default: false] - `subCluster String?`: The currently selected subCluster [Default: null]
- `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false] - `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false]
--> - `preInitialized Bool?`: If the parent component has a dedicated call to init() [Default: false]
- `configName String`: The config key for the last saved selection (constant)
- `applyMetrics Func`: The callback function to apply current selection
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -64,7 +67,7 @@
/* Reactive Effects */ /* Reactive Effects */
$effect(() => { $effect(() => {
totalMetrics = allMetrics.size; totalMetrics = allMetrics?.size || 0;
}); });
$effect(() => { $effect(() => {
@@ -1,10 +1,12 @@
<!-- <!--
@component Selector for sorting field and direction @component Selector for sorting field and direction
Properties: Properties:
- sorting: { field: String, order: "DESC" | "ASC" } (changes from inside) - `presetSorting Object?`: The latest sort selection state
- isOpen: Boolean (can change from inside and outside) - Default { field: "startTime", type: "col", order: "DESC" }
--> - `isOpen Bool?`: Is modal opened [Bindable, Default: false]
- `applySorting Func`: The callback function to apply current selection
-->
<script> <script>
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
@@ -1,15 +1,13 @@
<!-- <!--
@component Selector for specified real time ranges for data cutoff; used in systems and nodes view @component Selector for specified real time ranges for data cutoff; used in systems and nodes view
Properties: Properties:
- `from Date`: The datetime to start data display from - `presetFrom Date`: The latest "from" JS Date Object
- `to Date`: The datetime to end data display at - `presetTo Date`: The latest Date "to" JS Date Object
- `customEnabled Bool?`: Allow custom time window selection [Default: true] - `customEnabled Bool?`: Allow custom time window selection [Default: true]
- `options Object? {String:Number}`: The quick time selection options [Default: {..., "Last 24hrs": 24*60*60}] - `options Object? {String:Number}`: The quick time selection options [Default: {..., "Last 24hrs": 24*60*60}]
- `applyTime Func`: The callback function to apply current selection
Events: -->
- `change, {Date, Date}`: Set 'from, to' values in upstream component
-->
<script> <script>
import { import {
+1 -1
View File
@@ -6,7 +6,7 @@ const headerDomTarget = document.getElementById('svelte-header');
if (headerDomTarget != null) { if (headerDomTarget != null) {
mount(Header, { mount(Header, {
target: headerDomTarget, target: headerDomTarget,
props: { // { ...header }, props: {
username: hUsername, username: hUsername,
authlevel: hAuthlevel, authlevel: hAuthlevel,
clusters: hClusters, clusters: hClusters,
+7 -7
View File
@@ -1,12 +1,12 @@
<!-- <!--
@component Navbar component; renders in app navigation links as received from upstream @component Navbar component; renders in app navigation links as received from upstream
Properties: Properties:
- `clusters [String]`: List of cluster names - `clusters [String]`: List of cluster names
- `subClusters map[String][]string`: Map of subclusters by cluster names - `subClusters map[String][]string`: Map of subclusters by cluster names
- `links [Object]`: Pre-filtered link objects based on user auth - `links [Object]`: Pre-filtered link objects based on user auth
- `direction String?`: The direcion of the drop-down menue [default: down] - `direction String?`: The direcion of the drop-down menue [default: down]
--> -->
<script> <script>
import { import {
+7 -7
View File
@@ -1,12 +1,12 @@
<!-- <!--
@component Navbar component; renders in app resource links and user dropdown @component Navbar component; renders in app resource links and user dropdown
Properties: Properties:
- `username String!`: Empty string if auth. is disabled, otherwise the username as string - `username String!`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level - `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles - `roles [Number]`: Enum containing available roles
- `screenSize Number`: The current window size, will trigger different render variants - `screenSize Number`: The current window size, will trigger different render variants
--> -->
<script> <script>
import { import {
+6 -6
View File
@@ -1,11 +1,11 @@
<!-- <!--
@component Energy Summary component; Displays job energy information. @component Energy Summary component; Displays job energy information.
Properties: Properties:
- `jobId Number`: The job id - `jobId Number`: The job id
- `jobEnergy Number?`: The total job energy [Default: null] - `jobEnergy Number?`: The total job energy [Default: null]
- `jobEnergyFootprint [Object]?`: The partial job energy contributions [Default: null] - `jobEnergyFootprint [Object]?`: The partial job energy contributions [Default: null]
--> -->
<script> <script>
import { import {
+5 -5
View File
@@ -1,10 +1,10 @@
<!-- <!--
@component Job View Roofline component; Queries data for and renders roofline plot. @component Job View Roofline component; Queries data for and renders roofline plot.
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
- `clusters Array`: The GQL clusters array - `clusters Array`: The GQL clusters array
--> -->
<script> <script>
import { import {
+7 -7
View File
@@ -1,11 +1,11 @@
<!-- <!--
@component Job Summary component; Displays aggregated job footprint statistics and performance indicators @component Job Summary component; Displays aggregated job footprint statistics and performance indicators
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
- `width String?`: Width of the card [Default: 'auto'] - `width String?`: Width of the card [Default: 'auto']
- `height String?`: Height of the card [Default: '310px'] - `height String?`: Height of the card [Default: '400px']
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@@ -17,7 +17,6 @@
import JobFootprintBars from "./jobsummary/JobFootprintBars.svelte"; import JobFootprintBars from "./jobsummary/JobFootprintBars.svelte";
import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte"; import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
job, job,
@@ -25,6 +24,7 @@
height = "400px", height = "400px",
} = $props(); } = $props();
/* Const Init */
const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`]; const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`];
</script> </script>
+9 -10
View File
@@ -1,15 +1,14 @@
<!-- <!--
@component Metric plot wrapper with user scope selection; used in job detail view @component Metric plot wrapper with user scope selection; used in job detail view
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
- `metricName String`: The metrics name - `metricName String`: The metrics name
- `metricUnit Object`: The metrics GQL unit object - `metricUnit Object`: The metrics GQL unit object
- `nativeScope String`: The metrics native scope - `nativeScope String`: The metrics native scope
- `scopes [String]`: The scopes returned for this metric - `presetScopes [String]`: The preset scopes returned for this metric
- `rawData [Object]`: Metric data for all scopes returned for this metric - `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly in downstream plots [Default: false]
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly in downstream plots [Default: false] -->
-->
<script> <script>
import { import {
+6 -6
View File
@@ -1,11 +1,11 @@
<!-- <!--
@component Job-View subcomponent; Wraps the statsTable in a TabPane and contains GQL query for scoped statsData @component Job-View subcomponent; Wraps the statsTable in a TabPane and contains GQL query for scoped statsData
Properties: Properties:
- `job Object`: The job object - `job Object`: The job object
- `clusters Object`: The clusters object - `clusters Object`: The clusters object
- `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation - `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation
--> -->
<script> <script>
import { import {
@@ -1,215 +1,215 @@
<!-- <!--
@component Job Footprint Bar component; Displays job footprint db data as bars relative to thresholds. Displays quality indicators and tooltips. @component Job Footprint Bar component; Displays job footprint db data as bars relative to thresholds. Displays quality indicators and tooltips.
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { import {
CardBody, CardBody,
Progress, Progress,
Icon, Icon,
Tooltip, Tooltip,
Row, Row,
Col Col
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { findJobFootprintThresholds } from "../../generic/utils.js"; import { findJobFootprintThresholds } from "../../generic/utils.js";
/* Svelte 5 Props */ /* Svelte 5 Props */
let {job} = $props(); let {
job
/* Derived */ } = $props();
// Prepare Job Footprint Data Based On Values Saved In Database
const jobFootprintData = $derived(buildFootprint(job?.footprint)); /* Derived */
// Prepare Job Footprint Data Based On Values Saved In Database
/* Functions */ const jobFootprintData = $derived(buildFootprint(job?.footprint));
function buildFootprint(input) {
let result = input?.map((jf) => { /* Functions */
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name); function buildFootprint(input) {
if (fmc) { let result = input?.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 : "") if (fmc) {
// Unit
// Threshold / -Differences const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
// Threshold / -Differences
// Define basic data -> Value: Use as Provided const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
const fmBase = {
name: jf.name, // Define basic data -> Value: Use as Provided
stat: jf.stat, const fmBase = {
value: jf.value, name: jf.name,
unit: unit, stat: jf.stat,
peak: fmt.peak, value: jf.value,
dir: fmc.lowerIsBetter unit: unit,
}; peak: fmt.peak,
dir: fmc.lowerIsBetter
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) { };
return {
...fmBase, if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
color: "danger",
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
return {
...fmBase,
color: "warning",
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message:
"Footprint value above expected normal threshold: Check for artifacts recommended.",
impact: 0,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
};
}
} else { // No matching metric config: display as single value
return { return {
name: jf.name, ...fmBase,
stat: jf.stat, color: "danger",
value: jf.value, message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
return {
...fmBase,
color: "warning",
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message: message:
`No config for metric ${jf.name} found.`, "Footprint value above expected normal threshold: Check for artifacts recommended.",
impact: 4, impact: 0,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
}; };
} }
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically } else { // No matching metric config: display as single value
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)); return {
});; name: jf.name,
stat: jf.stat,
value: jf.value,
message:
`No config for metric ${jf.name} found.`,
impact: 4,
};
}
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
});;
return result return result
}; };
function evalFootprint(value, thresholds, lowerIsBetter, level) { function evalFootprint(value, thresholds, lowerIsBetter, level) {
// Handle Metrics in which less value is better // Handle Metrics in which less value is better
switch (level) { switch (level) {
case "peak": case "peak":
if (lowerIsBetter) if (lowerIsBetter)
return false; // metric over peak -> return false to trigger impact -1 return false; // metric over peak -> return false to trigger impact -1
else return value <= thresholds.peak && value > thresholds.normal; else return value <= thresholds.peak && value > thresholds.normal;
case "alert": case "alert":
if (lowerIsBetter) if (lowerIsBetter)
return value <= thresholds.peak && value >= thresholds.alert; return value <= thresholds.peak && value >= thresholds.alert;
else return value <= thresholds.alert && value >= 0; else return value <= thresholds.alert && value >= 0;
case "caution": case "caution":
if (lowerIsBetter) if (lowerIsBetter)
return value < thresholds.alert && value >= thresholds.caution; return value < thresholds.alert && value >= thresholds.caution;
else return value <= thresholds.caution && value > thresholds.alert; else return value <= thresholds.caution && value > thresholds.alert;
case "normal": case "normal":
if (lowerIsBetter) if (lowerIsBetter)
return value < thresholds.caution && value >= 0; return value < thresholds.caution && value >= 0;
else return value <= thresholds.normal && value > thresholds.caution; else return value <= thresholds.normal && value > thresholds.caution;
default: default:
return false; return false;
}
} }
}
</script> </script>
<CardBody> <CardBody>
{#if jobFootprintData.length === 0} {#if jobFootprintData.length === 0}
<div class="text-center">No footprint data for job available.</div> <div class="text-center">No footprint data for job available.</div>
{:else} {:else}
{#each jobFootprintData as fpd, index} {#each jobFootprintData as fpd, index}
{#if fpd.impact !== 4} {#if fpd.impact !== 4}
<div class="mb-1 d-flex justify-content-between"> <div class="mb-1 d-flex justify-content-between">
<div>&nbsp;<b>{fpd.name} ({fpd.stat})</b></div> <div>&nbsp;<b>{fpd.name} ({fpd.stat})</b></div>
<div <div
class="cursor-help d-inline-flex" class="cursor-help d-inline-flex"
id={`footprint-${job.jobId}-${index}`} id={`footprint-${job.jobId}-${index}`}
> >
<div class="mx-1"> <div class="mx-1">
{#if fpd.impact === 3} {#if fpd.impact === 3}
<Icon name="exclamation-triangle-fill" class="text-danger" /> <Icon name="exclamation-triangle-fill" class="text-danger" />
{:else if fpd.impact === 2} {:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning" /> <Icon name="exclamation-triangle" class="text-warning" />
{:else if fpd.impact === 0} {:else if fpd.impact === 0}
<Icon name="info-circle" class="text-info" /> <Icon name="info-circle" class="text-info" />
{:else if fpd.impact === -1} {:else if fpd.impact === -1}
<Icon name="info-circle-fill" class="text-danger" /> <Icon name="info-circle-fill" class="text-danger" />
{/if} {/if}
{#if fpd.impact === 3} {#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger" /> <Icon name="emoji-frown" class="text-danger" />
{:else if fpd.impact === 2} {:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning" /> <Icon name="emoji-neutral" class="text-warning" />
{:else if fpd.impact === 1} {:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success" /> <Icon name="emoji-smile" class="text-success" />
{:else if fpd.impact === 0} {:else if fpd.impact === 0}
<Icon name="emoji-smile" class="text-info" /> <Icon name="emoji-smile" class="text-info" />
{:else if fpd.impact === -1} {:else if fpd.impact === -1}
<Icon name="emoji-dizzy" class="text-danger" /> <Icon name="emoji-dizzy" class="text-danger" />
{/if} {/if}
</div> </div>
<div> <div>
{fpd.value} / {fpd.peak} {fpd.value} / {fpd.peak}
{fpd.unit} &nbsp; {fpd.unit} &nbsp;
</div> </div>
</div> </div>
<Tooltip <Tooltip
target={`footprint-${job.jobId}-${index}`} target={`footprint-${job.jobId}-${index}`}
placement="right" placement="right"
>{fpd.message}</Tooltip >{fpd.message}</Tooltip>
> </div>
</div> <Row cols={12} class={(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}>
<Row cols={12} class={(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}> {#if fpd.dir}
{#if fpd.dir} <Col xs="1">
<Col xs="1"> <Icon name="caret-left-fill" />
<Icon name="caret-left-fill" /> </Col>
</Col> {/if}
{/if} <Col xs="11" class="align-content-center">
<Col xs="11" class="align-content-center"> <Progress value={fpd.value} max={fpd.peak} color={fpd.color} />
<Progress value={fpd.value} max={fpd.peak} color={fpd.color} /> </Col>
</Col> {#if !fpd.dir}
{#if !fpd.dir} <Col xs="1">
<Col xs="1"> <Icon name="caret-right-fill" />
<Icon name="caret-right-fill" /> </Col>
</Col> {/if}
{/if} </Row>
</Row> {:else}
{:else} <div class="mb-1 d-flex justify-content-between">
<div class="mb-1 d-flex justify-content-between"> <div>
<div> &nbsp;<b>{fpd.name} ({fpd.stat})</b>
&nbsp;<b>{fpd.name} ({fpd.stat})</b> </div>
</div> <div
<div class="cursor-help d-inline-flex"
class="cursor-help d-inline-flex" id={`footprint-${job.jobId}-${index}`}
id={`footprint-${job.jobId}-${index}`} >
> <div class="mx-1">
<div class="mx-1"> <Icon name="info-circle"/>
<Icon name="info-circle"/> </div>
</div> <div>
<div> {fpd.value}&nbsp;
{fpd.value}&nbsp; </div>
</div> </div>
</div> </div>
</div> <Tooltip
<Tooltip target={`footprint-${job.jobId}-${index}`}
target={`footprint-${job.jobId}-${index}`} placement="right"
placement="right" >{fpd.message}</Tooltip>
>{fpd.message}</Tooltip {/if}
> {/each}
{/if} {/if}
{/each}
{/if}
</CardBody> </CardBody>
<style> <style>
@@ -1,64 +1,66 @@
<!-- <!--
@component Job Footprint Polar Plot component; Displays queried job metric statistics polar plot. @component Job Footprint Polar Plot component; Displays queried job metric statistics polar plot.
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { import {
queryStore, queryStore,
gql, gql,
getContextClient getContextClient
} from "@urql/svelte"; } from "@urql/svelte";
import { import {
Card, Card,
CardBody, CardBody,
Spinner Spinner
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { findJobFootprintThresholds } from "../../generic/utils.js"; import { findJobFootprintThresholds } from "../../generic/utils.js";
import Polar from "../../generic/plots/Polar.svelte"; import Polar from "../../generic/plots/Polar.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { job } = $props(); let {
job
/* Const Init */ } = $props();
// Metric Names Configured To Be Footprints For (sub)Cluster
const clusterFootprintMetrics = getContext("clusters") /* Const Init */
// Metric Names Configured To Be Footprints For (sub)Cluster
const clusterFootprintMetrics = getContext("clusters")
.find((c) => c.name == job.cluster)?.subClusters .find((c) => c.name == job.cluster)?.subClusters
.find((sc) => sc.name == job.subCluster)?.footprint || [] .find((sc) => sc.name == job.subCluster)?.footprint || []
// Get Scaled Peak Threshold Based on Footprint Type ([min, max, avg]) and Job Exclusivity // Get Scaled Peak Threshold Based on Footprint Type ([min, max, avg]) and Job Exclusivity
const polarMetrics = getContext("globalMetrics").reduce((pms, gm) => { const polarMetrics = getContext("globalMetrics").reduce((pms, gm) => {
if (clusterFootprintMetrics.includes(gm.name)) { if (clusterFootprintMetrics.includes(gm.name)) {
const fmt = findJobFootprintThresholds(job, gm.footprint, getContext("getMetricConfig")(job.cluster, job.subCluster, gm.name)); const fmt = findJobFootprintThresholds(job, gm.footprint, getContext("getMetricConfig")(job.cluster, job.subCluster, gm.name));
pms.push({ name: gm.name, peak: fmt ? fmt.peak : null }); pms.push({ name: gm.name, peak: fmt ? fmt.peak : null });
} }
return pms; return pms;
}, []) }, [])
// Pull All Series For Footprint Metrics Statistics Only On Node Scope // Pull All Series For Footprint Metrics Statistics Only On Node Scope
const client = getContextClient(); const client = getContextClient();
const polarQuery = gql` const polarQuery = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!) { query ($dbid: ID!, $selectedMetrics: [String!]!) {
jobStats(id: $dbid, metrics: $selectedMetrics) { jobStats(id: $dbid, metrics: $selectedMetrics) {
name name
data { data {
min min
avg avg
max max
}
} }
} }
`; }
`;
/* Derived */ /* Derived */
const polarData = $derived(queryStore({ const polarData = $derived(queryStore({
client: client, client: client,
query: polarQuery, query: polarQuery,
variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics }, variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics },
})); }));
</script> </script>
<CardBody> <CardBody>
@@ -1,11 +1,11 @@
<!--: <!--:
@component Job-View subcomponent; display table of metric data statistics with selectable scopes @component Job-View subcomponent; display table of metric data statistics with selectable scopes
Properties: Properties:
- `hosts [String]`: The list of hostnames of this job - `hosts [String]`: The list of hostnames of this job
- `jobStats Object`: The data object - `jobStats Object`: The data object
- `selectedMetrics [String]`: The selected metrics - `selectedMetrics [String]`: The selected metrics
--> -->
<script> <script>
import { import {
@@ -123,7 +123,6 @@
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat]; return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
}); });
} }
</script> </script>
<Table class="mb-0"> <Table class="mb-0">
@@ -1,10 +1,10 @@
<!-- <!--
@component Job-View subcomponent; Single Statistics entry component for statstable @component Job-View subcomponent; Single Statistics entry component for statstable
Properties: Properties:
- `data [Object]`: The jobs statsdata for host-metric-scope - `data [Object]`: The jobs statsdata for host-metric-scope
- `scope String`: The selected scope - `scope String`: The selected scope
--> -->
<script> <script>
import { Icon } from "@sveltestrap/sveltestrap"; import { Icon } from "@sveltestrap/sveltestrap";
+12 -8
View File
@@ -1,13 +1,17 @@
<!-- <!--
@component Cluster Per Node List component; renders current state of SELECTABLE metrics for ALL nodes @component Cluster Per Node List component; renders current state of SELECTABLE metrics for ALL nodes
Properties: Properties:
- `cluster String`: The nodes' cluster - `cluster String`: The nodes' cluster
- `subCluster String`: The nodes' subCluster - `subCluster String`: The nodes' subCluster [Default: ""]
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null] - `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
- `selectedMetrics [String]`: The array of selected metrics - `selectedMetrics [String]`: The array of selected metrics [Default []]
- `systemUnits Object`: The object of metric units - `selectedResolution Number?`: The selected data resolution [Default: 0]
--> - `hostnameFilter String?`: The active hostnamefilter [Default: ""]
- `presetSystemUnits Object`: The object of metric units [Default: null]
- `from Date?`: The selected "from" date [Default: null]
- `to Date?`: The selected "to" date [Default: null]
-->
<script> <script>
import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte"; import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte";
+9 -6
View File
@@ -1,11 +1,14 @@
<!-- <!--
@component Cluster Per Node Overview component; renders current state of ONE metric for ALL nodes @component Cluster Per Node Overview component; renders current state of ONE metric for ALL nodes
Properties: Properties:
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null] - `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
- `cluster String`: The cluster to show status information for - `cluster String`: The cluster to show status information for
- `selectedMetric String?`: The selectedMetric input [Default: ""] - `selectedMetric String?`: The selectedMetric input [Default: ""]
--> - `hostnameFilter String?`: The active hostnamefilter [Default: ""]
- `from Date?`: The selected "from" date [Default: null]
- `to Date?`: The selected "to" date [Default: null]
-->
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
@@ -1,11 +1,13 @@
<!-- <!--
@component Displays node info, serves links to single node page and lists @component Displays node info, serves links to single node page and lists
Properties: Properties:
- `cluster String`: The nodes' cluster - `cluster String`: The nodes' cluster
- `subCluster String`: The nodes' subCluster - `subCluster String`: The nodes' subCluster
- `cluster String`: The nodes' hostname - `hostname String`: The nodes' hostname
--> - `dataHealth [Bool]`: Array of Booleans depicting state of returned data per metric
- `nodeJobsData [Object]`: Data returned by GQL for jobs runninig on this node [Default: null]
-->
<script> <script>
import { import {
@@ -1,11 +1,11 @@
<!-- <!--
@component Data row for a single node displaying metric plots @component Data row for a single node displaying metric plots
Properties: Properties:
- `cluster String`: The nodes' cluster - `cluster String`: The nodes' cluster
- `nodeData Object`: The node data object including metric data - `nodeData Object`: The node data object including metric data
- `selectedMetrics [String]`: The array of selected metrics - `selectedMetrics [String]`: The array of selected metrics
--> -->
<script> <script>
import { import {