mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 05:19:05 +01:00
feat: add configurability to frontend plot zoom
This commit is contained in:
parent
f1893c596e
commit
21e4870e4c
@ -8,6 +8,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -225,8 +226,8 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
|
|||||||
|
|
||||||
// JobMetrics is the resolver for the jobMetrics field.
|
// JobMetrics is the resolver for the jobMetrics field.
|
||||||
func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) {
|
func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) {
|
||||||
defaultRes := 600
|
if resolution == nil && config.Keys.EnableResampling != nil {
|
||||||
if resolution == nil {
|
defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
|
||||||
resolution = &defaultRes
|
resolution = &defaultRes
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,11 +446,9 @@ func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
|||||||
// SubCluster returns generated.SubClusterResolver implementation.
|
// SubCluster returns generated.SubClusterResolver implementation.
|
||||||
func (r *Resolver) SubCluster() generated.SubClusterResolver { return &subClusterResolver{r} }
|
func (r *Resolver) SubCluster() generated.SubClusterResolver { return &subClusterResolver{r} }
|
||||||
|
|
||||||
type (
|
type clusterResolver struct{ *Resolver }
|
||||||
clusterResolver struct{ *Resolver }
|
type jobResolver struct{ *Resolver }
|
||||||
jobResolver struct{ *Resolver }
|
type metricValueResolver struct{ *Resolver }
|
||||||
metricValueResolver struct{ *Resolver }
|
type mutationResolver struct{ *Resolver }
|
||||||
mutationResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
queryResolver struct{ *Resolver }
|
type subClusterResolver struct{ *Resolver }
|
||||||
subClusterResolver struct{ *Resolver }
|
|
||||||
)
|
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
|
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/util"
|
"github.com/ClusterCockpit/cc-backend/internal/util"
|
||||||
@ -272,12 +273,13 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
|
|||||||
availableRoles, _ := schema.GetValidRolesMap(user)
|
availableRoles, _ := schema.GetValidRolesMap(user)
|
||||||
|
|
||||||
page := web.Page{
|
page := web.Page{
|
||||||
Title: title,
|
Title: title,
|
||||||
User: *user,
|
User: *user,
|
||||||
Roles: availableRoles,
|
Roles: availableRoles,
|
||||||
Build: buildInfo,
|
Build: buildInfo,
|
||||||
Config: conf,
|
Config: conf,
|
||||||
Infos: infos,
|
Resampling: config.Keys.EnableResampling,
|
||||||
|
Infos: infos,
|
||||||
}
|
}
|
||||||
|
|
||||||
if route.Filter {
|
if route.Filter {
|
||||||
|
@ -76,6 +76,13 @@ type Retention struct {
|
|||||||
IncludeDB bool `json:"includeDB"`
|
IncludeDB bool `json:"includeDB"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResampleConfig struct {
|
||||||
|
// Trigger next zoom level at less than this many visible datapoints
|
||||||
|
Trigger int `json:"trigger"`
|
||||||
|
// Array of resampling target resolutions, in seconds; Example: [600,300,60]
|
||||||
|
Resolutions []int `json:"resolutions"`
|
||||||
|
}
|
||||||
|
|
||||||
// Format of the configuration (file). See below for the defaults.
|
// Format of the configuration (file). See below for the defaults.
|
||||||
type ProgramConfig struct {
|
type ProgramConfig struct {
|
||||||
// Address where the http (or https) server will listen on (for example: 'localhost:80').
|
// Address where the http (or https) server will listen on (for example: 'localhost:80').
|
||||||
@ -133,6 +140,9 @@ type ProgramConfig struct {
|
|||||||
// be provided! Most options here can be overwritten by the user.
|
// be provided! Most options here can be overwritten by the user.
|
||||||
UiDefaults map[string]interface{} `json:"ui-defaults"`
|
UiDefaults map[string]interface{} `json:"ui-defaults"`
|
||||||
|
|
||||||
|
// If exists, will enable dynamic zoom in frontend metric plots using the configured values
|
||||||
|
EnableResampling *ResampleConfig `json:"enable-resampling"`
|
||||||
|
|
||||||
// Where to store MachineState files
|
// Where to store MachineState files
|
||||||
MachineStateDir string `json:"machine-state-dir"`
|
MachineStateDir string `json:"machine-state-dir"`
|
||||||
|
|
||||||
|
@ -424,6 +424,27 @@
|
|||||||
"plot_general_colorscheme",
|
"plot_general_colorscheme",
|
||||||
"plot_list_selectedMetrics"
|
"plot_list_selectedMetrics"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"enable-resampling": {
|
||||||
|
"description": "Enable dynamic zoom in frontend metric plots.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"trigger": {
|
||||||
|
"description": "Trigger next zoom level at less than this many visible datapoints.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"description": "Array of resampling target resolutions, in seconds.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"trigger",
|
||||||
|
"resolutions"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -9,6 +9,7 @@ new Config({
|
|||||||
username: username
|
username: username
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig]
|
['cc-config', clusterCockpitConfig],
|
||||||
|
['resampling', resampleConfig]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -51,7 +51,5 @@
|
|||||||
<Col>
|
<Col>
|
||||||
<EditProject on:reload={getUserList} />
|
<EditProject on:reload={getUserList} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Options />
|
||||||
<Options />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
import { Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
import { Col, Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
let scrambled;
|
let scrambled;
|
||||||
|
|
||||||
|
const resampleConfig = getContext("resampling");
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
|
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
|
||||||
});
|
});
|
||||||
@ -23,16 +25,30 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card class="h-100">
|
<Col>
|
||||||
<CardBody>
|
<Card class="h-100">
|
||||||
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
|
<CardBody>
|
||||||
<input
|
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
|
||||||
type="checkbox"
|
<input
|
||||||
id="scramble-names-checkbox"
|
type="checkbox"
|
||||||
style="margin-right: 1em;"
|
id="scramble-names-checkbox"
|
||||||
on:click={handleScramble}
|
style="margin-right: 1em;"
|
||||||
bind:checked={scrambled}
|
on:click={handleScramble}
|
||||||
/>
|
bind:checked={scrambled}
|
||||||
Active?
|
/>
|
||||||
</CardBody>
|
Active?
|
||||||
</Card>
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{#if resampleConfig}
|
||||||
|
<Col>
|
||||||
|
<Card class="h-100">
|
||||||
|
<CardBody>
|
||||||
|
<CardTitle class="mb-3">Metric Plot Resampling</CardTitle>
|
||||||
|
<p>Triggered at {resampleConfig.trigger} datapoints.</p>
|
||||||
|
<p>Configured resolutions: {resampleConfig.resolutions}</p>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
{/if}
|
||||||
|
@ -26,13 +26,16 @@
|
|||||||
export let showFootprint;
|
export let showFootprint;
|
||||||
export let triggerMetricRefresh = false;
|
export let triggerMetricRefresh = false;
|
||||||
|
|
||||||
|
const resampleConfig = getContext("resampling") || null;
|
||||||
|
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||||
|
|
||||||
let { id } = job;
|
let { id } = job;
|
||||||
let scopes = job.numNodes == 1
|
let scopes = job.numNodes == 1
|
||||||
? job.numAcc >= 1
|
? job.numAcc >= 1
|
||||||
? ["core", "accelerator"]
|
? ["core", "accelerator"]
|
||||||
: ["core"]
|
: ["core"]
|
||||||
: ["node"];
|
: ["node"];
|
||||||
let selectedResolution = 600;
|
let selectedResolution = resampleDefault;
|
||||||
let zoomStates = {};
|
let zoomStates = {};
|
||||||
|
|
||||||
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
||||||
@ -69,7 +72,7 @@
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function handleZoom(detail, metric) {
|
function handleZoom(detail, metric) {
|
||||||
if (
|
if ( // States have to differ, causes deathloop if just set
|
||||||
(zoomStates[metric]?.x?.min !== detail?.lastZoomState?.x?.min) &&
|
(zoomStates[metric]?.x?.min !== detail?.lastZoomState?.x?.min) &&
|
||||||
(zoomStates[metric]?.y?.max !== detail?.lastZoomState?.y?.max)
|
(zoomStates[metric]?.y?.max !== detail?.lastZoomState?.y?.max)
|
||||||
) {
|
) {
|
||||||
@ -187,7 +190,7 @@
|
|||||||
isShared={job.exclusive != 1}
|
isShared={job.exclusive != 1}
|
||||||
numhwthreads={job.numHWThreads}
|
numhwthreads={job.numHWThreads}
|
||||||
numaccs={job.numAcc}
|
numaccs={job.numAcc}
|
||||||
zoomState={zoomStates[metric.data.name]}
|
zoomState={zoomStates[metric.data.name] || null}
|
||||||
/>
|
/>
|
||||||
{:else if metric.disabled == true && metric.data}
|
{:else if metric.disabled == true && metric.data}
|
||||||
<Card body color="info"
|
<Card body color="info"
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
- `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]
|
||||||
- `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]
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script context="module">
|
<script context="module">
|
||||||
@ -39,7 +40,7 @@
|
|||||||
|
|
||||||
function timeIncrs(timestep, maxX, forNode) {
|
function timeIncrs(timestep, maxX, forNode) {
|
||||||
if (forNode === true) {
|
if (forNode === true) {
|
||||||
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
return [60, 120, 240, 300, 360, 480, 600, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
||||||
} else {
|
} else {
|
||||||
let incrs = [];
|
let incrs = [];
|
||||||
for (let t = timestep; t < maxX; t *= 10)
|
for (let t = timestep; t < maxX; t *= 10)
|
||||||
@ -131,8 +132,6 @@
|
|||||||
export let numaccs = 0;
|
export let numaccs = 0;
|
||||||
export let zoomState = null;
|
export let zoomState = null;
|
||||||
|
|
||||||
// $: console.log('Changed ZoomState for', metric, zoomState)
|
|
||||||
|
|
||||||
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
|
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
|
||||||
|
|
||||||
if (useStatsSeries == false && series == null) useStatsSeries = true;
|
if (useStatsSeries == false && series == null) useStatsSeries = true;
|
||||||
@ -160,6 +159,17 @@
|
|||||||
numaccs
|
numaccs
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resampleConfig = getContext("resampling");
|
||||||
|
let resampleTrigger;
|
||||||
|
let resampleResolutions;
|
||||||
|
let resampleMinimum;
|
||||||
|
|
||||||
|
if (resampleConfig) {
|
||||||
|
resampleTrigger = Number(resampleConfig.trigger)
|
||||||
|
resampleResolutions = [...resampleConfig.resolutions];
|
||||||
|
resampleMinimum = Math.min(...resampleConfig.resolutions);
|
||||||
|
}
|
||||||
|
|
||||||
// converts the legend into a simple tooltip
|
// converts the legend into a simple tooltip
|
||||||
function legendAsTooltipPlugin({
|
function legendAsTooltipPlugin({
|
||||||
className,
|
className,
|
||||||
@ -298,7 +308,6 @@
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const plotData = [new Array(longestSeries)];
|
const plotData = [new Array(longestSeries)];
|
||||||
|
|
||||||
if (forNode === true) {
|
if (forNode === true) {
|
||||||
// Negative Timestamp Buildup
|
// Negative Timestamp Buildup
|
||||||
for (let i = 0; i <= longestSeries; i++) {
|
for (let i = 0; i <= longestSeries; i++) {
|
||||||
@ -319,15 +328,15 @@
|
|||||||
plotData.push(statisticsSeries.min);
|
plotData.push(statisticsSeries.min);
|
||||||
plotData.push(statisticsSeries.max);
|
plotData.push(statisticsSeries.max);
|
||||||
plotData.push(statisticsSeries.median);
|
plotData.push(statisticsSeries.median);
|
||||||
// plotData.push(statisticsSeries.mean);
|
|
||||||
|
|
||||||
if (forNode === true) {
|
/* deprecated: sparse data handled by uplot */
|
||||||
// timestamp 0 with null value for reversed time axis
|
// if (forNode === true) {
|
||||||
if (plotData[1].length != 0) plotData[1].push(null);
|
// if (plotData[1][-1] != null && plotData[2][-1] != null && plotData[3][-1] != null) {
|
||||||
if (plotData[2].length != 0) plotData[2].push(null);
|
// if (plotData[1].length != 0) plotData[1].push(null);
|
||||||
if (plotData[3].length != 0) plotData[3].push(null);
|
// if (plotData[2].length != 0) plotData[2].push(null);
|
||||||
// if (plotData[4].length != 0) plotData[4].push(null);
|
// if (plotData[3].length != 0) plotData[3].push(null);
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
plotSeries.push({
|
plotSeries.push({
|
||||||
label: "min",
|
label: "min",
|
||||||
@ -347,12 +356,6 @@
|
|||||||
width: lineWidth,
|
width: lineWidth,
|
||||||
stroke: "black",
|
stroke: "black",
|
||||||
});
|
});
|
||||||
// plotSeries.push({
|
|
||||||
// label: "mean",
|
|
||||||
// scale: "y",
|
|
||||||
// width: lineWidth,
|
|
||||||
// stroke: "blue",
|
|
||||||
// });
|
|
||||||
|
|
||||||
plotBands = [
|
plotBands = [
|
||||||
{ series: [2, 3], fill: "rgba(0,255,0,0.1)" },
|
{ series: [2, 3], fill: "rgba(0,255,0,0.1)" },
|
||||||
@ -361,7 +364,13 @@
|
|||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < series.length; i++) {
|
for (let i = 0; i < series.length; i++) {
|
||||||
plotData.push(series[i].data);
|
plotData.push(series[i].data);
|
||||||
if (forNode === true && plotData[1].length != 0) plotData[1].push(null); // timestamp 0 with null value for reversed time axis
|
/* deprecated: sparse data handled by uplot */
|
||||||
|
// if (forNode === true && plotData[1].length != 0) {
|
||||||
|
// if (plotData[1][-1] != null) {
|
||||||
|
// plotData[1].push(null);
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
plotSeries.push({
|
plotSeries.push({
|
||||||
label:
|
label:
|
||||||
scope === "node"
|
scope === "node"
|
||||||
@ -398,16 +407,19 @@
|
|||||||
hooks: {
|
hooks: {
|
||||||
init: [
|
init: [
|
||||||
(u) => {
|
(u) => {
|
||||||
u.over.addEventListener("dblclick", (e) => {
|
/* IF Zoom Enabled */
|
||||||
// console.log('Dispatch Reset')
|
if (resampleConfig) {
|
||||||
dispatch('zoom', {
|
u.over.addEventListener("dblclick", (e) => {
|
||||||
lastZoomState: {
|
// console.log('Dispatch Reset')
|
||||||
x: { time: false },
|
dispatch('zoom', {
|
||||||
y: { auto: true }
|
lastZoomState: {
|
||||||
}
|
x: { time: false },
|
||||||
|
y: { auto: true }
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
draw: [
|
draw: [
|
||||||
(u) => {
|
(u) => {
|
||||||
@ -451,30 +463,32 @@
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
setScale: [
|
setScale: [
|
||||||
(u, key) => {
|
(u, key) => { // If ZoomResample is Configured && Not System/Node View
|
||||||
if (key === 'x') {
|
if (resampleConfig && !forNode && key === 'x') {
|
||||||
const numX = (u.series[0].idxs[1] - u.series[0].idxs[0])
|
const numX = (u.series[0].idxs[1] - u.series[0].idxs[0])
|
||||||
if (numX <= 20 && timestep !== 60) { // Zoom IN if not at MAX
|
if (numX <= resampleTrigger && timestep !== resampleMinimum) {
|
||||||
// console.log('Dispatch Zoom')
|
/* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */
|
||||||
if (timestep == 600) {
|
// Which resolution to theoretically request to achieve 30 or more visible data points:
|
||||||
|
const target = (numX * timestep) / resampleTrigger
|
||||||
|
// Which configured resolution actually matches the closest to theoretical target:
|
||||||
|
const closest = resampleResolutions.reduce(function(prev, curr) {
|
||||||
|
return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev);
|
||||||
|
});
|
||||||
|
// Prevents non-required dispatches
|
||||||
|
if (timestep !== closest) {
|
||||||
|
// console.log('Dispatch Zoom with Res from / to', timestep, closest)
|
||||||
dispatch('zoom', {
|
dispatch('zoom', {
|
||||||
newRes: 240,
|
newRes: closest,
|
||||||
lastZoomState: u?.scales
|
|
||||||
});
|
|
||||||
} else if (timestep === 240) {
|
|
||||||
dispatch('zoom', {
|
|
||||||
newRes: 60,
|
|
||||||
lastZoomState: u?.scales
|
lastZoomState: u?.scales
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log('Dispatch Update')
|
|
||||||
dispatch('zoom', {
|
dispatch('zoom', {
|
||||||
lastZoomState: u?.scales
|
lastZoomState: u?.scales
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
@ -507,7 +521,6 @@
|
|||||||
opts.width = width;
|
opts.width = width;
|
||||||
opts.height = height;
|
opts.height = height;
|
||||||
if (zoomState) {
|
if (zoomState) {
|
||||||
// console.log('Use last state for uPlot init:', metric, scope, zoomState)
|
|
||||||
opts.scales = {...zoomState}
|
opts.scales = {...zoomState}
|
||||||
}
|
}
|
||||||
uplot = new uPlot(opts, plotData, plotWrapper);
|
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||||
|
@ -69,6 +69,7 @@
|
|||||||
|
|
||||||
<InputGroup class="inline-from">
|
<InputGroup class="inline-from">
|
||||||
<InputGroupText><Icon name="clock-history" /></InputGroupText>
|
<InputGroupText><Icon name="clock-history" /></InputGroupText>
|
||||||
|
<InputGroupText>Range</InputGroupText>
|
||||||
<select
|
<select
|
||||||
class="form-select"
|
class="form-select"
|
||||||
bind:value={timeRange}
|
bind:value={timeRange}
|
||||||
|
@ -9,6 +9,7 @@ new Job({
|
|||||||
roles: roles
|
roles: roles
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig]
|
['cc-config', clusterCockpitConfig],
|
||||||
|
['resampling', resampleConfig]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
|
getContext,
|
||||||
createEventDispatcher
|
createEventDispatcher
|
||||||
} from "svelte";
|
} from "svelte";
|
||||||
import {
|
import {
|
||||||
@ -41,11 +42,14 @@
|
|||||||
export let rawData;
|
export let rawData;
|
||||||
export let isShared = false;
|
export let isShared = false;
|
||||||
|
|
||||||
|
const resampleConfig = getContext("resampling") || null;
|
||||||
|
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||||
|
|
||||||
let selectedHost = null;
|
let selectedHost = null;
|
||||||
let error = null;
|
let error = null;
|
||||||
let selectedScope = minScope(scopes);
|
let selectedScope = minScope(scopes);
|
||||||
let selectedResolution;
|
let selectedResolution = null;
|
||||||
let pendingResolution = 600;
|
let pendingResolution = resampleDefault;
|
||||||
let selectedScopeIndex = scopes.findIndex((s) => s == minScope(scopes));
|
let selectedScopeIndex = scopes.findIndex((s) => s == minScope(scopes));
|
||||||
let patternMatches = false;
|
let patternMatches = false;
|
||||||
let nodeOnly = false; // If, after load-all, still only node scope returned
|
let nodeOnly = false; // If, after load-all, still only node scope returned
|
||||||
@ -112,12 +116,13 @@
|
|||||||
selectedResolution = Number(pendingResolution)
|
selectedResolution = Number(pendingResolution)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (selectedScope == "load-all") {
|
if (selectedScope == "load-all") {
|
||||||
selectedScopes = [...scopes, "socket", "core", "accelerator"]
|
selectedScopes = [...scopes, "socket", "core", "accelerator"]
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedResolution = Number(pendingResolution)
|
if (pendingResolution) {
|
||||||
|
selectedResolution = Number(pendingResolution)
|
||||||
|
}
|
||||||
|
|
||||||
metricData = queryStore({
|
metricData = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
|
@ -9,6 +9,7 @@ new Jobs({
|
|||||||
roles: roles
|
roles: roles
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig]
|
['cc-config', clusterCockpitConfig],
|
||||||
|
['resampling', resampleConfig]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -8,6 +8,7 @@ new User({
|
|||||||
user: userInfos
|
user: userInfos
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig]
|
['cc-config', clusterCockpitConfig],
|
||||||
|
['resampling', resampleConfig]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
const username = {{ .User.Username }};
|
const username = {{ .User.Username }};
|
||||||
const filterPresets = {{ .FilterPresets }};
|
const filterPresets = {{ .FilterPresets }};
|
||||||
const clusterCockpitConfig = {{ .Config }};
|
const clusterCockpitConfig = {{ .Config }};
|
||||||
|
const resampleConfig = {{ .Resampling }};
|
||||||
</script>
|
</script>
|
||||||
<script src='/build/config.js'></script>
|
<script src='/build/config.js'></script>
|
||||||
{{end}}
|
{{end}}
|
@ -13,6 +13,7 @@
|
|||||||
const clusterCockpitConfig = {{ .Config }};
|
const clusterCockpitConfig = {{ .Config }};
|
||||||
const authlevel = {{ .User.GetAuthLevel }};
|
const authlevel = {{ .User.GetAuthLevel }};
|
||||||
const roles = {{ .Roles }};
|
const roles = {{ .Roles }};
|
||||||
|
const resampleConfig = {{ .Resampling }};
|
||||||
</script>
|
</script>
|
||||||
<script src='/build/job.js'></script>
|
<script src='/build/job.js'></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
const clusterCockpitConfig = {{ .Config }};
|
const clusterCockpitConfig = {{ .Config }};
|
||||||
const authlevel = {{ .User.GetAuthLevel }};
|
const authlevel = {{ .User.GetAuthLevel }};
|
||||||
const roles = {{ .Roles }};
|
const roles = {{ .Roles }};
|
||||||
|
const resampleConfig = {{ .Resampling }};
|
||||||
</script>
|
</script>
|
||||||
<script src='/build/jobs.js'></script>
|
<script src='/build/jobs.js'></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
const userInfos = {{ .Infos }};
|
const userInfos = {{ .Infos }};
|
||||||
const filterPresets = {{ .FilterPresets }};
|
const filterPresets = {{ .FilterPresets }};
|
||||||
const clusterCockpitConfig = {{ .Config }};
|
const clusterCockpitConfig = {{ .Config }};
|
||||||
|
const resampleConfig = {{ .Resampling }};
|
||||||
</script>
|
</script>
|
||||||
<script src='/build/user.js'></script>
|
<script src='/build/user.js'></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -98,6 +98,7 @@ type Page struct {
|
|||||||
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
|
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
|
||||||
Infos map[string]interface{} // For generic use (e.g. username for /monitoring/user/<id>, job id for /monitoring/job/<id>)
|
Infos map[string]interface{} // For generic use (e.g. username for /monitoring/user/<id>, job id for /monitoring/job/<id>)
|
||||||
Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
|
Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
|
||||||
|
Resampling *schema.ResampleConfig // If not nil, defines resampling trigger and resolutions
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
|
func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
|
||||||
|
Loading…
Reference in New Issue
Block a user