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 @@ -