-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(front): resources/jobs charts in dashboard [WIP]
- Loading branch information
Showing
7 changed files
with
354 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<!-- | ||
Copyright (c) 2024 Rackslab | ||
|
||
This file is part of Slurm-web. | ||
|
||
SPDX-License-Identifier: GPL-3.0-or-later | ||
--> | ||
|
||
<script setup lang="ts"> | ||
import HistoricalNodesDiagram from '@/components/dashboard/HistoricalNodesDiagram.vue' | ||
import HistoricalJobsDiagram from '@/components/dashboard/HistoricalJobsDiagram.vue' | ||
</script> | ||
|
||
<template> | ||
<HistoricalNodesDiagram /> | ||
<HistoricalJobsDiagram /> | ||
</template> |
40 changes: 40 additions & 0 deletions
40
frontend/src/components/dashboard/HistoricalJobsDiagram.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<!-- | ||
Copyright (c) 2024 Rackslab | ||
|
||
This file is part of Slurm-web. | ||
|
||
SPDX-License-Identifier: GPL-3.0-or-later | ||
--> | ||
|
||
<script setup lang="ts"></script> | ||
|
||
<template> | ||
<div class="border-gray-200p border-b pb-5 pt-16 sm:flex sm:items-center sm:justify-between"> | ||
<h3 class="text-base font-semibold text-gray-900">Jobs queue</h3> | ||
<div class="mt-3 flex sm:ml-4 sm:mt-0"> | ||
<span class="isolate inline-flex rounded-md shadow-sm"> | ||
<button | ||
type="button" | ||
class="relative inline-flex items-center rounded-l-md bg-white px-3 py-2 text-xs font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" | ||
> | ||
1w | ||
</button> | ||
<button | ||
type="button" | ||
class="relative -ml-px inline-flex items-center bg-white px-3 py-2 text-xs font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" | ||
> | ||
1d | ||
</button> | ||
<button | ||
type="button" | ||
class="relative -ml-px inline-flex items-center rounded-r-md bg-white px-3 py-2 text-xs font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" | ||
> | ||
1h | ||
</button> | ||
</span> | ||
</div> | ||
</div> | ||
<div class="h-96 w-full"> | ||
<canvas ref="chart"></canvas> | ||
</div> | ||
</template> |
237 changes: 237 additions & 0 deletions
237
frontend/src/components/dashboard/HistoricalNodesDiagram.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
<!-- | ||
Copyright (c) 2024 Rackslab | ||
|
||
This file is part of Slurm-web. | ||
|
||
SPDX-License-Identifier: GPL-3.0-or-later | ||
--> | ||
|
||
<script setup lang="ts"> | ||
import { computed, onMounted, ref, watch } from 'vue' | ||
import { useClusterDataGetter } from '@/composables/DataGetter' | ||
import { Chart } from 'chart.js/auto' | ||
import type { TimeScaleOptions, TimeUnit } from 'chart.js' | ||
import 'chartjs-adapter-luxon' | ||
import { DateTime } from 'luxon' | ||
import type { MetricValue, MetricResourceState } from '@/composables/GatewayAPI' | ||
import ErrorAlert from '@/components/ErrorAlert.vue' | ||
|
||
import { Switch } from '@headlessui/vue' | ||
|
||
const coresToggle = ref(false) | ||
|
||
type MetricTimeFrame = 'week' | 'day' | 'hour' | ||
const timeframe = ref<MetricTimeFrame>('hour') | ||
const metrics = useClusterDataGetter<Record<MetricResourceState, MetricValue[]>>( | ||
'metrics_nodes', | ||
timeframe.value | ||
) | ||
const chartCanvas = ref<HTMLCanvasElement | undefined>() | ||
|
||
let chart: Chart | undefined = undefined | ||
|
||
const states_colors: Record<MetricResourceState, string> = { | ||
idle: 'rgb(51, 204, 51, 0.7)', // green | ||
down: 'rgb(204, 0, 0, 0.7)', // ref | ||
mixed: 'rgba(255, 204, 0, 0.7)', // yellow | ||
allocated: 'rgba(204, 153, 0, 0.7)', // dark yellow | ||
drain: 'rgb(204, 0, 153, 0.7)', // purple | ||
unknown: 'rgb(192, 191, 188, 0.7)' // grey | ||
} | ||
|
||
/* Update charts datasets when metrics are received */ | ||
const datasets = computed(() => { | ||
if (!metrics.data.value) return [] | ||
let result = [] | ||
for (const state of ['unknown', 'down', 'drain', 'allocated', 'mixed', 'idle']) { | ||
if (!(state in metrics.data.value)) continue | ||
result.push({ | ||
label: state, | ||
data: metrics.data.value[state as MetricResourceState].map((value) => ({ | ||
x: value[0] * 1000, | ||
y: parseFloat(value[1]) | ||
})), | ||
barPercentage: 1, | ||
fill: 'stack', | ||
backgroundColor: states_colors[state as MetricResourceState] | ||
}) | ||
} | ||
return result | ||
}) | ||
|
||
watch(datasets, (new_datasets) => { | ||
if (chart) { | ||
chart.data.datasets = new_datasets | ||
if (chart.options.scales && chart.options.scales.x) { | ||
chart.options.scales.x.suggestedMin = suggestedMin() | ||
;(chart.options.scales.x as TimeScaleOptions).time.unit = timeframeUnit() | ||
} | ||
chart.update() | ||
} | ||
}) | ||
|
||
watch(coresToggle, (newCoresToggle) => { | ||
if (newCoresToggle) { | ||
metrics.setCallback('metrics_cores') | ||
} else { | ||
metrics.setCallback('metrics_nodes') | ||
} | ||
}) | ||
|
||
function updateTimeFrame(newTimeframe: MetricTimeFrame) { | ||
timeframe.value = newTimeframe | ||
metrics.setParam(newTimeframe) | ||
} | ||
|
||
function suggestedMin() { | ||
const now = Date.now() | ||
let result = 0 | ||
if (timeframe.value == 'hour') { | ||
result = now - 60 * 60 * 1000 | ||
} | ||
if (timeframe.value == 'day') { | ||
result = now - 24 * 60 * 60 * 1000 | ||
} | ||
if (timeframe.value == 'week') { | ||
result = now - 7 * 24 * 60 * 60 * 1000 | ||
} | ||
return result | ||
} | ||
|
||
function timeframeUnit(): TimeUnit { | ||
if (timeframe.value == 'hour') { | ||
return 'minute' | ||
} | ||
return 'hour' | ||
} | ||
|
||
function ticksCallback(value: number | string) { | ||
if (typeof value === 'number') { | ||
const dt = DateTime.fromMillis(value) | ||
if (timeframe.value == 'hour' && value % (1000 * 60 * 5) === 0) | ||
return dt.toLocaleString(DateTime.TIME_SIMPLE) | ||
if (timeframe.value == 'day' && value % (1000 * 60 * 60) === 0) | ||
return dt.toLocaleString(DateTime.TIME_SIMPLE) | ||
if (timeframe.value == 'week') { | ||
if (value % (1000 * 60 * 60 * 24) === 0) { | ||
return dt.toLocaleString({ month: 'numeric', day: 'numeric' }) | ||
} | ||
if (value % (1000 * 60 * 60 * 12) === 0) { | ||
return '' | ||
} | ||
} | ||
} | ||
} | ||
|
||
onMounted(() => { | ||
if (chartCanvas.value) { | ||
chart = new Chart(chartCanvas.value, { | ||
type: 'bar', | ||
data: { | ||
datasets: [] | ||
}, | ||
options: { | ||
responsive: true, | ||
maintainAspectRatio: false, | ||
scales: { | ||
y: { | ||
stacked: true, | ||
beginAtZero: true, | ||
ticks: { | ||
callback: (value) => { | ||
// only integer on y axis | ||
if (typeof value !== 'number') return value | ||
if (value % 1 === 0) { | ||
return value | ||
} | ||
} | ||
} | ||
}, | ||
x: { | ||
type: 'time', | ||
stacked: true, | ||
grid: { | ||
offset: false | ||
}, | ||
ticks: { | ||
callback: ticksCallback | ||
} | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div class="border-gray-200p border-b pb-5 pt-16 sm:flex sm:items-center sm:justify-between"> | ||
<h3 class="text-base font-semibold text-gray-900">Resources Status</h3> | ||
<div v-show="!metrics.unable.value" class="mt-3 flex sm:ml-4 sm:mt-0"> | ||
<span class="isolate inline-flex rounded-md shadow-sm"> | ||
<span class="inline-flex items-center pr-4 text-sm"> | ||
<span class="pr-2">Nodes</span> | ||
|
||
<Switch | ||
v-model="coresToggle" | ||
:class="[ | ||
coresToggle ? 'bg-slurmweb' : 'bg-gray-200', | ||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:bg-slurmweb focus:outline-none focus:ring-2 focus:ring-offset-2' | ||
]" | ||
> | ||
<span | ||
aria-hidden="true" | ||
:class="[ | ||
coresToggle ? 'translate-x-5' : 'translate-x-0', | ||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out' | ||
]" | ||
/> | ||
</Switch> | ||
<span class="pl-2">Cores</span> | ||
</span> | ||
<button | ||
type="button" | ||
:class="[ | ||
timeframe == 'week' | ||
? 'bg-slurmweb text-white' | ||
: 'bg-white text-gray-900 hover:bg-gray-50', | ||
'relative inline-flex items-center rounded-l-md px-3 py-2 text-xs font-semibold ring-1 ring-inset ring-gray-300 focus:z-10' | ||
]" | ||
@click="updateTimeFrame('week')" | ||
> | ||
week | ||
</button> | ||
<button | ||
type="button" | ||
:class="[ | ||
timeframe == 'day' | ||
? 'bg-slurmweb text-white' | ||
: 'bg-white text-gray-900 hover:bg-gray-50', | ||
'relative inline-flex items-center px-3 py-2 text-xs font-semibold ring-1 ring-inset ring-gray-300 focus:z-10' | ||
]" | ||
@click="updateTimeFrame('day')" | ||
> | ||
day | ||
</button> | ||
<button | ||
type="button" | ||
:class="[ | ||
timeframe == 'hour' | ||
? 'bg-slurmweb text-white' | ||
: 'bg-white text-gray-900 hover:bg-gray-50', | ||
'relative inline-flex items-center rounded-r-md px-3 py-2 text-xs font-semibold ring-1 ring-inset ring-gray-300 focus:z-10' | ||
]" | ||
@click="updateTimeFrame('hour')" | ||
> | ||
hour | ||
</button> | ||
</span> | ||
</div> | ||
</div> | ||
<ErrorAlert v-if="metrics.unable.value" class="mt-4" | ||
>Unable to retrieve resource metric.</ErrorAlert | ||
> | ||
<div v-else class="h-96 w-full"> | ||
<canvas ref="chartCanvas"></canvas> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.