Web: show job details column

This may be a nice moment to reconsider using Pinia as a data store, as
we now have two views (job table + job details) that should share a data
set.
This commit is contained in:
Sybren A. Stüvel 2022-04-12 15:28:18 +02:00
parent d650ff5dcf
commit 316ba6953b
4 changed files with 159 additions and 26 deletions

@ -1,10 +1,10 @@
<template>
<header>Flamenco</header>
<div class="col-1">
<jobs-table ref="jobsTable" :apiClient="apiClient" />
<jobs-table ref="jobsTable" :apiClient="apiClient" @activeJobChange="onActiveJobChanged" />
</div>
<div class="col-2">
<job-details :apiClient="apiClient" />
<job-details :apiClient="apiClient" :jobSummary="activeJobSummary" />
</div>
<div class="col-3">
<task-details :apiClient="apiClient" />
@ -33,10 +33,19 @@ export default {
apiClient: new ApiClient(urls.api()),
websocketURL: urls.ws(),
messages: [],
activeJobSummary: {},
};
},
mounted() { },
methods: {
// UI component event handlers:
onActiveJobChanged(jobSummary) {
console.log("Selected:", jobSummary);
this.activeJobSummary = jobSummary;
},
// SocketIO event handlers:
sendMessage(message) {
this.$refs.jobsListener.sendBroadcastMessage("typer", message);
},

@ -1,49 +1,132 @@
<template>
<h2 class="column-title">Job Details</h2>
<div class="job-details">
<h2 class="column-title">Job Details</h2>
<table class="details">
<tr class="field-id">
<th>ID</th>
<td>{{ jobData.id }}</td>
</tr>
<tr class="field-name">
<th>Name</th>
<td>{{ jobData.name }}</td>
</tr>
<tr class="field-status">
<th>Status</th>
<td>{{ jobData.status }}</td>
</tr>
<tr class="field-type">
<th>Type</th>
<td>{{ jobData.type }}</td>
</tr>
<tr class="field-priority">
<th>Prio</th>
<td>{{ jobData.priority }}</td>
</tr>
<tr class="field-created">
<th>Created</th>
<td>{{ datetime.relativeTime(jobData.created) }}</td>
</tr>
<tr class="field-updated">
<th>Updated</th>
<td>{{ datetime.relativeTime(jobData.updated) }}</td>
</tr>
</table>
<dl class="metadata">
</dl>
</div>
</template>
<script lang="js">
import { DateTime } from "luxon";
import * as datetime from "../datetime";
import {
JobsApi,
} from '../manager-api'
export default {
props: ["apiClient"],
props: [
"apiClient", // Flamenco Manager API client.
// Object, subset of job info, should at least contain an 'id' key. This ID
// determines the job that's shown here. The rest of the fields are used to
// initialise the details until the full job has been fetched from the API.
"jobSummary",
],
data: () => {
return {
jobData: {},
datetime: datetime,
};
},
mounted() {
// Allow testing from the JS console:
window.jobDetailsVue = this;
},
watch: {
jobSummary(newSummary, oldSummary) {
console.log("Updating job details:", JSON.parse(JSON.stringify(newSummary)));
this.jobData = newSummary;
// TODO: Fetch the rest of the job.
},
},
methods: {
onReconnected() {
// If the connection to the backend was lost, we have likely missed some
// updates. Just fetch the data and start from scratch.
this.fetchJob();
},
fetchAllJob() {
if (this.apiClient === undefined) {
throw "no apiClient set on JobsTable component";
fetchJob() {
if (!this.apiClient) {
throw "no apiClient set on JobDetails component";
}
if (!this.jobSummary || !this.jobSummary.id) {
// no job selected, which is fine.
this.clearJobDetails();
return "";
}
const jobsApi = new JobsApi(this.apiClient);
const jobID = ""; // TODO: get from outer scope.
const jobID = this.jobSummary.id;
jobsApi.fetchJob(jobID).then(this.onJobFetched, function (error) {
// TODO: error handling.
console.error(error);
});
return jobID;
},
onJobFetched(data) {
console.log("Job fetched:", data);
},
clearJobDetails() {
this.jobData = {};
},
}
};
</script>
<style scoped>
.job-details {
font-size: smaller;
font-family: 'Noto Mono', monospace;
}
tr:hover {
background-color: lightgrey;
}
tr.field-id td {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
th {
font-weight: bold;
text-align: right;
vertical-align: top;
}
</style>

@ -1,17 +1,17 @@
<template>
<div class="job-list" id="flamenco_job_list">
</div>
<div class="job-list" id="flamenco_job_list"></div>
</template>
<script lang="js">
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import { DateTime } from "luxon";
import * as datetime from "../datetime";
import {
JobsApi,
} from '../manager-api'
export default {
emits: ["activeJobChange"],
props: ["apiClient"],
data: () => {
const options = {
@ -28,17 +28,10 @@ export default {
sorter: 'alphanum', sorterParams: { alignEmptyValues: "top" },
formatter(cell, formatterParams) { // eslint-disable-line no-unused-vars
const cellValue = cell.getData().updated;
let updated = null;
if (cellValue instanceof Date) {
updated = DateTime.fromJSDate(cellValue);
} else {
updated = DateTime.fromISO(cellValue);
}
const now = DateTime.local();
const ageInDays = now.diff(updated).as('days');
if (ageInDays > 14)
return updated.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY);
return updated.toRelative();
// TODO: if any "{amount} {units} ago" shown, the table should be
// refreshed every few {units}, so that it doesn't show any stale "4
// seconds ago" for days.
return datetime.relativeTime(cellValue);
}
},
],
@ -46,7 +39,8 @@ export default {
{ column: "updated", dir: "desc" },
],
height: "80%",
data: [],
data: [], // Will be filled via a Flamenco API request.
selectable: 1, // Only allow a single row to be selected at a time.
};
return {
options: options,
@ -58,6 +52,7 @@ export default {
// jobsTableVue.processJobUpdate({id: "ad0a5a00-5cb8-4e31-860a-8a405e75910e", status: "heyy", updated: DateTime.local().toISO()});
window.jobsTableVue = this;
this.tabulator = new Tabulator('#flamenco_job_list', this.options);
this.tabulator.on("rowSelected", this.onRowSelected);
this.fetchAllJobs();
},
methods: {
@ -71,7 +66,7 @@ export default {
tab.setSort(tab.getSorters()); // This triggers re-sorting.
},
fetchAllJobs() {
if (this.apiClient === undefined) {
if (!this.apiClient) {
throw "no apiClient set on JobsTable component";
}
const jobsApi = new JobsApi(this.apiClient);
@ -83,6 +78,7 @@ export default {
},
onJobsFetched(data) {
this.tabulator.setData(data.jobs);
this.restoreRowSelection();
},
processJobUpdate(jobUpdate) {
// updateData() will only overwrite properties that are actually set on
@ -103,13 +99,31 @@ export default {
console.error(error);
});
},
// Selection handling.
onRowSelected(row) {
this.storeRowSelection();
const rowData = row.getData();
this.$emit("activeJobChange", rowData);
},
storeRowSelection() {
const selectedData = this.tabulator.getSelectedData();
const selectedJobIDs = selectedData.map((row) => row.id);
localStorage.setItem("selectedJobIDs", selectedJobIDs);
},
restoreRowSelection() {
const selectedJobIDs = localStorage.getItem('selectedJobIDs');
if (!selectedJobIDs) {
return;
}
this.tabulator.selectRow(selectedJobIDs);
},
}
};
</script>
<style scoped>
.job-list {
border: thick solid fuchsia;
font-family: 'Noto Mono', monospace;
font-size: smaller;
}

27
web/app/src/datetime.js Normal file

@ -0,0 +1,27 @@
import { DateTime } from "luxon";
const relativeTimeDefaultOptions = {
thresholdDays: 14,
format: DateTime.DATE_MED_WITH_WEEKDAY,
}
// relativeTime parses the timestamp (can be ISO-formatted string or JS Date
// object) and returns it in string form. The returned string is either "xxx
// time ago" if it's a relatively short time ago, or the formatted absolute time
// otherwise.
export function relativeTime(timestamp, options) {
let parsedTimestamp = null;
if (timestamp instanceof Date) {
parsedTimestamp = DateTime.fromJSDate(timestamp);
} else {
parsedTimestamp = DateTime.fromISO(timestamp);
}
if (!options) options = relativeTimeDefaultOptions;
const now = DateTime.local();
const ageInDays = now.diff(parsedTimestamp).as('days');
if (ageInDays > options.format)
return parsedTimestamp.toLocaleString(options.format);
return parsedTimestamp.toRelative();
}