simplify plotGrid, add cancel to metricSelect, improve metricPlot render logic

This commit is contained in:
Christoph Kluge 2024-10-16 12:41:15 +02:00
parent 673fdc443c
commit 3dfeabcec6
13 changed files with 90 additions and 154 deletions

View File

@ -519,7 +519,6 @@
<Col> <Col>
<PlotGrid <PlotGrid
let:item let:item
renderFor="analysis"
items={metricsInHistograms.map((metric) => ({ items={metricsInHistograms.map((metric) => ({
metric, metric,
...binsFromFootprint( ...binsFromFootprint(
@ -563,7 +562,6 @@
<PlotGrid <PlotGrid
let:item let:item
let:width let:width
renderFor="analysis"
items={metricsInScatterplots.map(([m1, m2]) => ({ items={metricsInScatterplots.map(([m1, m2]) => ({
m1, m1,
f1: $footprintsQuery.data.footprints.metrics.find( f1: $footprintsQuery.data.footprints.metrics.find(

View File

@ -351,7 +351,6 @@
{:else if $initq?.data && $jobMetrics?.data?.jobMetrics} {:else if $initq?.data && $jobMetrics?.data?.jobMetrics}
<PlotGrid <PlotGrid
let:item let:item
renderFor="job"
items={orderAndMap( items={orderAndMap(
groupByScope($jobMetrics.data.jobMetrics), groupByScope($jobMetrics.data.jobMetrics),
selectedMetrics, selectedMetrics,

View File

@ -191,7 +191,6 @@
{:else} {:else}
<PlotGrid <PlotGrid
let:item let:item
renderFor="node"
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodeMetricsData.data.nodeMetrics[0].metrics items={$nodeMetricsData.data.nodeMetrics[0].metrics
.map((m) => ({ .map((m) => ({

View File

@ -645,7 +645,6 @@
{#key $mainQuery.data.stats[0].histMetrics} {#key $mainQuery.data.stats[0].histMetrics}
<PlotGrid <PlotGrid
let:item let:item
renderFor="user"
items={$mainQuery.data.stats[0].histMetrics} items={$mainQuery.data.stats[0].histMetrics}
itemsPerRow={2} itemsPerRow={2}
> >

View File

@ -45,7 +45,7 @@
if (from == null || to == null) { if (from == null || to == null) {
to = new Date(Date.now()); to = new Date(Date.now());
from = new Date(to.getTime()); from = new Date(to.getTime());
from.setHours(from.getHours() - 4); from.setHours(from.getHours() - 2);
} }
const initialized = getContext("initialized"); const initialized = getContext("initialized");
@ -61,6 +61,7 @@
// Todo: Add Idle State Filter (== No allocated Jobs) // Todo: Add Idle State Filter (== No allocated Jobs)
// Todo: NodeList: Mindestens Accelerator Scope ... "Show Detail" Switch? // Todo: NodeList: Mindestens Accelerator Scope ... "Show Detail" Switch?
// Todo: Review performance // observed high client-side load frequency // Todo: Review performance // observed high client-side load frequency
// Is Svelte {#each} -> <MetricPlot/> -> onMount() related : Cannot be skipped ...
const client = getContextClient(); const client = getContextClient();
const nodeQuery = gql` const nodeQuery = gql`
@ -245,13 +246,13 @@
<Spinner /> <Spinner />
</Col> </Col>
</Row> </Row>
{:else if $initialized && $nodesQuery?.data} {:else if filteredData?.length > 0}
{#if displayNodeOverview} {#if displayNodeOverview}
<!-- ROW2-1: Node Overview (Grid Included)--> <!-- ROW2-1: Node Overview (Grid Included)-->
<NodeOverview {cluster} {ccconfig} data={filteredData} bind:selectedMetric/> <NodeOverview {cluster} {ccconfig} data={filteredData}/>
{:else} {:else}
<!-- ROW2-2: Node List (Grid Included)--> <!-- ROW2-2: Node List (Grid Included)-->
<NodeList {cluster} {selectedMetrics} {systemUnits} data={filteredData}/> <NodeList {cluster} {selectedMetrics} {systemUnits} data={filteredData} bind:selectedMetric/>
{/if} {/if}
{/if} {/if}

View File

@ -267,7 +267,6 @@
{#key $stats.data.jobsStatistics[0].histMetrics} {#key $stats.data.jobsStatistics[0].histMetrics}
<PlotGrid <PlotGrid
let:item let:item
renderFor="user"
items={$stats.data.jobsStatistics[0].histMetrics} items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3} itemsPerRow={3}
> >

View File

@ -4,7 +4,6 @@
Properties: Properties:
- `itemsPerRow Number`: Elements to render per row - `itemsPerRow Number`: Elements to render per row
- `items [Any]`: List of plot components to render - `items [Any]`: List of plot components to render
- `renderFor String`: If 'job', filter disabled metrics
--> -->
<script> <script>
@ -15,43 +14,13 @@
export let itemsPerRow export let itemsPerRow
export let items export let items
export let renderFor
let rows = [];
const isPlaceholder = x => x._is_placeholder === true;
function tile(items, itemsPerRow) {
const rows = []
for (let ri = 0; ri < items?.length; ri += itemsPerRow) {
const row = []
for (let ci = 0; ci < itemsPerRow; ci += 1) {
if (ri + ci < items?.length)
row.push(items[ri + ci])
else
row.push({ _is_placeholder: true, ri, ci })
}
rows.push(row)
}
return rows
}
$: if (renderFor === 'job') {
rows = tile(items.filter(item => item.disabled === false), itemsPerRow)
} else {
rows = tile(items, itemsPerRow)
}
</script> </script>
{#each rows as row} <Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}>
<Row cols={{ xs: 1, sm: 1, md: 2, lg: itemsPerRow}}> {#each items as item}
{#each row as item (item)}
<Col class="px-1"> <Col class="px-1">
{#if !isPlaceholder(item)} <slot {item}/>
<slot item={item}/>
{/if}
</Col> </Col>
{/each} {/each}
</Row> </Row>
{/each}

View File

@ -11,7 +11,7 @@
- `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: null] - `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: null]
- `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 GraphQL.Cluster`: Cluster Object of the parent job - `cluster String`: Cluster name of the parent job / data
- `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 ren[data, err := metricdata.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)](https://github.com/ClusterCockpit/cc-backend/blob/9fe7cdca9215220a19930779a60c8afc910276a3/internal/graph/schema.resolvers.go#L391-L392)der x-axis as negative time with $now as maximum [Default: false] - `forNode Bool?`: If this plot is used for node data display; will ren[data, err := metricdata.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)](https://github.com/ClusterCockpit/cc-backend/blob/9fe7cdca9215220a19930779a60c8afc910276a3/internal/graph/schema.resolvers.go#L391-L392)der x-axis as negative time with $now as maximum [Default: false]
@ -117,13 +117,13 @@
export let metric; export let metric;
export let scope = "node"; export let scope = "node";
export let width = null; export let width = 0;
export let height = 300; export let height = 300;
export let timestep; export let timestep;
export let series; export let series;
export let useStatsSeries = null; export let useStatsSeries = null;
export let statisticsSeries = null; export let statisticsSeries = null;
export let cluster; export let cluster = "";
export let subCluster; export let subCluster;
export let isShared = false; export let isShared = false;
export let forNode = false; export let forNode = false;
@ -522,17 +522,9 @@
} }
onMount(() => { onMount(() => {
// Setup Wrapper if (plotWrapper) {
if (series[0].data.length > 0) {
if (forNode) {
plotWrapper.style.paddingTop = "0.5rem"
plotWrapper.style.paddingBottom = "0.5rem"
}
plotWrapper.style.backgroundColor = backgroundColor();
plotWrapper.style.borderRadius = "5px";
}
// Init Plot
render(width, height); render(width, height);
}
}); });
onDestroy(() => { onDestroy(() => {
@ -540,22 +532,20 @@
if (uplot) uplot.destroy(); if (uplot) uplot.destroy();
}); });
// This updates it on all size changes // This updates plot on all size changes if wrapper (== data) exists
// Condition for reactive triggering (eg scope change) $: if (plotWrapper) {
$: if (series[0].data.length > 0) {
onSizeChange(width, height); onSizeChange(width, height);
} }
</script> </script>
<!-- Define Wrapper and NoData Card within $width --> <!-- Define $width Wrapper and NoData Card -->
<div bind:clientWidth={width}>
{#if series[0].data.length > 0} {#if series[0].data.length > 0}
<div bind:this={plotWrapper}/> <div bind:this={plotWrapper} bind:clientWidth={width}
style="background-color: {backgroundColor()};" class={forNode ? 'py-2 rounded' : 'rounded'}
/>
{:else} {:else}
<Card class="mx-4" body color="warning" <Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{metric}</code></Card >Cannot render plot: No series data returned for <code>{metric}</code></Card
> >
{/if} {/if}
</div>

View File

@ -178,6 +178,7 @@
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button> <Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Cancel</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -36,7 +36,7 @@
<th <th
class="position-sticky top-0 text-capitalize" class="position-sticky top-0 text-capitalize"
scope="col" scope="col"
style="padding-top: {headerPaddingTop}px" style="padding-top: {headerPaddingTop}px;"
> >
{cluster} Node Info {cluster} Node Info
</th> </th>
@ -53,7 +53,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each data as nodeData (nodeData)} {#each data as nodeData (nodeData.host)}
<NodeListRow {nodeData} {cluster} {selectedMetrics}/> <NodeListRow {nodeData} {cluster} {selectedMetrics}/>
{:else} {:else}
<tr> <tr>

View File

@ -9,9 +9,7 @@
--> -->
<script> <script>
import { getContext } from "svelte"; import { Row, Col, Card } from "@sveltestrap/sveltestrap";
import { Card } from "@sveltestrap/sveltestrap";
import PlotGrid from "../generic/PlotGrid.svelte";
import MetricPlot from "../generic/plots/MetricPlot.svelte"; import MetricPlot from "../generic/plots/MetricPlot.svelte";
export let ccconfig = null; export let ccconfig = null;
@ -19,15 +17,12 @@
export let cluster = ""; export let cluster = "";
export let selectedMetric = ""; export let selectedMetric = "";
const clusters = getContext("clusters");
</script> </script>
<PlotGrid <!-- PlotGrid flattened into this component -->
let:item <Row cols={{ xs: 1, sm: 2, md: 3, lg: ccconfig.plot_view_plotsPerRow}}>
renderFor="systems" {#each data as item (item.host)}
itemsPerRow={ccconfig.plot_view_plotsPerRow} <Col class="px-1">
items={data}
>
<h4 style="width: 100%; text-align: center;"> <h4 style="width: 100%; text-align: center;">
<a <a
style="display: block;padding-top: 15px;" style="display: block;padding-top: 15px;"
@ -35,29 +30,23 @@
>{item.host} ({item.subCluster})</a >{item.host} ({item.subCluster})</a
> >
</h4> </h4>
{#if item?.data[0]}
{#if item?.disabled[selectedMetric]} {#if item?.disabled[selectedMetric]}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info" <Card body class="mx-3" color="info"
>Metric disabled for subcluster <code >Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code >{selectedMetric}:{item.subCluster}</code
></Card ></Card
> >
{:else} {:else}
<!-- "No Data"-Warning included in MetricPlot-Component -->
<MetricPlot <MetricPlot
timestep={item.data[0].metric.timestep} timestep={item.data[0].metric.timestep}
series={item.data[0].metric.series} series={item.data[0].metric.series}
metric={item.data[0].name} metric={item.data[0].name}
cluster={clusters.find((c) => c.name == cluster)} {cluster}
subCluster={item.subCluster} subCluster={item.subCluster}
forNode={true} forNode
/> />
{/if} {/if}
{:else} </Col>
<Card {/each}
style="margin-left: 2rem;margin-right: 2rem;" </Row>
body
color="warning"
>No dataset returned for <code>{selectedMetric}</code></Card
>
{/if}
</PlotGrid>

View File

@ -56,7 +56,7 @@
</script> </script>
<Card class="pb-2"> <Card class="pb-3">
<CardHeader class="d-inline-flex justify-content-between align-items-end"> <CardHeader class="d-inline-flex justify-content-between align-items-end">
<div> <div>
<h5 class="mb-0"> <h5 class="mb-0">
@ -106,7 +106,7 @@
<hr class="mt-0 mb-3"/> <hr class="mt-0 mb-3"/>
<p> <p>
{#if $nodeJobsData.data.jobs.count > 0} {#if $nodeJobsData.data.jobs.count > 0}
<InputGroup class="justify-content-between"> <InputGroup size="sm" class="justify-content-between">
<InputGroupText> <InputGroupText>
<Icon name="activity"/> <Icon name="activity"/>
</InputGroupText> </InputGroupText>
@ -116,11 +116,11 @@
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{$nodeJobsData.data.jobs.count} Jobs" disabled /> <Input class="flex-grow-1" style="background-color: white;" type="text" value="{$nodeJobsData.data.jobs.count} Jobs" disabled />
<a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" > <a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
<Icon name="view-list" /> <Icon name="view-list" />
Show List List
</a> </a>
</InputGroup> </InputGroup>
{:else} {:else}
<InputGroup class="justify-content-between"> <InputGroup size="sm" class="justify-content-between">
<InputGroupText> <InputGroupText>
<Icon name="activity" /> <Icon name="activity" />
</InputGroupText> </InputGroupText>
@ -132,30 +132,30 @@
{/if} {/if}
</p> </p>
<p> <p>
<InputGroup class="justify-content-between"> <InputGroup size="sm" class="justify-content-between">
<InputGroupText> <InputGroupText>
<Icon name="people"/> <Icon name="people"/>
</InputGroupText> </InputGroupText>
<InputGroupText class="flex-fill"> <InputGroupText class="flex-fill">
Users on Node Show Users on Node
</InputGroupText> </InputGroupText>
<a title="Show jobs running on this node" href="/monitoring/users/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" > <a title="Show jobs running on this node" href="/monitoring/users/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
<Icon name="view-list" /> <Icon name="view-list" />
Show List List
</a> </a>
</InputGroup> </InputGroup>
</p> </p>
<p> <p>
<InputGroup class="justify-content-between"> <InputGroup size="sm" class="justify-content-between">
<InputGroupText> <InputGroupText>
<Icon name="journals"/> <Icon name="journals"/>
</InputGroupText> </InputGroupText>
<InputGroupText class="flex-fill"> <InputGroupText class="flex-fill">
Projects on Node Show Projects on Node
</InputGroupText> </InputGroupText>
<a title="Show projects active on this node" href="/monitoring/projects/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" > <a title="Show projects active on this node" href="/monitoring/projects/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
<Icon name="view-list" /> <Icon name="view-list" />
Show List List
</a> </a>
</InputGroup> </InputGroup>
</p> </p>

View File

@ -24,33 +24,25 @@
<td> <td>
<NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} /> <NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} />
</td> </td>
{#each sortOrder(nodeData?.data) as metricData} {#each sortOrder(nodeData?.data) as metricData (metricData.name)}
<td> <td>
{#if metricData}
{#if nodeData?.disabled[metricData.name]} {#if nodeData?.disabled[metricData.name]}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info" <Card body class="mx-3" color="info"
>Metric disabled for subcluster <code >Metric disabled for subcluster <code
>{metricData.name}:{nodeData.subCluster}</code >{metricData.name}:{nodeData.subCluster}</code
></Card ></Card
> >
{:else} {:else}
<!-- "No Data"-Warning included in MetricPlot-Component -->
<MetricPlot <MetricPlot
timestep={metricData.metric.timestep} timestep={metricData.metric.timestep}
series={metricData.metric.series} series={metricData.metric.series}
metric={metricData.name} metric={metricData.name}
{cluster} {cluster}
subCluster={nodeData.subCluster} subCluster={nodeData.subCluster}
forNode={true} forNode
/> />
{/if} {/if}
{:else}
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="warning"
>No dataset returned for <code>{metricData.name}</code></Card
>
{/if}
</td> </td>
{/each} {/each}
</tr> </tr>