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