From cc10d3e4bb137e756d841ef8374f35dedae2d1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 11 May 2022 15:02:02 +0200 Subject: [PATCH] Web: also let Vue Router track the active task This basically does the same as 63ac7287321a101c3f601eeb151be73154ef7720 but then for tasks. --- web/app/src/components/TaskActionsBar.vue | 3 +- web/app/src/components/TasksTable.vue | 65 +++++++++++++---------- web/app/src/router/index.js | 2 +- web/app/src/stores/tasks.js | 33 +++++------- web/app/src/views/JobsView.vue | 59 +++++++++++++++----- 5 files changed, 102 insertions(+), 60 deletions(-) diff --git a/web/app/src/components/TaskActionsBar.vue b/web/app/src/components/TaskActionsBar.vue index 203c75cf..b9ea9994 100644 --- a/web/app/src/components/TaskActionsBar.vue +++ b/web/app/src/components/TaskActionsBar.vue @@ -28,7 +28,8 @@ export default { }, _handleTaskActionPromise(promise, description) { - const numTasks = this.tasks.numSelected; + // const numTasks = this.tasks.numSelected; + const numTasks = 1; return promise .then(() => { let message; diff --git a/web/app/src/components/TasksTable.vue b/web/app/src/components/TasksTable.vue index bc15d2fb..f5b37c0d 100644 --- a/web/app/src/components/TasksTable.vue +++ b/web/app/src/components/TasksTable.vue @@ -16,9 +16,10 @@ import { useTasks } from '@/stores/tasks'; import TaskActionsBar from '@/components/TaskActionsBar.vue' export default { - emits: ["selectedTaskChange"], + emits: ["tableRowClicked"], props: [ "jobID", // ID of the job of which the tasks are shown here. + "taskID", // The active task. ], components: { TaskActionsBar, @@ -27,7 +28,11 @@ export default { const options = { // See pkg/api/flamenco-manager.yaml, schemas Task and TaskUpdate. columns: [ - { formatter: "rowSelection", titleFormatter: "rowSelection", hozAlign: "center", headerHozAlign: "center", headerSort: false }, + // { formatter: "rowSelection", titleFormatter: "rowSelection", hozAlign: "center", headerHozAlign: "center", headerSort: false }, + { + title: "ID", field: "id", headerSort: false, + formatter: (cell) => cell.getData().id.substr(0, 8), + }, { title: 'Status', field: 'status', sorter: 'string', formatter(cell, formatterParams) { // eslint-disable-line no-unused-vars @@ -53,7 +58,7 @@ export default { { column: "updated", dir: "desc" }, ], data: [], // Will be filled via a Flamenco API request. - selectable: 1, // Only allow a single row to be selected at a time. + selectable: false, // The active task is tracked by click events. }; return { options: options, @@ -65,16 +70,28 @@ export default { // tasksTableVue.processTaskUpdate({id: "ad0a5a00-5cb8-4e31-860a-8a405e75910e", status: "heyy", updated: DateTime.local().toISO(), previous_status: "uuuuh", name: "Updated manually"}); // tasksTableVue.processTaskUpdate({id: "ad0a5a00-5cb8-4e31-860a-8a405e75910e", status: "heyy", updated: DateTime.local().toISO()}); window.tasksTableVue = this; + + // Set the `rowFormatter` here (instead of with the rest of the options + // above) as it needs to refer to `this`, which isn't available in the + // `data` function. + this.options.rowFormatter = (row) => { + const data = row.getData(); + const isActive = (data.id === this.taskID); + row.getElement().classList.toggle("active-row", isActive); + }; + this.tabulator = new Tabulator('#flamenco_task_list', this.options); - this.tabulator.on("rowSelected", this.onRowSelected); - this.tabulator.on("rowDeselected", this.onRowDeselected); + this.tabulator.on("rowClick", this.onRowClick); this.tabulator.on("tableBuilt", this.fetchTasks); }, watch: { jobID() { - this.onRowDeselected([]); this.fetchTasks(); }, + taskID(oldID, newID) { + this._reformatRow(oldID); + this._reformatRow(newID); + }, }, methods: { onReconnected() { @@ -103,7 +120,6 @@ export default { // "Down-cast" to TaskUpdate to only get those fields, just for debugging things: // let tasks = data.tasks.map((j) => API.TaskUpdate.constructFromObject(j)); this.tabulator.setData(data.tasks); - this._restoreRowSelection(); }, processTaskUpdate(taskUpdate) { // updateData() will only overwrite properties that are actually set on @@ -112,27 +128,22 @@ export default { .then(this.sortData); }, - // Selection handling. - onRowSelected(selectedRow) { - const selectedData = selectedRow.getData(); - this._storeRowSelection([selectedData]); - this.$emit("selectedTaskChange", selectedData); - }, - onRowDeselected(deselectedRow) { - this._storeRowSelection([]); - this.$emit("selectedTaskChange", null); - }, - _storeRowSelection(selectedData) { - const selectedTaskIDs = selectedData.map((row) => row.id); - localStorage.setItem("selectedTaskIDs", selectedTaskIDs); - }, - _restoreRowSelection() { - // const selectedTaskIDs = localStorage.getItem('selectedTaskIDs'); - // if (!selectedTaskIDs) { - // return; - // } - // this.tabulator.selectRow(selectedTaskIDs); + onRowClick(event, row) { + // Take a copy of the data, so that it's decoupled from the tabulator data + // store. There were some issues where navigating to another job would + // overwrite the old job's ID, and this prevents that. + const rowData = plain(row.getData()); + this.$emit("tableRowClicked", rowData); }, + + _reformatRow(jobID) { + // Use tab.rowManager.findRow() instead of `tab.getRow()` as the latter + // logs a warning when the row cannot be found. + const row = this.tabulator.rowManager.findRow(jobID); + if (!row) return + if (row.reformat) row.reformat(); + else if (row.reinitialize) row.reinitialize(true); + } } }; diff --git a/web/app/src/router/index.js b/web/app/src/router/index.js index 2bc5188e..ebbb6aff 100644 --- a/web/app/src/router/index.js +++ b/web/app/src/router/index.js @@ -9,7 +9,7 @@ const router = createRouter({ component: () => import('../views/IndexView.vue'), }, { - path: '/jobs/:jobID?', + path: '/jobs/:jobID?/:taskID?', name: 'jobs', component: () => import('../views/JobsView.vue'), props: true, diff --git a/web/app/src/stores/tasks.js b/web/app/src/stores/tasks.js index a432b2fd..311b6dd4 100644 --- a/web/app/src/stores/tasks.js +++ b/web/app/src/stores/tasks.js @@ -10,8 +10,6 @@ const jobsAPI = new API.JobsApi(apiClient); // See https://pinia.vuejs.org/core-concepts/ export const useTasks = defineStore('tasks', { state: () => ({ - /** @type {API.Task[]} */ - selectedTasks: [], /** @type {API.Task} */ activeTask: null, /** @@ -21,9 +19,6 @@ export const useTasks = defineStore('tasks', { activeTaskID: "", }), getters: { - numSelected() { - return this.selectedTasks.length; - }, canCancel() { return this._anyTaskWithStatus(["queued", "active", "soft-failed"]) }, @@ -32,25 +27,20 @@ export const useTasks = defineStore('tasks', { }, }, actions: { - // Selection of tasks. - setSelectedTask(task) { + setActiveTaskID(taskID) { + this.$patch({ + activeTask: {id: taskID}, + activeTaskID: taskID, + }); + }, + setActiveTask(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: "", }); @@ -75,7 +65,8 @@ export const useTasks = defineStore('tasks', { * @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); + return !!this.activeTask && !!this.activeTask.status && statuses.includes(this.activeTask.status); + // return this.selectedTasks.reduce((foundTask, task) => (foundTask || statuses.includes(task.status)), false); }, /** @@ -84,8 +75,12 @@ export const useTasks = defineStore('tasks', { * @returns a Promise for the API request. */ _setTaskStatus(newStatus) { + if (!this.activeTaskID) { + console.warn(`_setTaskStatus(${newStatus}) impossible, no active task ID`); + return; + } const statuschange = new API.TaskStatusChange(newStatus, "requested from web interface"); - return jobsAPI.setTaskStatus(this.activeTask.id, statuschange); + return jobsAPI.setTaskStatus(this.activeTaskID, statuschange); }, }, }) diff --git a/web/app/src/views/JobsView.vue b/web/app/src/views/JobsView.vue index f9f0a1bd..1f1f778b 100644 --- a/web/app/src/views/JobsView.vue +++ b/web/app/src/views/JobsView.vue @@ -4,7 +4,7 @@
- +
@@ -33,7 +33,7 @@ import UpdateListener from '@/components/UpdateListener.vue' export default { name: 'JobsView', - props: ["jobID"], // provided by Vue Router. + props: ["jobID", "taskID"], // provided by Vue Router. components: { JobsTable, JobDetails, TaskDetails, TasksTable, NotificationBar, UpdateListener, }, @@ -47,19 +47,26 @@ export default { mounted() { window.jobsView = this; this._fetchJob(this.jobID); + this._fetchTask(this.taskID); }, watch: { jobID(newJobID, oldJobID) { this._fetchJob(newJobID); }, + taskID(newTaskID, oldTaskID) { + this._fetchTask(newTaskID); + }, }, methods: { onTableJobClicked(rowData) { this._routeToJob(rowData.id); }, + onTableTaskClicked(rowData) { + this._routeToTask(rowData.id); + }, onSelectedTaskChanged(taskSummary) { - if (!taskSummary) { // There is no selected task. + if (!taskSummary) { // There is no active task. this.tasks.deselectAllTasks(); return; } @@ -67,7 +74,7 @@ export default { const jobsAPI = new API.JobsApi(apiClient); jobsAPI.fetchTask(taskSummary.id) .then((task) => { - this.tasks.setSelectedTask(task); + this.tasks.setActiveTask(task); // Forward the full task to Tabulator, so that that gets updated too. if (this.$refs.tasksTable) this.$refs.tasksTable.processTaskUpdate(task); @@ -86,13 +93,33 @@ export default { this._fetchJob(this.jobID); }, + /** + * Event handler for SocketIO task updates. + * @param {API.SocketIOTaskUpdate} taskUpdate + */ + onSioTaskUpdate(taskUpdate) { + if (this.$refs.tasksTable) + this.$refs.tasksTable.processTaskUpdate(taskUpdate); + if (this.taskID == taskUpdate.id) + this._fetchTask(this.taskID); }, /** * @param {string} jobID job ID to navigate to, can be empty string for "no active job". */ _routeToJob(jobID) { - this.$router.push({ name: 'jobs', params: { jobID: jobID } }); + const route = { name: 'jobs', params: { jobID: jobID } }; + console.log("routing to job", route.params); + this.$router.push(route); + }, + /** + * @param {string} taskID task ID to navigate to within this job, can be + * empty string for "no active task". + */ + _routeToTask(taskID) { + const route = { name: 'jobs', params: { jobID: this.jobID, taskID: taskID } }; + console.log("routing to task", route.params); + this.$router.push(route); }, /** @@ -115,14 +142,22 @@ export default { }, /** - * Event handler for SocketIO task updates. - * @param {API.SocketIOTaskUpdate} taskUpdate + * Fetch task info and set the active task once it's received. + * @param {string} taskID task ID, can be empty string for "no task". */ - onSioTaskUpdate(taskUpdate) { - if (this.$refs.tasksTable) - this.$refs.tasksTable.processTaskUpdate(taskUpdate); - if (this.tasks.activeTaskID == taskUpdate.id) - this.onSelectedTaskChanged(taskUpdate); + _fetchTask(taskID) { + if (!taskID) { + this.tasks.deselectAllTasks(); + return; + } + + const jobsAPI = new API.JobsApi(apiClient); + return jobsAPI.fetchTask(taskID) + .then((task) => { + this.tasks.setActiveTask(task); + // Forward the full task to Tabulator, so that that gets updated too. + this.$refs.tasksTable.processTaskUpdate(task); + }); }, onChatMessage(message) {