mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-12-16 20:26:16 +01:00
288 lines
9.0 KiB
Svelte
288 lines
9.0 KiB
Svelte
<!--
|
|
@component Main cluster node status view component; renders overview or list depending on type
|
|
|
|
Properties:
|
|
- `displayType String?`: The type of node display ['OVERVIEW' || 'LIST']
|
|
- `cluster String`: The cluster to show status information for [Default: null]
|
|
- `subCluster String`: The subCluster to show status information for [Default: null]
|
|
- `presetFrom Date?`: Custom Time Range selection 'from' [Default: null]
|
|
- `presetTo Date?`: Custom Time Range selection 'to' [Default: null]
|
|
-->
|
|
|
|
<script>
|
|
import { getContext } from "svelte";
|
|
import {
|
|
Row,
|
|
Col,
|
|
Card,
|
|
Input,
|
|
InputGroup,
|
|
InputGroupText,
|
|
Icon,
|
|
Button,
|
|
} from "@sveltestrap/sveltestrap";
|
|
import {
|
|
gql,
|
|
getContextClient,
|
|
mutationStore,
|
|
} from "@urql/svelte";
|
|
|
|
import { init } from "./generic/utils.js";
|
|
import NodeOverview from "./systems/NodeOverview.svelte";
|
|
import NodeList from "./systems/NodeList.svelte";
|
|
import MetricSelection from "./generic/select/MetricSelection.svelte";
|
|
import TimeSelection from "./generic/select/TimeSelection.svelte";
|
|
import Refresher from "./generic/helper/Refresher.svelte";
|
|
|
|
/*
|
|
Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
|
|
Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
|
|
*/
|
|
|
|
/* Scelte 5 Props */
|
|
let {
|
|
displayType,
|
|
cluster = null,
|
|
subCluster = null,
|
|
presetFrom = null,
|
|
presetTo = null,
|
|
} = $props();
|
|
|
|
/* Const Init */
|
|
const { query: initq } = init();
|
|
const client = getContextClient();
|
|
const displayNodeOverview = (displayType === 'OVERVIEW');
|
|
const ccconfig = getContext("cc-config");
|
|
const initialized = getContext("initialized");
|
|
const globalMetrics = getContext("globalMetrics");
|
|
const resampleConfig = getContext("resampling") || null;
|
|
|
|
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
|
|
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
|
const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
|
|
const nowDate = new Date(Date.now());
|
|
|
|
/* Var Init */
|
|
let timeoutId = null;
|
|
|
|
/* State Init */
|
|
let to = $state(presetTo || new Date(Date.now()));
|
|
let from = $state(presetFrom || new Date(nowDate.setHours(nowDate.getHours() - 4)));
|
|
let selectedResolution = $state(resampleConfig ? resampleDefault : 0);
|
|
let hostnameFilter = $state("");
|
|
let hoststateFilter = $state("all");
|
|
let pendingHostnameFilter = $state("");
|
|
let isMetricsSelectionOpen = $state(false);
|
|
|
|
/* Derived States */
|
|
const systemMetrics = $derived($initialized ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
|
|
const presetSystemUnits = $derived(loadUnits(systemMetrics));
|
|
let selectedMetric = $derived.by(() => {
|
|
let configKey = `nodeOverview_selectedMetric`;
|
|
if (cluster) configKey += `:${cluster}`;
|
|
if (subCluster) configKey += `:${subCluster}`;
|
|
|
|
if ($initialized) {
|
|
if (ccconfig[configKey]) return ccconfig[configKey]
|
|
else if (systemMetrics.length !== 0) return systemMetrics[0].name
|
|
}
|
|
return ""
|
|
});
|
|
let selectedMetrics = $derived.by(() => {
|
|
let configKey = `nodeList_selectedMetrics`;
|
|
if (cluster) configKey += `:${cluster}`;
|
|
if (subCluster) configKey += `:${subCluster}`;
|
|
|
|
if ($initialized) {
|
|
if (ccconfig[configKey]) return ccconfig[configKey]
|
|
else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
|
|
}
|
|
return []
|
|
});
|
|
|
|
/* Effects */
|
|
$effect(() => {
|
|
if (displayNodeOverview) {
|
|
updateOverviewMetric(selectedMetric)
|
|
}
|
|
});
|
|
|
|
/* Functions */
|
|
function loadUnits(systemMetrics) {
|
|
let pendingUnits = {};
|
|
if (systemMetrics.length > 0) {
|
|
for (let sm of systemMetrics) {
|
|
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
|
};
|
|
};
|
|
return {...pendingUnits};
|
|
};
|
|
|
|
// Wait after input for some time to prevent too many requests
|
|
function updateHostnameFilter() {
|
|
if (timeoutId != null) clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(function () {
|
|
hostnameFilter = pendingHostnameFilter;
|
|
}, 500);
|
|
};
|
|
|
|
function updateOverviewMetric(newMetric) {
|
|
let configKey = `nodeOverview_selectedMetric`;
|
|
if (cluster) configKey += `:${cluster}`;
|
|
if (subCluster) configKey += `:${subCluster}`;
|
|
|
|
updateConfigurationMutation({
|
|
name: configKey,
|
|
value: JSON.stringify(newMetric),
|
|
}).subscribe((res) => {
|
|
if (res.fetching === false && res.error) {
|
|
throw res.error;
|
|
}
|
|
});
|
|
};
|
|
|
|
const updateConfigurationMutation = ({ name, value }) => {
|
|
return mutationStore({
|
|
client: client,
|
|
query: gql`
|
|
mutation ($name: String!, $value: String!) {
|
|
updateConfiguration(name: $name, value: $value)
|
|
}
|
|
`,
|
|
variables: { name, value },
|
|
});
|
|
};
|
|
|
|
</script>
|
|
|
|
<!-- ROW1: Tools-->
|
|
<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">
|
|
{#if $initq?.data}
|
|
<!-- List Metric Select Col-->
|
|
{#if !displayNodeOverview}
|
|
<Col>
|
|
<InputGroup>
|
|
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
|
<InputGroupText class="text-capitalize">Metrics</InputGroupText>
|
|
<Button
|
|
outline
|
|
color="primary"
|
|
onclick={() => (isMetricsSelectionOpen = true)}
|
|
>
|
|
{selectedMetrics.length} selected
|
|
</Button>
|
|
</InputGroup>
|
|
</Col>
|
|
{#if resampleConfig}
|
|
<Col>
|
|
<InputGroup>
|
|
<InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
|
|
<InputGroupText>Resolution</InputGroupText>
|
|
<Input type="select" bind:value={selectedResolution}>
|
|
{#each resampleResolutions as res}
|
|
<option value={res}
|
|
>{res} sec</option
|
|
>
|
|
{/each}
|
|
</Input>
|
|
</InputGroup>
|
|
</Col>
|
|
{/if}
|
|
{/if}
|
|
<!-- Node Col-->
|
|
<Col class="mt-2 mt-lg-0">
|
|
<InputGroup>
|
|
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
|
<InputGroupText>Node(s)</InputGroupText>
|
|
<Input
|
|
placeholder="Filter hostname ..."
|
|
type="text"
|
|
bind:value={pendingHostnameFilter}
|
|
oninput={updateHostnameFilter}
|
|
/>
|
|
</InputGroup>
|
|
</Col>
|
|
<!-- State Col-->
|
|
<Col class="mt-2 mt-lg-0">
|
|
<InputGroup>
|
|
<InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
|
|
<InputGroupText>State</InputGroupText>
|
|
<Input type="select" bind:value={hoststateFilter}>
|
|
{#each stateOptions as so}
|
|
<option value={so}>{so.charAt(0).toUpperCase() + so.slice(1)}</option>
|
|
{/each}
|
|
</Input>
|
|
</InputGroup>
|
|
</Col>
|
|
<!-- Range Col-->
|
|
<Col>
|
|
<TimeSelection
|
|
presetFrom={from}
|
|
presetTo={to}
|
|
applyTime={(newFrom, newTo) => {
|
|
from = newFrom;
|
|
to = newTo;
|
|
}}
|
|
/>
|
|
</Col>
|
|
<!-- Overview Metric Col-->
|
|
{#if displayNodeOverview}
|
|
<Col class="mt-2 mt-lg-0">
|
|
<InputGroup>
|
|
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
|
<InputGroupText>Metric</InputGroupText>
|
|
<Input type="select" bind:value={selectedMetric}>
|
|
{#each systemMetrics as metric (metric.name)}
|
|
<option value={metric.name}
|
|
>{metric.name} {presetSystemUnits[metric.name] ? "("+presetSystemUnits[metric.name]+")" : ""}</option
|
|
>
|
|
{:else}
|
|
<option disabled>No available options</option>
|
|
{/each}
|
|
</Input>
|
|
</InputGroup>
|
|
</Col>
|
|
{/if}
|
|
{/if}
|
|
<!-- Refresh Col-->
|
|
<Col class="mt-2 mt-lg-0">
|
|
<Refresher
|
|
onRefresh={() => {
|
|
const diff = Date.now() - to;
|
|
from = new Date(from.getTime() + diff);
|
|
to = new Date(to.getTime() + diff);
|
|
}}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
<!-- ROW2: Content-->
|
|
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
|
|
<Row>
|
|
<Col>
|
|
<Card body color="danger">Unknown displayList type! </Card>
|
|
</Col>
|
|
</Row>
|
|
{:else}
|
|
{#if displayNodeOverview}
|
|
<!-- ROW2-1: Node Overview (Grid Included)-->
|
|
<NodeOverview {cluster} {ccconfig} {selectedMetric} {from} {to} {hostnameFilter} {hoststateFilter}/>
|
|
{:else}
|
|
<!-- ROW2-2: Node List (Grid Included)-->
|
|
<NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {selectedResolution} {hostnameFilter} {hoststateFilter} {from} {to} {presetSystemUnits}/>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if !displayNodeOverview}
|
|
<MetricSelection
|
|
bind:isOpen={isMetricsSelectionOpen}
|
|
presetMetrics={selectedMetrics}
|
|
{cluster}
|
|
{subCluster}
|
|
configName="nodeList_selectedMetrics"
|
|
applyMetrics={(newMetrics) =>
|
|
selectedMetrics = [...newMetrics]
|
|
}
|
|
/>
|
|
{/if}
|