diff --git a/web/app/src/App.vue b/web/app/src/App.vue
index 67b51727..2646ef5e 100644
--- a/web/app/src/App.vue
+++ b/web/app/src/App.vue
@@ -8,7 +8,8 @@
@@ -24,6 +25,7 @@
import * as urls from '@/urls'
import * as API from '@/manager-api';
import { useJobs } from '@/stores/jobs';
+import { useTasks } from '@/stores/tasks';
import { useNotifs } from '@/stores/notifications';
import { apiClient } from '@/stores/api-query-count';
@@ -31,6 +33,7 @@ import ApiSpinner from '@/components/ApiSpinner.vue'
import JobsTable from '@/components/JobsTable.vue'
import JobDetails from '@/components/JobDetails.vue'
import TaskDetails from '@/components/TaskDetails.vue'
+import TasksTable from '@/components/TasksTable.vue'
import UpdateListener from '@/components/UpdateListener.vue'
const DEFAULT_FLAMENCO_NAME = "Flamenco";
@@ -39,13 +42,14 @@ const DEFAULT_FLAMENCO_VERSION = "unknown";
export default {
name: 'App',
components: {
- ApiSpinner, JobsTable, JobDetails, TaskDetails, UpdateListener,
+ ApiSpinner, JobsTable, JobDetails, TaskDetails, TasksTable, UpdateListener,
},
data: () => ({
websocketURL: urls.ws(),
messages: [],
jobs: useJobs(),
+ tasks: useTasks(),
notifs: useNotifs(),
flamencoName: DEFAULT_FLAMENCO_NAME,
@@ -55,9 +59,6 @@ export default {
window.app = this;
this.fetchManagerInfo();
},
- computed: {
- selectedJob() { return this.jobs ? this.jobs.activeJob : null; },
- },
methods: {
// UI component event handlers:
onSelectedJobChanged(jobSummary) {
@@ -74,6 +75,21 @@ export default {
this.$refs.jobsTable.processJobUpdate(job);
});
},
+ onSelectedTaskChanged(taskSummary) {
+ if (!taskSummary) { // There is no selected task.
+ this.tasks.deselectAllTasks();
+ return;
+ }
+ console.log("selected task changed:", taskSummary);
+ // const jobsAPI = new API.JobsApi(apiClient);
+ // jobsAPI.fetchTask(taskSummary.id)
+ // .then((task) => {
+ // this.tasks.setSelectedTask(task);
+ // // Forward the full task to Tabulator, so that that gets updated too.
+ // this.$refs.tasksTable.processTaskUpdate(task);
+ // });
+ },
+
sendMessage(message) {
this.$refs.jobsListener.sendBroadcastMessage("typer", message);
},
@@ -91,8 +107,8 @@ export default {
} else {
console.warn("App: this.$refs.jobsTable is", this.$refs.jobsTable);
}
- const selectedJob = this.selectedJob;
- if (selectedJob && selectedJob.id == jobUpdate.id) {
+ const activeJob = this.jobs.activeJob;
+ if (activeJob && activeJob.id == jobUpdate.id) {
this.onSelectedJobChanged(jobUpdate);
}
},
@@ -113,6 +129,7 @@ export default {
// SocketIO connection event handlers:
onSIOReconnected() {
this.$refs.jobsTable.onReconnected();
+ this.$refs.tasksTable.onReconnected();
this.fetchManagerInfo();
},
onSIODisconnected(reason) {
diff --git a/web/app/src/components/TaskActionsBar.vue b/web/app/src/components/TaskActionsBar.vue
new file mode 100644
index 00000000..1092da91
--- /dev/null
+++ b/web/app/src/components/TaskActionsBar.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
diff --git a/web/app/src/components/TasksTable.vue b/web/app/src/components/TasksTable.vue
new file mode 100644
index 00000000..7926192b
--- /dev/null
+++ b/web/app/src/components/TasksTable.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
diff --git a/web/app/src/stores/jobs.js b/web/app/src/stores/jobs.js
index 55bc16eb..7f8daee8 100644
--- a/web/app/src/stores/jobs.js
+++ b/web/app/src/stores/jobs.js
@@ -10,10 +10,15 @@ const jobsAPI = new API.JobsApi(apiClient);
// See https://pinia.vuejs.org/core-concepts/
export const useJobs = defineStore('jobs', {
state: () => ({
- /** @type API.Job[] */
+ /** @type {API.Job[]} */
selectedJobs: [],
- /** @type API.Job */
+ /** @type {API.Job} */
activeJob: null,
+ /**
+ * ID of the active job. Easier to query than `activeJob ? activeJob.id : ""`.
+ * @type {string}
+ */
+ activeJobID: "",
}),
getters: {
numSelected() {
@@ -32,16 +37,26 @@ export const useJobs = defineStore('jobs', {
actions: {
// Selection of jobs.
setSelectedJob(job) {
- this.selectedJobs = [job];
- this.activeJob = job;
+ this.$patch({
+ selectedJobs: [job],
+ activeJob: job,
+ activeJobID: job.id,
+ });
},
setSelectedJobs(jobs) {
- this.selectedJobs = jobs;
- this.activeJob = jobs[jobs.length-1]; // Last-selected is the active one.
+ const activeJob =jobs[jobs.length-1]; // Last-selected is the active one.
+ this.$patch({
+ selectedJobs: jobs,
+ activeJob: activeJob,
+ activeJobID: activeJob.id,
+ });
},
deselectAllJobs() {
- this.selectedJobs = [];
- this.activeJob = null;
+ this.$patch({
+ selectedJobs: [],
+ activeJob: null,
+ activeJobID: "",
+ });
},
/**
diff --git a/web/app/src/stores/tasks.js b/web/app/src/stores/tasks.js
new file mode 100644
index 00000000..09b4d727
--- /dev/null
+++ b/web/app/src/stores/tasks.js
@@ -0,0 +1,91 @@
+import { defineStore } from 'pinia'
+
+import * as API from '@/manager-api';
+import { apiClient } from '@/stores/api-query-count';
+
+
+const jobsAPI = new API.JobsApi(apiClient);
+
+// 'use' prefix is idiomatic for Pinia stores.
+// See https://pinia.vuejs.org/core-concepts/
+export const useTasks = defineStore('tasks', {
+ state: () => ({
+ /** @type {API.Task[]} */
+ selectedTasks: [],
+ /** @type {API.Task} */
+ activeTask: null,
+ /**
+ * ID of the active task. Easier to query than `activeTask ? activeTask.id : ""`.
+ * @type {string}
+ */
+ activeTaskID: "",
+ }),
+ getters: {
+ numSelected() {
+ return this.selectedTasks.length;
+ },
+ canCancel() {
+ return this._anyTaskWithStatus(["queued", "active", "soft-failed"])
+ },
+ canRequeue() {
+ return this._anyTaskWithStatus(["canceled", "completed", "failed"])
+ },
+ },
+ actions: {
+ // Selection of tasks.
+ setSelectedTask(task) {
+ this.$patch({
+ selectedTasks: [task],
+ activeTask: task,
+ activeTaskID: task.id,
+ });
+ },
+ setSelectedTasks(tasks) {
+ const activeTask =tasks[tasks.length-1]; // Last-selected is the active one.
+ this.$patch({
+ selectedTasks: tasks,
+ activeTask: activeTask,
+ activeTaskID: activeTask.id,
+ });
+ },
+ deselectAllTasks() {
+ this.$patch({
+ selectedTasks: [],
+ activeTask: null,
+ activeTaskID: "",
+ });
+ },
+
+ /**
+ * Actions on the selected tasks.
+ *
+ * All the action functions return a promise that resolves when the action has been performed.
+ *
+ * TODO: actually have these work on all selected tasks. For simplicity, the
+ * code now assumes that only the active task needs to be operated on.
+ */
+ cancelTasks() { return this._setTaskStatus("cancel-requested"); },
+ requeueTasks() { return this._setTaskStatus("requeued"); },
+
+ // Internal methods.
+
+ /**
+ *
+ * @param {string[]} statuses
+ * @returns bool indicating whether there is a selected task with any of the given statuses.
+ */
+ _anyTaskWithStatus(statuses) {
+ return this.selectedTasks.reduce((foundTask, task) => (foundTask || statuses.includes(task.status)), false);
+ },
+
+ /**
+ * Transition the selected task(s) to the new status.
+ * @param {string} newStatus
+ * @returns a Promise for the API request.
+ */
+ _setTaskStatus(newStatus) {
+ const statuschange = new API.TaskStatusChange(newStatus, "requested from web interface");
+ return jobsAPI.setTaskStatus(this.activeTask.id, statuschange);
+ },
+ },
+})