From 88346d8244a9c87b66ed6362ba80405f1e239d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Mon, 30 May 2022 18:50:58 +0200 Subject: [PATCH] Web: first implementation of Workers overview Show workers with their status, and allow clicking on a worker to activate it and show its details (which currently is limited to just its ID). Does include Vue Router handling of the active worker ID and CSS classes for worker statuses. This basically copies the `JobsTable` component to `workers/WorkersTable`. The intention is that all the jobs-specific components will move into a `jobs` subdirectory at some point. --- web/app/src/assets/base.css | 30 ++++ web/app/src/components/StatusFilterBar.vue | 4 +- web/app/src/components/UpdateListener.vue | 12 +- .../src/components/workers/WorkersTable.vue | 160 ++++++++++++++++++ web/app/src/router/index.js | 5 +- web/app/src/statusindicator.js | 6 +- web/app/src/stores/workers.js | 47 +++++ web/app/src/views/JobsView.vue | 4 +- web/app/src/views/WorkersView.vue | 51 ++++-- 9 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 web/app/src/components/workers/WorkersTable.vue create mode 100644 web/app/src/stores/workers.js diff --git a/web/app/src/assets/base.css b/web/app/src/assets/base.css index efa401d6..677db2de 100644 --- a/web/app/src/assets/base.css +++ b/web/app/src/assets/base.css @@ -55,6 +55,14 @@ --color-status-cancel-requested: hsl(194 30% 50%); --color-status-under-construction: hsl(194 30% 50%); + --color-worker-status-starting: hsl(68, 100%, 30%); + --color-worker-status-awake: var(--color-status-active); + --color-worker-status-asleep: hsl(194, 100%, 19%); + --color-worker-status-error: var(--color-status-failed); + --color-worker-status-shutdown: var(--color-status-paused); + --color-worker-status-testing: hsl(166 100% 46%); + --color-worker-status-offline: var(--color-status-canceled); + --color-connection-lost-text: hsl(17, 65%, 65%); --color-connection-lost-bg: hsl(17, 65%, 20%); } @@ -360,6 +368,28 @@ ul.status-filter-bar .status-filter-indicator .indicator { --indicator-color: var(--color-status-under-construction); } +.worker-status-starting { + --indicator-color: var(--color-worker-status-starting); +} +.worker-status-awake { + --indicator-color: var(--color-worker-status-awake); +} +.worker-status-asleep { + --indicator-color: var(--color-worker-status-asleep); +} +.worker-status-error { + --indicator-color: var(--color-worker-status-error); +} +.worker-status-shutdown { + --indicator-color: var(--color-worker-status-shutdown); +} +.worker-status-testing { + --indicator-color: var(--color-worker-status-testing); +} +.worker-status-offline { + --indicator-color: var(--color-worker-status-offline); +} + .status-archiving, .status-active, .status-queued, diff --git a/web/app/src/components/StatusFilterBar.vue b/web/app/src/components/StatusFilterBar.vue index fc1df0b0..c3aad1c8 100644 --- a/web/app/src/components/StatusFilterBar.vue +++ b/web/app/src/components/StatusFilterBar.vue @@ -2,7 +2,7 @@ import { computed } from 'vue' import { indicator } from '@/statusindicator'; -const props = defineProps(['availableStatuses', 'activeStatuses']); +const props = defineProps(['availableStatuses', 'activeStatuses', 'classPrefix']); const emit = defineEmits(['click']) /** @@ -23,7 +23,7 @@ const visibleStatuses = computed(() => { :data-status="status" :class="{active: activeStatuses.indexOf(status) >= 0}" @click="emit('click', status)" - v-html="indicator(status)" + v-html="indicator(status, this.classPrefix)" > diff --git a/web/app/src/components/UpdateListener.vue b/web/app/src/components/UpdateListener.vue index 0c8501ff..a183eeb8 100644 --- a/web/app/src/components/UpdateListener.vue +++ b/web/app/src/components/UpdateListener.vue @@ -4,9 +4,11 @@ diff --git a/web/app/src/router/index.js b/web/app/src/router/index.js index 3f39858b..32bd66e7 100644 --- a/web/app/src/router/index.js +++ b/web/app/src/router/index.js @@ -15,9 +15,10 @@ const router = createRouter({ props: true, }, { - path: '/workers', + path: '/workers/:workerID?', name: 'workers', - component: () => import('../views/WorkersView.vue') + component: () => import('../views/WorkersView.vue'), + props: true, }, { path: '/settings', diff --git a/web/app/src/statusindicator.js b/web/app/src/statusindicator.js index 7c5b2030..55e26c67 100644 --- a/web/app/src/statusindicator.js +++ b/web/app/src/statusindicator.js @@ -8,9 +8,11 @@ import { toTitleCase } from '@/strings'; * * @param {string} status The job/task status. Assumed to only consist of * letters and dashes, HTML-safe, and valid as a CSS class name. + * @param {string} classNamePrefix optional prefix used for the class name * @returns the HTML for the status indicator. */ -export function indicator(status) { +export function indicator(status, classNamePrefix) { const label = toTitleCase(status); - return ``; + if (!classNamePrefix) classNamePrefix = ""; // force an empty string for any false value. + return ``; } diff --git a/web/app/src/stores/workers.js b/web/app/src/stores/workers.js new file mode 100644 index 00000000..e111fe32 --- /dev/null +++ b/web/app/src/stores/workers.js @@ -0,0 +1,47 @@ +import { defineStore } from 'pinia' + +import { WorkerMgtApi } from '@/manager-api'; +import { apiClient } from '@/stores/api-query-count'; + + +const api = new WorkerMgtApi(apiClient); + +// 'use' prefix is idiomatic for Pinia stores. +// See https://pinia.vuejs.org/core-concepts/ +export const useWorkers = defineStore('workers', { + state: () => ({ + /** @type {API.Worker} */ + activeWorker: null, + /** + * ID of the active worker. Easier to query than `activeWorker ? activeWorker.id : ""`. + * @type {string} + */ + activeWorkerID: "", + }), + actions: { + setActiveWorkerID(workerID) { + this.$patch({ + activeWorker: {id: workerID, settings: {}, metadata: {}}, + activeWorkerID: workerID, + }); + }, + setActiveWorker(worker) { + // The "function" form of $patch is necessary here, as otherwise it'll + // merge `worker` into `state.activeWorker`. As a result, it won't touch missing + // keys, which means that metadata fields that existed on the previous worker + // but not on the new one will still linger around. By passing a function + // to `$patch` this is resolved. + this.$patch((state) => { + state.activeWorker = worker; + state.activeWorkerID = worker.id; + state.hasChanged = true; + }); + }, + deselectAllWorkers() { + this.$patch({ + activeWorker: null, + activeWorkerID: "", + }); + }, + }, +}) diff --git a/web/app/src/views/JobsView.vue b/web/app/src/views/JobsView.vue index 8f2d30e2..8f5d2e2c 100644 --- a/web/app/src/views/JobsView.vue +++ b/web/app/src/views/JobsView.vue @@ -13,7 +13,7 @@ -