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>
|
<template>
|
||||||
<header>Flamenco</header>
|
<header>Flamenco</header>
|
||||||
<div class="col-1">
|
<div class="col-1">
|
||||||
<jobs-table ref="jobsTable" :apiClient="apiClient" />
|
<jobs-table ref="jobsTable" :apiClient="apiClient" @activeJobChange="onActiveJobChanged" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<job-details :apiClient="apiClient" />
|
<job-details :apiClient="apiClient" :jobSummary="activeJobSummary" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<task-details :apiClient="apiClient" />
|
<task-details :apiClient="apiClient" />
|
||||||
@ -33,10 +33,19 @@ export default {
|
|||||||
apiClient: new ApiClient(urls.api()),
|
apiClient: new ApiClient(urls.api()),
|
||||||
websocketURL: urls.ws(),
|
websocketURL: urls.ws(),
|
||||||
messages: [],
|
messages: [],
|
||||||
|
|
||||||
|
activeJobSummary: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() { },
|
mounted() { },
|
||||||
methods: {
|
methods: {
|
||||||
|
// UI component event handlers:
|
||||||
|
onActiveJobChanged(jobSummary) {
|
||||||
|
console.log("Selected:", jobSummary);
|
||||||
|
this.activeJobSummary = jobSummary;
|
||||||
|
},
|
||||||
|
|
||||||
|
// SocketIO event handlers:
|
||||||
sendMessage(message) {
|
sendMessage(message) {
|
||||||
this.$refs.jobsListener.sendBroadcastMessage("typer", message);
|
this.$refs.jobsListener.sendBroadcastMessage("typer", message);
|
||||||
},
|
},
|
||||||
|
@ -1,49 +1,132 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<h2 class="column-title">Job Details</h2>
|
||||||
<div class="job-details">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import { DateTime } from "luxon";
|
import * as datetime from "../datetime";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
JobsApi,
|
JobsApi,
|
||||||
} from '../manager-api'
|
} from '../manager-api'
|
||||||
|
|
||||||
export default {
|
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: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
|
jobData: {},
|
||||||
|
datetime: datetime,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// Allow testing from the JS console:
|
// Allow testing from the JS console:
|
||||||
window.jobDetailsVue = this;
|
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: {
|
methods: {
|
||||||
onReconnected() {
|
onReconnected() {
|
||||||
// If the connection to the backend was lost, we have likely missed some
|
// If the connection to the backend was lost, we have likely missed some
|
||||||
// updates. Just fetch the data and start from scratch.
|
// updates. Just fetch the data and start from scratch.
|
||||||
this.fetchJob();
|
this.fetchJob();
|
||||||
},
|
},
|
||||||
fetchAllJob() {
|
fetchJob() {
|
||||||
if (this.apiClient === undefined) {
|
if (!this.apiClient) {
|
||||||
throw "no apiClient set on JobsTable component";
|
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 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) {
|
jobsApi.fetchJob(jobID).then(this.onJobFetched, function (error) {
|
||||||
// TODO: error handling.
|
// TODO: error handling.
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
|
return jobID;
|
||||||
},
|
},
|
||||||
onJobFetched(data) {
|
onJobFetched(data) {
|
||||||
console.log("Job fetched:", data);
|
console.log("Job fetched:", data);
|
||||||
},
|
},
|
||||||
|
clearJobDetails() {
|
||||||
|
this.jobData = {};
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="job-list" id="flamenco_job_list">
|
<div class="job-list" id="flamenco_job_list"></div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||||
import { DateTime } from "luxon";
|
import * as datetime from "../datetime";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
JobsApi,
|
JobsApi,
|
||||||
} from '../manager-api'
|
} from '../manager-api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
emits: ["activeJobChange"],
|
||||||
props: ["apiClient"],
|
props: ["apiClient"],
|
||||||
data: () => {
|
data: () => {
|
||||||
const options = {
|
const options = {
|
||||||
@ -28,17 +28,10 @@ export default {
|
|||||||
sorter: 'alphanum', sorterParams: { alignEmptyValues: "top" },
|
sorter: 'alphanum', sorterParams: { alignEmptyValues: "top" },
|
||||||
formatter(cell, formatterParams) { // eslint-disable-line no-unused-vars
|
formatter(cell, formatterParams) { // eslint-disable-line no-unused-vars
|
||||||
const cellValue = cell.getData().updated;
|
const cellValue = cell.getData().updated;
|
||||||
let updated = null;
|
// TODO: if any "{amount} {units} ago" shown, the table should be
|
||||||
if (cellValue instanceof Date) {
|
// refreshed every few {units}, so that it doesn't show any stale "4
|
||||||
updated = DateTime.fromJSDate(cellValue);
|
// seconds ago" for days.
|
||||||
} else {
|
return datetime.relativeTime(cellValue);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -46,7 +39,8 @@ export default {
|
|||||||
{ column: "updated", dir: "desc" },
|
{ column: "updated", dir: "desc" },
|
||||||
],
|
],
|
||||||
height: "80%",
|
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 {
|
return {
|
||||||
options: options,
|
options: options,
|
||||||
@ -58,6 +52,7 @@ export default {
|
|||||||
// jobsTableVue.processJobUpdate({id: "ad0a5a00-5cb8-4e31-860a-8a405e75910e", status: "heyy", updated: DateTime.local().toISO()});
|
// jobsTableVue.processJobUpdate({id: "ad0a5a00-5cb8-4e31-860a-8a405e75910e", status: "heyy", updated: DateTime.local().toISO()});
|
||||||
window.jobsTableVue = this;
|
window.jobsTableVue = this;
|
||||||
this.tabulator = new Tabulator('#flamenco_job_list', this.options);
|
this.tabulator = new Tabulator('#flamenco_job_list', this.options);
|
||||||
|
this.tabulator.on("rowSelected", this.onRowSelected);
|
||||||
this.fetchAllJobs();
|
this.fetchAllJobs();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -71,7 +66,7 @@ export default {
|
|||||||
tab.setSort(tab.getSorters()); // This triggers re-sorting.
|
tab.setSort(tab.getSorters()); // This triggers re-sorting.
|
||||||
},
|
},
|
||||||
fetchAllJobs() {
|
fetchAllJobs() {
|
||||||
if (this.apiClient === undefined) {
|
if (!this.apiClient) {
|
||||||
throw "no apiClient set on JobsTable component";
|
throw "no apiClient set on JobsTable component";
|
||||||
}
|
}
|
||||||
const jobsApi = new JobsApi(this.apiClient);
|
const jobsApi = new JobsApi(this.apiClient);
|
||||||
@ -83,6 +78,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onJobsFetched(data) {
|
onJobsFetched(data) {
|
||||||
this.tabulator.setData(data.jobs);
|
this.tabulator.setData(data.jobs);
|
||||||
|
this.restoreRowSelection();
|
||||||
},
|
},
|
||||||
processJobUpdate(jobUpdate) {
|
processJobUpdate(jobUpdate) {
|
||||||
// updateData() will only overwrite properties that are actually set on
|
// updateData() will only overwrite properties that are actually set on
|
||||||
@ -103,13 +99,31 @@ export default {
|
|||||||
console.error(error);
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.job-list {
|
.job-list {
|
||||||
border: thick solid fuchsia;
|
|
||||||
font-family: 'Noto Mono', monospace;
|
font-family: 'Noto Mono', monospace;
|
||||||
font-size: smaller;
|
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…
x
Reference in New Issue
Block a user