Web: also let Vue Router track the active task

This basically does the same as 63ac7287321a101c3f601eeb151be73154ef7720
but then for tasks.
This commit is contained in:
Sybren A. Stüvel 2022-05-11 15:02:02 +02:00
parent 6b9d7dba6d
commit cc10d3e4bb
5 changed files with 102 additions and 60 deletions

View File

@ -28,7 +28,8 @@ export default {
}, },
_handleTaskActionPromise(promise, description) { _handleTaskActionPromise(promise, description) {
const numTasks = this.tasks.numSelected; // const numTasks = this.tasks.numSelected;
const numTasks = 1;
return promise return promise
.then(() => { .then(() => {
let message; let message;

View File

@ -16,9 +16,10 @@ import { useTasks } from '@/stores/tasks';
import TaskActionsBar from '@/components/TaskActionsBar.vue' import TaskActionsBar from '@/components/TaskActionsBar.vue'
export default { export default {
emits: ["selectedTaskChange"], emits: ["tableRowClicked"],
props: [ props: [
"jobID", // ID of the job of which the tasks are shown here. "jobID", // ID of the job of which the tasks are shown here.
"taskID", // The active task.
], ],
components: { components: {
TaskActionsBar, TaskActionsBar,
@ -27,7 +28,11 @@ export default {
const options = { const options = {
// See pkg/api/flamenco-manager.yaml, schemas Task and TaskUpdate. // See pkg/api/flamenco-manager.yaml, schemas Task and TaskUpdate.
columns: [ 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', title: 'Status', field: 'status', sorter: 'string',
formatter(cell, formatterParams) { // eslint-disable-line no-unused-vars formatter(cell, formatterParams) { // eslint-disable-line no-unused-vars
@ -53,7 +58,7 @@ export default {
{ column: "updated", dir: "desc" }, { column: "updated", dir: "desc" },
], ],
data: [], // Will be filled via a Flamenco API request. 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 { return {
options: options, 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(), previous_status: "uuuuh", name: "Updated manually"});
// tasksTableVue.processTaskUpdate({id: "ad0a5a00-5cb8-4e31-860a-8a405e75910e", status: "heyy", updated: DateTime.local().toISO()}); // tasksTableVue.processTaskUpdate({id: "ad0a5a00-5cb8-4e31-860a-8a405e75910e", status: "heyy", updated: DateTime.local().toISO()});
window.tasksTableVue = this; 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 = new Tabulator('#flamenco_task_list', this.options);
this.tabulator.on("rowSelected", this.onRowSelected); this.tabulator.on("rowClick", this.onRowClick);
this.tabulator.on("rowDeselected", this.onRowDeselected);
this.tabulator.on("tableBuilt", this.fetchTasks); this.tabulator.on("tableBuilt", this.fetchTasks);
}, },
watch: { watch: {
jobID() { jobID() {
this.onRowDeselected([]);
this.fetchTasks(); this.fetchTasks();
}, },
taskID(oldID, newID) {
this._reformatRow(oldID);
this._reformatRow(newID);
},
}, },
methods: { methods: {
onReconnected() { onReconnected() {
@ -103,7 +120,6 @@ export default {
// "Down-cast" to TaskUpdate to only get those fields, just for debugging things: // "Down-cast" to TaskUpdate to only get those fields, just for debugging things:
// let tasks = data.tasks.map((j) => API.TaskUpdate.constructFromObject(j)); // let tasks = data.tasks.map((j) => API.TaskUpdate.constructFromObject(j));
this.tabulator.setData(data.tasks); this.tabulator.setData(data.tasks);
this._restoreRowSelection();
}, },
processTaskUpdate(taskUpdate) { processTaskUpdate(taskUpdate) {
// updateData() will only overwrite properties that are actually set on // updateData() will only overwrite properties that are actually set on
@ -112,27 +128,22 @@ export default {
.then(this.sortData); .then(this.sortData);
}, },
// Selection handling. onRowClick(event, row) {
onRowSelected(selectedRow) { // Take a copy of the data, so that it's decoupled from the tabulator data
const selectedData = selectedRow.getData(); // store. There were some issues where navigating to another job would
this._storeRowSelection([selectedData]); // overwrite the old job's ID, and this prevents that.
this.$emit("selectedTaskChange", selectedData); const rowData = plain(row.getData());
}, this.$emit("tableRowClicked", rowData);
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);
}, },
_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);
}
} }
}; };
</script> </script>

View File

@ -9,7 +9,7 @@ const router = createRouter({
component: () => import('../views/IndexView.vue'), component: () => import('../views/IndexView.vue'),
}, },
{ {
path: '/jobs/:jobID?', path: '/jobs/:jobID?/:taskID?',
name: 'jobs', name: 'jobs',
component: () => import('../views/JobsView.vue'), component: () => import('../views/JobsView.vue'),
props: true, props: true,

View File

@ -10,8 +10,6 @@ const jobsAPI = new API.JobsApi(apiClient);
// See https://pinia.vuejs.org/core-concepts/ // See https://pinia.vuejs.org/core-concepts/
export const useTasks = defineStore('tasks', { export const useTasks = defineStore('tasks', {
state: () => ({ state: () => ({
/** @type {API.Task[]} */
selectedTasks: [],
/** @type {API.Task} */ /** @type {API.Task} */
activeTask: null, activeTask: null,
/** /**
@ -21,9 +19,6 @@ export const useTasks = defineStore('tasks', {
activeTaskID: "", activeTaskID: "",
}), }),
getters: { getters: {
numSelected() {
return this.selectedTasks.length;
},
canCancel() { canCancel() {
return this._anyTaskWithStatus(["queued", "active", "soft-failed"]) return this._anyTaskWithStatus(["queued", "active", "soft-failed"])
}, },
@ -32,25 +27,20 @@ export const useTasks = defineStore('tasks', {
}, },
}, },
actions: { actions: {
// Selection of tasks. setActiveTaskID(taskID) {
setSelectedTask(task) { this.$patch({
activeTask: {id: taskID},
activeTaskID: taskID,
});
},
setActiveTask(task) {
this.$patch({ this.$patch({
selectedTasks: [task],
activeTask: task, activeTask: task,
activeTaskID: task.id, 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() { deselectAllTasks() {
this.$patch({ this.$patch({
selectedTasks: [],
activeTask: null, activeTask: null,
activeTaskID: "", 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. * @returns bool indicating whether there is a selected task with any of the given statuses.
*/ */
_anyTaskWithStatus(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. * @returns a Promise for the API request.
*/ */
_setTaskStatus(newStatus) { _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"); const statuschange = new API.TaskStatusChange(newStatus, "requested from web interface");
return jobsAPI.setTaskStatus(this.activeTask.id, statuschange); return jobsAPI.setTaskStatus(this.activeTaskID, statuschange);
}, },
}, },
}) })

View File

@ -4,7 +4,7 @@
</div> </div>
<div class="col col-2"> <div class="col col-2">
<job-details :jobData="jobs.activeJob" /> <job-details :jobData="jobs.activeJob" />
<tasks-table v-if="jobID" ref="tasksTable" :jobID="jobID" @selectedTaskChange="onSelectedTaskChanged" /> <tasks-table v-if="jobID" ref="tasksTable" :jobID="jobID" :taskID="taskID" @tableRowClicked="onTableTaskClicked" />
</div> </div>
<div class="col col-3"> <div class="col col-3">
<task-details :taskData="tasks.activeTask" /> <task-details :taskData="tasks.activeTask" />
@ -33,7 +33,7 @@ import UpdateListener from '@/components/UpdateListener.vue'
export default { export default {
name: 'JobsView', name: 'JobsView',
props: ["jobID"], // provided by Vue Router. props: ["jobID", "taskID"], // provided by Vue Router.
components: { components: {
JobsTable, JobDetails, TaskDetails, TasksTable, NotificationBar, UpdateListener, JobsTable, JobDetails, TaskDetails, TasksTable, NotificationBar, UpdateListener,
}, },
@ -47,19 +47,26 @@ export default {
mounted() { mounted() {
window.jobsView = this; window.jobsView = this;
this._fetchJob(this.jobID); this._fetchJob(this.jobID);
this._fetchTask(this.taskID);
}, },
watch: { watch: {
jobID(newJobID, oldJobID) { jobID(newJobID, oldJobID) {
this._fetchJob(newJobID); this._fetchJob(newJobID);
}, },
taskID(newTaskID, oldTaskID) {
this._fetchTask(newTaskID);
},
}, },
methods: { methods: {
onTableJobClicked(rowData) { onTableJobClicked(rowData) {
this._routeToJob(rowData.id); this._routeToJob(rowData.id);
}, },
onTableTaskClicked(rowData) {
this._routeToTask(rowData.id);
},
onSelectedTaskChanged(taskSummary) { onSelectedTaskChanged(taskSummary) {
if (!taskSummary) { // There is no selected task. if (!taskSummary) { // There is no active task.
this.tasks.deselectAllTasks(); this.tasks.deselectAllTasks();
return; return;
} }
@ -67,7 +74,7 @@ export default {
const jobsAPI = new API.JobsApi(apiClient); const jobsAPI = new API.JobsApi(apiClient);
jobsAPI.fetchTask(taskSummary.id) jobsAPI.fetchTask(taskSummary.id)
.then((task) => { .then((task) => {
this.tasks.setSelectedTask(task); this.tasks.setActiveTask(task);
// Forward the full task to Tabulator, so that that gets updated too. // Forward the full task to Tabulator, so that that gets updated too.
if (this.$refs.tasksTable) if (this.$refs.tasksTable)
this.$refs.tasksTable.processTaskUpdate(task); this.$refs.tasksTable.processTaskUpdate(task);
@ -86,13 +93,33 @@ export default {
this._fetchJob(this.jobID); 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". * @param {string} jobID job ID to navigate to, can be empty string for "no active job".
*/ */
_routeToJob(jobID) { _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. * Fetch task info and set the active task once it's received.
* @param {API.SocketIOTaskUpdate} taskUpdate * @param {string} taskID task ID, can be empty string for "no task".
*/ */
onSioTaskUpdate(taskUpdate) { _fetchTask(taskID) {
if (this.$refs.tasksTable) if (!taskID) {
this.$refs.tasksTable.processTaskUpdate(taskUpdate); this.tasks.deselectAllTasks();
if (this.tasks.activeTaskID == taskUpdate.id) return;
this.onSelectedTaskChanged(taskUpdate); }
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) { onChatMessage(message) {