
Try to get the `.editorconfig` and `.prettierrc` files as close as possible to the formatting that was used in Flamenco. Because these files weren't here during most of Flamenco's development so far, having them caused quite a few changes in the webapp files. No functional changes intended.
288 lines
9.1 KiB
Vue
288 lines
9.1 KiB
Vue
<template>
|
|
<template v-if="hasJobData">
|
|
<div class="preview-container">
|
|
<last-rendered-image ref="lastRenderedImage" :jobID="jobData.id" thumbnailSuffix="-small" />
|
|
</div>
|
|
|
|
<TabsWrapper @clicked-job-details-tab="emit_reshuffled_delayed">
|
|
<TabItem title="Job Settings">
|
|
<dl v-if="hasSettings">
|
|
<template v-for="(value, key) in settingsToDisplay">
|
|
<dt :class="`field-${key}`" :title="key">{{ key }}</dt>
|
|
<dd>{{ value }}</dd>
|
|
</template>
|
|
</dl>
|
|
<div v-else class="dl-no-data">
|
|
<span>This job has no settings.</span>
|
|
</div>
|
|
</TabItem>
|
|
<TabItem title="Metadata">
|
|
<dl v-if="hasMetadata">
|
|
<template v-for="(value, key) in jobData.metadata">
|
|
<dt :class="`field-${key}`" :title="key">{{ key }}</dt>
|
|
<dd>{{ value }}</dd>
|
|
</template>
|
|
</dl>
|
|
<div v-else class="dl-no-data">
|
|
<span>This job has no metadata.</span>
|
|
</div>
|
|
</TabItem>
|
|
<TabItem title="Details">
|
|
<dl>
|
|
<dt class="field-name" title="ID">ID</dt>
|
|
<dd>
|
|
<span @click="copyElementText" class="click-to-copy">{{ jobData.id }}</span>
|
|
</dd>
|
|
|
|
<template v-if="workerTag">
|
|
<!-- TODO: fetch tag name and show that instead, and allow editing of the tag. -->
|
|
<dt class="field-name" title="Worker Tag">Tag</dt>
|
|
<dd :title="workerTag.description">
|
|
<span @click="copyElementData" class="click-to-copy" :data-clipboard="workerTag.id">{{
|
|
workerTag.name
|
|
}}</span>
|
|
</dd>
|
|
</template>
|
|
|
|
<dt class="field-name" title="Name">Name</dt>
|
|
<dd>{{ jobData.name }}</dd>
|
|
|
|
<dt class="field-status" title="Status">Status</dt>
|
|
<dd class="field-status-label" :class="'status-' + jobData.status">
|
|
{{ jobData.status }}
|
|
</dd>
|
|
|
|
<dt class="field-type" title="Type">Type</dt>
|
|
<dd>{{ jobType ? jobType.label : jobData.type }}</dd>
|
|
|
|
<dt class="field-priority" title="Priority">Priority</dt>
|
|
<dd>
|
|
<PopoverEditableJobPriority :jobId="jobData.id" :priority="jobData.priority" />
|
|
</dd>
|
|
|
|
<dt class="field-created" title="Created">Created</dt>
|
|
<dd>{{ datetime.relativeTime(jobData.created) }}</dd>
|
|
|
|
<template v-if="isDeleteRequested">
|
|
<dt class="field-delete-requested-at" title="delete-requested-at">Delete Request</dt>
|
|
<dd>{{ datetime.relativeTime(jobData.delete_requested_at) }}</dd>
|
|
</template>
|
|
<template v-else>
|
|
<dt class="field-updated" title="Updated">Updated</dt>
|
|
<dd>{{ datetime.relativeTime(jobData.updated) }}</dd>
|
|
</template>
|
|
|
|
<dt class="field-activity" title="Activity">Activity</dt>
|
|
<dd>{{ jobData.activity }}</dd>
|
|
</dl>
|
|
</TabItem>
|
|
<TabItem title="Blocklist">
|
|
<blocklist :jobID="jobData.id" @reshuffled="emit_reshuffled_delayed" />
|
|
</TabItem>
|
|
</TabsWrapper>
|
|
</template>
|
|
|
|
<div v-else class="details-no-item-selected">
|
|
<p>Select a job to see its details.</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import * as datetime from '@/datetime';
|
|
import * as API from '@/manager-api';
|
|
import { getAPIClient } from '@/api-client';
|
|
import LastRenderedImage from '@/components/jobs/LastRenderedImage.vue';
|
|
import Blocklist from './Blocklist.vue';
|
|
import TabItem from '@/components/TabItem.vue';
|
|
import TabsWrapper from '@/components/TabsWrapper.vue';
|
|
import PopoverEditableJobPriority from '@/components/PopoverEditableJobPriority.vue';
|
|
import { copyElementText, copyElementData } from '@/clipboard';
|
|
import { useWorkers } from '@/stores/workers';
|
|
import { useNotifs } from '@/stores/notifications';
|
|
|
|
export default {
|
|
props: [
|
|
'jobData', // Job data to show.
|
|
],
|
|
emits: [
|
|
'reshuffled', // Emitted when the size of this component may have changed. Used to resize other components in response.
|
|
],
|
|
components: {
|
|
LastRenderedImage,
|
|
TabItem,
|
|
TabsWrapper,
|
|
Blocklist,
|
|
PopoverEditableJobPriority,
|
|
},
|
|
data() {
|
|
return {
|
|
datetime: datetime, // So that the template can access it.
|
|
copyElementText: copyElementText,
|
|
copyElementData: copyElementData,
|
|
simpleSettings: null, // Object with filtered job settings, or null if there is no job.
|
|
jobsApi: new API.JobsApi(getAPIClient()),
|
|
jobType: null, // API.AvailableJobType object for the current job type.
|
|
jobTypeSettings: null, // Mapping from setting key to its definition in the job type.
|
|
showAllSettings: false,
|
|
workers: useWorkers(),
|
|
notifs: useNotifs(),
|
|
};
|
|
},
|
|
mounted() {
|
|
// Allow testing from the JS console:
|
|
window.jobDetailsVue = this;
|
|
|
|
if (!objectEmpty(this.jobData)) {
|
|
this._refreshJobSettings(this.jobData);
|
|
}
|
|
|
|
this.workers.refreshTags().catch((error) => {
|
|
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
|
this.notifs.add(`Error: ${errorMsg}`);
|
|
});
|
|
},
|
|
computed: {
|
|
hasJobData() {
|
|
return !!this.jobData && !!this.jobData.id;
|
|
},
|
|
hasMetadata() {
|
|
return this.jobData && !objectEmpty(this.jobData.metadata);
|
|
},
|
|
hasSettings() {
|
|
return this.jobData && !objectEmpty(this.settingsToDisplay);
|
|
},
|
|
isDeleteRequested() {
|
|
return this.jobData && !!this.jobData.delete_requested_at;
|
|
},
|
|
settingsToDisplay() {
|
|
if (!this.showAllSettings) {
|
|
return this.simpleSettings;
|
|
}
|
|
if (objectEmpty(this.jobData) || objectEmpty(this.jobData.settings)) {
|
|
return {};
|
|
}
|
|
return this.jobData.settings;
|
|
},
|
|
workerTag() {
|
|
if (!this.jobData.worker_tag) return undefined;
|
|
return this.workers.tagsByID[this.jobData.worker_tag];
|
|
},
|
|
},
|
|
watch: {
|
|
jobData(newJobData) {
|
|
// This can be called when moving from "a job" to "no job", in which case there is no ID.
|
|
if (!newJobData || !newJobData.id) return;
|
|
this._refreshJobSettings(newJobData);
|
|
},
|
|
},
|
|
methods: {
|
|
/**
|
|
* @param {API.SocketIOLastRenderedUpdate} lastRenderedUpdate
|
|
*/
|
|
refreshLastRenderedImage(lastRenderedUpdate) {
|
|
this.$refs.lastRenderedImage.refreshLastRenderedImage(lastRenderedUpdate);
|
|
},
|
|
|
|
_refreshJobSettings(newJobData) {
|
|
if (objectEmpty(newJobData)) {
|
|
this._clearJobSettings();
|
|
return;
|
|
}
|
|
|
|
// Only fetch the job type if it's different from what's already loaded.
|
|
if (objectEmpty(this.jobType) || this.jobType.name != newJobData.type) {
|
|
this._clearJobSettings(); // They should only be shown when the type info is known.
|
|
|
|
this.jobsApi
|
|
.getJobType(newJobData.type)
|
|
.then(this.onJobTypeLoaded)
|
|
.catch((error) => {
|
|
console.warn('error fetching job type:', error);
|
|
});
|
|
} else {
|
|
this._setJobSettings(newJobData.settings);
|
|
}
|
|
},
|
|
|
|
onJobTypeLoaded(jobType) {
|
|
this.jobType = jobType;
|
|
|
|
// Construct a lookup table for the settings.
|
|
const jobTypeSettings = {};
|
|
for (let setting of jobType.settings) jobTypeSettings[setting.key] = setting;
|
|
this.jobTypeSettings = jobTypeSettings;
|
|
|
|
if (this.jobData) {
|
|
this._setJobSettings(this.jobData.settings);
|
|
}
|
|
this.$emit('reshuffled');
|
|
},
|
|
|
|
_clearJobSettings() {
|
|
this.simpleSettings = null;
|
|
this.$emit('reshuffled');
|
|
},
|
|
|
|
_setJobSettings(newJobSettings) {
|
|
if (objectEmpty(newJobSettings)) {
|
|
this._clearJobSettings();
|
|
return;
|
|
}
|
|
|
|
if (objectEmpty(this.jobTypeSettings)) {
|
|
console.warn('empty job type settings');
|
|
this._clearJobSettings();
|
|
return;
|
|
}
|
|
|
|
// Construct a set of `setting.visible` values that should make the
|
|
// setting visible here in the web interface.
|
|
const v = new API.AvailableJobSettingVisibility();
|
|
const visible = new Set([undefined, v.visible, v.web]);
|
|
|
|
const filtered = {};
|
|
for (let key in newJobSettings) {
|
|
const setting = this.jobTypeSettings[key];
|
|
if (typeof setting == 'undefined') {
|
|
// Jobs can have settings beyond what the job type defines, for
|
|
// example when the job is older than the latest change to a job type,
|
|
// or when the submission system simply added custom settings.
|
|
continue;
|
|
}
|
|
if (visible.has(setting.visible)) {
|
|
filtered[key] = newJobSettings[key];
|
|
}
|
|
}
|
|
|
|
this.simpleSettings = filtered;
|
|
this.$emit('reshuffled');
|
|
},
|
|
emit_reshuffled_delayed() {
|
|
const reshuffle = () => {
|
|
this.$emit('reshuffled');
|
|
};
|
|
|
|
// Changing tabs requires two sequential "reshuffled" events, at least it
|
|
// does on Firefox. Not sure what the reason is, but it works to get rid
|
|
// of the scrollbar.
|
|
reshuffle();
|
|
this.$nextTick(reshuffle);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Prevent fields with long IDs from overflowing. */
|
|
.field-id + dd {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.field-status-label {
|
|
color: var(--indicator-color);
|
|
font-weight: bold;
|
|
}
|
|
</style>
|