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