Merge pull request #4 from KarnamShyam1947/shyam-frontend

ui changes
This commit is contained in:
Sai Sanjay 2024-06-01 11:00:43 +05:30 committed by GitHub
commit 27183dd92f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 41 additions and 2139 deletions

View File

@ -337,11 +337,13 @@ func main() {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
})
r.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNotFound)
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, "404.tmpl", &web.Page{Title: "Page not found", Build: buildInfo})
})
-
secured := r.PathPrefix("/").Subrouter()

View File

@ -35,15 +35,15 @@ type Route struct {
var routes []Route = []Route{
{"/", "home.tmpl", "ClusterCockpit", false, setupHomeRoute},
{"/config", "config.tmpl", "Settings", false, func(i InfoType, r *http.Request) InfoType { return i }},
{"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }},
{"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job <ID> - ClusterCockpit", false, setupJobRoute},
{"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
{"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
{"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
{"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
{"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
// {"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }},
// {"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job <ID> - ClusterCockpit", false, setupJobRoute},
// {"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
// {"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
// {"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
// {"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
// {"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
{"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node <ID> - ClusterCockpit", false, setupNodeRoute},
{"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute},
// {"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute},
{"/monitoring/control/{cluster}", "monitoring/control.tmpl", "Status of <ID> - ClusterCockpit", false, setupClusterRoute},
{"/monitoring/partition/{cluster}", "partitions/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
{"/monitoring/history/", "monitoring/history.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},

View File

@ -6,8 +6,8 @@ import terser from '@rollup/plugin-terser';
import css from 'rollup-plugin-css-only';
import livereload from 'rollup-plugin-livereload';
const production = !process.env.ROLLUP_WATCH;
// const production = false
// const production = !process.env.ROLLUP_WATCH;
const production = false
const plugins = [
svelte({
@ -61,13 +61,14 @@ const entrypoint = (name, path) => ({
export default [
entrypoint('header', 'src/header.entrypoint.js'),
entrypoint('jobs', 'src/jobs.entrypoint.js'),
entrypoint('user', 'src/user.entrypoint.js'),
entrypoint('list', 'src/list.entrypoint.js'),
entrypoint('job', 'src/job.entrypoint.js'),
entrypoint('systems', 'src/systems.entrypoint.js'),
entrypoint('home', 'src/home.entrypoint.js'),
// entrypoint('jobs', 'src/jobs.entrypoint.js'),
// entrypoint('user', 'src/user.entrypoint.js'),
// entrypoint('list', 'src/list.entrypoint.js'),
// entrypoint('job', 'src/job.entrypoint.js'),
// entrypoint('systems', 'src/systems.entrypoint.js'),
entrypoint('node', 'src/node.entrypoint.js'),
entrypoint('analysis', 'src/analysis.entrypoint.js'),
// entrypoint('analysis', 'src/analysis.entrypoint.js'),
entrypoint('control', 'src/control.entrypoint.js'),
entrypoint('config', 'src/config.entrypoint.js'),
entrypoint('partitions', 'src/partitions.entrypoint.js'),

View File

@ -1,440 +0,0 @@
<script>
import { init, convert2uplot } from './utils.js'
import { getContext, onMount } from 'svelte'
import { queryStore, gql, getContextClient, mutationStore } from '@urql/svelte'
import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
import Filters from './filters/Filters.svelte'
import PlotSelection from './PlotSelection.svelte'
import Histogram from './plots/Histogram.svelte'
import Pie, { colors } from './plots/Pie.svelte'
import { binsFromFootprint } from './utils.js'
import ScatterPlot from './plots/Scatter.svelte'
import PlotTable from './PlotTable.svelte'
import RooflineHeatmap from './plots/RooflineHeatmap.svelte'
const { query: initq } = init()
export let filterPresets
// By default, look at the jobs of the last 6 hours:
if (filterPresets?.startTime == null) {
if (filterPresets == null)
filterPresets = {}
let now = new Date(Date.now())
let hourAgo = new Date(now)
hourAgo.setHours(hourAgo.getHours() - 6)
filterPresets.startTime = { from: hourAgo.toISOString(), to: now.toISOString() }
}
let cluster;
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobFilters = [];
let rooflineMaxY;
let colWidth1, colWidth2, colWidth3, colWidth4;
let numBins = 50;
let maxY = -1;
const ccconfig = getContext('cc-config')
const metricConfig = getContext('metrics')
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics
$: metrics = [...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
const sortOptions = [
{key: 'totalWalltime', label: 'Walltime'},
{key: 'totalNodeHours', label: 'Node Hours'},
{key: 'totalCoreHours', label: 'Core Hours'},
{key: 'totalAccHours', label: 'Accelerator Hours'}
]
const groupOptions = [
{key: 'user', label: 'User Name'},
{key: 'project', label: 'Project ID'}
]
let sortSelection = sortOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`]) || sortOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopCategory)
let groupSelection = groupOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`]) || groupOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopEntity)
getContext('on-init')(({ data }) => {
if (data != null) {
cluster = data.clusters.find(c => c.name == filterPresets.cluster)
console.assert(cluster != null, `This cluster could not be found: ${filterPresets.cluster}`)
rooflineMaxY = cluster.subClusters.reduce((max, part) => Math.max(max, part.flopRateSimd.value), 0)
maxY = rooflineMaxY
}
})
const client = getContextClient();
$: statsQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!) {
stats: jobsStatistics(filter: $jobFilters) {
totalJobs
shortJobs
totalWalltime
totalNodeHours
totalCoreHours
totalAccHours
histDuration { count, value }
histNumCores { count, value }
}
}
`,
variables: { jobFilters }
})
$: topQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $paging: PageRequest!, $sortBy: SortByAggregate!, $groupBy: Aggregate!) {
topList: jobsStatistics(filter: $jobFilters, page: $paging, sortBy: $sortBy, groupBy: $groupBy) {
id
totalWalltime
totalNodeHours
totalCoreHours
totalAccHours
}
}
`,
variables: { jobFilters, paging: { itemsPerPage: 10, page: 1 }, sortBy: sortSelection.key.toUpperCase(), groupBy: groupSelection.key.toUpperCase() }
})
$: footprintsQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
timeWeights { nodeHours, accHours, coreHours },
metrics { metric, data }
}
}`,
variables: { jobFilters, metrics }
})
$: rooflineQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $rows: Int!, $cols: Int!,
$minX: Float!, $minY: Float!, $maxX: Float!, $maxY: Float!) {
rooflineHeatmap(filter: $jobFilters, rows: $rows, cols: $cols,
minX: $minX, minY: $minY, maxX: $maxX, maxY: $maxY)
}
`,
variables: { jobFilters, rows: 50, cols: 50, minX: 0.01, minY: 1., maxX: 1000., maxY }
})
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
});
}
function updateEntityConfiguration(select) {
if (ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`] != select) {
updateConfigurationMutation({ name: `analysis_view_selectedTopEntity:${filterPresets.cluster}`, value: JSON.stringify(select) })
.subscribe(res => {
if (res.fetching === false && !res.error) {
// console.log(`analysis_view_selectedTopEntity:${filterPresets.cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error
}
})
} else {
// console.log('No Mutation Required: Entity')
}
};
function updateCategoryConfiguration(select) {
if (ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`] != select) {
updateConfigurationMutation({ name: `analysis_view_selectedTopCategory:${filterPresets.cluster}`, value: JSON.stringify(select) })
.subscribe(res => {
if (res.fetching === false && !res.error) {
// console.log(`analysis_view_selectedTopCategory:${filterPresets.cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error
}
})
} else {
// console.log('No Mutation Required: Category')
}
};
$: updateEntityConfiguration(groupSelection.key)
$: updateCategoryConfiguration(sortSelection.key)
onMount(() => filterComponent.update())
</script>
<Row>
{#if $initq.fetching || $statsQuery.fetching || $footprintsQuery.fetching}
<Col xs="auto">
<Spinner />
</Col>
{/if}
<Col xs="auto">
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if cluster}
<PlotSelection
availableMetrics={cluster.metricConfig.map(mc => mc.name)}
bind:metricsInHistograms={metricsInHistograms}
bind:metricsInScatterplots={metricsInScatterplots} />
{/if}
</Col>
<Col xs="auto">
<Filters
bind:this={filterComponent}
filterPresets={filterPresets}
disableClusterSelection={true}
startTimeQuickSelect={true}
on:update={({ detail }) => {
jobFilters = detail.filters;
}} />
</Col>
</Row>
<br/>
{#if $statsQuery.error}
<Row>
<Col>
<Card body color="danger">{$statsQuery.error.message}</Card>
</Col>
</Row>
{:else if $statsQuery.data}
<Row cols={3} class="mb-4">
<Col>
<Table>
<tr>
<th scope="col">Total Jobs</th>
<td>{$statsQuery.data.stats[0].totalJobs}</td>
</tr>
<tr>
<th scope="col">Short Jobs</th>
<td>{$statsQuery.data.stats[0].shortJobs}</td>
</tr>
<tr>
<th scope="col">Total Walltime</th>
<td>{$statsQuery.data.stats[0].totalWalltime}</td>
</tr>
<tr>
<th scope="col">Total Node Hours</th>
<td>{$statsQuery.data.stats[0].totalNodeHours}</td>
</tr>
<tr>
<th scope="col">Total Core Hours</th>
<td>{$statsQuery.data.stats[0].totalCoreHours}</td>
</tr>
<tr>
<th scope="col">Total Accelerator Hours</th>
<td>{$statsQuery.data.stats[0].totalAccHours}</td>
</tr>
</Table>
</Col>
<Col>
<div bind:clientWidth={colWidth1}>
<h5>Top
<select class="p-0" bind:value={groupSelection}>
{#each groupOptions as option}
<option value={option}>
{option.key.charAt(0).toUpperCase() + option.key.slice(1)}s
</option>
{/each}
</select>
</h5>
{#key $topQuery.data}
{#if $topQuery.fetching}
<Spinner/>
{:else if $topQuery.error}
<Card body color="danger">{$topQuery.error.message}</Card>
{:else}
<Pie
size={colWidth1}
sliceLabel={sortSelection.label}
quantities={$topQuery.data.topList.map((t) => t[sortSelection.key])}
entities={$topQuery.data.topList.map((t) => t.id)}
/>
{/if}
{/key}
</div>
</Col>
<Col>
{#key $topQuery.data}
{#if $topQuery.fetching}
<Spinner/>
{:else if $topQuery.error}
<Card body color="danger">{$topQuery.error.message}</Card>
{:else}
<Table>
<tr class="mb-2">
<th>Legend</th>
<th>{groupSelection.label}</th>
<th>
<select class="p-0" bind:value={sortSelection}>
{#each sortOptions as option}
<option value={option}>
{option.label}
</option>
{/each}
</select>
</th>
</tr>
{#each $topQuery.data.topList as te, i}
<tr>
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
{#if groupSelection.key == 'user'}
<th scope="col"><a href="/monitoring/user/{te.id}?cluster={cluster.name}">{te.id}</a></th>
{:else}
<th scope="col"><a href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq">{te.id}</a></th>
{/if}
<td>{te[sortSelection.key]}</td>
</tr>
{/each}
</Table>
{/if}
{/key}
</Col>
</Row>
<Row cols={3} class="mb-2">
<Col>
{#if $rooflineQuery.fetching}
<Spinner />
{:else if $rooflineQuery.error}
<Card body color="danger">{$rooflineQuery.error.message}</Card>
{:else if $rooflineQuery.data && cluster}
<div bind:clientWidth={colWidth2}>
{#key $rooflineQuery.data}
<RooflineHeatmap
width={colWidth2} height={300}
tiles={$rooflineQuery.data.rooflineHeatmap}
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
maxY={rooflineMaxY} />
{/key}
</div>
{/if}
</Col>
<Col>
<div bind:clientWidth={colWidth3}>
{#key $statsQuery.data.stats[0].histDuration}
<Histogram
width={colWidth3} height={300}
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
</Col>
<Col>
<div bind:clientWidth={colWidth4}>
{#key $statsQuery.data.stats[0].histNumCores}
<Histogram
width={colWidth4} height={300}
data={convert2uplot($statsQuery.data.stats[0].histNumCores)}
title="Number of Cores Distribution"
xlabel="Allocated Cores"
xunit="Cores"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
</Col>
</Row>
{/if}
<hr class="my-6"/>
{#if $footprintsQuery.error}
<Row>
<Col>
<Card body color="danger">{$footprintsQuery.error.message}</Card>
</Col>
</Row>
{:else if $footprintsQuery.data && $initq.data}
<Row>
<Col>
<Card body>
These histograms show the distribution of the averages of all jobs matching the filters. Each job/average is weighted by its node hours by default
(Accelerator hours for native accelerator scope metrics, coreHours for native core scope metrics).
Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
</Card>
<br/>
</Col>
</Row>
<Row>
<Col>
<PlotTable
let:item
let:width
renderFor="analysis"
items={metricsInHistograms.map(metric => ({ metric, ...binsFromFootprint(
$footprintsQuery.data.footprints.timeWeights,
metricConfig(cluster.name, metric)?.scope,
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
<Histogram
data={convert2uplot(item.bins)}
width={width} height={250}
usesBins={true}
title="Average Distribution of '{item.metric}'"
xlabel={`${item.metric} bin maximum ${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? '[' + metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base + ']' : '')}`}
xunit={`${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}`}
ylabel="Normalized Hours"
yunit="Hours"/>
</PlotTable>
</Col>
</Row>
<br/>
<Row>
<Col>
<Card body>
Each circle represents one job. The size of a circle is proportional to its node hours. Darker circles mean multiple jobs have the same averages for the respective metrics.
Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
</Card>
<br/>
</Col>
</Row>
<Row>
<Col>
<PlotTable
let:item
let:width
renderFor="analysis"
items={metricsInScatterplots.map(([m1, m2]) => ({
m1, f1: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m1).data,
m2, f2: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m2).data }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
<ScatterPlot
width={width} height={250} color={"rgba(0, 102, 204, 0.33)"}
xLabel={`${item.m1} [${(metricConfig(cluster.name, item.m1)?.unit?.prefix ? metricConfig(cluster.name, item.m1)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.m1)?.unit?.base ? metricConfig(cluster.name, item.m1)?.unit?.base : '')}]`}
yLabel={`${item.m2} [${(metricConfig(cluster.name, item.m2)?.unit?.prefix ? metricConfig(cluster.name, item.m2)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.m2)?.unit?.base ? metricConfig(cluster.name, item.m2)?.unit?.base : '')}]`}
X={item.f1} Y={item.f2} S={$footprintsQuery.data.footprints.timeWeights.nodeHours} />
</PlotTable>
</Col>
</Row>
{/if}
<style>
h5 {
text-align: center;
}
</style>

View File

@ -75,7 +75,7 @@
<!-- <Button color="primary" on:click={nextPage}> -->
<Button color="primary">Next</Button>
<!-- dropdown for number of pages to show -->
<div>
<!-- <div> -->
<Dropdown {isOpen} toggle={() => (isOpen = !isOpen)}>
<DropdownToggle caret>
{selectedPages}
@ -88,7 +88,7 @@
{/each}
</DropdownMenu>
</Dropdown>
</div>
<!-- </div> -->
</Col>
</Row>
</CardBody>

View File

@ -0,0 +1,5 @@
<script>
</script>
<h1>Home Page</h1>

View File

@ -1,468 +0,0 @@
<script>
import {
init,
groupByScope,
fetchMetricsStore,
checkMetricDisabled,
transformDataForRoofline
} from "./utils.js";
import {
Row,
Col,
Card,
Spinner,
TabContent,
TabPane,
CardBody,
CardHeader,
CardTitle,
Button,
Icon,
} from "sveltestrap";
import PlotTable from "./PlotTable.svelte";
import Metric from "./Metric.svelte";
import Polar from "./plots/Polar.svelte";
import Roofline from "./plots/Roofline.svelte";
import JobInfo from "./joblist/JobInfo.svelte";
import TagManagement from "./TagManagement.svelte";
import MetricSelection from "./MetricSelection.svelte";
import StatsTable from "./StatsTable.svelte";
import JobFootprint from "./JobFootprint.svelte";
import { getContext } from "svelte";
export let dbid;
export let authlevel;
export let roles;
const accMetrics = ['acc_utilization', 'acc_mem_used', 'acc_power', 'nv_mem_util', 'nv_sm_clock', 'nv_temp'];
let accNodeOnly
const { query: initq } = init(`
job(id: "${dbid}") {
id, jobId, user, project, cluster, startTime,
duration, numNodes, numHWThreads, numAcc,
SMT, exclusive, partition, subCluster, arrayJobId,
monitoringStatus, state, walltime,
tags { id, type, name },
resources { hostname, hwthreads, accelerators },
metaData,
userData { name, email },
concurrentJobs { items { id, jobId }, count, listQuery },
flopsAnyAvg, memBwAvg, loadAvg
}
`);
const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"),
metrics = getContext("metrics")
let isMetricsSelectionOpen = false,
selectedMetrics = [],
isFetched = new Set();
const [jobMetrics, startFetching] = fetchMetricsStore();
getContext("on-init")(() => {
let job = $initq.data.job;
if (!job) return;
selectedMetrics =
ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
clusters
.find((c) => c.name == job.cluster)
.metricConfig.map((mc) => mc.name);
let toFetch = new Set([
"flops_any",
"mem_bw",
...selectedMetrics,
...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
ccconfig[`job_view_polarPlotMetrics`]),
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
ccconfig[`job_view_nodestats_selectedMetrics`]),
]);
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
accNodeOnly = [...toFetch].some(function(m) {
if (accMetrics.includes(m)) {
const mc = metrics(job.cluster, m)
return mc.scope !== 'accelerator'
} else {
return false
}
})
if (job.numAcc === 0 || accNodeOnly === true) {
// No Accels or Accels on Node Scope
startFetching(
job,
[...toFetch],
job.numNodes > 2
? ["node"]
: ["node", "socket", "core"]
);
} else {
// Accels and not on node scope
startFetching(
job,
[...toFetch],
job.numNodes > 2
? ["node", "accelerator"]
: ["node", "accelerator", "socket", "core"]
);
}
isFetched = toFetch;
});
const lazyFetchMoreMetrics = () => {
let notYetFetched = new Set();
for (let m of selectedMetrics) {
if (!isFetched.has(m)) {
notYetFetched.add(m);
isFetched.add(m);
}
}
if (notYetFetched.size > 0)
startFetching(
$initq.data.job,
[...notYetFetched],
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"]
);
};
// Fetch more data once required:
$: if ($initq.data && $jobMetrics.data && selectedMetrics)
lazyFetchMoreMetrics();
let plots = {},
jobTags,
statsTable,
jobFootprint;
$: document.title = $initq.fetching
? "Loading..."
: $initq.error
? "Error"
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
// Find out what metrics or hosts are missing:
let missingMetrics = [],
missingHosts = [],
somethingMissing = false;
$: if ($initq.data && $jobMetrics.data) {
let job = $initq.data.job,
metrics = $jobMetrics.data.jobMetrics,
metricNames = clusters
.find((c) => c.name == job.cluster)
.metricConfig.map((mc) => mc.name);
// Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
missingMetrics = metricNames.filter(
(metric) =>
!metrics.some((jm) => jm.name == metric) &&
!checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster
)
);
missingHosts = job.resources
.map(({ hostname }) => ({
hostname: hostname,
metrics: metricNames.filter(
(metric) =>
!metrics.some(
(jm) =>
jm.scope == "node" &&
jm.metric.series.some(
(series) => series.hostname == hostname
)
)
),
}))
.filter(({ metrics }) => metrics.length > 0);
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
}
const orderAndMap = (grouped, selectedMetrics) =>
selectedMetrics.map((metric) => ({
metric: metric,
data: grouped.find((group) => group[0].name == metric),
disabled: checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster
),
}));
</script>
<Row>
<Col>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.data}
<JobInfo job={$initq.data.job} {jobTags} />
{:else}
<Spinner secondary />
{/if}
</Col>
{#if $jobMetrics.data}
{#key $jobMetrics.data}
<Col>
<JobFootprint
bind:this={jobFootprint}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
{/key}
{/if}
{#if $jobMetrics.data && $initq.data}
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
{#if authlevel > roles.manager}
<Col>
<h5>
Concurrent Jobs <Icon
name="info-circle"
style="cursor:help;"
title="Shared jobs running on the same node with overlapping runtimes"
/>
</h5>
<ul>
<li>
<a
href="/monitoring/jobs/?{$initq.data.job
.concurrentJobs.listQuery}"
target="_blank">See All</a
>
</li>
{#each $initq.data.job.concurrentJobs.items as pjob, index}
<li>
<a
href="/monitoring/job/{pjob.id}"
target="_blank">{pjob.jobId}</a
>
</li>
{/each}
</ul>
</Col>
{:else}
<Col>
<h5>
{$initq.data.job.concurrentJobs.items.length} Concurrent
Jobs
</h5>
<p>
Number of shared jobs on the same node with overlapping
runtimes.
</p>
</Col>
{/if}
{/if}
<Col>
<Polar
metrics={ccconfig[
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
] || ccconfig[`job_view_polarPlotMetrics`]}
cluster={$initq.data.job.cluster}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
<Col>
<Roofline
renderTime={true}
cluster={clusters
.find((c) => c.name == $initq.data.job.cluster)
.subClusters.find(
(sc) => sc.name == $initq.data.job.subCluster
)}
data={
transformDataForRoofline (
$jobMetrics.data.jobMetrics.find((m) => m.name == "flops_any" && m.scope == "node").metric,
$jobMetrics.data.jobMetrics.find((m) => m.name == "mem_bw" && m.scope == "node").metric
)
}
/>
</Col>
{:else}
<Col />
<Col />
{/if}
</Row>
<Row class="mb-3">
<Col xs="auto">
{#if $initq.data}
<TagManagement job={$initq.data.job} bind:jobTags />
{/if}
</Col>
<Col xs="auto">
{#if $initq.data}
<Button outline on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up" /> Metrics
</Button>
{/if}
</Col>
<!-- <Col xs="auto">
<Zoom timeseriesPlots={plots} />
</Col> -->
</Row>
<Row>
<Col>
{#if $jobMetrics.error}
{#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
<Card body color="warning"
>Not monitored or archiving failed</Card
>
<br />
{/if}
<Card body color="danger">{$jobMetrics.error.message}</Card>
{:else if $jobMetrics.fetching}
<Spinner secondary />
{:else if $jobMetrics.data && $initq.data}
<PlotTable
let:item
let:width
renderFor="job"
items={orderAndMap(
groupByScope($jobMetrics.data.jobMetrics),
selectedMetrics
)}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
>
{#if item.data}
<Metric
bind:this={plots[item.metric]}
on:more-loaded={({ detail }) =>
statsTable.moreLoaded(detail)}
job={$initq.data.job}
metricName={item.metric}
rawData={item.data.map((x) => x.metric)}
scopes={item.data.map((x) => x.scope)}
{width}
isShared={$initq.data.job.exclusive != 1}
resources={$initq.data.job.resources}
/>
{:else}
<Card body color="warning"
>No dataset returned for <code>{item.metric}</code
></Card
>
{/if}
</PlotTable>
{/if}
</Col>
</Row>
<Row class="mt-2">
<Col>
{#if $initq.data}
<TabContent>
{#if somethingMissing}
<TabPane
tabId="resources"
tab="Resources"
active={somethingMissing}
>
<div style="margin: 10px;">
<Card color="warning">
<CardHeader>
<CardTitle
>Missing Metrics/Reseources</CardTitle
>
</CardHeader>
<CardBody>
{#if missingMetrics.length > 0}
<p>
No data at all is available for the
metrics: {missingMetrics.join(", ")}
</p>
{/if}
{#if missingHosts.length > 0}
<p>
Some metrics are missing for the
following hosts:
</p>
<ul>
{#each missingHosts as missing}
<li>
{missing.hostname}: {missing.metrics.join(
", "
)}
</li>
{/each}
</ul>
{/if}
</CardBody>
</Card>
</div>
</TabPane>
{/if}
<TabPane
tabId="stats"
tab="Statistics Table"
active={!somethingMissing}
>
{#if $jobMetrics.data}
{#key $jobMetrics.data}
<StatsTable
bind:this={statsTable}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
{/key}
{/if}
</TabPane>
<TabPane tabId="job-script" tab="Job Script">
<div class="pre-wrapper">
{#if $initq.data.job.metaData?.jobScript}
<pre><code
>{$initq.data.job.metaData?.jobScript}</code
></pre>
{:else}
<Card body color="warning"
>No job script available</Card
>
{/if}
</div>
</TabPane>
<TabPane tabId="slurm-info" tab="Slurm Info">
<div class="pre-wrapper">
{#if $initq.data.job.metaData?.slurmInfo}
<pre><code
>{$initq.data.job.metaData?.slurmInfo}</code
></pre>
{:else}
<Card body color="warning"
>No additional slurm information available</Card
>
{/if}
</div>
</TabPane>
</TabContent>
{/if}
</Col>
</Row>
{#if $initq.data}
<MetricSelection
cluster={$initq.data.job.cluster}
configName="job_view_selectedMetrics"
bind:metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen}
/>
{/if}
<style>
.pre-wrapper {
font-size: 1.1rem;
margin: 10px;
border: 1px solid #bbb;
border-radius: 5px;
padding: 5px;
}
ul {
columns: 2;
-webkit-columns: 2;
-moz-columns: 2;
}
</style>

View File

@ -1,232 +0,0 @@
<script>
import { getContext } from 'svelte'
import {
Card,
CardHeader,
CardTitle,
CardBody,
Progress,
Icon,
Tooltip
} from "sveltestrap";
import { mean, round } from 'mathjs'
export let job
export let jobMetrics
export let view = 'job'
export let width = 'auto'
const clusters = getContext('clusters')
const subclusterConfig = clusters.find((c) => c.name == job.cluster).subClusters.find((sc) => sc.name == job.subCluster)
const footprintMetrics = (job.numAcc !== 0)
? (job.exclusive !== 1)
? ['cpu_load', 'flops_any', 'acc_utilization']
: ['cpu_load', 'flops_any', 'acc_utilization', 'mem_bw']
: (job.exclusive !== 1)
? ['cpu_load', 'flops_any', 'mem_used']
: ['cpu_load', 'flops_any', 'mem_used', 'mem_bw']
const footprintData = footprintMetrics.map((fm) => {
// Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
let mv = null
if (fm === 'cpu_load' && job.loadAvg !== 0) {
mv = round(job.loadAvg, 2)
} else if (fm === 'flops_any' && job.flopsAnyAvg !== 0) {
mv = round(job.flopsAnyAvg, 2)
} else if (fm === 'mem_bw' && job.memBwAvg !== 0) {
mv = round(job.memBwAvg, 2)
} else { // Calculate from jobMetrics
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === 'node')
if (jm?.metric?.statisticsSeries) {
mv = round(mean(jm.metric.statisticsSeries.mean), 2)
} else if (jm?.metric?.series?.length > 1) {
const avgs = jm.metric.series.map(jms => jms.statistics.avg)
mv = round(mean(avgs), 2)
} else if (jm?.metric?.series) {
mv = round(jm.metric.series[0].statistics.avg, 2)
} else {
mv = 0.0
}
}
// Unit
const fmc = getContext('metrics')(job.cluster, fm)
let unit = ''
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base
// Threshold / -Differences
const fmt = findJobThresholds(job, fmc, subclusterConfig)
if (fm === 'flops_any') fmt.peak = round((fmt.peak * 0.85), 0)
// Define basic data
const fmBase = {
name: fm,
unit: unit,
avg: mv,
max: fmt.peak
}
if (evalFootprint(fm, mv, fmt, 'alert')) {
return {
...fmBase,
color: 'danger',
message:`Metric average way ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
impact: 3
}
} else if (evalFootprint(fm, mv, fmt, 'caution')) {
return {
...fmBase,
color: 'warning',
message: `Metric average ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
impact: 2
}
} else if (evalFootprint(fm, mv, fmt, 'normal')) {
return {
...fmBase,
color: 'success',
message: 'Metric average within expected thresholds.',
impact: 1
}
} else if (evalFootprint(fm, mv, fmt, 'peak')) {
return {
...fmBase,
color: 'info',
message: 'Metric average above expected normal thresholds: Check for artifacts recommended.',
impact: 0
}
} else {
return {
...fmBase,
color: 'secondary',
message: 'Metric average above expected peak threshold: Check for artifacts!',
impact: -1
}
}
})
function evalFootprint(metric, mean, thresholds, level) {
// mem_used has inverse logic regarding threshold levels
switch (level) {
case 'peak':
if (metric === 'mem_used') return (mean <= thresholds.peak && mean > thresholds.alert)
else return (mean <= thresholds.peak && mean > thresholds.normal)
case 'alert':
if (metric === 'mem_used') return (mean <= thresholds.alert && mean > thresholds.caution)
else return (mean <= thresholds.alert && mean >= 0)
case 'caution':
if (metric === 'mem_used') return (mean <= thresholds.caution && mean > thresholds.normal)
else return (mean <= thresholds.caution && mean > thresholds.alert)
case 'normal':
if (metric === 'mem_used') return (mean <= thresholds.normal && mean >= 0)
else return (mean <= thresholds.normal && mean > thresholds.caution)
default:
return false
}
}
</script>
<script context="module">
export function findJobThresholds(job, metricConfig, subClusterConfig) {
if (!job || !metricConfig || !subClusterConfig) {
console.warn('Argument missing for findJobThresholds!')
return null
}
const subclusterThresholds = metricConfig.subClusters.find(sc => sc.name == subClusterConfig.name)
const defaultThresholds = {
peak: subclusterThresholds ? subclusterThresholds.peak : metricConfig.peak,
normal: subclusterThresholds ? subclusterThresholds.normal : metricConfig.normal,
caution: subclusterThresholds ? subclusterThresholds.caution : metricConfig.caution,
alert: subclusterThresholds ? subclusterThresholds.alert : metricConfig.alert
}
if (job.exclusive === 1) { // Exclusive: Use as defined
return defaultThresholds
} else { // Shared: Handle specifically
if (metricConfig.name === 'cpu_load') { // Special: Avg Aggregation BUT scaled based on #hwthreads
return {
peak: job.numHWThreads,
normal: job.numHWThreads,
caution: defaultThresholds.caution,
alert: defaultThresholds.alert
}
} else if (metricConfig.aggregation === 'avg' ){
return defaultThresholds
} else if (metricConfig.aggregation === 'sum' ){
const jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
return {
peak: round((defaultThresholds.peak * jobFraction), 0),
normal: round((defaultThresholds.normal * jobFraction), 0),
caution: round((defaultThresholds.caution * jobFraction), 0),
alert: round((defaultThresholds.alert * jobFraction), 0)
}
} else {
console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
return null
}
} // Other job.exclusive cases?
}
</script>
<Card class="h-auto mt-1" style="width: {width}px;">
{#if view === 'job'}
<CardHeader>
<CardTitle class="mb-0 d-flex justify-content-center">
Core Metrics Footprint
</CardTitle>
</CardHeader>
{/if}
<CardBody>
{#each footprintData as fpd, index}
<div class="mb-1 d-flex justify-content-between">
<div>&nbsp;<b>{fpd.name}</b></div> <!-- For symmetry, see below ...-->
<div class="cursor-help d-inline-flex" id={`footprint-${job.jobId}-${index}`}>
<div class="mx-1">
<!-- Alerts Only -->
{#if fpd.impact === 3 || fpd.impact === -1}
<Icon name="exclamation-triangle-fill" class="text-danger"/>
{:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning"/>
{/if}
<!-- Emoji for all states-->
{#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger"/>
{:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning"/>
{:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success"/>
{:else if fpd.impact === 0}
<Icon name="emoji-laughing" class="text-info"/>
{:else if fpd.impact === -1}
<Icon name="emoji-dizzy" class="text-danger"/>
{/if}
</div>
<div>
<!-- Print Values -->
{fpd.avg} / {fpd.max} {fpd.unit} &nbsp; <!-- To increase margin to tooltip: No other way manageable ... -->
</div>
</div>
<Tooltip target={`footprint-${job.jobId}-${index}`} placement="right" offset={[0, 20]}>{fpd.message}</Tooltip>
</div>
<div class="mb-2">
<Progress
value={fpd.avg}
max={fpd.max}
color={fpd.color}
/>
</div>
{/each}
{#if job?.metaData?.message}
<hr class="mt-1 mb-2"/>
{@html job.metaData.message}
{/if}
</CardBody>
</Card>
<style>
.cursor-help {
cursor: help;
}
</style>

View File

@ -1,102 +0,0 @@
<script>
import { onMount, getContext } from 'svelte'
import { init } from './utils.js'
import { Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte'
import Refresher from './joblist/Refresher.svelte'
import Sorting from './joblist/SortSelection.svelte'
import MetricSelection from './MetricSelection.svelte'
import UserOrProject from './filters/UserOrProject.svelte'
const { query: initq } = init()
const ccconfig = getContext('cc-config')
export let filterPresets = {}
export let authlevel
export let roles
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobList, matchedJobs = null
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false
let metrics = filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
// The filterPresets are handled by the Filters component,
// so we need to wait for it to be ready before we can start a query.
// This is also why JobList component starts out with a paused query.
onMount(() => filterComponent.update())
</script>
<Row>
{#if $initq.fetching}
<Col xs="auto">
<Spinner/>
</Col>
{:else if $initq.error}
<Col xs="auto">
<Card body color="danger">{$initq.error.message}</Card>
</Col>
{/if}
</Row>
<Row>
<Col xs="auto">
<Button
outline color="primary"
on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up"/> Sorting
</Button>
<Button
outline color="primary"
on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up"/> Metrics
</Button>
<Button disabled outline>{matchedJobs == null ? 'Loading...' : `${matchedJobs} jobs`}</Button>
</Col>
<Col xs="auto">
<Filters
filterPresets={filterPresets}
bind:this={filterComponent}
on:update={({ detail }) => {
selectedCluster = detail.filters[0]?.cluster ? detail.filters[0].cluster.eq : null
jobList.update(detail.filters)
}
} />
</Col>
<Col xs="3" style="margin-left: auto;">
<UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filterComponent.update(detail)}/>
</Col>
<Col xs="2">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
</Row>
<br/>
<Row>
<Col>
<JobList
bind:metrics={metrics}
bind:sorting={sorting}
bind:matchedJobs={matchedJobs}
bind:this={jobList}
bind:showFootprint={showFootprint} />
</Col>
</Row>
<Sorting
bind:sorting={sorting}
bind:isOpen={isSortingOpen} />
<MetricSelection
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics={metrics}
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>

View File

@ -1,242 +0,0 @@
<!--
@component List of users or projects
-->
<script>
import { onMount } from "svelte";
import { init } from "./utils.js";
import {
Row,
Col,
Button,
Icon,
Table,
Card,
Spinner,
InputGroup,
Input,
} from "sveltestrap";
import Filters from "./filters/Filters.svelte";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
const {} = init();
export let type;
export let filterPresets;
// By default, look at the jobs of the last 30 days:
if (filterPresets?.startTime == null) {
if (filterPresets == null)
filterPresets = {}
const lastMonth = (new Date(Date.now() - (30*24*60*60*1000))).toISOString()
const now = (new Date(Date.now())).toISOString()
filterPresets.startTime = { from: lastMonth, to: now, text: 'Last 30 Days', url: 'last30d' }
}
console.assert(
type == "USER" || type == "PROJECT",
"Invalid list type provided!"
);
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobFilters = [];
let nameFilter = "";
let sorting = { field: "totalJobs", direction: "down" };
const client = getContextClient();
$: stats = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!) {
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
id
name
totalJobs
totalWalltime
totalCoreHours
totalAccHours
}
}`,
variables: { jobFilters }
});
function changeSorting(event, field) {
let target = event.target;
while (target.tagName != "BUTTON") target = target.parentElement;
let direction = target.children[0].className.includes("up")
? "down"
: "up";
target.children[0].className = `bi-sort-numeric-${direction}`;
sorting = { field, direction };
}
function sort(stats, sorting, nameFilter) {
const cmp =
sorting.field == "id"
? sorting.direction == "up"
? (a, b) => a.id < b.id
: (a, b) => a.id > b.id
: sorting.direction == "up"
? (a, b) => a[sorting.field] - b[sorting.field]
: (a, b) => b[sorting.field] - a[sorting.field];
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
}
onMount(() => filterComponent.update());
</script>
<Row>
<Col xs="auto">
<InputGroup>
<Button disabled outline>
Search {type.toLowerCase()}s
</Button>
<Input
bind:value={nameFilter}
placeholder="Filter by {{
USER: 'username',
PROJECT: 'project',
}[type]}"
/>
</InputGroup>
</Col>
<Col xs="auto">
<Filters
bind:this={filterComponent}
{filterPresets}
startTimeQuickSelect={true}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
on:update={({ detail }) => {
jobFilters = detail.filters;
}}
/>
</Col>
</Row>
<Table>
<thead>
<tr>
<th scope="col">
{({
USER: "Username",
PROJECT: "Project Name",
})[type]}
<Button
color={sorting.field == "id" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "id")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{#if type == "USER"}
<th scope="col">
Name
<Button
color={sorting.field == "name" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "name")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{/if}
<th scope="col">
Total Jobs
<Button
color={sorting.field == "totalJobs" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalJobs")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Walltime
<Button
color={sorting.field == "totalWalltime"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalWalltime")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Core Hours
<Button
color={sorting.field == "totalCoreHours"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalCoreHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Accelerator Hours
<Button
color={sorting.field == "totalAccHours"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalAccHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
</tr>
</thead>
<tbody>
{#if $stats.fetching}
<tr>
<td colspan="4" style="text-align: center;"
><Spinner secondary /></td
>
</tr>
{:else if $stats.error}
<tr>
<td colspan="4"
><Card body color="danger" class="mb-3"
>{$stats.error.message}</Card
></td
>
</tr>
{:else if $stats.data}
{#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
<tr>
<td>
{#if type == "USER"}
<a href="/monitoring/user/{row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else if type == "PROJECT"}
<a href="/monitoring/jobs/?project={row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else}
{row.id}
{/if}
</td>
{#if type == "USER"}
<td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
{/if}
<td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td>
<td>{row.totalCoreHours}</td>
<td>{row.totalAccHours}</td>
</tr>
{:else}
<tr>
<td colspan="4"
><i>No {type.toLowerCase()}s/jobs found</i></td
>
</tr>
{/each}
{/if}
</tbody>
</Table>

View File

@ -1,160 +0,0 @@
<script>
import { init, checkMetricDisabled } from './utils.js'
import Refresher from './joblist/Refresher.svelte'
import { Row, Col, Input, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap'
import { queryStore, gql, getContextClient } from '@urql/svelte'
import TimeSelection from './filters/TimeSelection.svelte'
import PlotTable from './PlotTable.svelte'
import MetricPlot from './plots/MetricPlot.svelte'
import { getContext } from 'svelte'
export let cluster
export let from = null
export let to = null
const { query: initq } = init()
if (from == null || to == null) {
to = new Date(Date.now())
from = new Date(to.getTime())
from.setMinutes(from.getMinutes() - 30)
}
const clusters = getContext('clusters')
console.log(clusters)
const ccconfig = getContext('cc-config')
const metricConfig = getContext('metrics')
let plotHeight = 300
let hostnameFilter = ''
let selectedMetric = ccconfig.system_view_selectedMetric
const client = getContextClient();
$: nodesQuery = queryStore({
client: client,
query: gql`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit { base, prefix }
series {
statistics { min, avg, max }
data
}
}
}
}
}`,
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString()
}
})
let metricUnits = {}
$: if ($nodesQuery.data) {
let thisCluster = clusters.find(c => c.name == cluster)
if (thisCluster) {
for (let metric of thisCluster.metricConfig) {
if (metric.unit.prefix || metric.unit.base) {
metricUnits[metric.name] = '(' + (metric.unit.prefix ? metric.unit.prefix : '') + (metric.unit.base ? metric.unit.base : '') + ')'
} else { // If no unit defined: Omit Unit Display
metricUnits[metric.name] = ''
}
}
}
}
</script>
<Row>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
<Spinner/>
{:else}
<Col>
<Refresher on:reload={() => {
const diff = Date.now() - to
from = new Date(from.getTime() + diff)
to = new Date(to.getTime() + diff)
}} />
</Col>
<Col>
<TimeSelection
bind:from={from}
bind:to={to} />
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}>
{#each clusters.find(c => c.name == cluster).metricConfig as metric}
<option value={metric.name}>{metric.name} {metricUnits[metric.name]}</option>
{/each}
</select>
</InputGroup>
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText>
<Input placeholder="hostname..." type="text" bind:value={hostnameFilter} />
</InputGroup>
</Col>
{/if}
</Row>
<br/>
<Row>
<Col>
{#if $nodesQuery.error}
<Card body color="danger">{$nodesQuery.error.message}</Card>
{:else if $nodesQuery.fetching || $initq.fetching}
<Spinner/>
{:else}
<PlotTable
let:item
let:width
renderFor="systems"
itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics
.filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.scope == 'node'))
.map(h => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.find(m => m.name == selectedMetric && m.scope == 'node'),
disabled: checkMetricDisabled(selectedMetric, cluster, h.subCluster)
}))
.sort((a, b) => a.host.localeCompare(b.host))
}>
<h4 style="width: 100%; text-align: center;"><a style="display: block;padding-top: 15px;" href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4>
{#if item.disabled === false && item.data}
<MetricPlot
width={width}
height={plotHeight}
timestep={item.data.metric.timestep}
series={item.data.metric.series}
metric={item.data.name}
cluster={clusters.find(c => c.name == cluster)}
subCluster={item.subCluster}
resources={[{hostname: item.host}]}
forNode={true}/>
{:else if item.disabled === true && item.data}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{selectedMetric}:{item.subCluster}</code></Card>
{:else}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{selectedMetric}</code></Card>
{/if}
</PlotTable>
{/if}
</Col>
</Row>

View File

@ -1,237 +0,0 @@
<script>
import { onMount, getContext } from 'svelte'
import { init, convert2uplot } from './utils.js'
import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
import { queryStore, gql, getContextClient } from '@urql/svelte'
import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte'
import Sorting from './joblist/SortSelection.svelte'
import Refresher from './joblist/Refresher.svelte'
import Histogram from './plots/Histogram.svelte'
import MetricSelection from './MetricSelection.svelte'
import HistogramSelection from './HistogramSelection.svelte'
import PlotTable from './PlotTable.svelte'
import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
const { query: initq } = init()
const ccconfig = getContext('cc-config')
export let user
export let filterPresets
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobList;
let jobFilters = [];
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
let w1, w2, histogramHeight = 250, isHistogramSelectionOpen = false
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
$: metricsInHistograms = selectedCluster ? (ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
const client = getContextClient();
$: stats = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
totalJobs
shortJobs
totalWalltime
totalCoreHours
histDuration { count, value }
histNumNodes { count, value }
histMetrics { metric, unit, data { min, max, count, bin } }
}}`,
variables: { jobFilters, metricsInHistograms }
})
onMount(() => filterComponent.update())
</script>
<Row>
{#if $initq.fetching}
<Col>
<Spinner/>
</Col>
{:else if $initq.error}
<Col xs="auto">
<Card body color="danger">{$initq.error.message}</Card>
</Col>
{/if}
<Col xs="auto">
<Button
outline color="primary"
on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up"/> Sorting
</Button>
<Button
outline color="primary"
on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up"/> Metrics
</Button>
<Button
outline color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}>
<Icon name="bar-chart-line"/> Select Histograms
</Button>
</Col>
<Col xs="auto">
<Filters
filterPresets={filterPresets}
startTimeQuickSelect={true}
bind:this={filterComponent}
on:update={({ detail }) => {
jobFilters = [...detail.filters, { user: { eq: user.username } }]
selectedCluster = jobFilters[0]?.cluster ? jobFilters[0].cluster.eq : null
jobList.update(jobFilters)
}} />
</Col>
<Col xs="auto" style="margin-left: auto;">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
</Row>
<br/>
<Row>
{#if $stats.error}
<Col>
<Card body color="danger">{$stats.error.message}</Card>
</Col>
{:else if !$stats.data}
<Col>
<Spinner secondary />
</Col>
{:else}
<Col xs="4">
<Table>
<tbody>
<tr>
<th scope="row">Username</th>
<td>{scrambleNames ? scramble(user.username) : user.username}</td>
</tr>
{#if user.name}
<tr>
<th scope="row">Name</th>
<td>{scrambleNames ? scramble(user.name) : user.name}</td>
</tr>
{/if}
{#if user.email}
<tr>
<th scope="row">Email</th>
<td>{user.email}</td>
</tr>
{/if}
<tr>
<th scope="row">Total Jobs</th>
<td>{$stats.data.jobsStatistics[0].totalJobs}</td>
</tr>
<tr>
<th scope="row">Short Jobs</th>
<td>{$stats.data.jobsStatistics[0].shortJobs}</td>
</tr>
<tr>
<th scope="row">Total Walltime</th>
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
</tr>
<tr>
<th scope="row">Total Core Hours</th>
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
</tr>
</tbody>
</Table>
</Col>
<div class="col-4 text-center" bind:clientWidth={w1}>
{#key $stats.data.jobsStatistics[0].histDuration}
<Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
width={w1 - 25} height={histogramHeight}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
<div class="col-4 text-center" bind:clientWidth={w2}>
{#key $stats.data.jobsStatistics[0].histNumNodes}
<Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
width={w2 - 25} height={histogramHeight}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
{/if}
</Row>
{#if metricsInHistograms}
<Row>
{#if $stats.error}
<Col>
<Card body color="danger">{$stats.error.message}</Card>
</Col>
{:else if !$stats.data}
<Col>
<Spinner secondary />
</Col>
{:else}
<Col>
{#key $stats.data.jobsStatistics[0].histMetrics}
<PlotTable
let:item
let:width
renderFor="user"
items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3}>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
width={width} height={250}
title="Distribution of '{item.metric}' averages"
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
xunit={item.unit}
ylabel="Number of Jobs"
yunit="Jobs"/>
</PlotTable>
{/key}
</Col>
{/if}
</Row>
{/if}
<br/>
<Row>
<Col>
<JobList
bind:metrics={metrics}
bind:sorting={sorting}
bind:this={jobList}
bind:showFootprint={showFootprint} />
</Col>
</Row>
<Sorting
bind:sorting={sorting}
bind:isOpen={isSortingOpen} />
<MetricSelection
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics={metrics}
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>
<HistogramSelection
bind:cluster={selectedCluster}
bind:metricsInHistograms={metricsInHistograms}
bind:isOpen={isHistogramSelectionOpen} />

View File

@ -1,14 +0,0 @@
import {} from './header.entrypoint.js'
import Analysis from './Analysis.root.svelte'
filterPresets.cluster = cluster
new Analysis({
target: document.getElementById('svelte-app'),
props: {
filterPresets: filterPresets
},
context: new Map([
['cc-config', clusterCockpitConfig]
])
})

View File

@ -1,11 +1,10 @@
import {} from './header.entrypoint.js'
import User from './User.root.svelte'
import Home from './Home.root.svelte'
new User({
new Home({
target: document.getElementById('svelte-app'),
props: {
filterPresets: filterPresets,
user: userInfos
isAdmin: isAdmin
},
context: new Map([
['cc-config', clusterCockpitConfig]

View File

@ -1,14 +0,0 @@
import {} from './header.entrypoint.js'
import Job from './Job.root.svelte'
new Job({
target: document.getElementById('svelte-app'),
props: {
dbid: jobInfos.id,
authlevel: authlevel,
roles: roles
},
context: new Map([
['cc-config', clusterCockpitConfig]
])
})

View File

@ -1,14 +0,0 @@
import {} from './header.entrypoint.js'
import Jobs from './Jobs.root.svelte'
new Jobs({
target: document.getElementById('svelte-app'),
props: {
filterPresets: filterPresets,
authlevel: authlevel,
roles: roles
},
context: new Map([
['cc-config', clusterCockpitConfig]
])
})

View File

@ -1,13 +0,0 @@
import {} from './header.entrypoint.js'
import List from './List.root.svelte'
new List({
target: document.getElementById('svelte-app'),
props: {
filterPresets: filterPresets,
type: listType,
},
context: new Map([
['cc-config', clusterCockpitConfig]
])
})

View File

@ -1,14 +0,0 @@
import {} from './header.entrypoint.js'
import Systems from './Systems.root.svelte'
new Systems({
target: document.getElementById('svelte-app'),
props: {
cluster: infos.cluster,
from: infos.from,
to: infos.to
},
context: new Map([
['cc-config', clusterCockpitConfig]
])
})

View File

@ -1,57 +1,15 @@
{{define "content"}}
{{if .Infos.message }}
<div class="row justify-content-center">
<div class="col-6">
<div class="alert alert-info p-3" role="alert">
<div class="row align-items-center">
<div class="col-2">
<h2><i class="bi-info-circle-fill m-3"></i></h2>
</div>
<div class="col-10">
{{.Infos.message}}
</div>
</div>
</div>
</div>
</div>
<div id="svelte-app"></div>
{{end}}
<div class="row">
<div class="col">
<h2>Clusters</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Running Jobs</th>
<th>Total Jobs</th>
{{if .User.HasRole .Roles.admin}}
<th>Status View</th>
<th>System View</th>
{{end}}
</tr>
</thead>
<tbody>
{{if .User.HasRole .Roles.admin}}
{{range .Infos.clusters}}
<tr>
<td>{{.ID}}</td>
<td><a href="/monitoring/jobs/?cluster={{.ID}}&state=running">{{.RunningJobs}} jobs</a></td>
<td><a href="/monitoring/jobs/?cluster={{.ID}}">{{.TotalJobs}} jobs</a></td>
<td><a href="/monitoring/status/{{.ID}}">Status View</a></td>
<td><a href="/monitoring/systems/{{.ID}}">System View</a></td>
</tr>
{{end}}
{{else}}
{{range .Infos.clusters}}
<tr>
<td>{{.ID}}</td>
<td><a href="/monitoring/jobs/?cluster={{.ID}}&state=running">{{.RunningJobs}} jobs</a></td>
<td><a href="/monitoring/jobs/?cluster={{.ID}}">{{.TotalJobs}} jobs</a></td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
</div>
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/home.css'>
{{end}}
{{define "javascript"}}
<script>
const isAdmin = {{ .User.HasRole .Roles.admin }};
const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }};
</script>
<script src='/build/home.js'></script>
{{end}}

View File

@ -1,15 +0,0 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/analysis.css'>
{{end}}
{{define "javascript"}}
<script>
const cluster = {{ .Infos.cluster }};
const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }};
</script>
<script src='/build/analysis.js'></script>
{{end}}

View File

@ -1,20 +0,0 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/job.css'>
{{end}}
{{define "javascript"}}
<script>
const jobInfos = {
id: "{{ .Infos.id }}",
jobId: "{{ .Infos.jobId }}",
clusterId: "{{ .Infos.clusterId }}"
};
const clusterCockpitConfig = {{ .Config }};
const authlevel = {{ .User.GetAuthLevel }};
const roles = {{ .Roles }};
</script>
<script src='/build/job.js'></script>
{{end}}

View File

@ -1,17 +0,0 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/jobs.css'>
{{end}}
{{define "javascript"}}
<script>
const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }};
const authlevel = {{ .User.GetAuthLevel }};
const roles = {{ .Roles }};
</script>
<script src='/build/jobs.js'></script>
{{end}}

View File

@ -1,15 +0,0 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/list.css'>
{{end}}
{{define "javascript"}}
<script>
const listType = {{ .Infos.listType }};
const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }};
</script>
<script src='/build/list.js'></script>
{{end}}

View File

@ -1,14 +0,0 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/systems.css'>
{{end}}
{{define "javascript"}}
<script>
const infos = {{ .Infos }};
const clusterCockpitConfig = {{ .Config }};
</script>
<script src='/build/systems.js'></script>
{{end}}

View File

@ -1,17 +0,0 @@
{{define "content"}}
<div class="container">
<div class="row justify-content-center">
<div class="col-10">
{{ range $tagType, $tagList := .Infos.tagmap }}
<div class="my-3 p-2 bg-secondary text-white text-capitalize">
{{ $tagType }}
</div>
{{ range $tagList }}
<a class="btn btn-lg btn-warning" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{end}}
{{end}}
</div>
</div>
</div>
{{end}}

View File

@ -1,15 +0,0 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/user.css'>
{{end}}
{{define "javascript"}}
<script>
const userInfos = {{ .Infos }};
const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }};
</script>
<script src='/build/user.js'></script>
{{end}}