Skip to content

Commit

Permalink
feat(front): resources/jobs charts in dashboard
Browse files Browse the repository at this point in the history
Display charts of resources (nodes/cores) status and jobs queue in
dashboard page based on metrics from Prometheus, when metrics feature is
enabled.

fix #275
  • Loading branch information
rezib committed Nov 7, 2024
1 parent 8dd4ca9 commit bcf8eab
Show file tree
Hide file tree
Showing 11 changed files with 585 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Display service message below login form if defined (#253).
- Add dependency on _charts.js_ and _luxon_ adapter and draw charts with
timeseries metrics.
- Display charts of resources (nodes/cores) status and jobs queue in dashboard
page based on metrics from Prometheus (#275).
- conf:
- Add `racksdb` > `infrastructure` parameter for the agent.
- Add `metrics` > `enabled` parameter for the agent.
Expand Down
1 change: 1 addition & 0 deletions frontend/public/chart_placeholder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions frontend/src/components/dashboard/ChartJobsHistogram.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!--
Copyright (c) 2024 Rackslab

This file is part of Slurm-web.

SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import { ref } from 'vue'
import { useDashboardLiveChart } from '@/composables/dashboard/LiveChart'
import type { MetricJobState } from '@/composables/GatewayAPI'
import ErrorAlert from '@/components/ErrorAlert.vue'

const chartCanvas = ref<HTMLCanvasElement | null>(null)

const statesColors: Record<MetricJobState, string> = {
running: 'rgb(51, 204, 51, 0.7)', // green
pending: 'rgba(255, 204, 0, 0.7)', // yellow
completing: 'rgba(204, 153, 0, 0.7)', // dark yellow
completed: 'rgb(192, 191, 188, 0.7)', // grey
cancelled: 'rgb(204, 0, 153, 0.7)', // purple
unknown: 'rgb(30, 30, 30, 0.7)' // dark grey
}

const liveChart = useDashboardLiveChart<MetricJobState>('metrics_jobs', chartCanvas, statesColors, [
'unknown',
'cancelled',
'completed',
'completing',
'running',
'pending'
])
</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>
<ErrorAlert v-if="liveChart.metrics.unable.value" class="mt-4"
>Unable to retrieve jobs metrics.</ErrorAlert
>
<div v-else class="h-96 w-full">
<img
v-show="!liveChart.metrics.loaded.value"
class="h-full object-fill"
src="/chart_placeholder.svg"
alt="Loading chart"
/>
<canvas v-show="liveChart.metrics.loaded.value" ref="chartCanvas"></canvas>
</div>
</template>
97 changes: 97 additions & 0 deletions frontend/src/components/dashboard/ChartResourcesHistogram.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<!--
Copyright (c) 2024 Rackslab

This file is part of Slurm-web.

SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import { onBeforeMount, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import type { LocationQueryRaw } from 'vue-router'
import { useRuntimeStore } from '@/stores/runtime'
import { useDashboardLiveChart } from '@/composables/dashboard/LiveChart'
import type { MetricResourceState } from '@/composables/GatewayAPI'
import ErrorAlert from '@/components/ErrorAlert.vue'
import { Switch } from '@headlessui/vue'

const router = useRouter()
const route = useRoute()
const runtimeStore = useRuntimeStore()
const chartCanvas = ref<HTMLCanvasElement | null>(null)

const statesColors: Record<MetricResourceState, string> = {
idle: 'rgb(51, 204, 51, 0.7)', // green
down: 'rgb(204, 0, 0, 0.7)', // red
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
}

const liveChart = useDashboardLiveChart<MetricResourceState>(
runtimeStore.dashboard.coresToggle ? 'metrics_cores' : 'metrics_nodes',
chartCanvas,
statesColors,
['unknown', 'down', 'drain', 'allocated', 'mixed', 'idle']
)

/* Clear chart datasets and set new poller callback when dashboard range is
* modified. */
watch(
() => runtimeStore.dashboard.coresToggle,
() => {
router.push({ name: 'dashboard', query: runtimeStore.dashboard.query() as LocationQueryRaw })
if (runtimeStore.dashboard.coresToggle) {
liveChart.setCallback('metrics_cores')
} else {
liveChart.setCallback('metrics_nodes')
}
}
)

onBeforeMount(() => {
if (route.query.cores && typeof route.query.cores === 'string' && route.query.cores === 'true') {
/* Retrieve the range criteria from query and update the store */
runtimeStore.dashboard.coresToggle = true
}
})
</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 class="mt-3 inline-flex items-center text-sm sm:ml-4 sm:mt-0">
<span class="pr-2">Nodes</span>
<Switch
v-model="runtimeStore.dashboard.coresToggle"
:class="[
runtimeStore.dashboard.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="[
runtimeStore.dashboard.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>
</div>
</div>
<ErrorAlert v-if="liveChart.metrics.unable.value" class="mt-4"
>Unable to retrieve resource metrics.</ErrorAlert
>
<div v-else class="h-96 w-full">
<img
v-show="!liveChart.metrics.loaded.value"
class="h-full object-fill"
src="/chart_placeholder.svg"
alt="Loading chart"
/>
<canvas v-show="liveChart.metrics.loaded.value" ref="chartCanvas"></canvas>
</div>
</template>
82 changes: 82 additions & 0 deletions frontend/src/components/dashboard/DashboardCharts.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!--
Copyright (c) 2024 Rackslab

This file is part of Slurm-web.

SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import { onBeforeMount } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import type { LocationQueryRaw } from 'vue-router'
import { useRuntimeStore } from '@/stores/runtime'
import ChartResourcesHistogram from '@/components/dashboard/ChartResourcesHistogram.vue'
import ChartJobsHistogram from '@/components/dashboard/ChartJobsHistogram.vue'
import { isMetricRange, type MetricRange } from '@/composables/GatewayAPI'

const router = useRouter()
const route = useRoute()
const runtimeStore = useRuntimeStore()

function setRange(range: MetricRange) {
runtimeStore.dashboard.range = range
router.push({ name: 'dashboard', query: runtimeStore.dashboard.query() as LocationQueryRaw })
}

onBeforeMount(() => {
if (route.query.range && isMetricRange(route.query.range)) {
/* Retrieve the range criteria from query and update the store */
runtimeStore.dashboard.range = route.query.range
} else {
runtimeStore.dashboard.range = 'hour'
}
})
</script>

<template>
<div class="border-gray-200p pt-16">
<div class="mt-3 text-right sm:mt-0">
<span class="isolate inline-flex rounded-md shadow-sm">
<button
type="button"
:class="[
runtimeStore.dashboard.range == '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="setRange('week')"
>
week
</button>
<button
type="button"
:class="[
runtimeStore.dashboard.range == '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="setRange('day')"
>
day
</button>
<button
type="button"
:class="[
runtimeStore.dashboard.range == '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="setRange('hour')"
>
hour
</button>
</span>
</div>
</div>
<ChartResourcesHistogram v-if="runtimeStore.hasPermission('view-nodes')" />
<ChartJobsHistogram v-if="runtimeStore.hasPermission('view-jobs')" />
</template>
25 changes: 21 additions & 4 deletions frontend/src/composables/DataGetter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ export function useGatewayDataGetter<Type>(
}

export function useClusterDataGetter<Type>(
callback: GatewayAnyClusterApiKey,
otherParam?: string | number
initialCallback: GatewayAnyClusterApiKey,
initialOtherParam?: string | number
) {
let callback = initialCallback
let otherParam = initialOtherParam
const data: Ref<Type | undefined> = ref()
const unable: Ref<boolean> = ref(false)
const loaded: Ref<boolean> = ref(false)
Expand All @@ -98,7 +100,6 @@ export function useClusterDataGetter<Type>(
async function get(cluster: string) {
try {
unable.value = false

if (gateway.isValidGatewayClusterWithStringAPIKey(callback)) {
data.value = (await gateway[callback](cluster, otherParam as string)) as Type
} else if (gateway.isValidGatewayClusterWithNumberAPIKey(callback)) {
Expand All @@ -124,6 +125,22 @@ export function useClusterDataGetter<Type>(
}
}

function setCallback(newCallback: GatewayAnyClusterApiKey) {
callback = newCallback
loaded.value = false
if (runtime.currentCluster) {
get(runtime.currentCluster.name)
}
}

function setParam(newOtherParam: string | number) {
otherParam = newOtherParam
loaded.value = false
if (runtime.currentCluster) {
get(runtime.currentCluster.name)
}
}

watch(
() => runtime.currentCluster,
(newCluster, oldCluster) => {
Expand All @@ -141,5 +158,5 @@ export function useClusterDataGetter<Type>(
get(runtime.currentCluster.name)
}
})
return { data, unable, loaded }
return { data, unable, loaded, setCallback, setParam }
}
32 changes: 27 additions & 5 deletions frontend/src/composables/DataPoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ import { useGatewayAPI } from '@/composables/GatewayAPI'
import type { GatewayAnyClusterApiKey } from '@/composables/GatewayAPI'
import { useRuntimeStore } from '@/stores/runtime'

type ClusterDataPoller<Type> = {
data: Ref<Type | undefined>
export interface ClusterDataPoller<ResponseType> {
data: Ref<ResponseType | undefined>
unable: Ref<boolean>
loaded: Ref<boolean>
setCallback: (newCallback: GatewayAnyClusterApiKey) => void
setParam: (newOtherParam: string | number) => void
}

export function useClusterDataPoller<Type>(
callback: GatewayAnyClusterApiKey,
initialCallback: GatewayAnyClusterApiKey,
timeout: number,
otherParam?: number | string
initialOtherParam?: number | string
): ClusterDataPoller<Type> {
let callback = initialCallback
let otherParam = initialOtherParam
const data: Ref<Type | undefined> = ref()
const unable: Ref<boolean> = ref(false)
const loaded: Ref<boolean> = ref(false)
Expand Down Expand Up @@ -95,6 +99,24 @@ export function useClusterDataPoller<Type>(
gateway.abort()
}

function setCallback(newCallback: GatewayAnyClusterApiKey) {
if (runtime.currentCluster) stop(runtime.currentCluster.name)
callback = newCallback
loaded.value = false
if (runtime.currentCluster) {
start(runtime.currentCluster.name)
}
}

function setParam(newOtherParam: string | number) {
if (runtime.currentCluster) stop(runtime.currentCluster.name)
otherParam = newOtherParam
loaded.value = false
if (runtime.currentCluster) {
start(runtime.currentCluster.name)
}
}

watch(
() => runtime.currentCluster,
(newCluster, oldCluster) => {
Expand Down Expand Up @@ -123,5 +145,5 @@ export function useClusterDataPoller<Type>(
}
})

return { data, unable, loaded }
return { data, unable, loaded, setCallback, setParam }
}
Loading

0 comments on commit bcf8eab

Please sign in to comment.