Skip to content

Commit

Permalink
feat(front): hash on job fields
Browse files Browse the repository at this point in the history
Display hash near all jobs fields in job details page to generate link
to highlight specific field.

fix #251
  • Loading branch information
rezib committed Nov 11, 2024
1 parent 4ff5234 commit 1611ac3
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 75 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
timeseries metrics.
- Display charts of resources (nodes/cores) status and jobs queue in dashboard
page based on metrics from Prometheus (#275).
- Display hash near all jobs fields in job details page to generate link to
highlight specific field (#251).
- conf:
- Add `racksdb` > `infrastructure` parameter for the agent.
- Add `metrics` > `enabled` parameter for the agent.
Expand Down
239 changes: 164 additions & 75 deletions frontend/src/views/JobView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
-->

<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { computed, onMounted, ref } from 'vue'
import type { Component } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import type { LocationQueryRaw } from 'vue-router'
import ClusterMainLayout from '@/components/ClusterMainLayout.vue'
import { useClusterDataPoller } from '@/composables/DataPoller'
Expand All @@ -19,6 +20,7 @@ import { useRuntimeStore } from '@/stores/runtime'
import ErrorAlert from '@/components/ErrorAlert.vue'
import LoadingSpinner from '@/components/LoadingSpinner.vue'
import { ChevronLeftIcon } from '@heroicons/vue/20/solid'
import { HashtagIcon } from '@heroicons/vue/24/outline'
import JobFieldRaw from '@/components/job/JobFieldRaw.vue'
import JobFieldComment from '@/components/job/JobFieldComment.vue'
import JobFieldExitCode from '@/components/job/JobFieldExitCode.vue'
Expand All @@ -37,6 +39,7 @@ const props = defineProps({
const runtimeStore = useRuntimeStore()
const router = useRouter()
const route = useRoute()
function backToJobs() {
router.push({
Expand All @@ -46,78 +49,146 @@ function backToJobs() {
})
}
const JobsFields = [
'user',
'group',
'account',
'wckeys',
'priority',
'name',
'comments',
'submit-line',
'script',
'workdir',
'exit-code',
'nodes',
'partition',
'qos',
'tres-requested',
'tres-allocated'
] as const
type JobField = (typeof JobsFields)[number]
function isValidJobField(key: string): key is JobField {
return typeof key === 'string' && JobsFields.includes(key as JobField)
}
const { data, unable, loaded } = useClusterDataPoller<ClusterIndividualJob>('job', 5000, props.id)
const jobFields = computed(() => {
if (!data.value) return []
return [
{ id: 'user', label: 'User', component: JobFieldRaw, props: { field: data.value.user } },
{ id: 'group', label: 'Group', component: JobFieldRaw, props: { field: data.value.group } },
{
id: 'account',
label: 'Account',
component: JobFieldRaw,
props: { field: data.value.association.account }
},
{
id: 'wckeys',
label: 'Wckeys',
component: JobFieldRaw,
props: { field: data.value.wckey.wckey }
},
{
id: 'priority',
label: 'Priority',
component: JobFieldRaw,
props: { field: data.value.priority.number }
},
{ id: 'name', label: 'Name', component: JobFieldRaw, props: { field: data.value.name } },
{
id: 'comments',
label: 'Comments',
component: JobFieldComment,
props: { comment: data.value.comment }
},
{
id: 'submit-line',
label: 'Submit line',
component: JobFieldRaw,
props: { field: data.value.submit_line, monospace: true }
},
{ id: 'script', label: 'Script', component: JobFieldRaw, props: { field: data.value.script } },
{
id: 'workdir',
label: 'Working directory',
component: JobFieldRaw,
props: { field: data.value.working_directory, monospace: true }
},
{
id: 'exit-code',
label: 'Exit Code',
component: JobFieldExitCode,
props: { exit_code: data.value.exit_code }
},
{ id: 'nodes', label: 'Nodes', component: JobFieldRaw, props: { field: data.value.nodes } },
{
id: 'partition',
label: 'Partition',
component: JobFieldRaw,
props: { field: data.value.partition }
},
{ id: 'qos', label: 'QOS', component: JobFieldRaw, props: { field: data.value.qos } },
{
id: 'tres-requested',
label: 'Requested',
component: JobFieldTRES,
props: { tres: data.value.tres.requested }
},
{
id: 'tres-allocated',
label: 'Allocated',
component: JobFieldTRES,
props: { tres: data.value.tres.allocated }
const displayTags = ref<Record<JobField, { show: boolean; highlight: boolean }>>({
user: { show: false, highlight: false },
group: { show: false, highlight: false },
account: { show: false, highlight: false },
wckeys: { show: false, highlight: false },
priority: { show: false, highlight: false },
name: { show: false, highlight: false },
comments: { show: false, highlight: false },
'submit-line': { show: false, highlight: false },
script: { show: false, highlight: false },
workdir: { show: false, highlight: false },
'exit-code': { show: false, highlight: false },
nodes: { show: false, highlight: false },
partition: { show: false, highlight: false },
qos: { show: false, highlight: false },
'tres-allocated': { show: false, highlight: false },
'tres-requested': { show: false, highlight: false }
})
const jobFieldsContent = computed(
(): { id: JobField; label: string; component: Component; props: Object }[] => {
if (!data.value) return []
return [
{ id: 'user', label: 'User', component: JobFieldRaw, props: { field: data.value.user } },
{ id: 'group', label: 'Group', component: JobFieldRaw, props: { field: data.value.group } },
{
id: 'account',
label: 'Account',
component: JobFieldRaw,
props: { field: data.value.association.account }
},
{
id: 'wckeys',
label: 'Wckeys',
component: JobFieldRaw,
props: { field: data.value.wckey.wckey }
},
{
id: 'priority',
label: 'Priority',
component: JobFieldRaw,
props: { field: data.value.priority.number }
},
{ id: 'name', label: 'Name', component: JobFieldRaw, props: { field: data.value.name } },
{
id: 'comments',
label: 'Comments',
component: JobFieldComment,
props: { comment: data.value.comment }
},
{
id: 'submit-line',
label: 'Submit line',
component: JobFieldRaw,
props: { field: data.value.submit_line, monospace: true }
},
{
id: 'script',
label: 'Script',
component: JobFieldRaw,
props: { field: data.value.script }
},
{
id: 'workdir',
label: 'Working directory',
component: JobFieldRaw,
props: { field: data.value.working_directory, monospace: true }
},
{
id: 'exit-code',
label: 'Exit Code',
component: JobFieldExitCode,
props: { exit_code: data.value.exit_code }
},
{ id: 'nodes', label: 'Nodes', component: JobFieldRaw, props: { field: data.value.nodes } },
{
id: 'partition',
label: 'Partition',
component: JobFieldRaw,
props: { field: data.value.partition }
},
{ id: 'qos', label: 'QOS', component: JobFieldRaw, props: { field: data.value.qos } },
{
id: 'tres-requested',
label: 'Requested',
component: JobFieldTRES,
props: { tres: data.value.tres.requested }
},
{
id: 'tres-allocated',
label: 'Allocated',
component: JobFieldTRES,
props: { tres: data.value.tres.allocated }
}
]
}
)
/* highlight this field for some time */
function highlightField(field: JobField) {
displayTags.value[field].highlight = true
setTimeout(() => {
displayTags.value[field].highlight = false
}, 2000)
}
onMounted(() => {
/* If a job field is in route hash, highlight this field. */
if (route.hash) {
const field = route.hash.slice(1) // remove initial hash
if (isValidJobField(field)) {
highlightField(field)
}
]
}
})
</script>

Expand Down Expand Up @@ -162,12 +233,30 @@ const jobFields = computed(() => {
<div class="border-t border-gray-100">
<dl class="divide-y divide-gray-100">
<div
v-for="field in jobFields"
v-for="field in jobFieldsContent"
:key="field.id"
:id="`job-${field.id}`"
class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"
:id="`${field.id}`"
:class="[
displayTags[field.id].highlight ? 'bg-slurmweb-light' : '',
' px-4 py-2 transition-colors duration-700 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0'
]"
>
<dt class="text-sm font-medium leading-6 text-gray-900">{{ field.label }}</dt>
<dt class="text-sm font-medium leading-6 text-gray-900">
<a :href="`#${field.id}`">
<span
class="flex items-center"
@mouseover="displayTags[field.id].show = true"
@mouseleave="displayTags[field.id].show = false"
>
<HashtagIcon
v-show="displayTags[field.id].show"
class="-ml-5 mr-2 h-3 w-3 text-gray-500"
aria-hidden="true"
/>
{{ field.label }}
</span>
</a>
</dt>
<component :is="field.component" v-bind="field.props" />
</div>
</dl>
Expand Down

0 comments on commit 1611ac3

Please sign in to comment.