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:
parent
d650ff5dcf
commit
316ba6953b
@ -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
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();
|
||||
}
|
Loading…
Reference in New Issue
Block a user