Web: add support for worker clusters
The support is still fairly minimal. Clusters cannot be managed via the webapp yet, so the API has to be used directly for that. Workers can be assigned to clusters via the webapp though.
This commit is contained in:
parent
8408d28a6b
commit
3306c7fc8d
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
const flashAfterCopyDuration = 150;
|
const flashAfterCopyDuration = 150;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy the inner text of an element to the clipboard.
|
* Copy the inner text of an element to the clipboard.
|
||||||
*
|
*
|
||||||
@ -14,9 +15,24 @@ const flashAfterCopyDuration = 150;
|
|||||||
*/
|
*/
|
||||||
export function copyElementText(clickEvent) {
|
export function copyElementText(clickEvent) {
|
||||||
const sourceElement = clickEvent.target;
|
const sourceElement = clickEvent.target;
|
||||||
|
copyElementValue(sourceElement, sourceElement.innerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the inner text of an element to the clipboard.
|
||||||
|
*
|
||||||
|
* @param {Event } clickEvent the click event that triggered this function call.
|
||||||
|
*/
|
||||||
|
export function copyElementData(clickEvent) {
|
||||||
|
const sourceElement = clickEvent.target;
|
||||||
|
window.sourceElement = sourceElement;
|
||||||
|
copyElementValue(sourceElement, sourceElement.dataset.clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyElementValue(sourceElement, value) {
|
||||||
const inputElement = document.createElement("input");
|
const inputElement = document.createElement("input");
|
||||||
document.body.appendChild(inputElement);
|
document.body.appendChild(inputElement);
|
||||||
inputElement.setAttribute("value", sourceElement.innerText);
|
inputElement.setAttribute("value", value);
|
||||||
inputElement.select();
|
inputElement.select();
|
||||||
|
|
||||||
// Note that the `navigator.clipboard` interface is only available when using
|
// Note that the `navigator.clipboard` interface is only available when using
|
||||||
@ -27,7 +43,6 @@ export function copyElementText(clickEvent) {
|
|||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
|
|
||||||
document.body.removeChild(inputElement);
|
document.body.removeChild(inputElement);
|
||||||
|
|
||||||
flashElement(sourceElement);
|
flashElement(sourceElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<label>
|
<label :title="title">
|
||||||
<span class="switch">
|
<span class="switch">
|
||||||
<input type="checkbox" :checked="isChecked" @change="$emit('switchToggle')">
|
<input type="checkbox" :checked="isChecked" @change="$emit('switchToggle')">
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps(['isChecked', 'label']);
|
const props = defineProps(['isChecked', 'label', 'title']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -32,6 +32,14 @@
|
|||||||
<dt class="field-name" title="ID">ID</dt>
|
<dt class="field-name" title="ID">ID</dt>
|
||||||
<dd><span @click="copyElementText" class="click-to-copy">{{ jobData.id }}</span></dd>
|
<dd><span @click="copyElementText" class="click-to-copy">{{ jobData.id }}</span></dd>
|
||||||
|
|
||||||
|
<template v-if="workerCluster">
|
||||||
|
<!-- TODO: fetch cluster name and show that instead, and allow editing of the cluster. -->
|
||||||
|
<dt class="field-name" title="Worker Cluster">Cluster</dt>
|
||||||
|
<dd :title="workerCluster.description"><span @click="copyElementData" class="click-to-copy"
|
||||||
|
:data-clipboard="workerCluster.id">{{
|
||||||
|
workerCluster.name }}</span></dd>
|
||||||
|
</template>
|
||||||
|
|
||||||
<dt class="field-name" title="Name">Name</dt>
|
<dt class="field-name" title="Name">Name</dt>
|
||||||
<dd>{{ jobData.name }}</dd>
|
<dd>{{ jobData.name }}</dd>
|
||||||
|
|
||||||
@ -82,7 +90,8 @@ import Blocklist from './Blocklist.vue'
|
|||||||
import TabItem from '@/components/TabItem.vue'
|
import TabItem from '@/components/TabItem.vue'
|
||||||
import TabsWrapper from '@/components/TabsWrapper.vue'
|
import TabsWrapper from '@/components/TabsWrapper.vue'
|
||||||
import PopoverEditableJobPriority from '@/components/PopoverEditableJobPriority.vue'
|
import PopoverEditableJobPriority from '@/components/PopoverEditableJobPriority.vue'
|
||||||
import { copyElementText } from '@/clipboard';
|
import { copyElementText, copyElementData } from '@/clipboard';
|
||||||
|
import { useWorkers } from '@/stores/workers'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
@ -102,11 +111,13 @@ export default {
|
|||||||
return {
|
return {
|
||||||
datetime: datetime, // So that the template can access it.
|
datetime: datetime, // So that the template can access it.
|
||||||
copyElementText: copyElementText,
|
copyElementText: copyElementText,
|
||||||
|
copyElementData: copyElementData,
|
||||||
simpleSettings: null, // Object with filtered job settings, or null if there is no job.
|
simpleSettings: null, // Object with filtered job settings, or null if there is no job.
|
||||||
jobsApi: new API.JobsApi(getAPIClient()),
|
jobsApi: new API.JobsApi(getAPIClient()),
|
||||||
jobType: null, // API.AvailableJobType object for the current job type.
|
jobType: null, // API.AvailableJobType object for the current job type.
|
||||||
jobTypeSettings: null, // Mapping from setting key to its definition in the job type.
|
jobTypeSettings: null, // Mapping from setting key to its definition in the job type.
|
||||||
showAllSettings: false,
|
showAllSettings: false,
|
||||||
|
workers: useWorkers(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -116,6 +127,12 @@ export default {
|
|||||||
if (!objectEmpty(this.jobData)) {
|
if (!objectEmpty(this.jobData)) {
|
||||||
this._refreshJobSettings(this.jobData);
|
this._refreshJobSettings(this.jobData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.workers.refreshClusters()
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
||||||
|
this.notifs.add(`Error: ${errorMsg}`);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasJobData() {
|
hasJobData() {
|
||||||
@ -139,6 +156,10 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.jobData.settings;
|
return this.jobData.settings;
|
||||||
},
|
},
|
||||||
|
workerCluster() {
|
||||||
|
if (!this.jobData.worker_cluster) return undefined;
|
||||||
|
return this.workers.clustersByID[this.jobData.worker_cluster];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
jobData(newJobData) {
|
jobData(newJobData) {
|
||||||
|
@ -34,6 +34,20 @@
|
|||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<section class="worker-clusters" v-if="workers.clusters">
|
||||||
|
<h3 class="sub-title">Clusters</h3>
|
||||||
|
<ul>
|
||||||
|
<li v-for="cluster in workers.clusters">
|
||||||
|
<switch-checkbox :isChecked="thisWorkerClusters[cluster.id]" :label="cluster.name" :title="cluster.description"
|
||||||
|
@switch-toggle="toggleWorkerCluster(cluster.id)">
|
||||||
|
</switch-checkbox>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="hint">
|
||||||
|
When a worker is assigned to one or more cluster, it will ignore jobs assigned to other clusters.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="sleep-schedule" :class="{ 'is-schedule-active': workerSleepSchedule.is_active }">
|
<section class="sleep-schedule" :class="{ 'is-schedule-active': workerSleepSchedule.is_active }">
|
||||||
<h3 class="sub-title">
|
<h3 class="sub-title">
|
||||||
<switch-checkbox :isChecked="workerSleepSchedule.is_active" @switch-toggle="toggleWorkerSleepSchedule">
|
<switch-checkbox :isChecked="workerSleepSchedule.is_active" @switch-toggle="toggleWorkerSleepSchedule">
|
||||||
@ -120,9 +134,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useNotifs } from '@/stores/notifications'
|
import { useNotifs } from '@/stores/notifications'
|
||||||
|
import { useWorkers } from '@/stores/workers'
|
||||||
|
|
||||||
import * as datetime from "@/datetime";
|
import * as datetime from "@/datetime";
|
||||||
import { WorkerMgtApi, WorkerSleepSchedule } from '@/manager-api';
|
import { WorkerMgtApi, WorkerSleepSchedule, WorkerClusterChangeRequest } from '@/manager-api';
|
||||||
import { getAPIClient } from "@/api-client";
|
import { getAPIClient } from "@/api-client";
|
||||||
import { workerStatus } from "../../statusindicator";
|
import { workerStatus } from "../../statusindicator";
|
||||||
import LinkWorkerTask from '@/components/LinkWorkerTask.vue';
|
import LinkWorkerTask from '@/components/LinkWorkerTask.vue';
|
||||||
@ -146,11 +161,19 @@ export default {
|
|||||||
isScheduleEditing: false,
|
isScheduleEditing: false,
|
||||||
notifs: useNotifs(),
|
notifs: useNotifs(),
|
||||||
copyElementText: copyElementText,
|
copyElementText: copyElementText,
|
||||||
|
workers: useWorkers(),
|
||||||
|
thisWorkerClusters: {}, // Mapping from UUID to 'isAssigned' boolean.
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// Allow testing from the JS console:
|
// Allow testing from the JS console:
|
||||||
window.workerDetailsVue = this;
|
window.workerDetailsVue = this;
|
||||||
|
|
||||||
|
this.workers.refreshClusters()
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
||||||
|
this.notifs.add(`Error: ${errorMsg}`);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
workerData(newData, oldData) {
|
workerData(newData, oldData) {
|
||||||
@ -164,6 +187,8 @@ export default {
|
|||||||
if (((oldData && newData) && (oldData.id != newData.id)) || !oldData && newData) {
|
if (((oldData && newData) && (oldData.id != newData.id)) || !oldData && newData) {
|
||||||
this.fetchWorkerSleepSchedule();
|
this.fetchWorkerSleepSchedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateThisWorkerClusters(newData);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -230,6 +255,41 @@ export default {
|
|||||||
}
|
}
|
||||||
this.api.deleteWorker(this.workerData.id);
|
this.api.deleteWorker(this.workerData.id);
|
||||||
},
|
},
|
||||||
|
updateThisWorkerClusters(newWorkerData) {
|
||||||
|
if (!newWorkerData || !newWorkerData.clusters) {
|
||||||
|
this.thisWorkerClusters = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignedClusters = newWorkerData.clusters.reduce(
|
||||||
|
(accu, cluster) => { accu[cluster.id] = true; return accu; },
|
||||||
|
{});
|
||||||
|
this.thisWorkerClusters = assignedClusters;
|
||||||
|
},
|
||||||
|
toggleWorkerCluster(clusterID) {
|
||||||
|
console.log("Toggled", clusterID);
|
||||||
|
this.thisWorkerClusters[clusterID] = !this.thisWorkerClusters[clusterID];
|
||||||
|
console.log("New assignment:", plain(this.thisWorkerClusters))
|
||||||
|
|
||||||
|
// Construct cluster change request.
|
||||||
|
const clusterIDs = [];
|
||||||
|
for (clusterID in this.thisWorkerClusters) {
|
||||||
|
// Values can exist and be set to 'false'.
|
||||||
|
const isAssigned = this.thisWorkerClusters[clusterID];
|
||||||
|
if (isAssigned) clusterIDs.push(clusterID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to the Manager.
|
||||||
|
const changeRequest = new WorkerClusterChangeRequest(clusterIDs);
|
||||||
|
this.api.setWorkerClusters(this.workerData.id, changeRequest)
|
||||||
|
.then(() => {
|
||||||
|
this.notifs.add('Cluster assignment updated');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
||||||
|
this.notifs.add(`Error: ${errorMsg}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -305,4 +365,12 @@ export default {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worker-clusters ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worker-clusters ul li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
import { WorkerMgtApi } from '@/manager-api';
|
||||||
|
import { getAPIClient } from "@/api-client";
|
||||||
|
|
||||||
// 'use' prefix is idiomatic for Pinia stores.
|
// 'use' prefix is idiomatic for Pinia stores.
|
||||||
// See https://pinia.vuejs.org/core-concepts/
|
// See https://pinia.vuejs.org/core-concepts/
|
||||||
export const useWorkers = defineStore('workers', {
|
export const useWorkers = defineStore('workers', {
|
||||||
@ -11,6 +14,12 @@ export const useWorkers = defineStore('workers', {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
activeWorkerID: "",
|
activeWorkerID: "",
|
||||||
|
|
||||||
|
/** @type {API.WorkerCluster[]} */
|
||||||
|
clusters: [],
|
||||||
|
|
||||||
|
/* Mapping from cluster UUID to API.WorkerCluster. */
|
||||||
|
clustersByID: {},
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setActiveWorkerID(workerID) {
|
setActiveWorkerID(workerID) {
|
||||||
@ -37,5 +46,23 @@ export const useWorkers = defineStore('workers', {
|
|||||||
activeWorkerID: "",
|
activeWorkerID: "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Fetch the available worker clusters from the Manager.
|
||||||
|
*
|
||||||
|
* @returns a promise.
|
||||||
|
*/
|
||||||
|
refreshClusters() {
|
||||||
|
const api = new WorkerMgtApi(getAPIClient());
|
||||||
|
return api.fetchWorkerClusters()
|
||||||
|
.then((resp) => {
|
||||||
|
this.clusters = resp.clusters;
|
||||||
|
|
||||||
|
let clustersByID = {};
|
||||||
|
for (let cluster of this.clusters) {
|
||||||
|
clustersByID[cluster.id] = cluster;
|
||||||
|
}
|
||||||
|
this.clustersByID = clustersByID;
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user