Skip to content

Commit

Permalink
feat(front): resources/jobs charts in dashboard [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
rezib committed Nov 5, 2024
1 parent cf1bf51 commit fdde83e
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 7 deletions.
17 changes: 17 additions & 0 deletions frontend/src/components/dashboard/DashboardDiagrams.vue
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 frontend/src/components/dashboard/HistoricalJobsDiagram.vue
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 frontend/src/components/dashboard/HistoricalNodesDiagram.vue
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>
27 changes: 21 additions & 6 deletions frontend/src/composables/DataGetter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export function useClusterDataGetter<Type>(
callback: GatewayAnyClusterApiKey,
otherParam?: string | number
) {
let _callback = callback
let _otherParam = otherParam
const data: Ref<Type | undefined> = ref()
const unable: Ref<boolean> = ref(false)
const loaded: Ref<boolean> = ref(false)
Expand All @@ -99,12 +101,12 @@ export function useClusterDataGetter<Type>(
try {
unable.value = false

if (gateway.isValidGatewayClusterWithStringAPIKey(callback)) {
data.value = (await gateway[callback](cluster, otherParam as string)) as Type
} else if (gateway.isValidGatewayClusterWithNumberAPIKey(callback)) {
data.value = (await gateway[callback](cluster, otherParam as number)) as Type
if (gateway.isValidGatewayClusterWithStringAPIKey(_callback)) {
data.value = (await gateway[_callback](cluster, _otherParam as string)) as Type
} else if (gateway.isValidGatewayClusterWithNumberAPIKey(_callback)) {
data.value = (await gateway[_callback](cluster, _otherParam as number)) as Type
} else {
data.value = (await gateway[callback](cluster)) as Type
data.value = (await gateway[_callback](cluster)) as Type
}
loaded.value = true
} catch (error: any) {
Expand All @@ -124,6 +126,19 @@ export function useClusterDataGetter<Type>(
}
}

function setCallback(callback: GatewayAnyClusterApiKey, otherParam?: string | number) {
_callback = callback
if (runtime.currentCluster) {
get(runtime.currentCluster.name)
}
}
function setParam(otherParam: string | number) {
_otherParam = otherParam
if (runtime.currentCluster) {
get(runtime.currentCluster.name)
}
}

watch(
() => runtime.currentCluster,
(newCluster, oldCluster) => {
Expand All @@ -141,5 +156,5 @@ export function useClusterDataGetter<Type>(
get(runtime.currentCluster.name)
}
})
return { data, unable, loaded }
return { data, unable, loaded, setCallback, setParam }
}
Loading

0 comments on commit fdde83e

Please sign in to comment.