Skip to content

Commit

Permalink
refactor(front): split ClustersView
Browse files Browse the repository at this point in the history
Introduce ClusterListItem component in order to move some code out of
ClustersView and simplify logic of parallel and asynchronous cluster
stats retrieval.
  • Loading branch information
rezib committed Jan 10, 2025
1 parent b7ff5e4 commit 3feeea9
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 113 deletions.
137 changes: 137 additions & 0 deletions frontend/src/components/clusters/ClusterListItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<!--
Copyright (c) 2025 Rackslab

This file is part of Slurm-web.

SPDX-License-Identifier: GPL-3.0-or-later
-->
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import type { PropType } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { useRuntimeStore } from '@/stores/runtime'
import { useGatewayAPI } from '@/composables/GatewayAPI'
import type { ClusterDescription } from '@/composables/GatewayAPI'
import { AuthenticationError } from '@/composables/HTTPErrors'
import { ChevronRightIcon } from '@heroicons/vue/20/solid'
import { TagIcon } from '@heroicons/vue/20/solid'
import { ServerIcon, PlayCircleIcon } from '@heroicons/vue/24/outline'

const props = defineProps({
cluster: {
type: Object as PropType<ClusterDescription>,
required: true
}
})

const runtimeStore = useRuntimeStore()
const clusterError = ref<boolean>(false)
const loading = ref<boolean>(true)

function reportAuthenticationError(error: AuthenticationError) {
runtimeStore.reportError(`Authentication error: ${error.message}`)
router.push({ name: 'signout' })
}

function reportOtherError(error: Error) {
runtimeStore.reportError(`Server error: ${error.message}`)
clusterError.value = true
}

const gateway = useGatewayAPI()
const router = useRouter()

async function getClusterStats() {
try {
props.cluster.stats = await gateway.stats(props.cluster.name)
} catch (error: any) {
if (error instanceof AuthenticationError) {
reportAuthenticationError(error)
} else {
reportOtherError(error)
clusterError.value = true
}
}
loading.value = false
}

onMounted(() => {
getClusterStats()
})
</script>
<template>
<li
:class="[
cluster.permissions.actions.length > 0
? 'cursor-pointer hover:bg-gray-50'
: 'cursor-not-allowed bg-gray-100',
'relative flex h-20 items-center justify-between px-4 py-5 sm:px-6'
]"
@click="
cluster.permissions.actions.length > 0 &&
router.push({ name: 'dashboard', params: { cluster: cluster.name } })
"
>
<span class="w-64 text-sm font-semibold leading-6 text-gray-900">
<RouterLink :to="{ name: 'dashboard', params: { cluster: cluster.name } }">
<span class="inset-x-0 -top-px bottom-0" />
{{ cluster.name }}
</RouterLink>
<span
v-if="cluster.stats"
class="ml-2 hidden items-center gap-x-1.5 rounded-full bg-gray-100 px-1.5 py-0.5 text-xs font-normal text-gray-600 md:inline-flex"
>
<TagIcon class="h-3" />
Slurm {{ cluster.stats.version }}
</span>
</span>
<span v-if="cluster.stats" class="hidden text-center md:flex">
<span class="mt-1 w-20 text-xs leading-5 text-gray-500">
<ServerIcon class="h-6 w-full" />
<p class="w-full">
{{ cluster.stats.resources.nodes }} node{{ cluster.stats.resources.nodes > 1 ? 's' : '' }}
</p>
</span>
<span class="mt-1 w-20 text-xs leading-5 text-gray-500">
<PlayCircleIcon class="h-6 w-full" />
<p class="w-full">
{{ cluster.stats.jobs.running }} job{{ cluster.stats.jobs.running > 1 ? 's' : '' }}
</p>
</span>
</span>
<div class="mr-0 w-64 shrink-0 items-end gap-x-4">
<div class="hidden sm:flex sm:flex-col sm:items-end">
<div v-if="loading" class="mt-1 flex items-center gap-x-1.5">
<div class="flex-none rounded-full bg-gray-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-gray-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Loading</p>
<ChevronRightIcon class="h-5 w-5 flex-none text-gray-400" aria-hidden="true" />
</div>
<div
v-else-if="cluster.permissions.actions.length == 0"
class="mt-1 flex items-center gap-x-1.5"
>
<div class="flex-none rounded-full bg-red-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-red-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Denied</p>
</div>
<div v-else-if="clusterError" class="mt-1 flex items-center gap-x-1.5">
<div class="flex-none rounded-full bg-orange-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-orange-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Ongoing issue</p>
<ChevronRightIcon class="h-5 w-5 flex-none text-gray-400" aria-hidden="true" />
</div>
<div v-else class="mt-1 flex items-center gap-x-1.5">
<div class="flex-none rounded-full bg-emerald-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-emerald-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Available</p>
<ChevronRightIcon class="h-5 w-5 flex-none text-gray-400" aria-hidden="true" />
</div>
</div>
</div>
</li>
</template>
120 changes: 7 additions & 113 deletions frontend/src/views/ClustersView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { useRuntimeStore } from '@/stores/runtime'
import { useRuntimeConfiguration } from '@/plugins/runtimeConfiguration'
import { useGatewayAPI, type ClusterDescription } from '@/composables/GatewayAPI'
import { AuthenticationError } from '@/composables/HTTPErrors'
import ClusterListItem from '@/components/clusters/ClusterListItem.vue'
import LoadingSpinner from '@/components/LoadingSpinner.vue'
import { ChevronRightIcon, XCircleIcon } from '@heroicons/vue/20/solid'
import { TagIcon } from '@heroicons/vue/20/solid'
import { ServerIcon, PlayCircleIcon, ArrowRightOnRectangleIcon } from '@heroicons/vue/24/outline'
import { XCircleIcon } from '@heroicons/vue/20/solid'
import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/outline'

const runtimeStore = useRuntimeStore()
const runtimeConfiguration = useRuntimeConfiguration()
Expand All @@ -26,7 +26,6 @@ const router = useRouter()
const clusters: Ref<Array<ClusterDescription>> = ref([])
const loaded: Ref<Boolean> = ref(false)
const unable: Ref<Boolean> = ref(false)
const clustersErrors = ref<Record<string, boolean>>({})

function reportAuthenticationError(error: AuthenticationError) {
runtimeStore.reportError(`Authentication error: ${error.message}`)
Expand All @@ -35,6 +34,7 @@ function reportAuthenticationError(error: AuthenticationError) {

function reportOtherError(error: Error) {
runtimeStore.reportError(`Server error: ${error.message}`)
unable.value = true
}

async function getClustersDescriptions() {
Expand All @@ -45,40 +45,19 @@ async function getClustersDescriptions() {
if (element.permissions.actions.length > 0) {
runtimeStore.addCluster(element)
}
clustersErrors.value[element.name] = false
})
loaded.value = true
} catch (error: any) {
if (error instanceof AuthenticationError) {
reportAuthenticationError(error)
} else {
reportOtherError(error)
unable.value = true
}
}
}

function getClustersStats() {
runtimeStore.availableClusters.forEach((cluster) => {
gateway
.stats(cluster.name)
.then((result) => {
cluster.stats = result
})
.catch((error: any) => {
if (error instanceof AuthenticationError) {
reportAuthenticationError(error)
} else {
reportOtherError(error)
clustersErrors.value[cluster.name] = true
}
})
})
}

onMounted(async () => {
await getClustersDescriptions()
getClustersStats()
onMounted(() => {
getClustersDescriptions()
})
</script>

Expand Down Expand Up @@ -137,92 +116,7 @@ onMounted(async () => {
role="list"
class="divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 lg:rounded-xl"
>
<li
v-for="cluster in clusters"
:key="cluster.name"
:class="[
cluster.permissions.actions.length > 0
? 'cursor-pointer hover:bg-gray-50'
: 'cursor-not-allowed bg-gray-100',
'relative flex h-20 items-center justify-between px-4 py-5 sm:px-6'
]"
@click="
cluster.permissions.actions.length > 0 &&
router.push({ name: 'dashboard', params: { cluster: cluster.name } })
"
>
<span class="w-64 text-sm font-semibold leading-6 text-gray-900">
<RouterLink :to="{ name: 'dashboard', params: { cluster: cluster.name } }">
<span class="inset-x-0 -top-px bottom-0" />
{{ cluster.name }}
</RouterLink>
<span
v-if="cluster.stats"
class="ml-2 hidden items-center gap-x-1.5 rounded-full bg-gray-100 px-1.5 py-0.5 text-xs font-normal text-gray-600 md:inline-flex"
>
<TagIcon class="h-3" />
Slurm {{ cluster.stats.version }}
</span>
</span>
<span v-if="cluster.stats" class="hidden text-center md:flex">
<span class="mt-1 w-20 text-xs leading-5 text-gray-500">
<ServerIcon class="h-6 w-full" />
<p class="w-full">
{{ cluster.stats.resources.nodes }} node{{
cluster.stats.resources.nodes > 1 ? 's' : ''
}}
</p>
</span>
<span class="mt-1 w-20 text-xs leading-5 text-gray-500">
<PlayCircleIcon class="h-6 w-full" />
<p class="w-full">
{{ cluster.stats.jobs.running }} job{{
cluster.stats.jobs.running > 1 ? 's' : ''
}}
</p>
</span>
</span>
<div class="mr-0 w-64 shrink-0 items-end gap-x-4">
<div class="hidden sm:flex sm:flex-col sm:items-end">
<div
v-if="cluster.permissions.actions.length == 0"
class="mt-1 flex items-center gap-x-1.5"
>
<div class="flex-none rounded-full bg-red-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-red-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Denied</p>
</div>
<div
v-else-if="cluster.name in clustersErrors && clustersErrors[cluster.name]"
class="mt-1 flex items-center gap-x-1.5"
>
<div class="flex-none rounded-full bg-orange-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-orange-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Ongoing issue</p>
<ChevronRightIcon class="h-5 w-5 flex-none text-gray-400" aria-hidden="true" />
</div>
<div
v-else-if="!cluster.stats"
class="mt-1 flex items-center gap-x-1.5"
>
<div class="flex-none rounded-full bg-gray-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-gray-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Loading</p>
<ChevronRightIcon class="h-5 w-5 flex-none text-gray-400" aria-hidden="true" />
</div>
<div v-else class="mt-1 flex items-center gap-x-1.5">
<div class="flex-none rounded-full bg-emerald-500/20 p-1">
<div class="h-1.5 w-1.5 rounded-full bg-emerald-500" />
</div>
<p class="text-xs leading-5 text-gray-500">Available</p>
<ChevronRightIcon class="h-5 w-5 flex-none text-gray-400" aria-hidden="true" />
</div>
</div>
</div>
</li>
<ClusterListItem v-for="cluster in clusters" :key="cluster.name" :cluster="cluster" />
</ul>
</div>
</section>
Expand Down

0 comments on commit 3feeea9

Please sign in to comment.