Webapp: tweak the .editorconfig and .prettierrc files + re-format
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.
This commit is contained in:
parent
68c55f97be
commit
819767ea1a
@ -8,6 +8,7 @@ insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 100
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
@ -3,6 +3,7 @@
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"bracketSpacing": false,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
|
@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel='icon' href='/favicon.ico'>
|
||||
<link rel="apple-touch-icon-precomposed" type='image/png' href="/apple-touch-icon.png" />
|
||||
<meta name="theme-color" content="#8982c9">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon-precomposed" type="image/png" href="/apple-touch-icon.png" />
|
||||
<meta name="theme-color" content="#8982c9" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Flamenco Manager</title>
|
||||
</head>
|
||||
|
@ -20,8 +20,7 @@
|
||||
<api-spinner />
|
||||
<span class="app-version">
|
||||
<a :href="backendURL('/flamenco-addon.zip')">add-on</a>
|
||||
| <a :href="backendURL('/api/v3/swagger-ui/')">API</a>
|
||||
| version: {{ flamencoVersion }}
|
||||
| <a :href="backendURL('/api/v3/swagger-ui/')">API</a> | version: {{ flamencoVersion }}
|
||||
</span>
|
||||
</header>
|
||||
<router-view></router-view>
|
||||
@ -29,14 +28,14 @@
|
||||
|
||||
<script>
|
||||
import * as API from '@/manager-api';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { backendURL } from '@/urls';
|
||||
import { useSocketStatus } from '@/stores/socket-status';
|
||||
|
||||
import ApiSpinner from '@/components/ApiSpinner.vue';
|
||||
|
||||
const DEFAULT_FLAMENCO_NAME = "Flamenco";
|
||||
const DEFAULT_FLAMENCO_VERSION = "unknown";
|
||||
const DEFAULT_FLAMENCO_NAME = 'Flamenco';
|
||||
const DEFAULT_FLAMENCO_VERSION = 'unknown';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@ -53,12 +52,14 @@ export default {
|
||||
this.fetchManagerInfo();
|
||||
|
||||
const sockStatus = useSocketStatus();
|
||||
this.$watch(() => sockStatus.isConnected, (isConnected) => {
|
||||
if (!isConnected) return;
|
||||
if (!sockStatus.wasEverDisconnected) return;
|
||||
this.socketIOReconnect();
|
||||
});
|
||||
|
||||
this.$watch(
|
||||
() => sockStatus.isConnected,
|
||||
(isConnected) => {
|
||||
if (!isConnected) return;
|
||||
if (!sockStatus.wasEverDisconnected) return;
|
||||
this.socketIOReconnect();
|
||||
}
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
fetchManagerInfo() {
|
||||
@ -67,23 +68,22 @@ export default {
|
||||
this.flamencoName = version.name;
|
||||
this.flamencoVersion = version.version;
|
||||
document.title = version.name;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
socketIOReconnect() {
|
||||
const metaAPI = new API.MetaApi(getAPIClient())
|
||||
const metaAPI = new API.MetaApi(getAPIClient());
|
||||
metaAPI.getVersion().then((version) => {
|
||||
if (version.name === this.flamencoName && version.version == this.flamencoVersion)
|
||||
return;
|
||||
if (version.name === this.flamencoName && version.version == this.flamencoVersion) return;
|
||||
console.log(`Updated from ${this.flamencoVersion} to ${version.version}`);
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "assets/base.css";
|
||||
@import "assets/tabulator.css";
|
||||
@import 'assets/base.css';
|
||||
@import 'assets/tabulator.css';
|
||||
</style>
|
||||
|
@ -12,12 +12,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
const DEFAULT_FLAMENCO_NAME = "Flamenco";
|
||||
const DEFAULT_FLAMENCO_VERSION = "unknown";
|
||||
import ApiSpinner from '@/components/ApiSpinner.vue'
|
||||
import { MetaApi } from "@/manager-api";
|
||||
import { getAPIClient } from "@/api-client";
|
||||
const DEFAULT_FLAMENCO_NAME = 'Flamenco';
|
||||
const DEFAULT_FLAMENCO_VERSION = 'unknown';
|
||||
import ApiSpinner from '@/components/ApiSpinner.vue';
|
||||
import { MetaApi } from '@/manager-api';
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
export default {
|
||||
name: 'SetupAssistant',
|
||||
@ -39,13 +38,13 @@ export default {
|
||||
metaAPI.getVersion().then((version) => {
|
||||
this.flamencoName = version.name;
|
||||
this.flamencoVersion = version.version;
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "assets/base.css";
|
||||
@import "assets/tabulator.css";
|
||||
@import 'assets/base.css';
|
||||
@import 'assets/tabulator.css';
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ApiClient } from "@/manager-api";
|
||||
import { CountingApiClient } from "@/stores/api-query-count";
|
||||
import { api as apiURL } from '@/urls'
|
||||
import { ApiClient } from '@/manager-api';
|
||||
import { CountingApiClient } from '@/stores/api-query-count';
|
||||
import { api as apiURL } from '@/urls';
|
||||
|
||||
/**
|
||||
* Scrub the custom User-Agent header from the API client, for those webbrowsers
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { DateTime } from "luxon";
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
// Do a full refresh once per hour. This is just to make sure that long-lived
|
||||
// displays (like the TV in the hallway at Blender HQ) pick up on HTML/JS/CSS
|
||||
// changes eventually.
|
||||
const reloadAfter = {minute: 60};
|
||||
const reloadAfter = { minute: 60 };
|
||||
|
||||
function getReloadDeadline() {
|
||||
return DateTime.now().plus(reloadAfter);
|
||||
@ -27,10 +27,10 @@ export default function autoreload() {
|
||||
// Check whether reloading is needed every minute.
|
||||
window.setInterval(maybeReload, 60 * 1000);
|
||||
|
||||
window.addEventListener("resize", deferReload);
|
||||
window.addEventListener("mousedown", deferReload);
|
||||
window.addEventListener("mouseup", deferReload);
|
||||
window.addEventListener("mousemove", deferReload);
|
||||
window.addEventListener("keydown", deferReload);
|
||||
window.addEventListener("keyup", deferReload);
|
||||
window.addEventListener('resize', deferReload);
|
||||
window.addEventListener('mousedown', deferReload);
|
||||
window.addEventListener('mouseup', deferReload);
|
||||
window.addEventListener('mousemove', deferReload);
|
||||
window.addEventListener('keydown', deferReload);
|
||||
window.addEventListener('keyup', deferReload);
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* The duration in milliseconds of the "flash" effect, when an element has been
|
||||
* copied.
|
||||
@ -7,7 +6,6 @@
|
||||
*/
|
||||
const flashAfterCopyDuration = 150;
|
||||
|
||||
|
||||
/**
|
||||
* Copy the inner text of an element to the clipboard.
|
||||
*
|
||||
@ -30,9 +28,9 @@ export function copyElementData(clickEvent) {
|
||||
}
|
||||
|
||||
function copyElementValue(sourceElement, value) {
|
||||
const inputElement = document.createElement("input");
|
||||
const inputElement = document.createElement('input');
|
||||
document.body.appendChild(inputElement);
|
||||
inputElement.setAttribute("value", value);
|
||||
inputElement.setAttribute('value', value);
|
||||
inputElement.select();
|
||||
|
||||
// Note that the `navigator.clipboard` interface is only available when using
|
||||
@ -40,15 +38,15 @@ function copyElementValue(sourceElement, value) {
|
||||
// This is why this code falls back to the deprecated `document.execCommand()`
|
||||
// call.
|
||||
// Source: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard
|
||||
document.execCommand("copy");
|
||||
document.execCommand('copy');
|
||||
|
||||
document.body.removeChild(inputElement);
|
||||
flashElement(sourceElement);
|
||||
}
|
||||
|
||||
function flashElement(element) {
|
||||
element.classList.add("copied");
|
||||
element.classList.add('copied');
|
||||
window.setTimeout(() => {
|
||||
element.classList.remove("copied");
|
||||
element.classList.remove('copied');
|
||||
}, 150);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ span {
|
||||
|
||||
.spinner {
|
||||
-webkit-animation: rotate 2s linear infinite;
|
||||
animation: rotate 2s linear infinite;
|
||||
animation: rotate 2s linear infinite;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@ -42,7 +42,7 @@ span {
|
||||
stroke: var(--color-text-hint);
|
||||
stroke-linecap: round;
|
||||
-webkit-animation: dash 1.5s ease-in-out infinite;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotate {
|
||||
|
@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<span class='socket-status' v-if="!sockStatus.isConnected" :title="sockStatus.message">Connection Lost</span>
|
||||
<span class="socket-status" v-if="!sockStatus.isConnected" :title="sockStatus.message"
|
||||
>Connection Lost</span
|
||||
>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
@ -2,9 +2,15 @@
|
||||
<div class="details-no-item-selected">
|
||||
<div class="get-the-addon">
|
||||
<p>Get the Blender add-on and submit a job.</p>
|
||||
<p><a class="btn btn-primary" :href="backendURL('/flamenco-addon.zip')">Get the add-on!</a></p>
|
||||
<p>
|
||||
<a class="btn btn-primary" :href="backendURL('/flamenco-addon.zip')">Get the add-on!</a>
|
||||
</p>
|
||||
<p>Use the URL below in the add-on preferences. Click on it to copy.</p>
|
||||
<p><span class="click-to-copy" title="Click to copy this URL" @click="copyElementText">{{ api() }}</span></p>
|
||||
<p>
|
||||
<span class="click-to-copy" title="Click to copy this URL" @click="copyElementText">{{
|
||||
api()
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -5,12 +5,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 'worker' should be a Worker or TaskWorker (see schemas defined in `flamenco-openapi.yaml`).
|
||||
const props = defineProps(['worker']);
|
||||
const workerAddress = computed(() => {
|
||||
if (props.worker.address) return `(${props.worker.address})`;
|
||||
return "";
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<span>
|
||||
<router-link v-if="workerTask" :to="{ name: 'jobs', params: { jobID: workerTask.job_id, taskID: workerTask.id } }">
|
||||
<router-link
|
||||
v-if="workerTask"
|
||||
:to="{ name: 'jobs', params: { jobID: workerTask.job_id, taskID: workerTask.id } }">
|
||||
{{ workerTask.name }}
|
||||
(<span class="status-label" :class="'status-' + workerTask.status">{{ workerTask.status }}</span>)
|
||||
(<span class="status-label" :class="'status-' + workerTask.status">{{
|
||||
workerTask.status
|
||||
}}</span
|
||||
>)
|
||||
</router-link>
|
||||
<span v-else>-</span>
|
||||
</span>
|
||||
|
@ -9,12 +9,10 @@
|
||||
<button @click="togglePopover">✖</button>
|
||||
</div>
|
||||
<div class="popover-form">
|
||||
<input type="number" v-model="priorityState">
|
||||
<input type="number" v-model="priorityState" />
|
||||
<button @click="updateJobPriority" class="btn-primary">Set</button>
|
||||
</div>
|
||||
<div class="input-help-text">
|
||||
Range 1-100.
|
||||
</div>
|
||||
<div class="input-help-text">Range 1-100.</div>
|
||||
<div class="popover-error" v-if="errorMessage">
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
@ -26,12 +24,12 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { JobsApi, JobPriorityChange } from '@/manager-api';
|
||||
|
||||
const props = defineProps({
|
||||
jobId: String,
|
||||
priority: Number
|
||||
priority: Number,
|
||||
});
|
||||
|
||||
// Init notification state
|
||||
@ -46,9 +44,10 @@ const errorMessage = ref('');
|
||||
function updateJobPriority() {
|
||||
const jobPriorityChange = new JobPriorityChange(priorityState.value);
|
||||
const jobsAPI = new JobsApi(getAPIClient());
|
||||
return jobsAPI.setJobPriority(props.jobId, jobPriorityChange)
|
||||
return jobsAPI
|
||||
.setJobPriority(props.jobId, jobPriorityChange)
|
||||
.then(() => {
|
||||
notifs.add(`Updated job priority to ${priorityState.value}`)
|
||||
notifs.add(`Updated job priority to ${priorityState.value}`);
|
||||
showPopover.value = false;
|
||||
errorMessage.value = '';
|
||||
})
|
||||
@ -122,7 +121,7 @@ function togglePopover() {
|
||||
|
||||
/* Save/Set button. */
|
||||
.popover-form button {
|
||||
flex: 1
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.input-help-text {
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed } from 'vue';
|
||||
import { indicator } from '@/statusindicator';
|
||||
|
||||
const props = defineProps(['availableStatuses', 'activeStatuses', 'classPrefix']);
|
||||
const emit = defineEmits(['click'])
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
/**
|
||||
* visibleStatuses is a union between `availableStatuses` and `activeStatuses`,
|
||||
@ -14,17 +14,17 @@ const visibleStatuses = computed(() => {
|
||||
const available = props.availableStatuses;
|
||||
const unavailable = props.activeStatuses.filter((status) => available.indexOf(status) == -1);
|
||||
return available.concat(unavailable);
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="status-filter-bar"
|
||||
:class="{'is-filtered': activeStatuses.length > 0}">
|
||||
<li v-for="status in visibleStatuses" class="status-filter-indicator"
|
||||
<ul class="status-filter-bar" :class="{ 'is-filtered': activeStatuses.length > 0 }">
|
||||
<li
|
||||
v-for="status in visibleStatuses"
|
||||
class="status-filter-indicator"
|
||||
:data-status="status"
|
||||
:class="{active: activeStatuses.indexOf(status) >= 0}"
|
||||
:class="{ active: activeStatuses.indexOf(status) >= 0 }"
|
||||
@click="emit('click', status)"
|
||||
v-html="indicator(status, classPrefix)"
|
||||
></li>
|
||||
v-html="indicator(status, classPrefix)"></li>
|
||||
</ul>
|
||||
</template>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<label :title="title">
|
||||
<span class="switch">
|
||||
<input type="checkbox" :checked="isChecked" @change="$emit('switchToggle')">
|
||||
<input type="checkbox" :checked="isChecked" @change="$emit('switchToggle')" />
|
||||
<span class="slider round"></span>
|
||||
</span>
|
||||
<span class="switch-label">{{ label }}</span>
|
||||
@ -13,7 +13,6 @@ const props = defineProps(['isChecked', 'label', 'title']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
@ -38,20 +37,20 @@ label {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-text-muted);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
content: '';
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
left: 4px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
@ -80,5 +79,4 @@ input:checked + .slider:before {
|
||||
margin-left: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import { inject, computed, provide } from "vue";
|
||||
import { inject, computed, provide } from 'vue';
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
});
|
||||
const selectedTitle = inject("selectedTitle");
|
||||
const isVisible = computed(() => selectedTitle.value === props.title)
|
||||
provide("isVisible", isVisible);
|
||||
const selectedTitle = inject('selectedTitle');
|
||||
const isVisible = computed(() => selectedTitle.value === props.title);
|
||||
provide('isVisible', isVisible);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,17 +1,16 @@
|
||||
<script setup>
|
||||
import { useSlots, ref, provide } from "vue";
|
||||
const emit = defineEmits(['clickedJobDetailsTab',])
|
||||
import { useSlots, ref, provide } from 'vue';
|
||||
const emit = defineEmits(['clickedJobDetailsTab']);
|
||||
const slots = useSlots();
|
||||
|
||||
const tabTitles = ref(slots.default().map((tab) => tab.props.title));
|
||||
const selectedTitle = ref(tabTitles.value[0]);
|
||||
provide("selectedTitle", selectedTitle);
|
||||
provide('selectedTitle', selectedTitle);
|
||||
|
||||
function updateTabTitle(title) {
|
||||
selectedTitle.value = title;
|
||||
emit('clickedJobDetailsTab');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -22,8 +21,7 @@ function updateTabTitle(title) {
|
||||
:key="title"
|
||||
class="tab-item"
|
||||
:class="{ active: selectedTitle === title }"
|
||||
@click="updateTabTitle(title)"
|
||||
>
|
||||
@click="updateTabTitle(title)">
|
||||
{{ title }}
|
||||
</li>
|
||||
</ul>
|
||||
@ -47,7 +45,8 @@ nav {
|
||||
color: var(--color-text-hint);
|
||||
cursor: pointer;
|
||||
padding: var(--spacer-sm) 0;
|
||||
transition: border-color var(--transition-speed) ease-in-out, color var(--transition-speed) ease-in-out;
|
||||
transition: border-color var(--transition-speed) ease-in-out,
|
||||
color var(--transition-speed) ease-in-out;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import io from "socket.io-client";
|
||||
import { ws } from '@/urls'
|
||||
import * as API from "@/manager-api"
|
||||
import io from 'socket.io-client';
|
||||
import { ws } from '@/urls';
|
||||
import * as API from '@/manager-api';
|
||||
import { useSocketStatus } from '@/stores/socket-status';
|
||||
|
||||
const websocketURL = ws();
|
||||
@ -13,26 +13,32 @@ const websocketURL = ws();
|
||||
export default {
|
||||
emits: [
|
||||
// Data from Flamenco Manager:
|
||||
"jobUpdate", "taskUpdate", "taskLogUpdate", "message", "workerUpdate",
|
||||
"lastRenderedUpdate", "workerTagUpdate",
|
||||
'jobUpdate',
|
||||
'taskUpdate',
|
||||
'taskLogUpdate',
|
||||
'message',
|
||||
'workerUpdate',
|
||||
'lastRenderedUpdate',
|
||||
'workerTagUpdate',
|
||||
// SocketIO events:
|
||||
"sioReconnected", "sioDisconnected"
|
||||
'sioReconnected',
|
||||
'sioDisconnected',
|
||||
],
|
||||
props: [
|
||||
"mainSubscription", // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
|
||||
"extraSubscription", // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
|
||||
"subscribedJobID",
|
||||
"subscribedTaskID",
|
||||
'mainSubscription', // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
|
||||
'extraSubscription', // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
|
||||
'subscribedJobID',
|
||||
'subscribedTaskID',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
socket: null,
|
||||
sockStatus: useSocketStatus(),
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
if (!websocketURL) {
|
||||
console.warn("UpdateListener: no websocketURL given, cannot do anything");
|
||||
console.warn('UpdateListener: no websocketURL given, cannot do anything');
|
||||
return;
|
||||
}
|
||||
this.connectToWebsocket();
|
||||
@ -46,34 +52,34 @@ export default {
|
||||
watch: {
|
||||
subscribedJobID(newJobID, oldJobID) {
|
||||
if (oldJobID) {
|
||||
this._updateJobSubscription("unsubscribe", oldJobID);
|
||||
this._updateJobSubscription('unsubscribe', oldJobID);
|
||||
}
|
||||
if (newJobID) {
|
||||
this._updateJobSubscription("subscribe", newJobID);
|
||||
this._updateJobSubscription('subscribe', newJobID);
|
||||
}
|
||||
},
|
||||
subscribedTaskID(newTaskID, oldTaskID) {
|
||||
if (oldTaskID) {
|
||||
this._updateTaskLogSubscription("unsubscribe", oldTaskID);
|
||||
this._updateTaskLogSubscription('unsubscribe', oldTaskID);
|
||||
}
|
||||
if (newTaskID) {
|
||||
this._updateTaskLogSubscription("subscribe", newTaskID);
|
||||
this._updateTaskLogSubscription('subscribe', newTaskID);
|
||||
}
|
||||
},
|
||||
mainSubscription(newType, oldType) {
|
||||
if (oldType) {
|
||||
this._updateMainSubscription("unsubscribe", oldType);
|
||||
this._updateMainSubscription('unsubscribe', oldType);
|
||||
}
|
||||
if (newType) {
|
||||
this._updateMainSubscription("subscribe", newType);
|
||||
this._updateMainSubscription('subscribe', newType);
|
||||
}
|
||||
},
|
||||
extraSubscription(newType, oldType) {
|
||||
if (oldType) {
|
||||
this._updateMainSubscription("unsubscribe", oldType);
|
||||
this._updateMainSubscription('unsubscribe', oldType);
|
||||
}
|
||||
if (newType) {
|
||||
this._updateMainSubscription("subscribe", newType);
|
||||
this._updateMainSubscription('subscribe', newType);
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -83,7 +89,7 @@ export default {
|
||||
// https://github.com/socketio/socket.io-client/blob/2.4.x/docs/API.md
|
||||
// console.log("connecting JobsListener to WS", websocketURL);
|
||||
const ws = io(websocketURL, {
|
||||
transports: ["websocket"],
|
||||
transports: ['websocket'],
|
||||
});
|
||||
this.socket = ws;
|
||||
|
||||
@ -99,104 +105,103 @@ export default {
|
||||
});
|
||||
this.socket.on('connect_error', (error) => {
|
||||
// Don't log the error here, it's too long and noisy for regular logs.
|
||||
console.log("socketIO connection error");
|
||||
console.log('socketIO connection error');
|
||||
this.sockStatus.disconnected(error);
|
||||
});
|
||||
this.socket.on('error', (error) => {
|
||||
console.log("socketIO error:", error);
|
||||
console.log('socketIO error:', error);
|
||||
this.sockStatus.disconnected(error);
|
||||
});
|
||||
this.socket.on('connect_timeout', (timeout) => {
|
||||
console.log("socketIO connection timeout:", timeout);
|
||||
this.sockStatus.disconnected("Connection timeout");
|
||||
console.log('socketIO connection timeout:', timeout);
|
||||
this.sockStatus.disconnected('Connection timeout');
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", (reason) => {
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
// console.log("socketIO disconnected:", reason);
|
||||
this.$emit("sioDisconnected", reason);
|
||||
this.$emit('sioDisconnected', reason);
|
||||
this.sockStatus.disconnected(reason);
|
||||
|
||||
if (reason === 'io server disconnect') {
|
||||
// The disconnection was initiated by the server, need to reconnect
|
||||
// manually. If the disconnect was for other reasons, the socket
|
||||
// should automatically try to reconnect.
|
||||
|
||||
// Intentionally commented out function call, because this should
|
||||
// happen with some nice exponential backoff instead of hammering the
|
||||
// server:
|
||||
// socket.connect();
|
||||
}
|
||||
});
|
||||
this.socket.on("reconnect", (attemptNumber) => {
|
||||
console.log("socketIO reconnected after", attemptNumber, "attempts");
|
||||
this.socket.on('reconnect', (attemptNumber) => {
|
||||
console.log('socketIO reconnected after', attemptNumber, 'attempts');
|
||||
this.sockStatus.connected();
|
||||
this._resubscribe();
|
||||
|
||||
this.$emit("sioReconnected", attemptNumber);
|
||||
this.$emit('sioReconnected', attemptNumber);
|
||||
});
|
||||
|
||||
this.socket.on("/jobs", (jobUpdate) => {
|
||||
this.socket.on('/jobs', (jobUpdate) => {
|
||||
// Convert to API object, in order to have the same parsing of data as
|
||||
// when we'd do an API call.
|
||||
const apiJobUpdate = API.SocketIOJobUpdate.constructFromObject(jobUpdate)
|
||||
this.$emit("jobUpdate", apiJobUpdate);
|
||||
const apiJobUpdate = API.SocketIOJobUpdate.constructFromObject(jobUpdate);
|
||||
this.$emit('jobUpdate', apiJobUpdate);
|
||||
});
|
||||
|
||||
this.socket.on("/last-rendered", (update) => {
|
||||
this.socket.on('/last-rendered', (update) => {
|
||||
// Convert to API object, in order to have the same parsing of data as
|
||||
// when we'd do an API call.
|
||||
const apiUpdate = API.SocketIOLastRenderedUpdate.constructFromObject(update)
|
||||
this.$emit("lastRenderedUpdate", apiUpdate);
|
||||
const apiUpdate = API.SocketIOLastRenderedUpdate.constructFromObject(update);
|
||||
this.$emit('lastRenderedUpdate', apiUpdate);
|
||||
});
|
||||
|
||||
this.socket.on("/task", (taskUpdate) => {
|
||||
this.socket.on('/task', (taskUpdate) => {
|
||||
// Convert to API object, in order to have the same parsing of data as
|
||||
// when we'd do an API call.
|
||||
const apiTaskUpdate = API.SocketIOTaskUpdate.constructFromObject(taskUpdate)
|
||||
this.$emit("taskUpdate", apiTaskUpdate);
|
||||
const apiTaskUpdate = API.SocketIOTaskUpdate.constructFromObject(taskUpdate);
|
||||
this.$emit('taskUpdate', apiTaskUpdate);
|
||||
});
|
||||
|
||||
this.socket.on("/tasklog", (taskLogUpdate) => {
|
||||
this.socket.on('/tasklog', (taskLogUpdate) => {
|
||||
// Convert to API object, in order to have the same parsing of data as
|
||||
// when we'd do an API call.
|
||||
const apiTaskLogUpdate = API.SocketIOTaskLogUpdate.constructFromObject(taskLogUpdate)
|
||||
this.$emit("taskLogUpdate", apiTaskLogUpdate);
|
||||
const apiTaskLogUpdate = API.SocketIOTaskLogUpdate.constructFromObject(taskLogUpdate);
|
||||
this.$emit('taskLogUpdate', apiTaskLogUpdate);
|
||||
});
|
||||
|
||||
this.socket.on("/workers", (workerUpdate) => {
|
||||
this.socket.on('/workers', (workerUpdate) => {
|
||||
// Convert to API object, in order to have the same parsing of data as
|
||||
// when we'd do an API call.
|
||||
const apiWorkerUpdate = API.SocketIOWorkerUpdate.constructFromObject(workerUpdate)
|
||||
this.$emit("workerUpdate", apiWorkerUpdate);
|
||||
const apiWorkerUpdate = API.SocketIOWorkerUpdate.constructFromObject(workerUpdate);
|
||||
this.$emit('workerUpdate', apiWorkerUpdate);
|
||||
});
|
||||
|
||||
this.socket.on("/workertags", (workerTagUpdate) => {
|
||||
this.socket.on('/workertags', (workerTagUpdate) => {
|
||||
// Convert to API object, in order to have the same parsing of data as
|
||||
// when we'd do an API call.
|
||||
const apiWorkerTagUpdate = API.SocketIOWorkerTagUpdate.constructFromObject(workerTagUpdate)
|
||||
this.$emit("workerTagUpdate", apiWorkerTagUpdate);
|
||||
const apiWorkerTagUpdate = API.SocketIOWorkerTagUpdate.constructFromObject(workerTagUpdate);
|
||||
this.$emit('workerTagUpdate', apiWorkerTagUpdate);
|
||||
});
|
||||
|
||||
// Chat system, useful for debugging.
|
||||
this.socket.on("/message", (message) => {
|
||||
this.$emit("message", message);
|
||||
this.socket.on('/message', (message) => {
|
||||
this.$emit('message', message);
|
||||
});
|
||||
},
|
||||
|
||||
disconnectWebsocket() {
|
||||
if (this.socket == null) {
|
||||
console.log("no JobListener socket to disconnect");
|
||||
console.log('no JobListener socket to disconnect');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("disconnecting JobsListener WS", websocketURL);
|
||||
console.log('disconnecting JobsListener WS', websocketURL);
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
},
|
||||
|
||||
sendBroadcastMessage(name, message) {
|
||||
const payload = { name: name, text: message };
|
||||
this.socket.emit("/chat", payload);
|
||||
this.socket.emit('/chat', payload);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -206,7 +211,7 @@ export default {
|
||||
*/
|
||||
_updateMainSubscription(operation, type) {
|
||||
const payload = new API.SocketIOSubscription(operation, type);
|
||||
this.socket.emit("/subscription", payload);
|
||||
this.socket.emit('/subscription', payload);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -215,9 +220,9 @@ export default {
|
||||
* @param {string} jobID
|
||||
*/
|
||||
_updateJobSubscription(operation, jobID) {
|
||||
const payload = new API.SocketIOSubscription(operation, "job");
|
||||
const payload = new API.SocketIOSubscription(operation, 'job');
|
||||
payload.uuid = jobID;
|
||||
this.socket.emit("/subscription", payload);
|
||||
this.socket.emit('/subscription', payload);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -226,17 +231,18 @@ export default {
|
||||
* @param {string} jobID
|
||||
*/
|
||||
_updateTaskLogSubscription(operation, taskID) {
|
||||
const payload = new API.SocketIOSubscription(operation, "tasklog");
|
||||
const payload = new API.SocketIOSubscription(operation, 'tasklog');
|
||||
payload.uuid = taskID;
|
||||
this.socket.emit("/subscription", payload);
|
||||
this.socket.emit('/subscription', payload);
|
||||
},
|
||||
|
||||
// Resubscribe to whatever we want to be subscribed to:
|
||||
_resubscribe() {
|
||||
if (this.subscribedJobID) this._updateJobSubscription("subscribe", this.subscribedJobID);
|
||||
if (this.subscribedTaskID) this._updateTaskLogSubscription("subscribe", this.subscribedTaskID);
|
||||
if (this.mainSubscription) this._updateMainSubscription("subscribe", this.mainSubscription);
|
||||
if (this.extraSubscription) this._updateMainSubscription("subscribe", this.extraSubscription);
|
||||
if (this.subscribedJobID) this._updateJobSubscription('subscribe', this.subscribedJobID);
|
||||
if (this.subscribedTaskID)
|
||||
this._updateTaskLogSubscription('subscribe', this.subscribedTaskID);
|
||||
if (this.mainSubscription) this._updateMainSubscription('subscribe', this.mainSubscription);
|
||||
if (this.extraSubscription) this._updateMainSubscription('subscribe', this.extraSubscription);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,17 +1,17 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import NotificationList from './NotificationList.vue'
|
||||
import TaskLog from './TaskLog.vue'
|
||||
import ConnectionStatus from '@/components/ConnectionStatus.vue'
|
||||
import { ref, watch } from 'vue';
|
||||
import NotificationList from './NotificationList.vue';
|
||||
import TaskLog from './TaskLog.vue';
|
||||
import ConnectionStatus from '@/components/ConnectionStatus.vue';
|
||||
|
||||
const emit = defineEmits(['clickClose'])
|
||||
const emit = defineEmits(['clickClose']);
|
||||
|
||||
const initialTab = localStorage.getItem("footer-popover-active-tab") || 'NotificationList';
|
||||
const currentTab = ref(initialTab)
|
||||
const tabs = { NotificationList, TaskLog }
|
||||
const initialTab = localStorage.getItem('footer-popover-active-tab') || 'NotificationList';
|
||||
const currentTab = ref(initialTab);
|
||||
const tabs = { NotificationList, TaskLog };
|
||||
|
||||
watch(currentTab, async (newTab) => {
|
||||
localStorage.setItem("footer-popover-active-tab", newTab);
|
||||
localStorage.setItem('footer-popover-active-tab', newTab);
|
||||
});
|
||||
|
||||
function showTaskLogTail() {
|
||||
@ -27,22 +27,17 @@ defineExpose({
|
||||
<nav>
|
||||
<ul>
|
||||
<li
|
||||
:class='["footer-tab", {"active": currentTab == "NotificationList"}]'
|
||||
:class="['footer-tab', { active: currentTab == 'NotificationList' }]"
|
||||
@click="currentTab = 'NotificationList'">
|
||||
Notifications
|
||||
Notifications
|
||||
</li>
|
||||
<li
|
||||
:class='["footer-tab", {"active": currentTab == "TaskLog"}]'
|
||||
:class="['footer-tab', { active: currentTab == 'TaskLog' }]"
|
||||
@click="currentTab = 'TaskLog'">
|
||||
Task Log
|
||||
Task Log
|
||||
</li>
|
||||
<connection-status />
|
||||
<li
|
||||
class="collapse"
|
||||
@click="emit('clickClose')"
|
||||
title="Collapse">
|
||||
✕
|
||||
</li>
|
||||
<li class="collapse" @click="emit('clickClose')" title="Collapse">✕</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<component :is="tabs[currentTab]" class="tab"></component>
|
||||
@ -54,7 +49,7 @@ footer {
|
||||
background-color: var(--color-background-column);
|
||||
border-radius: var(--border-radius);
|
||||
bottom: var(--grid-gap);
|
||||
box-shadow: 0 0 5rem rgba(0, 0, 0, .66), 0 0 1.33rem rgba(0, 0, 0, .66);
|
||||
box-shadow: 0 0 5rem rgba(0, 0, 0, 0.66), 0 0 1.33rem rgba(0, 0, 0, 0.66);
|
||||
left: var(--grid-gap);
|
||||
padding: var(--spacer-xs) var(--spacer-sm) var(--spacer-sm);
|
||||
position: fixed;
|
||||
@ -77,7 +72,8 @@ footer nav ul li {
|
||||
color: var(--color-text-hint);
|
||||
cursor: pointer;
|
||||
padding: var(--spacer-sm) 0;
|
||||
transition: border-color var(--transition-speed) ease-in-out, color var(--transition-speed) ease-in-out;
|
||||
transition: border-color var(--transition-speed) ease-in-out,
|
||||
color var(--transition-speed) ease-in-out;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@ -99,7 +95,6 @@ footer nav ul li.active {
|
||||
padding: 0 var(--spacer-sm) 0;
|
||||
}
|
||||
|
||||
|
||||
footer button.footer-tab {
|
||||
border: none;
|
||||
margin-right: 1rem;
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<section class="notification-bar">
|
||||
<span class='notifications' v-if="notifs.last">{{ notifs.last.msg }}</span>
|
||||
<span class="notifications" v-if="notifs.last">{{ notifs.last.msg }}</span>
|
||||
<connection-status />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import ConnectionStatus from '@/components/ConnectionStatus.vue'
|
||||
import ConnectionStatus from '@/components/ConnectionStatus.vue';
|
||||
|
||||
export default {
|
||||
name: 'NotificationBar',
|
||||
@ -17,7 +17,7 @@ export default {
|
||||
data: () => ({
|
||||
notifs: useNotifs(),
|
||||
}),
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -1,16 +1,18 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted } from 'vue';
|
||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||
import { useNotifs } from '@/stores/notifications'
|
||||
import * as datetime from "@/datetime";
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import * as datetime from '@/datetime';
|
||||
|
||||
const notifs = useNotifs();
|
||||
|
||||
const tabOptions = {
|
||||
columns: [
|
||||
{
|
||||
title: 'Time', field: 'time',
|
||||
sorter: 'alphanum', sorterParams: { alignEmptyValues: "top" },
|
||||
title: 'Time',
|
||||
field: 'time',
|
||||
sorter: 'alphanum',
|
||||
sorterParams: { alignEmptyValues: 'top' },
|
||||
formatter(cell) {
|
||||
const cellValue = cell.getData().time;
|
||||
return datetime.shortened(cellValue);
|
||||
@ -26,37 +28,34 @@ const tabOptions = {
|
||||
resizable: true,
|
||||
},
|
||||
],
|
||||
initialSort: [
|
||||
{ column: "time", dir: "asc" },
|
||||
],
|
||||
initialSort: [{ column: 'time', dir: 'asc' }],
|
||||
headerVisible: false,
|
||||
layout: "fitDataStretch",
|
||||
layout: 'fitDataStretch',
|
||||
resizableColumnFit: true,
|
||||
height: "calc(25vh - 3rem)", // Must be set in order for the virtual DOM to function correctly.
|
||||
height: 'calc(25vh - 3rem)', // Must be set in order for the virtual DOM to function correctly.
|
||||
data: notifs.history,
|
||||
placeholder: "Notification history will appear here",
|
||||
placeholder: 'Notification history will appear here',
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
let tabulator = null;
|
||||
onMounted(() => {
|
||||
tabulator = new Tabulator('#notification_list', tabOptions);
|
||||
tabulator.on("tableBuilt", _scrollToBottom);
|
||||
tabulator.on("tableBuilt", _subscribeToPinia);
|
||||
tabulator.on('tableBuilt', _scrollToBottom);
|
||||
tabulator.on('tableBuilt', _subscribeToPinia);
|
||||
});
|
||||
|
||||
function _scrollToBottom() {
|
||||
if (notifs.empty) return;
|
||||
tabulator.scrollToRow(notifs.lastID, "bottom", false);
|
||||
tabulator.scrollToRow(notifs.lastID, 'bottom', false);
|
||||
}
|
||||
function _subscribeToPinia() {
|
||||
notifs.$subscribe(() => {
|
||||
tabulator.setData(notifs.history)
|
||||
.then(_scrollToBottom)
|
||||
})
|
||||
tabulator.setData(notifs.history).then(_scrollToBottom);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="notification_list"></div>
|
||||
<div id="notification_list"></div>
|
||||
</template>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||
import { useTaskLog } from '@/stores/tasklog'
|
||||
import { useTasks } from '@/stores/tasks'
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { useTaskLog } from '@/stores/tasklog';
|
||||
import { useTasks } from '@/stores/tasks';
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { JobsApi } from '@/manager-api';
|
||||
|
||||
const taskLog = useTaskLog();
|
||||
@ -20,19 +20,19 @@ const tabOptions = {
|
||||
},
|
||||
],
|
||||
headerVisible: false,
|
||||
layout: "fitDataStretch",
|
||||
layout: 'fitDataStretch',
|
||||
resizableColumnFit: true,
|
||||
height: "calc(25vh - 3rem)", // Must be set in order for the virtual DOM to function correctly.
|
||||
height: 'calc(25vh - 3rem)', // Must be set in order for the virtual DOM to function correctly.
|
||||
data: taskLog.history,
|
||||
placeholder: "Task log will appear here",
|
||||
placeholder: 'Task log will appear here',
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
let tabulator = null;
|
||||
onMounted(() => {
|
||||
tabulator = new Tabulator('#task_log_list', tabOptions);
|
||||
tabulator.on("tableBuilt", _scrollToBottom);
|
||||
tabulator.on("tableBuilt", _subscribeToPinia);
|
||||
tabulator.on('tableBuilt', _scrollToBottom);
|
||||
tabulator.on('tableBuilt', _subscribeToPinia);
|
||||
_fetchLogTail(tasks.activeTaskID);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
@ -45,13 +45,12 @@ tasks.$subscribe((_, state) => {
|
||||
|
||||
function _scrollToBottom() {
|
||||
if (taskLog.empty) return;
|
||||
tabulator.scrollToRow(taskLog.lastID, "bottom", false);
|
||||
tabulator.scrollToRow(taskLog.lastID, 'bottom', false);
|
||||
}
|
||||
function _subscribeToPinia() {
|
||||
taskLog.$subscribe(() => {
|
||||
tabulator.setData(taskLog.history)
|
||||
.then(_scrollToBottom)
|
||||
})
|
||||
tabulator.setData(taskLog.history).then(_scrollToBottom);
|
||||
});
|
||||
}
|
||||
|
||||
function _fetchLogTail(taskID) {
|
||||
@ -60,10 +59,9 @@ function _fetchLogTail(taskID) {
|
||||
if (!taskID) return;
|
||||
|
||||
const jobsAPI = new JobsApi(getAPIClient());
|
||||
return jobsAPI.fetchTaskLogTail(taskID)
|
||||
.then((logTail) => {
|
||||
taskLog.addChunk(logTail);
|
||||
});
|
||||
return jobsAPI.fetchTaskLogTail(taskID).then((logTail) => {
|
||||
taskLog.addChunk(logTail);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -14,8 +14,14 @@
|
||||
<link-worker :worker="{ id: entry.worker_id, name: entry.worker_name }" />
|
||||
</td>
|
||||
<td>{{ entry.task_type }}</td>
|
||||
<td><button class="btn in-table-row" @click="removeBlocklistEntry(entry)"
|
||||
title="Allow this worker to execute these task types">❌</button></td>
|
||||
<td>
|
||||
<button
|
||||
class="btn in-table-row"
|
||||
@click="removeBlocklistEntry(entry)"
|
||||
title="Allow this worker to execute these task types">
|
||||
❌
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div v-else class="dl-no-data">
|
||||
@ -26,19 +32,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { JobsApi } from '@/manager-api';
|
||||
import LinkWorker from '@/components/LinkWorker.vue';
|
||||
import { watch, onMounted, inject, ref, nextTick } from 'vue'
|
||||
import { watch, onMounted, inject, ref, nextTick } from 'vue';
|
||||
|
||||
// jobID should be the job UUID string.
|
||||
const props = defineProps(['jobID']);
|
||||
const emit = defineEmits(['reshuffled'])
|
||||
const emit = defineEmits(['reshuffled']);
|
||||
|
||||
const jobsApi = new JobsApi(getAPIClient());
|
||||
const isVisible = inject("isVisible");
|
||||
const isVisible = inject('isVisible');
|
||||
const isFetching = ref(false);
|
||||
const errorMsg = ref("");
|
||||
const errorMsg = ref('');
|
||||
const blocklist = ref([]);
|
||||
|
||||
function refreshBlocklist() {
|
||||
@ -47,7 +53,8 @@ function refreshBlocklist() {
|
||||
}
|
||||
|
||||
isFetching.value = true;
|
||||
jobsApi.fetchJobBlocklist(props.jobID)
|
||||
jobsApi
|
||||
.fetchJobBlocklist(props.jobID)
|
||||
.then((newBlocklist) => {
|
||||
blocklist.value = newBlocklist;
|
||||
})
|
||||
@ -56,28 +63,36 @@ function refreshBlocklist() {
|
||||
})
|
||||
.finally(() => {
|
||||
isFetching.value = false;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function removeBlocklistEntry(blocklistEntry) {
|
||||
jobsApi.removeJobBlocklist(props.jobID, { jobBlocklistEntry: [blocklistEntry] })
|
||||
jobsApi
|
||||
.removeJobBlocklist(props.jobID, { jobBlocklistEntry: [blocklistEntry] })
|
||||
.then(() => {
|
||||
blocklist.value = blocklist.value.filter(
|
||||
(entry) => !(entry.worker_id == blocklistEntry.worker_id && entry.task_type == blocklistEntry.task_type));
|
||||
(entry) =>
|
||||
!(
|
||||
entry.worker_id == blocklistEntry.worker_id &&
|
||||
entry.task_type == blocklistEntry.task_type
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error removing entry from blocklist", error);
|
||||
console.log('Error removing entry from blocklist', error);
|
||||
refreshBlocklist();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.jobID, refreshBlocklist);
|
||||
watch(blocklist, () => {
|
||||
const emitter = () => { emit("reshuffled") };
|
||||
const emitter = () => {
|
||||
emit('reshuffled');
|
||||
};
|
||||
nextTick(() => {
|
||||
nextTick(emitter);
|
||||
});
|
||||
})
|
||||
});
|
||||
watch(isVisible, refreshBlocklist);
|
||||
onMounted(refreshBlocklist);
|
||||
</script>
|
||||
@ -93,7 +108,7 @@ table.blocklist {
|
||||
table.blocklist td,
|
||||
table.blocklist th {
|
||||
text-align: left;
|
||||
padding: calc(var(--spacer-sm)/2) var(--spacer-sm);
|
||||
padding: calc(var(--spacer-sm) / 2) var(--spacer-sm);
|
||||
}
|
||||
|
||||
table.blocklist th {
|
||||
|
@ -8,26 +8,32 @@
|
||||
<button class="btn delete dangerous" v-on:click="onButtonDeleteConfirmed">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn cancel" :disabled="!jobs.canCancel" v-on:click="onButtonCancel">Cancel Job</button>
|
||||
<button class="btn requeue" :disabled="!jobs.canRequeue" v-on:click="onButtonRequeue">Requeue</button>
|
||||
<button class="action delete dangerous" title="Mark this job for deletion, after asking for a confirmation."
|
||||
:disabled="!jobs.canDelete" v-on:click="onButtonDelete">Delete...</button>
|
||||
<button class="btn cancel" :disabled="!jobs.canCancel" v-on:click="onButtonCancel">
|
||||
Cancel Job
|
||||
</button>
|
||||
<button class="btn requeue" :disabled="!jobs.canRequeue" v-on:click="onButtonRequeue">
|
||||
Requeue
|
||||
</button>
|
||||
<button
|
||||
class="action delete dangerous"
|
||||
title="Mark this job for deletion, after asking for a confirmation."
|
||||
:disabled="!jobs.canDelete"
|
||||
v-on:click="onButtonDelete">
|
||||
Delete...
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useJobs } from '@/stores/jobs';
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { JobsApi } from '@/manager-api';
|
||||
import { JobDeletionInfo } from '@/manager-api';
|
||||
|
||||
|
||||
export default {
|
||||
name: "JobActionsBar",
|
||||
props: [
|
||||
"activeJobID",
|
||||
],
|
||||
name: 'JobActionsBar',
|
||||
props: ['activeJobID'],
|
||||
data: () => ({
|
||||
jobs: useJobs(),
|
||||
notifs: useNotifs(),
|
||||
@ -35,8 +41,7 @@ export default {
|
||||
|
||||
deleteInfo: null,
|
||||
}),
|
||||
computed: {
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
activeJobID() {
|
||||
this._hideDeleteJobPopup();
|
||||
@ -47,51 +52,48 @@ export default {
|
||||
this._startJobDeletionFlow();
|
||||
},
|
||||
onButtonDeleteConfirmed() {
|
||||
return this.jobs.deleteJobs()
|
||||
return this.jobs
|
||||
.deleteJobs()
|
||||
.then(() => {
|
||||
this.notifs.add("job marked for deletion");
|
||||
this.notifs.add('job marked for deletion');
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
||||
this.notifs.add(`Error: ${errorMsg}`);
|
||||
})
|
||||
.finally(this._hideDeleteJobPopup)
|
||||
;
|
||||
.finally(this._hideDeleteJobPopup);
|
||||
},
|
||||
onButtonCancel() {
|
||||
return this._handleJobActionPromise(
|
||||
this.jobs.cancelJobs(), "marked for cancellation");
|
||||
return this._handleJobActionPromise(this.jobs.cancelJobs(), 'marked for cancellation');
|
||||
},
|
||||
onButtonRequeue() {
|
||||
return this._handleJobActionPromise(
|
||||
this.jobs.requeueJobs(), "requeueing");
|
||||
return this._handleJobActionPromise(this.jobs.requeueJobs(), 'requeueing');
|
||||
},
|
||||
|
||||
_handleJobActionPromise(promise, description) {
|
||||
return promise
|
||||
.then(() => {
|
||||
// There used to be a call to `this.notifs.add(message)` here, but now
|
||||
// that job status changes are logged in the notifications anyway,
|
||||
// it's no longer necessary.
|
||||
// This function is still kept, in case we want to bring back the
|
||||
// notifications when multiple jobs can be selected. Then a summary
|
||||
// ("N jobs requeued") could be logged here.btn-bar-popover
|
||||
})
|
||||
return promise.then(() => {
|
||||
// There used to be a call to `this.notifs.add(message)` here, but now
|
||||
// that job status changes are logged in the notifications anyway,
|
||||
// it's no longer necessary.
|
||||
// This function is still kept, in case we want to bring back the
|
||||
// notifications when multiple jobs can be selected. Then a summary
|
||||
// ("N jobs requeued") could be logged here.btn-bar-popover
|
||||
});
|
||||
},
|
||||
|
||||
_startJobDeletionFlow() {
|
||||
if (!this.activeJobID) {
|
||||
this.notifs.add("No active job, unable to delete anything");
|
||||
this.notifs.add('No active job, unable to delete anything');
|
||||
return;
|
||||
}
|
||||
|
||||
this.jobsAPI.deleteJobWhatWouldItDo(this.activeJobID)
|
||||
this.jobsAPI
|
||||
.deleteJobWhatWouldItDo(this.activeJobID)
|
||||
.then(this._showDeleteJobPopup)
|
||||
.catch((error) => {
|
||||
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
||||
this.notifs.add(`Error: ${errorMsg}`);
|
||||
})
|
||||
;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -103,10 +105,9 @@ export default {
|
||||
|
||||
_hideDeleteJobPopup() {
|
||||
this.deleteInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<TabsWrapper @clicked-job-details-tab="emit_reshuffled_delayed">
|
||||
<TabItem title="Job Settings">
|
||||
<dl v-if="hasSettings">
|
||||
<template v-for="value, key in settingsToDisplay">
|
||||
<template v-for="(value, key) in settingsToDisplay">
|
||||
<dt :class="`field-${key}`" :title="key">{{ key }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
</template>
|
||||
@ -18,7 +18,7 @@
|
||||
</TabItem>
|
||||
<TabItem title="Metadata">
|
||||
<dl v-if="hasMetadata">
|
||||
<template v-for="value, key in jobData.metadata">
|
||||
<template v-for="(value, key) in jobData.metadata">
|
||||
<dt :class="`field-${key}`" :title="key">{{ key }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
</template>
|
||||
@ -30,18 +30,17 @@
|
||||
<TabItem title="Details">
|
||||
<dl>
|
||||
<dt class="field-name" title="ID">ID</dt>
|
||||
<dd><span @click="copyElementText" class="click-to-copy">{{ jobData.id }}</span></dd>
|
||||
<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
|
||||
>
|
||||
<span @click="copyElementData" class="click-to-copy" :data-clipboard="workerTag.id">{{
|
||||
workerTag.name
|
||||
}}</span>
|
||||
</dd>
|
||||
</template>
|
||||
|
||||
@ -49,7 +48,9 @@
|
||||
<dd>{{ jobData.name }}</dd>
|
||||
|
||||
<dt class="field-status" title="Status">Status</dt>
|
||||
<dd class="field-status-label" :class="'status-' + jobData.status">{{ jobData.status }}</dd>
|
||||
<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>
|
||||
@ -87,24 +88,24 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as datetime from "@/datetime";
|
||||
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 { 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 { useWorkers } from '@/stores/workers';
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
"jobData", // Job data to show.
|
||||
'jobData', // Job data to show.
|
||||
],
|
||||
emits: [
|
||||
"reshuffled", // Emitted when the size of this component may have changed. Used to resize other components in response.
|
||||
'reshuffled', // Emitted when the size of this component may have changed. Used to resize other components in response.
|
||||
],
|
||||
components: {
|
||||
LastRenderedImage,
|
||||
@ -192,9 +193,12 @@ export default {
|
||||
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)
|
||||
this.jobsApi
|
||||
.getJobType(newJobData.type)
|
||||
.then(this.onJobTypeLoaded)
|
||||
.catch((error) => { console.warn("error fetching job type:", error) });
|
||||
.catch((error) => {
|
||||
console.warn('error fetching job type:', error);
|
||||
});
|
||||
} else {
|
||||
this._setJobSettings(newJobData.settings);
|
||||
}
|
||||
@ -205,8 +209,7 @@ export default {
|
||||
|
||||
// Construct a lookup table for the settings.
|
||||
const jobTypeSettings = {};
|
||||
for (let setting of jobType.settings)
|
||||
jobTypeSettings[setting.key] = setting;
|
||||
for (let setting of jobType.settings) jobTypeSettings[setting.key] = setting;
|
||||
this.jobTypeSettings = jobTypeSettings;
|
||||
|
||||
if (this.jobData) {
|
||||
@ -227,7 +230,7 @@ export default {
|
||||
}
|
||||
|
||||
if (objectEmpty(this.jobTypeSettings)) {
|
||||
console.warn("empty job type settings");
|
||||
console.warn('empty job type settings');
|
||||
this._clearJobSettings();
|
||||
return;
|
||||
}
|
||||
@ -255,7 +258,9 @@ export default {
|
||||
this.$emit('reshuffled');
|
||||
},
|
||||
emit_reshuffled_delayed() {
|
||||
const reshuffle = () => { this.$emit('reshuffled'); }
|
||||
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
|
||||
@ -269,7 +274,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
/* Prevent fields with long IDs from overflowing. */
|
||||
.field-id+dd {
|
||||
.field-id + dd {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -3,7 +3,9 @@
|
||||
<div class="btn-bar-group">
|
||||
<job-actions-bar :activeJobID="jobs.activeJobID" />
|
||||
<div class="align-right">
|
||||
<status-filter-bar :availableStatuses="availableStatuses" :activeStatuses="shownStatuses"
|
||||
<status-filter-bar
|
||||
:availableStatuses="availableStatuses"
|
||||
:activeStatuses="shownStatuses"
|
||||
@click="toggleStatusFilter" />
|
||||
</div>
|
||||
</div>
|
||||
@ -14,21 +16,22 @@
|
||||
|
||||
<script>
|
||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||
import * as datetime from "@/datetime";
|
||||
import * as API from '@/manager-api'
|
||||
import * as datetime from '@/datetime';
|
||||
import * as API from '@/manager-api';
|
||||
import { indicator } from '@/statusindicator';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { useJobs } from '@/stores/jobs';
|
||||
|
||||
import JobActionsBar from '@/components/jobs/JobActionsBar.vue'
|
||||
import StatusFilterBar from '@/components/StatusFilterBar.vue'
|
||||
import JobActionsBar from '@/components/jobs/JobActionsBar.vue';
|
||||
import StatusFilterBar from '@/components/StatusFilterBar.vue';
|
||||
|
||||
export default {
|
||||
name: 'JobsTable',
|
||||
props: ["activeJobID"],
|
||||
emits: ["tableRowClicked", "activeJobDeleted"],
|
||||
props: ['activeJobID'],
|
||||
emits: ['tableRowClicked', 'activeJobDeleted'],
|
||||
components: {
|
||||
JobActionsBar, StatusFilterBar,
|
||||
JobActionsBar,
|
||||
StatusFilterBar,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
@ -51,7 +54,9 @@ export default {
|
||||
// Useful for debugging when there are many similar jobs:
|
||||
// { 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) => {
|
||||
const status = cell.getData().status;
|
||||
const dot = indicator(status);
|
||||
@ -62,8 +67,10 @@ export default {
|
||||
{ title: 'Type', field: 'type', sorter: 'string' },
|
||||
{ title: 'Prio', field: 'priority', sorter: 'number' },
|
||||
{
|
||||
title: 'Updated', field: 'updated',
|
||||
sorter: 'alphanum', sorterParams: { alignEmptyValues: "top" },
|
||||
title: 'Updated',
|
||||
field: 'updated',
|
||||
sorter: 'alphanum',
|
||||
sorterParams: { alignEmptyValues: 'top' },
|
||||
formatter(cell) {
|
||||
const cellValue = cell.getData().updated;
|
||||
// TODO: if any "{amount} {units} ago" shown, the table should be
|
||||
@ -75,23 +82,21 @@ export default {
|
||||
],
|
||||
rowFormatter(row) {
|
||||
const data = row.getData();
|
||||
const isActive = (data.id === vueComponent.activeJobID);
|
||||
const isActive = data.id === vueComponent.activeJobID;
|
||||
const classList = row.getElement().classList;
|
||||
classList.toggle("active-row", isActive);
|
||||
classList.toggle("deletion-requested", !!data.delete_requested_at);
|
||||
classList.toggle('active-row', isActive);
|
||||
classList.toggle('deletion-requested', !!data.delete_requested_at);
|
||||
},
|
||||
initialSort: [
|
||||
{ column: "updated", dir: "desc" },
|
||||
],
|
||||
layout: "fitData",
|
||||
initialSort: [{ column: 'updated', dir: 'desc' }],
|
||||
layout: 'fitData',
|
||||
layoutColumnsOnNewData: true,
|
||||
height: "720px", // Must be set in order for the virtual DOM to function correctly.
|
||||
height: '720px', // Must be set in order for the virtual DOM to function correctly.
|
||||
data: [], // Will be filled via a Flamenco API request.
|
||||
selectable: false, // The active job is tracked by click events, not row selection.
|
||||
};
|
||||
this.tabulator = new Tabulator('#flamenco_job_list', options);
|
||||
this.tabulator.on("rowClick", this.onRowClick);
|
||||
this.tabulator.on("tableBuilt", this._onTableBuilt);
|
||||
this.tabulator.on('rowClick', this.onRowClick);
|
||||
this.tabulator.on('tableBuilt', this._onTableBuilt);
|
||||
|
||||
window.addEventListener('resize', this.recalcTableHeight);
|
||||
},
|
||||
@ -113,7 +118,7 @@ export default {
|
||||
computed: {
|
||||
selectedIDs() {
|
||||
return this.tabulator.getSelectedData().map((job) => job.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onReconnected() {
|
||||
@ -160,18 +165,20 @@ export default {
|
||||
if (jobUpdate.was_deleted) {
|
||||
if (row) promise = row.delete();
|
||||
else promise = Promise.resolve();
|
||||
promise.finally(() => { this.$emit("activeJobDeleted", jobUpdate.id); });
|
||||
}
|
||||
else {
|
||||
promise.finally(() => {
|
||||
this.$emit('activeJobDeleted', jobUpdate.id);
|
||||
});
|
||||
} else {
|
||||
if (row) promise = this.tabulator.updateData([jobUpdate]);
|
||||
else promise = this.tabulator.addData([jobUpdate]);
|
||||
}
|
||||
|
||||
promise
|
||||
.then(this.sortData)
|
||||
.then(() => { this.tabulator.redraw(); }) // Resize columns based on new data.
|
||||
.then(this._refreshAvailableStatuses)
|
||||
;
|
||||
.then(() => {
|
||||
this.tabulator.redraw();
|
||||
}) // Resize columns based on new data.
|
||||
.then(this._refreshAvailableStatuses);
|
||||
},
|
||||
|
||||
onRowClick(event, row) {
|
||||
@ -179,7 +186,7 @@ export default {
|
||||
// store. There were some issues where navigating to another job would
|
||||
// overwrite the old job's ID, and this prevents that.
|
||||
const rowData = plain(row.getData());
|
||||
this.$emit("tableRowClicked", rowData);
|
||||
this.$emit('tableRowClicked', rowData);
|
||||
},
|
||||
toggleStatusFilter(status) {
|
||||
const asSet = new Set(this.shownStatuses);
|
||||
@ -207,7 +214,7 @@ export default {
|
||||
// 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) return;
|
||||
if (row.reformat) row.reformat();
|
||||
else if (row.reinitialize) row.reinitialize(true);
|
||||
},
|
||||
@ -237,7 +244,9 @@ export default {
|
||||
// `offsetParent` is assumed to be the actual column in the 3-column
|
||||
// view. To ensure this, it's given `position: relative` in the CSS
|
||||
// styling.
|
||||
console.warn("JobsTable.recalcTableHeight() only works when the offset parent is the real parent of the element.");
|
||||
console.warn(
|
||||
'JobsTable.recalcTableHeight() only works when the offset parent is the real parent of the element.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
|
||||
<template>
|
||||
<div v-if="imageURL != ''" :class="cssClasses">
|
||||
<img :src="imageURL" alt="Last-rendered image for this job">
|
||||
<img :src="imageURL" alt="Last-rendered image for this job" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { api } from '@/urls';
|
||||
import { JobsApi, JobLastRenderedImageInfo, SocketIOLastRenderedUpdate } from '@/manager-api';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
const props = defineProps([
|
||||
/* The job UUID to show renders for, or some false-y value if renders from all
|
||||
@ -25,7 +24,7 @@ const imageURL = ref('');
|
||||
const cssClasses = reactive({
|
||||
'last-rendered': true,
|
||||
'nothing-rendered-yet': true,
|
||||
})
|
||||
});
|
||||
|
||||
const jobsApi = new JobsApi(getAPIClient());
|
||||
|
||||
@ -34,14 +33,12 @@ const jobsApi = new JobsApi(getAPIClient());
|
||||
*/
|
||||
function fetchImageURL(jobID) {
|
||||
let promise;
|
||||
if (jobID)
|
||||
promise = jobsApi.fetchJobLastRenderedInfo(jobID);
|
||||
else
|
||||
promise = jobsApi.fetchGlobalLastRenderedInfo();
|
||||
if (jobID) promise = jobsApi.fetchJobLastRenderedInfo(jobID);
|
||||
else promise = jobsApi.fetchGlobalLastRenderedInfo();
|
||||
|
||||
promise
|
||||
.then(setImageURL)
|
||||
.catch((error) => { console.warn("error fetching last-rendered image info:", error) });
|
||||
promise.then(setImageURL).catch((error) => {
|
||||
console.warn('error fetching last-rendered image info:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,7 +48,7 @@ function setImageURL(thumbnailInfo) {
|
||||
if (thumbnailInfo == null) {
|
||||
// This indicates that there is no last-rendered image.
|
||||
// Default to a hard-coded 'nothing to be seen here, move along' image.
|
||||
imageURL.value = "/app/nothing-rendered-yet.svg";
|
||||
imageURL.value = '/app/nothing-rendered-yet.svg';
|
||||
cssClasses['nothing-rendered-yet'] = true;
|
||||
return;
|
||||
}
|
||||
@ -66,14 +63,17 @@ function setImageURL(thumbnailInfo) {
|
||||
// Flamenco Manager, and not from any development server that might be
|
||||
// serving the webapp.
|
||||
let url = new URL(api());
|
||||
url.pathname = thumbnailInfo.base + "/" + suffix
|
||||
url.pathname = thumbnailInfo.base + '/' + suffix;
|
||||
url.search = new Date().getTime(); // This forces the image to be reloaded.
|
||||
imageURL.value = url.toString();
|
||||
foundThumbnail = true;
|
||||
break;
|
||||
}
|
||||
if (!foundThumbnail) {
|
||||
console.warn(`LastRenderedImage.vue: could not find thumbnail with suffix "${suffixToFind}"; available are:`, thumbnailInfo.suffixes);
|
||||
console.warn(
|
||||
`LastRenderedImage.vue: could not find thumbnail with suffix "${suffixToFind}"; available are:`,
|
||||
thumbnailInfo.suffixes
|
||||
);
|
||||
}
|
||||
cssClasses['nothing-rendered-yet'] = !foundThumbnail;
|
||||
}
|
||||
@ -85,9 +85,11 @@ function refreshLastRenderedImage(lastRenderedUpdate) {
|
||||
// Only filter out other job IDs if this component has actually a non-empty job ID.
|
||||
if (props.jobID && lastRenderedUpdate.job_id != props.jobID) {
|
||||
console.log(
|
||||
"LastRenderedImage.vue: refreshLastRenderedImage() received update for job",
|
||||
'LastRenderedImage.vue: refreshLastRenderedImage() received update for job',
|
||||
lastRenderedUpdate.job_id,
|
||||
"but this component is showing job", props.jobID);
|
||||
'but this component is showing job',
|
||||
props.jobID
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -95,9 +97,12 @@ function refreshLastRenderedImage(lastRenderedUpdate) {
|
||||
}
|
||||
|
||||
// Call fetchImageURL(jobID) whenever the job ID prop changes value.
|
||||
watch(() => props.jobID, (newJobID) => {
|
||||
fetchImageURL(newJobID);
|
||||
});
|
||||
watch(
|
||||
() => props.jobID,
|
||||
(newJobID) => {
|
||||
fetchImageURL(newJobID);
|
||||
}
|
||||
);
|
||||
fetchImageURL(props.jobID);
|
||||
|
||||
// Expose refreshLastRenderedImage() so that it can be called from the parent
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<section class="btn-bar tasks">
|
||||
<button class="btn cancel" :disabled="!tasks.canCancel" v-on:click="onButtonCancel">Cancel Task</button>
|
||||
<button class="btn requeue" :disabled="!tasks.canRequeue" v-on:click="onButtonRequeue">Requeue</button>
|
||||
<button class="btn cancel" :disabled="!tasks.canCancel" v-on:click="onButtonCancel">
|
||||
Cancel Task
|
||||
</button>
|
||||
<button class="btn requeue" :disabled="!tasks.canRequeue" v-on:click="onButtonRequeue">
|
||||
Requeue
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -10,21 +14,18 @@ import { useTasks } from '@/stores/tasks';
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
|
||||
export default {
|
||||
name: "TaskActionsBar",
|
||||
name: 'TaskActionsBar',
|
||||
data: () => ({
|
||||
tasks: useTasks(),
|
||||
notifs: useNotifs(),
|
||||
}),
|
||||
computed: {
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
onButtonCancel() {
|
||||
return this._handleTaskActionPromise(
|
||||
this.tasks.cancelTasks(), "cancelled");
|
||||
return this._handleTaskActionPromise(this.tasks.cancelTasks(), 'cancelled');
|
||||
},
|
||||
onButtonRequeue() {
|
||||
return this._handleTaskActionPromise(
|
||||
this.tasks.requeueTasks(), "requeueing");
|
||||
return this._handleTaskActionPromise(this.tasks.requeueTasks(), 'requeueing');
|
||||
},
|
||||
|
||||
_handleTaskActionPromise(promise, description) {
|
||||
@ -42,8 +43,8 @@ export default {
|
||||
.catch((error) => {
|
||||
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
||||
this.notifs.add(`Error: ${errorMsg}`);
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -4,7 +4,9 @@
|
||||
<template v-if="hasTaskData">
|
||||
<dl>
|
||||
<dt class="field-id" title="ID">ID</dt>
|
||||
<dd><span @click="copyElementText" class="click-to-copy">{{ taskData.id }}</span></dd>
|
||||
<dd>
|
||||
<span @click="copyElementText" class="click-to-copy">{{ taskData.id }}</span>
|
||||
</dd>
|
||||
|
||||
<dt class="field-name" title="Name">Name</dt>
|
||||
<dd>{{ taskData.name }}</dd>
|
||||
@ -57,9 +59,15 @@
|
||||
<h3 class="sub-title">Task Log</h3>
|
||||
<div class="btn-bar-group">
|
||||
<section class="btn-bar tasklog">
|
||||
<button class="btn" @click="$emit('showTaskLogTail')" title="Open the task log tail in the footer.">
|
||||
Follow Task Log</button>
|
||||
<button class="btn" @click="openFullLog" title="Opens the task log in a new window.">Open Full Log</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="$emit('showTaskLogTail')"
|
||||
title="Open the task log tail in the footer.">
|
||||
Follow Task Log
|
||||
</button>
|
||||
<button class="btn" @click="openFullLog" title="Opens the task log in a new window.">
|
||||
Open Full Log
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@ -70,20 +78,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as datetime from "@/datetime";
|
||||
import * as datetime from '@/datetime';
|
||||
import { JobsApi } from '@/manager-api';
|
||||
import { backendURL } from '@/urls';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { useNotifs } from "@/stores/notifications";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import LinkWorker from '@/components/LinkWorker.vue';
|
||||
import { copyElementText } from '@/clipboard';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
"taskData", // Task data to show.
|
||||
'taskData', // Task data to show.
|
||||
],
|
||||
emits: [
|
||||
"showTaskLogTail", // Emitted when the user presses the "follow task log" button.
|
||||
'showTaskLogTail', // Emitted when the user presses the "follow task log" button.
|
||||
],
|
||||
components: { LinkWorker },
|
||||
data() {
|
||||
@ -107,20 +115,21 @@ export default {
|
||||
openFullLog() {
|
||||
const taskUUID = this.taskData.id;
|
||||
|
||||
this.jobsApi.fetchTaskLogInfo(taskUUID)
|
||||
this.jobsApi
|
||||
.fetchTaskLogInfo(taskUUID)
|
||||
.then((logInfo) => {
|
||||
if (logInfo == null) {
|
||||
this.notifs.add(`Task ${taskUUID} has no log yet`)
|
||||
this.notifs.add(`Task ${taskUUID} has no log yet`);
|
||||
return;
|
||||
}
|
||||
console.log(`task ${taskUUID} log info:`, logInfo);
|
||||
|
||||
const url = backendURL(logInfo.url);
|
||||
window.open(url, "_blank");
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`Error fetching task ${taskUUID} log info:`, error);
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -128,7 +137,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
/* Prevent fields with long IDs from overflowing. */
|
||||
.field-id+dd {
|
||||
.field-id + dd {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -3,7 +3,9 @@
|
||||
<div class="btn-bar-group">
|
||||
<task-actions-bar />
|
||||
<div class="align-right">
|
||||
<status-filter-bar :availableStatuses="availableStatuses" :activeStatuses="shownStatuses"
|
||||
<status-filter-bar
|
||||
:availableStatuses="availableStatuses"
|
||||
:activeStatuses="shownStatuses"
|
||||
@click="toggleStatusFilter" />
|
||||
</div>
|
||||
</div>
|
||||
@ -14,23 +16,24 @@
|
||||
|
||||
<script>
|
||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||
import * as datetime from "@/datetime";
|
||||
import * as API from '@/manager-api'
|
||||
import * as datetime from '@/datetime';
|
||||
import * as API from '@/manager-api';
|
||||
import { indicator } from '@/statusindicator';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { useTasks } from '@/stores/tasks';
|
||||
|
||||
import TaskActionsBar from '@/components/jobs/TaskActionsBar.vue'
|
||||
import StatusFilterBar from '@/components/StatusFilterBar.vue'
|
||||
import TaskActionsBar from '@/components/jobs/TaskActionsBar.vue';
|
||||
import StatusFilterBar from '@/components/StatusFilterBar.vue';
|
||||
|
||||
export default {
|
||||
emits: ["tableRowClicked"],
|
||||
emits: ['tableRowClicked'],
|
||||
props: [
|
||||
"jobID", // ID of the job of which the tasks are shown here.
|
||||
"taskID", // The active task.
|
||||
'jobID', // ID of the job of which the tasks are shown here.
|
||||
'taskID', // The active task.
|
||||
],
|
||||
components: {
|
||||
TaskActionsBar, StatusFilterBar,
|
||||
TaskActionsBar,
|
||||
StatusFilterBar,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
@ -52,7 +55,9 @@ export default {
|
||||
// Useful for debugging when there are many similar tasks:
|
||||
// { 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) => {
|
||||
const status = cell.getData().status;
|
||||
const dot = indicator(status);
|
||||
@ -61,36 +66,36 @@ export default {
|
||||
},
|
||||
{ title: 'Name', field: 'name', sorter: 'string' },
|
||||
{
|
||||
title: 'Updated', field: 'updated',
|
||||
sorter: 'alphanum', sorterParams: { alignEmptyValues: "top" },
|
||||
title: 'Updated',
|
||||
field: 'updated',
|
||||
sorter: 'alphanum',
|
||||
sorterParams: { alignEmptyValues: 'top' },
|
||||
formatter(cell) {
|
||||
const cellValue = cell.getData().updated;
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
rowFormatter(row) {
|
||||
const data = row.getData();
|
||||
const isActive = (data.id === vueComponent.taskID);
|
||||
row.getElement().classList.toggle("active-row", isActive);
|
||||
const isActive = data.id === vueComponent.taskID;
|
||||
row.getElement().classList.toggle('active-row', isActive);
|
||||
},
|
||||
initialSort: [
|
||||
{ column: "updated", dir: "desc" },
|
||||
],
|
||||
layout: "fitData",
|
||||
initialSort: [{ column: 'updated', dir: 'desc' }],
|
||||
layout: 'fitData',
|
||||
layoutColumnsOnNewData: true,
|
||||
height: "100%", // Must be set in order for the virtual DOM to function correctly.
|
||||
maxHeight: "100%",
|
||||
height: '100%', // Must be set in order for the virtual DOM to function correctly.
|
||||
maxHeight: '100%',
|
||||
data: [], // Will be filled via a Flamenco API request.
|
||||
selectable: false, // The active task is tracked by click events.
|
||||
};
|
||||
|
||||
this.tabulator = new Tabulator('#flamenco_task_list', options);
|
||||
this.tabulator.on("rowClick", this.onRowClick);
|
||||
this.tabulator.on("tableBuilt", this._onTableBuilt);
|
||||
this.tabulator.on('rowClick', this.onRowClick);
|
||||
this.tabulator.on('tableBuilt', this._onTableBuilt);
|
||||
|
||||
window.addEventListener('resize', this.recalcTableHeight);
|
||||
},
|
||||
@ -133,11 +138,10 @@ export default {
|
||||
}
|
||||
|
||||
const jobsApi = new API.JobsApi(getAPIClient());
|
||||
jobsApi.fetchJobTasks(this.jobID)
|
||||
.then(this.onTasksFetched, function (error) {
|
||||
// TODO: error handling.
|
||||
console.error(error);
|
||||
})
|
||||
jobsApi.fetchJobTasks(this.jobID).then(this.onTasksFetched, function (error) {
|
||||
// TODO: error handling.
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
onTasksFetched(data) {
|
||||
// "Down-cast" to TaskUpdate to only get those fields, just for debugging things:
|
||||
@ -151,9 +155,12 @@ export default {
|
||||
// updateData() will only overwrite properties that are actually set on
|
||||
// taskUpdate, and leave the rest as-is.
|
||||
if (this.tabulator.initialized) {
|
||||
this.tabulator.updateData([taskUpdate])
|
||||
this.tabulator
|
||||
.updateData([taskUpdate])
|
||||
.then(this.sortData)
|
||||
.then(() => { this.tabulator.redraw(); }) // Resize columns based on new data.
|
||||
.then(() => {
|
||||
this.tabulator.redraw();
|
||||
}); // Resize columns based on new data.
|
||||
}
|
||||
this._refreshAvailableStatuses();
|
||||
},
|
||||
@ -163,7 +170,7 @@ export default {
|
||||
// store. There were some issues where navigating to another job would
|
||||
// overwrite the old job's ID, and this prevents that.
|
||||
const rowData = plain(row.getData());
|
||||
this.$emit("tableRowClicked", rowData);
|
||||
this.$emit('tableRowClicked', rowData);
|
||||
},
|
||||
toggleStatusFilter(status) {
|
||||
const asSet = new Set(this.shownStatuses);
|
||||
@ -191,7 +198,7 @@ export default {
|
||||
// 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) return;
|
||||
if (row.reformat) row.reformat();
|
||||
else if (row.reinitialize) row.reinitialize(true);
|
||||
},
|
||||
@ -221,7 +228,9 @@ export default {
|
||||
// `offsetParent` is assumed to be the actual column in the 3-column
|
||||
// view. To ensure this, it's given `position: relative` in the CSS
|
||||
// styling.
|
||||
console.warn("TaskTable.recalcTableHeight() only works when the offset parent is the real parent of the element.");
|
||||
console.warn(
|
||||
'TaskTable.recalcTableHeight() only works when the offset parent is the real parent of the element.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -234,7 +243,6 @@ export default {
|
||||
|
||||
this.tabulator.setHeight(tableHeight);
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
@ -3,15 +3,15 @@ const props = defineProps({
|
||||
title: String,
|
||||
nextLabel: {
|
||||
type: String,
|
||||
default: 'Next'
|
||||
default: 'Next',
|
||||
},
|
||||
isBackVisible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
isNextClickable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -21,18 +21,12 @@ const props = defineProps({
|
||||
<h2>{{ title }}</h2>
|
||||
<slot></slot>
|
||||
<div class="btn-bar btn-bar-wide">
|
||||
<button
|
||||
v-show="isBackVisible"
|
||||
@click="$emit('backClicked')"
|
||||
class="btn btn-lg"
|
||||
>Back
|
||||
</button>
|
||||
<button v-show="isBackVisible" @click="$emit('backClicked')" class="btn btn-lg">Back</button>
|
||||
<button
|
||||
@click="$emit('nextClicked')"
|
||||
:disabled="!isNextClickable"
|
||||
class="btn btn-lg btn-primary"
|
||||
>
|
||||
{{ nextLabel }}
|
||||
class="btn btn-lg btn-primary">
|
||||
{{ nextLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,15 +8,17 @@
|
||||
<option :value="key" v-if="action.condition()">{{ action.label }}</option>
|
||||
</template>
|
||||
</select>
|
||||
<button :disabled="!canPerformAction" class="btn" @click.prevent="performWorkerAction">Apply</button>
|
||||
<button :disabled="!canPerformAction" class="btn" @click.prevent="performWorkerAction">
|
||||
Apply
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref } from 'vue';
|
||||
import { useWorkers } from '@/stores/workers';
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import { WorkerMgtApi, WorkerStatusChangeRequest } from '@/manager-api';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
/* Freeze to prevent Vue.js from creating getters & setters all over this object.
|
||||
* We don't need it to be tracked, as it won't be changed anyway. */
|
||||
@ -24,7 +26,8 @@ const WORKER_ACTIONS = Object.freeze({
|
||||
offline_lazy: {
|
||||
label: 'Shut Down (after task is finished)',
|
||||
icon: '✝',
|
||||
title: 'Shut down the worker after the current task finishes. The worker may automatically restart.',
|
||||
title:
|
||||
'Shut down the worker after the current task finishes. The worker may automatically restart.',
|
||||
target_status: 'offline',
|
||||
lazy: true,
|
||||
condition: () => true,
|
||||
@ -88,19 +91,19 @@ const notifs = useNotifs();
|
||||
function performWorkerAction() {
|
||||
const workerID = workers.activeWorkerID;
|
||||
if (!workerID) {
|
||||
notifs.add("Select a Worker before applying an action.");
|
||||
notifs.add('Select a Worker before applying an action.');
|
||||
return;
|
||||
}
|
||||
|
||||
const api = new WorkerMgtApi(getAPIClient());
|
||||
const action = WORKER_ACTIONS[selectedAction.value];
|
||||
const statuschange = new WorkerStatusChangeRequest(action.target_status, action.lazy);
|
||||
console.log("Requesting worker status change", statuschange);
|
||||
api.requestWorkerStatusChange(workerID, statuschange)
|
||||
console.log('Requesting worker status change', statuschange);
|
||||
api
|
||||
.requestWorkerStatusChange(workerID, statuschange)
|
||||
.then((result) => notifs.add(`Worker status change to ${action.target_status} confirmed.`))
|
||||
.catch((error) => {
|
||||
notifs.add(`Error requesting worker status change: ${error.body.message}`)
|
||||
notifs.add(`Error requesting worker status change: ${error.body.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -4,7 +4,9 @@
|
||||
<template v-if="hasWorkerData">
|
||||
<dl>
|
||||
<dt class="field-id">ID</dt>
|
||||
<dd><span @click="copyElementText" class="click-to-copy">{{ workerData.id }}</span></dd>
|
||||
<dd>
|
||||
<span @click="copyElementText" class="click-to-copy">{{ workerData.id }}</span>
|
||||
</dd>
|
||||
|
||||
<dt class="field-name">Name</dt>
|
||||
<dd>{{ workerData.name }}</dd>
|
||||
@ -20,7 +22,9 @@
|
||||
<dd title="Version of Flamenco">{{ workerData.version }}</dd>
|
||||
|
||||
<dt class="field-ip_address">IP Addr</dt>
|
||||
<dd><span @click="copyElementText" class="click-to-copy">{{ workerData.ip_address }}</span></dd>
|
||||
<dd>
|
||||
<span @click="copyElementText" class="click-to-copy">{{ workerData.ip_address }}</span>
|
||||
</dd>
|
||||
|
||||
<dt class="field-platform">Platform</dt>
|
||||
<dd>{{ workerData.platform }}</dd>
|
||||
@ -35,7 +39,7 @@
|
||||
|
||||
<template v-if="workerData.can_restart">
|
||||
<dt class="field-can-restart">Can Restart</dt>
|
||||
<dd>{{ workerData.can_restart }}</dd>
|
||||
<dd>{{ workerData.can_restart }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
|
||||
@ -47,56 +51,73 @@
|
||||
:isChecked="thisWorkerTags[tag.id]"
|
||||
:label="tag.name"
|
||||
:title="tag.description"
|
||||
@switch-toggle="toggleWorkerTag(tag.id)"
|
||||
>
|
||||
@switch-toggle="toggleWorkerTag(tag.id)">
|
||||
</switch-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="hint" v-if="hasTagsAssigned">
|
||||
This worker will only pick up jobs assigned to one of its tags, and
|
||||
tagless jobs.
|
||||
This worker will only pick up jobs assigned to one of its tags, and tagless jobs.
|
||||
</p>
|
||||
<p class="hint" v-else>This worker will only pick up tagless jobs.</p>
|
||||
</section>
|
||||
|
||||
<section class="sleep-schedule" :class="{ 'is-schedule-active': workerSleepSchedule.is_active }">
|
||||
<section
|
||||
class="sleep-schedule"
|
||||
:class="{ 'is-schedule-active': workerSleepSchedule.is_active }">
|
||||
<h3 class="sub-title">
|
||||
<switch-checkbox :isChecked="workerSleepSchedule.is_active" @switch-toggle="toggleWorkerSleepSchedule">
|
||||
<switch-checkbox
|
||||
:isChecked="workerSleepSchedule.is_active"
|
||||
@switch-toggle="toggleWorkerSleepSchedule">
|
||||
</switch-checkbox>
|
||||
Sleep Schedule
|
||||
<div v-if="!isScheduleEditing" class="sub-title-buttons">
|
||||
<button @click="isScheduleEditing = true">Edit</button>
|
||||
</div>
|
||||
</h3>
|
||||
<p>Time of the day (and on which days) this worker should go to sleep. </p>
|
||||
<p>Time of the day (and on which days) this worker should go to sleep.</p>
|
||||
|
||||
<div class="sleep-schedule-edit" v-if="isScheduleEditing">
|
||||
<div>
|
||||
<label>Days of the week</label>
|
||||
<input type="text" placeholder="mo tu we th fr" v-model="workerSleepSchedule.days_of_week">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="mo tu we th fr"
|
||||
v-model="workerSleepSchedule.days_of_week" />
|
||||
<span class="input-help-text">
|
||||
Write each day name using their first two letters, separated by spaces.
|
||||
(e.g. mo tu we th fr)
|
||||
Write each day name using their first two letters, separated by spaces. (e.g. mo tu we
|
||||
th fr)
|
||||
</span>
|
||||
</div>
|
||||
<div class="sleep-schedule-edit-time">
|
||||
<div>
|
||||
<label>Start Time</label>
|
||||
<input type="text" placeholder="09:00" v-model="workerSleepSchedule.start_time" class="time">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="09:00"
|
||||
v-model="workerSleepSchedule.start_time"
|
||||
class="time" />
|
||||
</div>
|
||||
<div>
|
||||
<label>End Time</label>
|
||||
<input type="text" placeholder="18:00" v-model="workerSleepSchedule.end_time" class="time">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="18:00"
|
||||
v-model="workerSleepSchedule.end_time"
|
||||
class="time" />
|
||||
</div>
|
||||
</div>
|
||||
<span class="input-help-text">
|
||||
Use 24-hour format.
|
||||
</span>
|
||||
<span class="input-help-text"> Use 24-hour format. </span>
|
||||
<div class="btn-bar-group">
|
||||
<div class="btn-bar">
|
||||
<button v-if="isScheduleEditing" @click="cancelEditWorkerSleepSchedule" class="btn">Cancel</button>
|
||||
<button v-if="isScheduleEditing" @click="saveWorkerSleepSchedule" class="btn btn-primary">Save
|
||||
Schedule</button>
|
||||
<button v-if="isScheduleEditing" @click="cancelEditWorkerSleepSchedule" class="btn">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="isScheduleEditing"
|
||||
@click="saveWorkerSleepSchedule"
|
||||
class="btn btn-primary">
|
||||
Save Schedule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -130,15 +151,17 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="workerData.status == 'error'">
|
||||
in <span class="worker-status">error</span> state
|
||||
in <span class="worker-status">error</span> state
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="worker-status">{{ workerData.status }}</span>
|
||||
</template>, which means removing it now can cause it to log errors. It
|
||||
is advised to shut down the Worker before removing it from the system.
|
||||
<span class="worker-status">{{ workerData.status }}</span> </template
|
||||
>, which means removing it now can cause it to log errors. It is advised to shut down the
|
||||
Worker before removing it from the system.
|
||||
</template>
|
||||
</p>
|
||||
<p><button @click="deleteWorker">Remove {{ workerData.name }}</button></p>
|
||||
<p>
|
||||
<button @click="deleteWorker">Remove {{ workerData.name }}</button>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -148,20 +171,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useNotifs } from '@/stores/notifications'
|
||||
import { useWorkers } from '@/stores/workers'
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import { useWorkers } from '@/stores/workers';
|
||||
|
||||
import * as datetime from "@/datetime";
|
||||
import { WorkerMgtApi, WorkerSleepSchedule, WorkerTagChangeRequest } from "@/manager-api";
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { workerStatus } from "../../statusindicator";
|
||||
import * as datetime from '@/datetime';
|
||||
import { WorkerMgtApi, WorkerSleepSchedule, WorkerTagChangeRequest } from '@/manager-api';
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { workerStatus } from '../../statusindicator';
|
||||
import LinkWorkerTask from '@/components/LinkWorkerTask.vue';
|
||||
import SwitchCheckbox from '@/components/SwitchCheckbox.vue';
|
||||
import { copyElementText } from '@/clipboard';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
"workerData", // Worker data to show.
|
||||
'workerData', // Worker data to show.
|
||||
],
|
||||
components: {
|
||||
LinkWorkerTask,
|
||||
@ -171,7 +194,7 @@ export default {
|
||||
return {
|
||||
datetime: datetime, // So that the template can access it.
|
||||
api: new WorkerMgtApi(getAPIClient()),
|
||||
workerStatusHTML: "",
|
||||
workerStatusHTML: '',
|
||||
workerSleepSchedule: this.defaultWorkerSleepSchedule(),
|
||||
isScheduleEditing: false,
|
||||
notifs: useNotifs(),
|
||||
@ -194,11 +217,11 @@ export default {
|
||||
if (newData) {
|
||||
this.workerStatusHTML = workerStatus(newData);
|
||||
} else {
|
||||
this.workerStatusHTML = "";
|
||||
this.workerStatusHTML = '';
|
||||
}
|
||||
// Update workerSleepSchedule only if oldData and newData have different ids, or if there is no oldData
|
||||
// and we provide newData.
|
||||
if (((oldData && newData) && (oldData.id != newData.id)) || !oldData && newData) {
|
||||
if ((oldData && newData && oldData.id != newData.id) || (!oldData && newData)) {
|
||||
this.fetchWorkerSleepSchedule();
|
||||
}
|
||||
|
||||
@ -213,10 +236,17 @@ export default {
|
||||
// Utility to display workerSleepSchedule, taking into account the case when the default values are used.
|
||||
// This way, empty strings are represented more meaningfully.
|
||||
return {
|
||||
'days_of_week': this.workerSleepSchedule.days_of_week === '' ? 'every day' : this.workerSleepSchedule.days_of_week,
|
||||
'start_time': this.workerSleepSchedule.start_time === '' ? '00:00' : this.workerSleepSchedule.start_time,
|
||||
'end_time': this.workerSleepSchedule.end_time === '' ? '24:00' : this.workerSleepSchedule.end_time,
|
||||
}
|
||||
days_of_week:
|
||||
this.workerSleepSchedule.days_of_week === ''
|
||||
? 'every day'
|
||||
: this.workerSleepSchedule.days_of_week,
|
||||
start_time:
|
||||
this.workerSleepSchedule.start_time === ''
|
||||
? '00:00'
|
||||
: this.workerSleepSchedule.start_time,
|
||||
end_time:
|
||||
this.workerSleepSchedule.end_time === '' ? '24:00' : this.workerSleepSchedule.end_time,
|
||||
};
|
||||
},
|
||||
workerSleepScheduleStatusLabel() {
|
||||
return this.workerSleepSchedule.is_active ? 'Enabled' : 'Disabled';
|
||||
@ -228,7 +258,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
fetchWorkerSleepSchedule() {
|
||||
this.api.fetchWorkerSleepSchedule(this.workerData.id)
|
||||
this.api
|
||||
.fetchWorkerSleepSchedule(this.workerData.id)
|
||||
.then((schedule) => {
|
||||
// Replace the default workerSleepSchedule if the Worker has one
|
||||
|
||||
@ -244,8 +275,9 @@ export default {
|
||||
});
|
||||
},
|
||||
setWorkerSleepSchedule(notifMessage) {
|
||||
this.api.setWorkerSleepSchedule(this.workerData.id, this.workerSleepSchedule).then(
|
||||
this.notifs.add(notifMessage));
|
||||
this.api
|
||||
.setWorkerSleepSchedule(this.workerData.id, this.workerSleepSchedule)
|
||||
.then(this.notifs.add(notifMessage));
|
||||
},
|
||||
toggleWorkerSleepSchedule() {
|
||||
this.workerSleepSchedule.is_active = !this.workerSleepSchedule.is_active;
|
||||
@ -261,12 +293,12 @@ export default {
|
||||
this.isScheduleEditing = false;
|
||||
},
|
||||
defaultWorkerSleepSchedule() {
|
||||
return new WorkerSleepSchedule(false, '', '', '') // Default values in OpenAPI
|
||||
return new WorkerSleepSchedule(false, '', '', ''); // Default values in OpenAPI
|
||||
},
|
||||
deleteWorker() {
|
||||
let msg = `Are you sure you want to remove ${this.workerData.name}?`;
|
||||
if (this.workerData.status != "offline") {
|
||||
msg += "\nRemoving it without first shutting it down will cause it to log errors.";
|
||||
if (this.workerData.status != 'offline') {
|
||||
msg += '\nRemoving it without first shutting it down will cause it to log errors.';
|
||||
}
|
||||
if (!confirm(msg)) {
|
||||
return;
|
||||
@ -286,9 +318,9 @@ export default {
|
||||
this.thisWorkerTags = assignedTags;
|
||||
},
|
||||
toggleWorkerTag(tagID) {
|
||||
console.log("Toggled", tagID);
|
||||
console.log('Toggled', tagID);
|
||||
this.thisWorkerTags[tagID] = !this.thisWorkerTags[tagID];
|
||||
console.log("New assignment:", plain(this.thisWorkerTags));
|
||||
console.log('New assignment:', plain(this.thisWorkerTags));
|
||||
|
||||
// Construct tag change request.
|
||||
const tagIDs = this.getAssignedTagIDs();
|
||||
@ -298,7 +330,7 @@ export default {
|
||||
this.api
|
||||
.setWorkerTags(this.workerData.id, changeRequest)
|
||||
.then(() => {
|
||||
this.notifs.add("Tag assignment updated");
|
||||
this.notifs.add('Tag assignment updated');
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = JSON.stringify(error); // TODO: handle API errors better.
|
||||
@ -333,7 +365,7 @@ export default {
|
||||
bottom: var(--spacer-xs);
|
||||
}
|
||||
|
||||
.sleep-schedule .btn-bar label+.btn {
|
||||
.sleep-schedule .btn-bar label + .btn {
|
||||
margin-left: var(--spacer-sm);
|
||||
}
|
||||
|
||||
@ -351,7 +383,7 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sleep-schedule-edit>div {
|
||||
.sleep-schedule-edit > div {
|
||||
margin: var(--spacer-sm) 0;
|
||||
}
|
||||
|
||||
@ -362,7 +394,7 @@ export default {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.sleep-schedule-edit>.sleep-schedule-edit-time {
|
||||
.sleep-schedule-edit > .sleep-schedule-edit-time {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -372,19 +404,19 @@ export default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sleep-schedule-edit input[type="text"] {
|
||||
.sleep-schedule-edit input[type='text'] {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
width: 23ch;
|
||||
}
|
||||
|
||||
.sleep-schedule-edit input[type="text"].time {
|
||||
.sleep-schedule-edit input[type='text'].time {
|
||||
width: 10ch;
|
||||
margin-right: var(--spacer);
|
||||
}
|
||||
|
||||
/* Prevent fields with long IDs from overflowing. */
|
||||
.field-id+dd {
|
||||
.field-id + dd {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -5,7 +5,10 @@
|
||||
<worker-actions-bar />
|
||||
|
||||
<div class="align-right">
|
||||
<status-filter-bar :availableStatuses="availableStatuses" :activeStatuses="shownStatuses" classPrefix="worker-"
|
||||
<status-filter-bar
|
||||
:availableStatuses="availableStatuses"
|
||||
:activeStatuses="shownStatuses"
|
||||
classPrefix="worker-"
|
||||
@click="toggleStatusFilter" />
|
||||
</div>
|
||||
</div>
|
||||
@ -17,18 +20,18 @@
|
||||
|
||||
<script>
|
||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||
import { WorkerMgtApi } from '@/manager-api'
|
||||
import { WorkerMgtApi } from '@/manager-api';
|
||||
import { indicator, workerStatus } from '@/statusindicator';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import { useWorkers } from '@/stores/workers';
|
||||
|
||||
import StatusFilterBar from '@/components/StatusFilterBar.vue'
|
||||
import WorkerActionsBar from '@/components/workers/WorkerActionsBar.vue'
|
||||
import StatusFilterBar from '@/components/StatusFilterBar.vue';
|
||||
import WorkerActionsBar from '@/components/workers/WorkerActionsBar.vue';
|
||||
|
||||
export default {
|
||||
name: 'WorkersTable',
|
||||
props: ["activeWorkerID"],
|
||||
emits: ["tableRowClicked"],
|
||||
props: ['activeWorkerID'],
|
||||
emits: ['tableRowClicked'],
|
||||
components: {
|
||||
StatusFilterBar,
|
||||
WorkerActionsBar,
|
||||
@ -51,7 +54,9 @@ export default {
|
||||
// Useful for debugging when there are many similar workers:
|
||||
// { 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) => {
|
||||
const data = cell.getData();
|
||||
const dot = indicator(data.status, 'worker-');
|
||||
@ -64,21 +69,19 @@ export default {
|
||||
],
|
||||
rowFormatter(row) {
|
||||
const data = row.getData();
|
||||
const isActive = (data.id === vueComponent.activeWorkerID);
|
||||
row.getElement().classList.toggle("active-row", isActive);
|
||||
const isActive = data.id === vueComponent.activeWorkerID;
|
||||
row.getElement().classList.toggle('active-row', isActive);
|
||||
},
|
||||
initialSort: [
|
||||
{ column: "name", dir: "asc" },
|
||||
],
|
||||
layout: "fitData",
|
||||
initialSort: [{ column: 'name', dir: 'asc' }],
|
||||
layout: 'fitData',
|
||||
layoutColumnsOnNewData: true,
|
||||
height: "360px", // Must be set in order for the virtual DOM to function correctly.
|
||||
height: '360px', // Must be set in order for the virtual DOM to function correctly.
|
||||
data: [], // Will be filled via a Flamenco API request.
|
||||
selectable: false, // The active worker is tracked by click events, not row selection.
|
||||
};
|
||||
this.tabulator = new Tabulator('#flamenco_workers_list', options);
|
||||
this.tabulator.on("rowClick", this.onRowClick);
|
||||
this.tabulator.on("tableBuilt", this._onTableBuilt);
|
||||
this.tabulator.on('rowClick', this.onRowClick);
|
||||
this.tabulator.on('tableBuilt', this._onTableBuilt);
|
||||
|
||||
window.addEventListener('resize', this.recalcTableHeight);
|
||||
},
|
||||
@ -100,7 +103,7 @@ export default {
|
||||
computed: {
|
||||
selectedIDs() {
|
||||
return this.tabulator.getSelectedData().map((worker) => worker.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onReconnected() {
|
||||
@ -159,7 +162,9 @@ export default {
|
||||
}
|
||||
promise
|
||||
.then(this.sortData)
|
||||
.then(() => { this.tabulator.redraw(); }) // Resize columns based on new data.
|
||||
.then(() => {
|
||||
this.tabulator.redraw();
|
||||
}) // Resize columns based on new data.
|
||||
.then(this._refreshAvailableStatuses);
|
||||
|
||||
// TODO: this should also resize the columns, as the status column can
|
||||
@ -171,7 +176,7 @@ export default {
|
||||
// store. There were some issues where navigating to another worker would
|
||||
// overwrite the old worker's ID, and this prevents that.
|
||||
const rowData = plain(row.getData());
|
||||
this.$emit("tableRowClicked", rowData);
|
||||
this.$emit('tableRowClicked', rowData);
|
||||
},
|
||||
toggleStatusFilter(status) {
|
||||
const asSet = new Set(this.shownStatuses);
|
||||
@ -199,7 +204,7 @@ export default {
|
||||
// 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(workerID);
|
||||
if (!row) return
|
||||
if (!row) return;
|
||||
if (row.reformat) row.reformat();
|
||||
else if (row.reinitialize) row.reinitialize(true);
|
||||
},
|
||||
@ -229,7 +234,9 @@ export default {
|
||||
// `offsetParent` is assumed to be the actual column in the 3-column
|
||||
// view. To ensure this, it's given `position: relative` in the CSS
|
||||
// styling.
|
||||
console.warn("JobsTable.recalcTableHeight() only works when the offset parent is the real parent of the element.");
|
||||
console.warn(
|
||||
'JobsTable.recalcTableHeight() only works when the offset parent is the real parent of the element.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { DateTime } from "luxon";
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const relativeTimeDefaultOptions = {
|
||||
thresholdDays: 14,
|
||||
format: DateTime.DATE_MED_WITH_WEEKDAY,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the given timestamp to a Luxon time object.
|
||||
@ -29,16 +29,14 @@ export function relativeTime(timestamp, options) {
|
||||
|
||||
const now = DateTime.local();
|
||||
const ageInDays = now.diff(parsedTimestamp).as('days');
|
||||
if (ageInDays > options.format)
|
||||
return parsedTimestamp.toLocaleString(options.format);
|
||||
return parsedTimestamp.toRelative({style: "narrow"});
|
||||
if (ageInDays > options.format) return parsedTimestamp.toLocaleString(options.format);
|
||||
return parsedTimestamp.toRelative({ style: 'narrow' });
|
||||
}
|
||||
|
||||
export function shortened(timestamp) {
|
||||
const parsedTimestamp = parseTimestamp(timestamp);
|
||||
const now = DateTime.local();
|
||||
const ageInHours = now.diff(parsedTimestamp).as('hours');
|
||||
if (ageInHours < 24)
|
||||
return parsedTimestamp.toLocaleString(DateTime.TIME_24_SIMPLE);
|
||||
if (ageInHours < 24) return parsedTimestamp.toLocaleString(DateTime.TIME_24_SIMPLE);
|
||||
return parsedTimestamp.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY);
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import App from '@/App.vue'
|
||||
import SetupAssistant from '@/SetupAssistant.vue'
|
||||
import autoreload from '@/autoreloader'
|
||||
import router from '@/router/index'
|
||||
import setupAssistantRouter from '@/router/setup-assistant'
|
||||
import { MetaApi } from "@/manager-api";
|
||||
import { newBareAPIClient } from "@/api-client";
|
||||
import * as urls from '@/urls'
|
||||
import App from '@/App.vue';
|
||||
import SetupAssistant from '@/SetupAssistant.vue';
|
||||
import autoreload from '@/autoreloader';
|
||||
import router from '@/router/index';
|
||||
import setupAssistantRouter from '@/router/setup-assistant';
|
||||
import { MetaApi } from '@/manager-api';
|
||||
import { newBareAPIClient } from '@/api-client';
|
||||
import * as urls from '@/urls';
|
||||
|
||||
// Ensure Tabulator can find `luxon`, which it needs for sorting by
|
||||
// date/time/datetime.
|
||||
@ -22,21 +22,21 @@ window.objectEmpty = (o) => !o || Object.entries(o).length == 0;
|
||||
// Automatically reload the window after a period of inactivity from the user.
|
||||
autoreload();
|
||||
|
||||
const pinia = createPinia()
|
||||
const pinia = createPinia();
|
||||
|
||||
function normalMode() {
|
||||
const app = createApp(App)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
const app = createApp(App);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
function setupAssistantMode() {
|
||||
console.log("Flamenco Setup Assistant is starting");
|
||||
const app = createApp(SetupAssistant)
|
||||
app.use(pinia)
|
||||
app.use(setupAssistantRouter)
|
||||
app.mount('#app')
|
||||
console.log('Flamenco Setup Assistant is starting');
|
||||
const app = createApp(SetupAssistant);
|
||||
app.use(pinia);
|
||||
app.use(setupAssistantRouter);
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
/* This cannot use the client from '@/stores/api-query-count', as that would
|
||||
@ -44,11 +44,12 @@ function setupAssistantMode() {
|
||||
* know which app to start, this API call needs to return data. */
|
||||
const apiClient = newBareAPIClient();
|
||||
const metaAPI = new MetaApi(apiClient);
|
||||
metaAPI.getConfiguration()
|
||||
metaAPI
|
||||
.getConfiguration()
|
||||
.then((config) => {
|
||||
if (config.isFirstRun) setupAssistantMode();
|
||||
else normalMode();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("Error getting Manager configuration:", error);
|
||||
})
|
||||
console.warn('Error getting Manager configuration:', error);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -32,6 +32,6 @@ const router = createRouter({
|
||||
component: () => import('../views/LastRenderedView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "index",
|
||||
component: () => import("../views/SetupAssistantView.vue"),
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: () => import('../views/SetupAssistantView.vue'),
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "redirect-to-index",
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'redirect-to-index',
|
||||
redirect: '/',
|
||||
},
|
||||
],
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import SetupAssistant from '@/SetupAssistant.vue'
|
||||
import router from '@/router/setup-assistant'
|
||||
import SetupAssistant from '@/SetupAssistant.vue';
|
||||
import router from '@/router/setup-assistant';
|
||||
|
||||
// Ensure Tabulator can find `luxon`, which it needs for sorting by
|
||||
// date/time/datetime.
|
||||
@ -14,13 +14,13 @@ window.plain = (x) => JSON.parse(JSON.stringify(x));
|
||||
// objectEmpty returns whether the object is empty or not.
|
||||
window.objectEmpty = (o) => !o || Object.entries(o).length == 0;
|
||||
|
||||
const app = createApp(SetupAssistant)
|
||||
const pinia = createPinia()
|
||||
const app = createApp(SetupAssistant);
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
|
||||
// Automatically reload the window after a period of inactivity from the user.
|
||||
import autoreload from '@/autoreloader'
|
||||
import autoreload from '@/autoreloader';
|
||||
autoreload();
|
||||
|
@ -13,7 +13,7 @@ import { toTitleCase } from '@/strings';
|
||||
*/
|
||||
export function indicator(status, classNamePrefix) {
|
||||
const label = toTitleCase(status);
|
||||
if (!classNamePrefix) classNamePrefix = ""; // force an empty string for any false value.
|
||||
if (!classNamePrefix) classNamePrefix = ''; // force an empty string for any false value.
|
||||
return `<span title="${label}" class="indicator ${classNamePrefix}status-${status}"></span>`;
|
||||
}
|
||||
|
||||
@ -31,9 +31,9 @@ export function workerStatus(worker) {
|
||||
|
||||
let arrow;
|
||||
if (worker.status_change.is_lazy) {
|
||||
arrow = `<span class='state-transition-arrow lazy' title='lazy status transition'>➠</span>`
|
||||
arrow = `<span class='state-transition-arrow lazy' title='lazy status transition'>➠</span>`;
|
||||
} else {
|
||||
arrow = `<span class='state-transition-arrow forced' title='forced status transition'>➜</span>`
|
||||
arrow = `<span class='state-transition-arrow forced' title='forced status transition'>➜</span>`;
|
||||
}
|
||||
|
||||
return `<span class="worker-status-${worker.status}">${worker.status}</span>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ApiClient } from "@/manager-api";
|
||||
import { defineStore } from 'pinia';
|
||||
import { ApiClient } from '@/manager-api';
|
||||
|
||||
/**
|
||||
* Keep track of running API queries.
|
||||
*/
|
||||
export const useAPIQueryCount = defineStore("apiQueryCount", {
|
||||
export const useAPIQueryCount = defineStore('apiQueryCount', {
|
||||
state: () => ({
|
||||
/**
|
||||
* Number of running queries.
|
||||
@ -28,14 +28,38 @@ export const useAPIQueryCount = defineStore("apiQueryCount", {
|
||||
});
|
||||
|
||||
export class CountingApiClient extends ApiClient {
|
||||
callApi(path, httpMethod, pathParams, queryParams, headerParams, formParams,
|
||||
bodyParam, authNames, contentTypes, accepts, returnType, apiBasePath ) {
|
||||
callApi(
|
||||
path,
|
||||
httpMethod,
|
||||
pathParams,
|
||||
queryParams,
|
||||
headerParams,
|
||||
formParams,
|
||||
bodyParam,
|
||||
authNames,
|
||||
contentTypes,
|
||||
accepts,
|
||||
returnType,
|
||||
apiBasePath
|
||||
) {
|
||||
const apiQueryCount = useAPIQueryCount();
|
||||
apiQueryCount.num++;
|
||||
|
||||
return super
|
||||
.callApi(path, httpMethod, pathParams, queryParams, headerParams, formParams,
|
||||
bodyParam, authNames, contentTypes, accepts, returnType, apiBasePath)
|
||||
.callApi(
|
||||
path,
|
||||
httpMethod,
|
||||
pathParams,
|
||||
queryParams,
|
||||
headerParams,
|
||||
formParams,
|
||||
bodyParam,
|
||||
authNames,
|
||||
contentTypes,
|
||||
accepts,
|
||||
returnType,
|
||||
apiBasePath
|
||||
)
|
||||
.finally(() => {
|
||||
apiQueryCount.num--;
|
||||
});
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import * as API from '@/manager-api';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
const jobsAPI = new API.JobsApi(getAPIClient());
|
||||
|
||||
@ -16,7 +15,7 @@ export const useJobs = defineStore('jobs', {
|
||||
* ID of the active job. Easier to query than `activeJob ? activeJob.id : ""`.
|
||||
* @type {string}
|
||||
*/
|
||||
activeJobID: "",
|
||||
activeJobID: '',
|
||||
|
||||
/**
|
||||
* Set to true when it is known that there are no jobs at all in the system.
|
||||
@ -26,13 +25,13 @@ export const useJobs = defineStore('jobs', {
|
||||
}),
|
||||
getters: {
|
||||
canDelete() {
|
||||
return this._anyJobWithStatus(["queued", "paused", "failed", "completed", "canceled"])
|
||||
return this._anyJobWithStatus(['queued', 'paused', 'failed', 'completed', 'canceled']);
|
||||
},
|
||||
canCancel() {
|
||||
return this._anyJobWithStatus(["queued", "active", "failed"])
|
||||
return this._anyJobWithStatus(['queued', 'active', 'failed']);
|
||||
},
|
||||
canRequeue() {
|
||||
return this._anyJobWithStatus(["canceled", "completed", "failed", "paused"])
|
||||
return this._anyJobWithStatus(['canceled', 'completed', 'failed', 'paused']);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -41,7 +40,7 @@ export const useJobs = defineStore('jobs', {
|
||||
},
|
||||
setActiveJobID(jobID) {
|
||||
this.$patch({
|
||||
activeJob: {id: jobID, settings: {}, metadata: {}},
|
||||
activeJob: { id: jobID, settings: {}, metadata: {} },
|
||||
activeJobID: jobID,
|
||||
});
|
||||
},
|
||||
@ -60,7 +59,7 @@ export const useJobs = defineStore('jobs', {
|
||||
deselectAllJobs() {
|
||||
this.$patch({
|
||||
activeJob: null,
|
||||
activeJobID: "",
|
||||
activeJobID: '',
|
||||
});
|
||||
},
|
||||
|
||||
@ -72,13 +71,17 @@ export const useJobs = defineStore('jobs', {
|
||||
* TODO: actually have these work on all selected jobs. For simplicity, the
|
||||
* code now assumes that only the active job needs to be operated on.
|
||||
*/
|
||||
cancelJobs() { return this._setJobStatus("cancel-requested"); },
|
||||
requeueJobs() { return this._setJobStatus("requeueing"); },
|
||||
cancelJobs() {
|
||||
return this._setJobStatus('cancel-requested');
|
||||
},
|
||||
requeueJobs() {
|
||||
return this._setJobStatus('requeueing');
|
||||
},
|
||||
deleteJobs() {
|
||||
if (!this.activeJobID) {
|
||||
console.warn(`deleteJobs() impossible, no active job ID`);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject("No job selected, unable to delete");
|
||||
reject('No job selected, unable to delete');
|
||||
});
|
||||
}
|
||||
|
||||
@ -93,7 +96,9 @@ export const useJobs = defineStore('jobs', {
|
||||
* @returns bool indicating whether there is a selected job with any of the given statuses.
|
||||
*/
|
||||
_anyJobWithStatus(statuses) {
|
||||
return !!this.activeJob && !!this.activeJob.status && statuses.includes(this.activeJob.status);
|
||||
return (
|
||||
!!this.activeJob && !!this.activeJob.status && statuses.includes(this.activeJob.status)
|
||||
);
|
||||
// return this.selectedJobs.reduce((foundJob, job) => (foundJob || statuses.includes(job.status)), false);
|
||||
},
|
||||
|
||||
@ -107,8 +112,8 @@ export const useJobs = defineStore('jobs', {
|
||||
console.warn(`_setJobStatus(${newStatus}) impossible, no active job ID`);
|
||||
return;
|
||||
}
|
||||
const statuschange = new API.JobStatusChange(newStatus, "requested from web interface");
|
||||
const statuschange = new API.JobStatusChange(newStatus, 'requested from web interface');
|
||||
return jobsAPI.setJobStatus(this.activeJobID, statuschange);
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/** Time after which a notification is hidden. */
|
||||
const MESSAGE_HIDE_DELAY_MS = 5000;
|
||||
@ -17,7 +17,7 @@ export const useNotifs = defineStore('notifications', {
|
||||
* @type {{ id: Number, msg: string, time: Date }[]} */
|
||||
history: [],
|
||||
/** @type { id: Number, msg: string, time: Date } */
|
||||
last: "",
|
||||
last: '',
|
||||
|
||||
hideTimerID: 0,
|
||||
lastID: 0,
|
||||
@ -31,7 +31,7 @@ export const useNotifs = defineStore('notifications', {
|
||||
* @param {string} message
|
||||
*/
|
||||
add(message) {
|
||||
const notif = {id: this._generateID(), msg: message, time: new Date()};
|
||||
const notif = { id: this._generateID(), msg: message, time: new Date() };
|
||||
this.history.push(notif);
|
||||
this.last = notif;
|
||||
this._prune();
|
||||
@ -42,19 +42,17 @@ export const useNotifs = defineStore('notifications', {
|
||||
* @param {API.SocketIOJobUpdate} jobUpdate Job update received via SocketIO.
|
||||
*/
|
||||
addJobUpdate(jobUpdate) {
|
||||
let msg = "Job";
|
||||
let msg = 'Job';
|
||||
if (jobUpdate.name) msg += ` ${jobUpdate.name}`;
|
||||
if (jobUpdate.was_deleted) {
|
||||
msg += " was deleted";
|
||||
}
|
||||
else if (jobUpdate.previous_status && jobUpdate.previous_status != jobUpdate.status) {
|
||||
msg += ' was deleted';
|
||||
} else if (jobUpdate.previous_status && jobUpdate.previous_status != jobUpdate.status) {
|
||||
msg += ` changed status ${jobUpdate.previous_status} ➜ ${jobUpdate.status}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Don't bother logging just "Job" + its name, as it conveys no info.
|
||||
return;
|
||||
}
|
||||
this.add(msg)
|
||||
this.add(msg);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -68,19 +66,19 @@ export const useNotifs = defineStore('notifications', {
|
||||
if (taskUpdate.activity) {
|
||||
msg += `: ${taskUpdate.activity}`;
|
||||
}
|
||||
this.add(msg)
|
||||
this.add(msg);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {API.SocketIOWorkerUpdate} workerUpdate Worker update received via SocketIO.
|
||||
*/
|
||||
addWorkerUpdate(workerUpdate) {
|
||||
addWorkerUpdate(workerUpdate) {
|
||||
let msg = `Worker ${workerUpdate.name}`;
|
||||
if (workerUpdate.previous_status && workerUpdate.previous_status != workerUpdate.status) {
|
||||
msg += ` changed status ${workerUpdate.previous_status} ➜ ${workerUpdate.status}`;
|
||||
this.add(msg);
|
||||
} else if (workerUpdate.deleted_at) {
|
||||
msg += " was removed from the system";
|
||||
msg += ' was removed from the system';
|
||||
this.add(msg);
|
||||
}
|
||||
},
|
||||
@ -98,11 +96,11 @@ export const useNotifs = defineStore('notifications', {
|
||||
_hideMessage() {
|
||||
this.$patch({
|
||||
hideTimerID: 0,
|
||||
last: "",
|
||||
last: '',
|
||||
});
|
||||
},
|
||||
_generateID() {
|
||||
return ++this.lastID;
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useNotifs } from '@/stores/notifications'
|
||||
import { defineStore } from 'pinia';
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
|
||||
/**
|
||||
* Status of the SocketIO/Websocket connection to Flamenco Manager.
|
||||
@ -12,7 +12,7 @@ export const useSocketStatus = defineStore('socket-status', {
|
||||
wasEverDisconnected: false,
|
||||
|
||||
/** @type {string} */
|
||||
message: "",
|
||||
message: '',
|
||||
}),
|
||||
actions: {
|
||||
/**
|
||||
@ -21,8 +21,7 @@ export const useSocketStatus = defineStore('socket-status', {
|
||||
*/
|
||||
disconnected(reason) {
|
||||
// Only patch the state if it actually will change.
|
||||
if (!this.isConnected)
|
||||
return;
|
||||
if (!this.isConnected) return;
|
||||
this._get_notifs().add(`Connection to Flamenco Manager lost`);
|
||||
this.$patch({
|
||||
isConnected: false,
|
||||
@ -35,14 +34,13 @@ export const useSocketStatus = defineStore('socket-status', {
|
||||
*/
|
||||
connected() {
|
||||
// Only patch the state if it actually will change.
|
||||
if (this.isConnected)
|
||||
return;
|
||||
if (this.isConnected) return;
|
||||
|
||||
if (this.wasEverDisconnected)
|
||||
this._get_notifs().add("Connection to Flamenco Manager established");
|
||||
this._get_notifs().add('Connection to Flamenco Manager established');
|
||||
this.$patch({
|
||||
isConnected: true,
|
||||
message: "",
|
||||
message: '',
|
||||
});
|
||||
},
|
||||
|
||||
@ -53,6 +51,6 @@ export const useSocketStatus = defineStore('socket-status', {
|
||||
// that'll cause the Notifications popover to be handled at the app-global
|
||||
// level, instead of per view, creating a better place to put this logic.
|
||||
return useNotifs();
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
// Maximum number of task log lines that will be stored.
|
||||
const capacity = 1000;
|
||||
@ -17,7 +17,7 @@ export const useTaskLog = defineStore('taskLog', {
|
||||
* @type {{ id: Number, line: string }[]} */
|
||||
history: [],
|
||||
/** @type { id: Number, line: string } */
|
||||
last: "",
|
||||
last: '',
|
||||
|
||||
lastID: 0,
|
||||
}),
|
||||
@ -52,8 +52,7 @@ export const useTaskLog = defineStore('taskLog', {
|
||||
if (!logChunk) return;
|
||||
|
||||
const lines = logChunk.trimEnd().split('\n');
|
||||
if (lines.length == 0)
|
||||
return;
|
||||
if (lines.length == 0) return;
|
||||
|
||||
if (lines.length > capacity) {
|
||||
// Only keep the `capacity` last lines, so that adding them to the
|
||||
@ -73,7 +72,7 @@ export const useTaskLog = defineStore('taskLog', {
|
||||
}
|
||||
|
||||
if (entry == null) {
|
||||
console.warn("taskLog.addChunk: there were lines to add, but no entry created. Weird.");
|
||||
console.warn('taskLog.addChunk: there were lines to add, but no entry created. Weird.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -84,7 +83,7 @@ export const useTaskLog = defineStore('taskLog', {
|
||||
},
|
||||
|
||||
_createEntry(state, line) {
|
||||
return {id: this._generateID(state), line: line};
|
||||
return { id: this._generateID(state), line: line };
|
||||
},
|
||||
|
||||
/**
|
||||
@ -105,6 +104,6 @@ export const useTaskLog = defineStore('taskLog', {
|
||||
},
|
||||
_generateID(state) {
|
||||
return ++state.lastID;
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import * as API from '@/manager-api';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
const jobsAPI = new API.JobsApi(getAPIClient());
|
||||
|
||||
@ -16,20 +15,20 @@ export const useTasks = defineStore('tasks', {
|
||||
* ID of the active task. Easier to query than `activeTask ? activeTask.id : ""`.
|
||||
* @type {string}
|
||||
*/
|
||||
activeTaskID: "",
|
||||
activeTaskID: '',
|
||||
}),
|
||||
getters: {
|
||||
canCancel() {
|
||||
return this._anyTaskWithStatus(["queued", "active", "soft-failed"])
|
||||
return this._anyTaskWithStatus(['queued', 'active', 'soft-failed']);
|
||||
},
|
||||
canRequeue() {
|
||||
return this._anyTaskWithStatus(["canceled", "completed", "failed"])
|
||||
return this._anyTaskWithStatus(['canceled', 'completed', 'failed']);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setActiveTaskID(taskID) {
|
||||
this.$patch({
|
||||
activeTask: {id: taskID},
|
||||
activeTask: { id: taskID },
|
||||
activeTaskID: taskID,
|
||||
});
|
||||
},
|
||||
@ -42,7 +41,7 @@ export const useTasks = defineStore('tasks', {
|
||||
deselectAllTasks() {
|
||||
this.$patch({
|
||||
activeTask: null,
|
||||
activeTaskID: "",
|
||||
activeTaskID: '',
|
||||
});
|
||||
},
|
||||
|
||||
@ -54,8 +53,12 @@ export const useTasks = defineStore('tasks', {
|
||||
* TODO: actually have these work on all selected tasks. For simplicity, the
|
||||
* code now assumes that only the active task needs to be operated on.
|
||||
*/
|
||||
cancelTasks() { return this._setTaskStatus("canceled"); },
|
||||
requeueTasks() { return this._setTaskStatus("queued"); },
|
||||
cancelTasks() {
|
||||
return this._setTaskStatus('canceled');
|
||||
},
|
||||
requeueTasks() {
|
||||
return this._setTaskStatus('queued');
|
||||
},
|
||||
|
||||
// Internal methods.
|
||||
|
||||
@ -65,7 +68,9 @@ export const useTasks = defineStore('tasks', {
|
||||
* @returns bool indicating whether there is a selected task with any of the given statuses.
|
||||
*/
|
||||
_anyTaskWithStatus(statuses) {
|
||||
return !!this.activeTask && !!this.activeTask.status && statuses.includes(this.activeTask.status);
|
||||
return (
|
||||
!!this.activeTask && !!this.activeTask.status && statuses.includes(this.activeTask.status)
|
||||
);
|
||||
// return this.selectedTasks.reduce((foundTask, task) => (foundTask || statuses.includes(task.status)), false);
|
||||
},
|
||||
|
||||
@ -79,8 +84,8 @@ export const useTasks = defineStore('tasks', {
|
||||
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.activeTaskID, statuschange);
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { WorkerMgtApi } from '@/manager-api';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
// 'use' prefix is idiomatic for Pinia stores.
|
||||
// See https://pinia.vuejs.org/core-concepts/
|
||||
@ -13,7 +13,7 @@ export const useWorkers = defineStore('workers', {
|
||||
* ID of the active worker. Easier to query than `activeWorker ? activeWorker.id : ""`.
|
||||
* @type {string}
|
||||
*/
|
||||
activeWorkerID: "",
|
||||
activeWorkerID: '',
|
||||
|
||||
/** @type {API.WorkerTag[]} */
|
||||
tags: [],
|
||||
@ -43,7 +43,7 @@ export const useWorkers = defineStore('workers', {
|
||||
deselectAllWorkers() {
|
||||
this.$patch({
|
||||
activeWorker: null,
|
||||
activeWorkerID: "",
|
||||
activeWorkerID: '',
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
@ -1,14 +1,13 @@
|
||||
|
||||
let url = new URL(window.location.href);
|
||||
// Uncomment this when the web interface is running on a different port than the
|
||||
// API, for example when using the Vite devserver. Set the API port here.
|
||||
if (url.port == "8081") {
|
||||
url.port = "8080";
|
||||
if (url.port == '8081') {
|
||||
url.port = '8080';
|
||||
}
|
||||
url.pathname = "/";
|
||||
url.pathname = '/';
|
||||
const flamencoAPIURL = url.href;
|
||||
|
||||
url.protocol = "ws:";
|
||||
url.protocol = 'ws:';
|
||||
const websocketURL = url.href;
|
||||
|
||||
const URLs = {
|
||||
|
@ -1,25 +1,45 @@
|
||||
<template>
|
||||
<div class="col col-1">
|
||||
<jobs-table ref="jobsTable" :activeJobID="jobID" @tableRowClicked="onTableJobClicked"
|
||||
<jobs-table
|
||||
ref="jobsTable"
|
||||
:activeJobID="jobID"
|
||||
@tableRowClicked="onTableJobClicked"
|
||||
@activeJobDeleted="onActiveJobDeleted" />
|
||||
</div>
|
||||
<div class="col col-2 job-details-column" id="col-job-details">
|
||||
<get-the-addon v-if="jobs.isJobless" />
|
||||
<template v-else>
|
||||
<job-details ref="jobDetails" :jobData="jobs.activeJob" @reshuffled="_recalcTasksTableHeight" />
|
||||
<tasks-table v-if="hasJobData" ref="tasksTable" :jobID="jobID" :taskID="taskID"
|
||||
<job-details
|
||||
ref="jobDetails"
|
||||
:jobData="jobs.activeJob"
|
||||
@reshuffled="_recalcTasksTableHeight" />
|
||||
<tasks-table
|
||||
v-if="hasJobData"
|
||||
ref="tasksTable"
|
||||
:jobID="jobID"
|
||||
:taskID="taskID"
|
||||
@tableRowClicked="onTableTaskClicked" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="col col-3">
|
||||
<task-details v-if="hasJobData" :taskData="tasks.activeTask" @showTaskLogTail="showTaskLogTail" />
|
||||
<task-details
|
||||
v-if="hasJobData"
|
||||
:taskData="tasks.activeTask"
|
||||
@showTaskLogTail="showTaskLogTail" />
|
||||
</div>
|
||||
|
||||
<footer class="app-footer" v-if="!showFooterPopup" @click="showFooterPopup = true">
|
||||
<notification-bar />
|
||||
<div class="app-footer-expand">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
@ -27,9 +47,17 @@
|
||||
</footer>
|
||||
<footer-popup v-if="showFooterPopup" ref="footerPopup" @clickClose="showFooterPopup = false" />
|
||||
|
||||
<update-listener ref="updateListener" mainSubscription="allJobs" :subscribedJobID="jobID" :subscribedTaskID="taskID"
|
||||
@jobUpdate="onSioJobUpdate" @taskUpdate="onSioTaskUpdate" @taskLogUpdate="onSioTaskLogUpdate"
|
||||
@lastRenderedUpdate="onSioLastRenderedUpdate" @message="onChatMessage" @sioReconnected="onSIOReconnected"
|
||||
<update-listener
|
||||
ref="updateListener"
|
||||
mainSubscription="allJobs"
|
||||
:subscribedJobID="jobID"
|
||||
:subscribedTaskID="taskID"
|
||||
@jobUpdate="onSioJobUpdate"
|
||||
@taskUpdate="onSioTaskUpdate"
|
||||
@taskLogUpdate="onSioTaskLogUpdate"
|
||||
@lastRenderedUpdate="onSioLastRenderedUpdate"
|
||||
@message="onChatMessage"
|
||||
@sioReconnected="onSIOReconnected"
|
||||
@sioDisconnected="onSIODisconnected" />
|
||||
</template>
|
||||
|
||||
@ -37,22 +65,22 @@
|
||||
import * as API from '@/manager-api';
|
||||
import { useJobs } from '@/stores/jobs';
|
||||
import { useTasks } from '@/stores/tasks';
|
||||
import { useNotifs } from '@/stores/notifications'
|
||||
import { useTaskLog } from '@/stores/tasklog'
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import { useTaskLog } from '@/stores/tasklog';
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
import FooterPopup from '@/components/footer/FooterPopup.vue'
|
||||
import GetTheAddon from '@/components/GetTheAddon.vue'
|
||||
import JobDetails from '@/components/jobs/JobDetails.vue'
|
||||
import JobsTable from '@/components/jobs/JobsTable.vue'
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue'
|
||||
import TaskDetails from '@/components/jobs/TaskDetails.vue'
|
||||
import TasksTable from '@/components/jobs/TasksTable.vue'
|
||||
import UpdateListener from '@/components/UpdateListener.vue'
|
||||
import FooterPopup from '@/components/footer/FooterPopup.vue';
|
||||
import GetTheAddon from '@/components/GetTheAddon.vue';
|
||||
import JobDetails from '@/components/jobs/JobDetails.vue';
|
||||
import JobsTable from '@/components/jobs/JobsTable.vue';
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue';
|
||||
import TaskDetails from '@/components/jobs/TaskDetails.vue';
|
||||
import TasksTable from '@/components/jobs/TasksTable.vue';
|
||||
import UpdateListener from '@/components/UpdateListener.vue';
|
||||
|
||||
export default {
|
||||
name: 'JobsView',
|
||||
props: ["jobID", "taskID"], // provided by Vue Router.
|
||||
props: ['jobID', 'taskID'], // provided by Vue Router.
|
||||
components: {
|
||||
FooterPopup,
|
||||
GetTheAddon,
|
||||
@ -70,7 +98,7 @@ export default {
|
||||
tasks: useTasks(),
|
||||
notifs: useNotifs(),
|
||||
taskLog: useTaskLog(),
|
||||
showFooterPopup: !!localStorage.getItem("footer-popover-visible"),
|
||||
showFooterPopup: !!localStorage.getItem('footer-popover-visible'),
|
||||
}),
|
||||
computed: {
|
||||
hasJobData() {
|
||||
@ -90,10 +118,10 @@ export default {
|
||||
this._fetchJob(this.jobID);
|
||||
this._fetchTask(this.taskID);
|
||||
|
||||
window.addEventListener("resize", this._recalcTasksTableHeight);
|
||||
window.addEventListener('resize', this._recalcTasksTableHeight);
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener("resize", this._recalcTasksTableHeight);
|
||||
window.removeEventListener('resize', this._recalcTasksTableHeight);
|
||||
},
|
||||
watch: {
|
||||
jobID(newJobID, oldJobID) {
|
||||
@ -103,8 +131,8 @@ export default {
|
||||
this._fetchTask(newTaskID);
|
||||
},
|
||||
showFooterPopup(shown) {
|
||||
if (shown) localStorage.setItem("footer-popover-visible", "true");
|
||||
else localStorage.removeItem("footer-popover-visible");
|
||||
if (shown) localStorage.setItem('footer-popover-visible', 'true');
|
||||
else localStorage.removeItem('footer-popover-visible');
|
||||
this._recalcTasksTableHeight();
|
||||
},
|
||||
},
|
||||
@ -122,26 +150,25 @@ export default {
|
||||
},
|
||||
|
||||
onSelectedTaskChanged(taskSummary) {
|
||||
if (!taskSummary) { // There is no active task.
|
||||
if (!taskSummary) {
|
||||
// There is no active task.
|
||||
this.tasks.deselectAllTasks();
|
||||
return;
|
||||
}
|
||||
|
||||
const jobsAPI = new API.JobsApi(getAPIClient());
|
||||
jobsAPI.fetchTask(taskSummary.id)
|
||||
.then((task) => {
|
||||
this.tasks.setActiveTask(task);
|
||||
// Forward the full task to Tabulator, so that that gets updated too.
|
||||
if (this.$refs.tasksTable)
|
||||
this.$refs.tasksTable.processTaskUpdate(task);
|
||||
});
|
||||
jobsAPI.fetchTask(taskSummary.id).then((task) => {
|
||||
this.tasks.setActiveTask(task);
|
||||
// Forward the full task to Tabulator, so that that gets updated too.
|
||||
if (this.$refs.tasksTable) this.$refs.tasksTable.processTaskUpdate(task);
|
||||
});
|
||||
},
|
||||
|
||||
showTaskLogTail() {
|
||||
this.showFooterPopup = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.footerPopup.showTaskLogTail();
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
// SocketIO data event handlers:
|
||||
@ -158,8 +185,7 @@ export default {
|
||||
|
||||
this._fetchJob(this.jobID);
|
||||
if (jobUpdate.refresh_tasks) {
|
||||
if (this.$refs.tasksTable)
|
||||
this.$refs.tasksTable.fetchTasks();
|
||||
if (this.$refs.tasksTable) this.$refs.tasksTable.fetchTasks();
|
||||
this._fetchTask(this.taskID);
|
||||
}
|
||||
},
|
||||
@ -169,10 +195,8 @@ export default {
|
||||
* @param {API.SocketIOTaskUpdate} taskUpdate
|
||||
*/
|
||||
onSioTaskUpdate(taskUpdate) {
|
||||
if (this.$refs.tasksTable)
|
||||
this.$refs.tasksTable.processTaskUpdate(taskUpdate);
|
||||
if (this.taskID == taskUpdate.id)
|
||||
this._fetchTask(this.taskID);
|
||||
if (this.$refs.tasksTable) this.$refs.tasksTable.processTaskUpdate(taskUpdate);
|
||||
if (this.taskID == taskUpdate.id) this._fetchTask(this.taskID);
|
||||
this.notifs.addTaskUpdate(taskUpdate);
|
||||
},
|
||||
|
||||
@ -226,7 +250,8 @@ export default {
|
||||
}
|
||||
|
||||
const jobsAPI = new API.JobsApi(getAPIClient());
|
||||
return jobsAPI.fetchJob(jobID)
|
||||
return jobsAPI
|
||||
.fetchJob(jobID)
|
||||
.then((job) => {
|
||||
this.jobs.setActiveJob(job);
|
||||
// Forward the full job to Tabulator, so that that gets updated too.
|
||||
@ -240,8 +265,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
console.log(`Unable to fetch job ${jobID}:`, err);
|
||||
})
|
||||
;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -255,28 +279,24 @@ export default {
|
||||
}
|
||||
|
||||
const jobsAPI = new API.JobsApi(getAPIClient());
|
||||
return jobsAPI.fetchTask(taskID)
|
||||
.then((task) => {
|
||||
this.tasks.setActiveTask(task);
|
||||
// Forward the full task to Tabulator, so that that gets updated too.\
|
||||
if (this.$refs.tasksTable)
|
||||
this.$refs.tasksTable.processTaskUpdate(task);
|
||||
});
|
||||
return jobsAPI.fetchTask(taskID).then((task) => {
|
||||
this.tasks.setActiveTask(task);
|
||||
// Forward the full task to Tabulator, so that that gets updated too.\
|
||||
if (this.$refs.tasksTable) this.$refs.tasksTable.processTaskUpdate(task);
|
||||
});
|
||||
},
|
||||
|
||||
onChatMessage(message) {
|
||||
console.log("chat message received:", message);
|
||||
console.log('chat message received:', message);
|
||||
this.messages.push(`${message.text}`);
|
||||
},
|
||||
|
||||
// SocketIO connection event handlers:
|
||||
onSIOReconnected() {
|
||||
this.$refs.jobsTable.onReconnected();
|
||||
if (this.$refs.tasksTable)
|
||||
this.$refs.tasksTable.onReconnected();
|
||||
},
|
||||
onSIODisconnected(reason) {
|
||||
if (this.$refs.tasksTable) this.$refs.tasksTable.onReconnected();
|
||||
},
|
||||
onSIODisconnected(reason) {},
|
||||
|
||||
_recalcTasksTableHeight() {
|
||||
if (!this.$refs.tasksTable) return;
|
||||
@ -284,7 +304,7 @@ export default {
|
||||
this.$nextTick(this.$refs.tasksTable.recalcTableHeight);
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -8,15 +8,18 @@
|
||||
|
||||
<footer class="app-footer"><notification-bar /></footer>
|
||||
|
||||
<update-listener ref="updateListener" mainSubscription="allLastRendered"
|
||||
<update-listener
|
||||
ref="updateListener"
|
||||
mainSubscription="allLastRendered"
|
||||
@lastRenderedUpdate="onSioLastRenderedUpdate"
|
||||
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
||||
@sioReconnected="onSIOReconnected"
|
||||
@sioDisconnected="onSIODisconnected" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LastRenderedImage from '@/components/jobs/LastRenderedImage.vue'
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue'
|
||||
import UpdateListener from '@/components/UpdateListener.vue'
|
||||
import LastRenderedImage from '@/components/jobs/LastRenderedImage.vue';
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue';
|
||||
import UpdateListener from '@/components/UpdateListener.vue';
|
||||
|
||||
export default {
|
||||
name: 'LastRenderedView',
|
||||
@ -25,8 +28,7 @@ export default {
|
||||
NotificationBar,
|
||||
UpdateListener,
|
||||
},
|
||||
data: () => ({
|
||||
}),
|
||||
data: () => ({}),
|
||||
methods: {
|
||||
/**
|
||||
* Event handler for SocketIO "last-rendered" updates.
|
||||
@ -37,12 +39,10 @@ export default {
|
||||
},
|
||||
|
||||
// SocketIO connection event handlers:
|
||||
onSIOReconnected() {
|
||||
},
|
||||
onSIODisconnected(reason) {
|
||||
},
|
||||
onSIOReconnected() {},
|
||||
onSIODisconnected(reason) {},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -52,7 +52,9 @@ export default {
|
||||
grid-column-end: col-3;
|
||||
grid-column-start: col-1;
|
||||
justify-content: center;
|
||||
height: calc(100vh - calc(var(--header-height) - var(--footer-height) - calc(var(--grid-gap) * -12)));
|
||||
height: calc(
|
||||
100vh - calc(var(--header-height) - var(--footer-height) - calc(var(--grid-gap) * -12))
|
||||
);
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
|
@ -2,24 +2,32 @@
|
||||
<div class="setup-container">
|
||||
<h1>Flamenco Setup Assistant</h1>
|
||||
<ul class="progress">
|
||||
<li v-for="step in totalSetupSteps" :key="step" @click="jumpToStep(step)" :class="{
|
||||
current: step == currentSetupStep,
|
||||
done: step < overallSetupStep,
|
||||
done_previously: (step < overallSetupStep && currentSetupStep > step),
|
||||
done_and_current: step == currentSetupStep && (step < overallSetupStep || step == 1),
|
||||
disabled: step > overallSetupStep,
|
||||
}">
|
||||
<li
|
||||
v-for="step in totalSetupSteps"
|
||||
:key="step"
|
||||
@click="jumpToStep(step)"
|
||||
:class="{
|
||||
current: step == currentSetupStep,
|
||||
done: step < overallSetupStep,
|
||||
done_previously: step < overallSetupStep && currentSetupStep > step,
|
||||
done_and_current: step == currentSetupStep && (step < overallSetupStep || step == 1),
|
||||
disabled: step > overallSetupStep,
|
||||
}">
|
||||
<span></span>
|
||||
</li>
|
||||
<div class="progress-bar"></div>
|
||||
</ul>
|
||||
<div class="setup-step step-welcome">
|
||||
|
||||
<step-item v-show="currentSetupStep == 1" @next-clicked="nextStep" :is-next-clickable="true"
|
||||
:is-back-visible="false" title="Welcome!" next-label="Let's go">
|
||||
<step-item
|
||||
v-show="currentSetupStep == 1"
|
||||
@next-clicked="nextStep"
|
||||
:is-next-clickable="true"
|
||||
:is-back-visible="false"
|
||||
title="Welcome!"
|
||||
next-label="Let's go">
|
||||
<p>
|
||||
This setup assistant will guide you through the initial configuration of Flamenco. You will be up
|
||||
and running in a few minutes!
|
||||
This setup assistant will guide you through the initial configuration of Flamenco. You
|
||||
will be up and running in a few minutes!
|
||||
</p>
|
||||
<p>Before we start, here is a quick overview of the Flamenco architecture.</p>
|
||||
|
||||
@ -27,32 +35,36 @@
|
||||
|
||||
<p>The illustration shows the key components and how they interact together:</p>
|
||||
<ul>
|
||||
<li><strong>Manager</strong>: This application. It coordinates all the activity.</li>
|
||||
<li>
|
||||
<strong>Manager</strong>: This application. It coordinates all the activity.
|
||||
<strong>Worker</strong>: A workstation or dedicated rendering machine. It executes the
|
||||
tasks assigned by the Manager.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Worker</strong>: A workstation or dedicated rendering machine. It executes the tasks assigned by the
|
||||
Manager.
|
||||
<strong>Shared Storage</strong>: A location accessible by the Manager and the Workers,
|
||||
where the files to be rendered should be saved.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Shared Storage</strong>: A location accessible by the Manager and the Workers, where the files to be
|
||||
rendered should be saved.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Blender Add-on</strong>: This is needed to connect to the Manager and submit a job from Blender.
|
||||
<strong>Blender Add-on</strong>: This is needed to connect to the Manager and submit a
|
||||
job from Blender.
|
||||
</li>
|
||||
</ul>
|
||||
<p>More information is available on the online documentation at
|
||||
<p>
|
||||
More information is available on the online documentation at
|
||||
<a href="https://flamenco.blender.org">flamenco.blender.org</a>.
|
||||
</p>
|
||||
</step-item>
|
||||
|
||||
<step-item v-show="currentSetupStep == 2" @next-clicked="nextStepAfterCheckSharedStoragePath"
|
||||
@back-clicked="prevStep" :is-next-clickable="sharedStoragePath.length > 0" title="Shared Storage">
|
||||
<step-item
|
||||
v-show="currentSetupStep == 2"
|
||||
@next-clicked="nextStepAfterCheckSharedStoragePath"
|
||||
@back-clicked="prevStep"
|
||||
:is-next-clickable="sharedStoragePath.length > 0"
|
||||
title="Shared Storage">
|
||||
<p>Specify a path (or drive) where you want to store your Flamenco data.</p>
|
||||
<p>
|
||||
The location of the shared storage should be accessible by Flamenco Manager and by the Workers.
|
||||
This could be:
|
||||
The location of the shared storage should be accessible by Flamenco Manager and by the
|
||||
Workers. This could be:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
@ -62,33 +74,42 @@
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Using services like Dropbox, Syncthing, or ownCloud for
|
||||
this is not recommended, as Flamenco can't coordinate data synchronization.
|
||||
Using services like Dropbox, Syncthing, or ownCloud for this is not recommended, as
|
||||
Flamenco can't coordinate data synchronization.
|
||||
<a href="https://flamenco.blender.org/usage/shared-storage/">Learn more</a>.
|
||||
</p>
|
||||
|
||||
<input v-model="sharedStoragePath" @keyup.enter="nextStepAfterCheckSharedStoragePath" type="text"
|
||||
placeholder="Path to shared storage" :class="{
|
||||
'is-invalid': (sharedStorageCheckResult != null) && !sharedStorageCheckResult.is_usable
|
||||
<input
|
||||
v-model="sharedStoragePath"
|
||||
@keyup.enter="nextStepAfterCheckSharedStoragePath"
|
||||
type="text"
|
||||
placeholder="Path to shared storage"
|
||||
:class="{
|
||||
'is-invalid': sharedStorageCheckResult != null && !sharedStorageCheckResult.is_usable,
|
||||
}" />
|
||||
<p
|
||||
v-if="sharedStorageCheckResult != null"
|
||||
:class="{
|
||||
'check-ok': sharedStorageCheckResult.is_usable,
|
||||
'check-failed': !sharedStorageCheckResult.is_usable,
|
||||
}">
|
||||
<p v-if="sharedStorageCheckResult != null" :class="{
|
||||
'check-ok': sharedStorageCheckResult.is_usable,
|
||||
'check-failed': !sharedStorageCheckResult.is_usable
|
||||
}">
|
||||
{{ sharedStorageCheckResult.cause }}
|
||||
</p>
|
||||
</step-item>
|
||||
|
||||
<step-item v-show="currentSetupStep == 3" @next-clicked="nextStepAfterCheckBlenderExePath" @back-clicked="prevStep"
|
||||
:is-next-clickable="selectedBlender != null || customBlenderExe != (null || '')" title="Blender">
|
||||
|
||||
<step-item
|
||||
v-show="currentSetupStep == 3"
|
||||
@next-clicked="nextStepAfterCheckBlenderExePath"
|
||||
@back-clicked="prevStep"
|
||||
:is-next-clickable="selectedBlender != null || customBlenderExe != (null || '')"
|
||||
title="Blender">
|
||||
<div v-if="isBlenderExeFinding" class="is-in-progress">Looking for Blender installs...</div>
|
||||
|
||||
<p v-if="autoFoundBlenders.length === 0">
|
||||
Provide a path to a Blender executable accessible by all Workers.
|
||||
<br /><br />
|
||||
If your rendering setup features operating systems different from the one you are currently using,
|
||||
you can manually set up the other paths later.
|
||||
If your rendering setup features operating systems different from the one you are
|
||||
currently using, you can manually set up the other paths later.
|
||||
</p>
|
||||
|
||||
<p v-else>Choose how a Worker should invoke the Blender command when performing a task:</p>
|
||||
@ -96,16 +117,23 @@
|
||||
<fieldset v-if="autoFoundBlenders.length >= 1">
|
||||
<label v-if="autoFoundBlenderPathEnvvar" for="blender-path_envvar">
|
||||
<div>
|
||||
<input v-model="selectedBlender" :value="autoFoundBlenderPathEnvvar" id="blender-path_envvar" name="blender"
|
||||
type="radio">
|
||||
<input
|
||||
v-model="selectedBlender"
|
||||
:value="autoFoundBlenderPathEnvvar"
|
||||
id="blender-path_envvar"
|
||||
name="blender"
|
||||
type="radio" />
|
||||
{{ sourceLabels[autoFoundBlenderPathEnvvar.source] }}
|
||||
</div>
|
||||
<div class="setup-path-command">
|
||||
<span class="path">
|
||||
{{ autoFoundBlenderPathEnvvar.path }}
|
||||
</span>
|
||||
<span aria-label="Console output when running with --version" class="command-preview"
|
||||
data-microtip-position="top" role="tooltip">
|
||||
<span
|
||||
aria-label="Console output when running with --version"
|
||||
class="command-preview"
|
||||
data-microtip-position="top"
|
||||
role="tooltip">
|
||||
{{ autoFoundBlenderPathEnvvar.cause }}
|
||||
</span>
|
||||
</div>
|
||||
@ -113,16 +141,23 @@
|
||||
|
||||
<label v-if="autoFoundBlenderFileAssociation" for="blender-file_association">
|
||||
<div>
|
||||
<input v-model="selectedBlender" :value="autoFoundBlenderFileAssociation" id="blender-file_association"
|
||||
name="blender" type="radio">
|
||||
<input
|
||||
v-model="selectedBlender"
|
||||
:value="autoFoundBlenderFileAssociation"
|
||||
id="blender-file_association"
|
||||
name="blender"
|
||||
type="radio" />
|
||||
{{ sourceLabels[autoFoundBlenderFileAssociation.source] }}
|
||||
</div>
|
||||
<div class="setup-path-command">
|
||||
<span class="path">
|
||||
{{ autoFoundBlenderFileAssociation.path }}
|
||||
</span>
|
||||
<span aria-label="Console output when running with --version" class="command-preview"
|
||||
data-microtip-position="top" role="tooltip">
|
||||
<span
|
||||
aria-label="Console output when running with --version"
|
||||
class="command-preview"
|
||||
data-microtip-position="top"
|
||||
role="tooltip">
|
||||
{{ autoFoundBlenderFileAssociation.cause }}
|
||||
</span>
|
||||
</div>
|
||||
@ -130,35 +165,60 @@
|
||||
|
||||
<label for="blender-input_path">
|
||||
<div>
|
||||
<input type="radio" v-model="selectedBlender" name="blender" :value="blenderFromInputPath"
|
||||
id="blender-input_path">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="selectedBlender"
|
||||
name="blender"
|
||||
:value="blenderFromInputPath"
|
||||
id="blender-input_path" />
|
||||
{{ sourceLabels['input_path'] }}
|
||||
</div>
|
||||
<div>
|
||||
<input v-model="customBlenderExe" @keyup.enter="nextStepAfterCheckBlenderExePath"
|
||||
<input
|
||||
v-model="customBlenderExe"
|
||||
@keyup.enter="nextStepAfterCheckBlenderExePath"
|
||||
@focus="selectedBlender = null"
|
||||
:class="{ 'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable }" type="text"
|
||||
placeholder="Path to Blender">
|
||||
:class="{
|
||||
'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable,
|
||||
}"
|
||||
type="text"
|
||||
placeholder="Path to Blender" />
|
||||
<p v-if="isBlenderExeChecking" class="is-in-progress">Checking...</p>
|
||||
<p v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
|
||||
{{ blenderExeCheckResult.cause }}</p>
|
||||
<p
|
||||
v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable"
|
||||
class="check-failed">
|
||||
{{ blenderExeCheckResult.cause }}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div v-if="autoFoundBlenders.length === 0">
|
||||
<input v-model="customBlenderExe" @keyup.enter="nextStepAfterCheckBlenderExePath"
|
||||
:class="{ 'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable }" type="text"
|
||||
placeholder="Path to Blender executable">
|
||||
<input
|
||||
v-model="customBlenderExe"
|
||||
@keyup.enter="nextStepAfterCheckBlenderExePath"
|
||||
:class="{
|
||||
'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable,
|
||||
}"
|
||||
type="text"
|
||||
placeholder="Path to Blender executable" />
|
||||
|
||||
<p v-if="isBlenderExeChecking" class="is-in-progress">Checking...</p>
|
||||
<p v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
|
||||
{{ blenderExeCheckResult.cause }}</p>
|
||||
<p
|
||||
v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable"
|
||||
class="check-failed">
|
||||
{{ blenderExeCheckResult.cause }}
|
||||
</p>
|
||||
</div>
|
||||
</step-item>
|
||||
|
||||
<step-item v-show="currentSetupStep == 4" @next-clicked="confirmSetupAssistant" @back-clicked="prevStep"
|
||||
next-label="Confirm" title="Review" :is-next-clickable="setupConfirmIsClickable">
|
||||
<step-item
|
||||
v-show="currentSetupStep == 4"
|
||||
@next-clicked="confirmSetupAssistant"
|
||||
@back-clicked="prevStep"
|
||||
next-label="Confirm"
|
||||
title="Review"
|
||||
:is-next-clickable="setupConfirmIsClickable">
|
||||
<div v-if="isConfigComplete">
|
||||
<p>This is the configuration that will be used by Flamenco:</p>
|
||||
<dl>
|
||||
@ -166,20 +226,25 @@
|
||||
<dd>{{ sharedStorageCheckResult.path }}</dd>
|
||||
<dt>Blender Command</dt>
|
||||
<dd v-if="selectedBlender.source == 'file_association'">
|
||||
Whatever Blender is associated with .blend files
|
||||
(currently "<code>{{ selectedBlender.path }}</code>")
|
||||
Whatever Blender is associated with .blend files (currently "<code>{{
|
||||
selectedBlender.path
|
||||
}}</code
|
||||
>")
|
||||
</dd>
|
||||
<dd v-if="selectedBlender.source == 'path_envvar'">
|
||||
The command "<code>{{ selectedBlender.input }}</code>" as found on <code>$PATH</code>
|
||||
(currently "<code>{{ selectedBlender.path }}</code>")
|
||||
The command "<code>{{ selectedBlender.input }}</code
|
||||
>" as found on <code>$PATH</code> (currently "<code>{{ selectedBlender.path }}</code
|
||||
>")
|
||||
</dd>
|
||||
<dd v-if="selectedBlender.source == 'input_path'">
|
||||
The command you provided:
|
||||
"<code>{{ selectedBlender.path }}</code>"
|
||||
The command you provided: "<code>{{ selectedBlender.path }}</code
|
||||
>"
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<p v-if="isConfirmed" class="check-ok">Configuration has been saved, Flamenco will restart.</p>
|
||||
<p v-if="isConfirmed" class="check-ok">
|
||||
Configuration has been saved, Flamenco will restart.
|
||||
</p>
|
||||
</step-item>
|
||||
</div>
|
||||
</div>
|
||||
@ -188,16 +253,19 @@
|
||||
<notification-bar />
|
||||
</footer>
|
||||
|
||||
<update-listener ref="updateListener" @sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
||||
<update-listener
|
||||
ref="updateListener"
|
||||
@sioReconnected="onSIOReconnected"
|
||||
@sioDisconnected="onSIODisconnected" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'microtip/microtip.css'
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue'
|
||||
import UpdateListener from '@/components/UpdateListener.vue'
|
||||
import 'microtip/microtip.css';
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue';
|
||||
import UpdateListener from '@/components/UpdateListener.vue';
|
||||
import StepItem from '@/components/steps/StepItem.vue';
|
||||
import { MetaApi, PathCheckInput, SetupAssistantConfig } from "@/manager-api";
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { MetaApi, PathCheckInput, SetupAssistantConfig } from '@/manager-api';
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
export default {
|
||||
name: 'SetupAssistantView',
|
||||
@ -207,7 +275,7 @@ export default {
|
||||
NotificationBar,
|
||||
},
|
||||
data: () => ({
|
||||
sharedStoragePath: "",
|
||||
sharedStoragePath: '',
|
||||
sharedStorageCheckResult: null, // api.PathCheckResult
|
||||
metaAPI: new MetaApi(getAPIClient()),
|
||||
|
||||
@ -217,13 +285,13 @@ export default {
|
||||
isBlenderExeFinding: false,
|
||||
selectedBlender: null, // the chosen api.BlenderPathCheckResult
|
||||
|
||||
customBlenderExe: "",
|
||||
customBlenderExe: '',
|
||||
isBlenderExeChecking: false,
|
||||
blenderExeCheckResult: null, // api.BlenderPathCheckResult
|
||||
sourceLabels: {
|
||||
file_association: "Blender that runs when you double-click a .blend file:",
|
||||
path_envvar: "Blender found on the $PATH environment:",
|
||||
input_path: "Another Blender executable:",
|
||||
file_association: 'Blender that runs when you double-click a .blend file:',
|
||||
path_envvar: 'Blender found on the $PATH environment:',
|
||||
input_path: 'Another Blender executable:',
|
||||
},
|
||||
isConfirming: false,
|
||||
isConfirmed: false,
|
||||
@ -248,13 +316,13 @@ export default {
|
||||
return this.isSharedStorageValid && this.isSelectedBlenderValid;
|
||||
},
|
||||
autoFoundBlenderPathEnvvar() {
|
||||
return this.autoFoundBlenders.find(b => b.source === 'path_envvar');
|
||||
return this.autoFoundBlenders.find((b) => b.source === 'path_envvar');
|
||||
},
|
||||
autoFoundBlenderFileAssociation() {
|
||||
return this.autoFoundBlenders.find(b => b.source === 'file_association');
|
||||
return this.autoFoundBlenders.find((b) => b.source === 'file_association');
|
||||
},
|
||||
blenderFromInputPath() {
|
||||
return this.allBlenders.find(b => b.source === 'input_path');
|
||||
return this.allBlenders.find((b) => b.source === 'input_path');
|
||||
},
|
||||
setupConfirmIsClickable() {
|
||||
if (this.isConfirming || this.isConfirmed) {
|
||||
@ -262,7 +330,7 @@ export default {
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.findBlenderExePath();
|
||||
@ -271,25 +339,24 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
// SocketIO connection event handlers:
|
||||
onSIOReconnected() {
|
||||
},
|
||||
onSIODisconnected(reason) {
|
||||
},
|
||||
onSIOReconnected() {},
|
||||
onSIODisconnected(reason) {},
|
||||
|
||||
nextStepAfterCheckSharedStoragePath() {
|
||||
const pathCheck = new PathCheckInput(this.cleanSharedStoragePath);
|
||||
console.log("requesting path check:", pathCheck);
|
||||
return this.metaAPI.checkSharedStoragePath({ pathCheckInput: pathCheck })
|
||||
console.log('requesting path check:', pathCheck);
|
||||
return this.metaAPI
|
||||
.checkSharedStoragePath({ pathCheckInput: pathCheck })
|
||||
.then((result) => {
|
||||
console.log("Storage path check result:", result);
|
||||
console.log('Storage path check result:', result);
|
||||
this.sharedStorageCheckResult = result;
|
||||
if (this.isSharedStorageValid) {
|
||||
this.nextStep();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error checking storage path:", error);
|
||||
})
|
||||
console.log('Error checking storage path:', error);
|
||||
});
|
||||
},
|
||||
|
||||
nextStepAfterCheckBlenderExePath() {
|
||||
@ -303,24 +370,25 @@ export default {
|
||||
this.isBlenderExeFinding = true;
|
||||
this.autoFoundBlenders = [];
|
||||
|
||||
console.log("Finding Blender");
|
||||
this.metaAPI.findBlenderExePath()
|
||||
console.log('Finding Blender');
|
||||
this.metaAPI
|
||||
.findBlenderExePath()
|
||||
.then((result) => {
|
||||
console.log("Result of finding Blender:", result);
|
||||
console.log('Result of finding Blender:', result);
|
||||
this.autoFoundBlenders = result;
|
||||
this._refreshAllBlenders();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error finding Blender:", error);
|
||||
console.log('Error finding Blender:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isBlenderExeFinding = false;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
checkBlenderExePath() {
|
||||
const exeToTry = this.cleanCustomBlenderExe;
|
||||
if (exeToTry == "") {
|
||||
if (exeToTry == '') {
|
||||
// Just erase any previously-found custom Blender executable.
|
||||
this.isBlenderExeChecking = false;
|
||||
this.blenderExeCheckResult = null;
|
||||
@ -332,10 +400,11 @@ export default {
|
||||
this.blenderExeCheckResult = null;
|
||||
|
||||
const pathCheck = new PathCheckInput(exeToTry);
|
||||
console.log("requesting path check:", pathCheck);
|
||||
this.metaAPI.checkBlenderExePath({ pathCheckInput: pathCheck })
|
||||
console.log('requesting path check:', pathCheck);
|
||||
this.metaAPI
|
||||
.checkBlenderExePath({ pathCheckInput: pathCheck })
|
||||
.then((result) => {
|
||||
console.log("Blender exe path check result:", result);
|
||||
console.log('Blender exe path check result:', result);
|
||||
this.blenderExeCheckResult = result;
|
||||
if (result.is_usable) {
|
||||
this.selectedBlender = result;
|
||||
@ -345,11 +414,11 @@ export default {
|
||||
this._refreshAllBlenders();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error checking storage path:", error);
|
||||
console.log('Error checking storage path:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isBlenderExeChecking = false;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
_refreshAllBlenders() {
|
||||
@ -380,26 +449,29 @@ export default {
|
||||
confirmSetupAssistant() {
|
||||
const setupAssistantConfig = new SetupAssistantConfig(
|
||||
this.sharedStorageCheckResult.path,
|
||||
this.selectedBlender,
|
||||
this.selectedBlender
|
||||
);
|
||||
console.log("saving configuration:", setupAssistantConfig);
|
||||
console.log('saving configuration:', setupAssistantConfig);
|
||||
this.isConfirming = true;
|
||||
this.isConfirmed = false;
|
||||
this.metaAPI.saveSetupAssistantConfig({ setupAssistantConfig: setupAssistantConfig })
|
||||
this.metaAPI
|
||||
.saveSetupAssistantConfig({ setupAssistantConfig: setupAssistantConfig })
|
||||
.then((result) => {
|
||||
console.log("Setup Assistant config saved, reload the page");
|
||||
console.log('Setup Assistant config saved, reload the page');
|
||||
this.isConfirmed = true;
|
||||
// Give the Manager some time to restart.
|
||||
window.setTimeout(() => { window.location.reload() }, 2000);
|
||||
window.setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error saving setup assistan config:", error);
|
||||
console.log('Error saving setup assistan config:', error);
|
||||
// Only clear this flag on an error.
|
||||
this.isConfirming = false;
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.step-welcome ul {
|
||||
@ -462,7 +534,9 @@ export default {
|
||||
.progress-bar {
|
||||
--width-each-segment: calc(100% / calc(v-bind('totalSetupSteps') - 1));
|
||||
/* Substract 1 because the first step has no progress. */
|
||||
--progress-bar-width-at-current-step: calc(var(--width-each-segment) * calc(v-bind('currentSetupStep') - 1));
|
||||
--progress-bar-width-at-current-step: calc(
|
||||
var(--width-each-segment) * calc(v-bind('currentSetupStep') - 1)
|
||||
);
|
||||
|
||||
position: absolute;
|
||||
top: calc(50% - calc(var(--setup-progress-indicator-border-width) / 2));
|
||||
@ -481,7 +555,8 @@ export default {
|
||||
background-color: var(--color-background);
|
||||
border-radius: 50%;
|
||||
border: var(--setup-progress-indicator-border-width) solid var(--color-background);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width) var(--setup-progress-indicator-color);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width)
|
||||
var(--setup-progress-indicator-color);
|
||||
content: '';
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@ -504,38 +579,42 @@ export default {
|
||||
|
||||
.progress li.done span {
|
||||
background-color: var(--setup-progress-indicator-color);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width) var(--setup-progress-indicator-color);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width)
|
||||
var(--setup-progress-indicator-color);
|
||||
}
|
||||
|
||||
.progress li.done_previously span {
|
||||
background-color: var(--setup-progress-indicator-color-done);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width) var(--setup-progress-indicator-color-done);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width)
|
||||
var(--setup-progress-indicator-color-done);
|
||||
}
|
||||
|
||||
.progress li.current span {
|
||||
background-color: var(--color-background-column);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width) var(--setup-progress-indicator-color-current);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width)
|
||||
var(--setup-progress-indicator-color-current);
|
||||
}
|
||||
|
||||
.progress li.done_and_current span {
|
||||
background-color: var(--setup-progress-indicator-color-current);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width) var(--setup-progress-indicator-color-current);
|
||||
box-shadow: 0 0 0 var(--setup-progress-indicator-border-width)
|
||||
var(--setup-progress-indicator-color-current);
|
||||
}
|
||||
|
||||
body.is-setup-assistant #app {
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"col-full-width"
|
||||
"footer";
|
||||
'header'
|
||||
'col-full-width'
|
||||
'footer';
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
body.is-setup-assistant #app {
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"col-full-width"
|
||||
"footer";
|
||||
'header'
|
||||
'col-full-width'
|
||||
'footer';
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: var(--header-height) 1fr var(--footer-height);
|
||||
}
|
||||
@ -573,7 +652,7 @@ body.is-setup-assistant #app {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
input[type='text'] {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
@ -627,7 +706,12 @@ h2 {
|
||||
|
||||
.is-in-progress {
|
||||
animation: is-in-progress 3s infinite linear;
|
||||
background-image: linear-gradient(to left, var(--color-text-muted), rgba(255, 255, 255, 0.25), var(--color-text-muted));
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
var(--color-text-muted),
|
||||
rgba(255, 255, 255, 0.25),
|
||||
var(--color-text-muted)
|
||||
);
|
||||
background-size: 200px;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
@ -644,7 +728,7 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
fieldset input[type="text"],
|
||||
fieldset input[type='text'],
|
||||
.setup-path-command {
|
||||
margin-left: 1.66rem;
|
||||
margin-top: var(--spacer-sm);
|
||||
|
@ -16,13 +16,8 @@
|
||||
name="newtagname"
|
||||
v-model="newTagName"
|
||||
placeholder="New Tag Name"
|
||||
class="create-tag-input"
|
||||
/>
|
||||
<button
|
||||
id="submit-button"
|
||||
type="submit"
|
||||
:disabled="newTagName.trim() === ''"
|
||||
>
|
||||
class="create-tag-input" />
|
||||
<button id="submit-button" type="submit" :disabled="newTagName.trim() === ''">
|
||||
Create Tag
|
||||
</button>
|
||||
</div>
|
||||
@ -35,32 +30,25 @@
|
||||
<h2 class="column-title">Information</h2>
|
||||
|
||||
<p>
|
||||
Workers and jobs can be tagged. With these tags you can assign a job to a
|
||||
subset of your workers.
|
||||
Workers and jobs can be tagged. With these tags you can assign a job to a subset of your
|
||||
workers.
|
||||
</p>
|
||||
|
||||
<h4>Job Perspective:</h4>
|
||||
<ul>
|
||||
<li>A job can have one tag, or no tag.</li>
|
||||
<li>
|
||||
A job <strong>with</strong> a tag will only be assigned to workers with
|
||||
that tag.
|
||||
</li>
|
||||
<li>
|
||||
A job <strong>without</strong> tag will be assigned to any worker.
|
||||
</li>
|
||||
<li>A job <strong>with</strong> a tag will only be assigned to workers with that tag.</li>
|
||||
<li>A job <strong>without</strong> tag will be assigned to any worker.</li>
|
||||
</ul>
|
||||
|
||||
<h4>Worker Perspective:</h4>
|
||||
<ul>
|
||||
<li>A worker can have any number of tags.</li>
|
||||
<li>
|
||||
A worker <strong>with</strong> one or more tags will work only on jobs
|
||||
with one those tags, and on tagless jobs.
|
||||
</li>
|
||||
<li>
|
||||
A worker <strong>without</strong> tags will only work on tagless jobs.
|
||||
A worker <strong>with</strong> one or more tags will work only on jobs with one those tags,
|
||||
and on tagless jobs.
|
||||
</li>
|
||||
<li>A worker <strong>without</strong> tags will only work on tagless jobs.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<footer class="app-footer">
|
||||
@ -70,8 +58,7 @@
|
||||
mainSubscription="allWorkerTags"
|
||||
@workerTagUpdate="onSIOWorkerTagsUpdate"
|
||||
@sioReconnected="onSIOReconnected"
|
||||
@sioDisconnected="onSIODisconnected"
|
||||
/>
|
||||
@sioDisconnected="onSIODisconnected" />
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@ -108,14 +95,14 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { TabulatorFull as Tabulator } from "tabulator-tables";
|
||||
import { useWorkers } from "@/stores/workers";
|
||||
import { useNotifs } from "@/stores/notifications";
|
||||
import { WorkerMgtApi } from "@/manager-api";
|
||||
import { WorkerTag } from "@/manager-api";
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import NotificationBar from "@/components/footer/NotificationBar.vue";
|
||||
import UpdateListener from "@/components/UpdateListener.vue";
|
||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||
import { useWorkers } from '@/stores/workers';
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import { WorkerMgtApi } from '@/manager-api';
|
||||
import { WorkerTag } from '@/manager-api';
|
||||
import { getAPIClient } from '@/api-client';
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue';
|
||||
import UpdateListener from '@/components/UpdateListener.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -126,25 +113,25 @@ export default {
|
||||
return {
|
||||
tags: [],
|
||||
selectedTag: null,
|
||||
newTagName: "",
|
||||
newTagName: '',
|
||||
workers: useWorkers(),
|
||||
activeRowIndex: -1,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.body.classList.add("is-two-columns");
|
||||
document.body.classList.add('is-two-columns');
|
||||
|
||||
this.fetchTags();
|
||||
|
||||
const tag_options = {
|
||||
columns: [
|
||||
{ title: "Name", field: "name", sorter: "string", editor: "input" },
|
||||
{ title: 'Name', field: 'name', sorter: 'string', editor: 'input' },
|
||||
{
|
||||
title: "Description",
|
||||
field: "description",
|
||||
sorter: "string",
|
||||
editor: "input",
|
||||
title: 'Description',
|
||||
field: 'description',
|
||||
sorter: 'string',
|
||||
editor: 'input',
|
||||
formatter(cell) {
|
||||
const cellValue = cell.getData().description;
|
||||
if (!cellValue) {
|
||||
@ -154,24 +141,24 @@ export default {
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: "fitData",
|
||||
layout: 'fitData',
|
||||
layoutColumnsOnNewData: true,
|
||||
height: "82%",
|
||||
height: '82%',
|
||||
selectable: true,
|
||||
};
|
||||
|
||||
this.tabulator = new Tabulator("#tag-table-container", tag_options);
|
||||
this.tabulator.on("rowClick", this.onRowClick);
|
||||
this.tabulator.on("tableBuilt", () => {
|
||||
this.tabulator = new Tabulator('#tag-table-container', tag_options);
|
||||
this.tabulator.on('rowClick', this.onRowClick);
|
||||
this.tabulator.on('tableBuilt', () => {
|
||||
this.fetchTags();
|
||||
});
|
||||
this.tabulator.on("cellEdited", (cell) => {
|
||||
this.tabulator.on('cellEdited', (cell) => {
|
||||
const editedTag = cell.getRow().getData();
|
||||
this.updateTagInAPI(editedTag);
|
||||
});
|
||||
},
|
||||
unmounted() {
|
||||
document.body.classList.remove("is-two-columns");
|
||||
document.body.classList.remove('is-two-columns');
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -201,7 +188,7 @@ export default {
|
||||
.createWorkerTag(newTag)
|
||||
.then(() => {
|
||||
this.fetchTags(); // Refresh table data
|
||||
this.newTagName = "";
|
||||
this.newTagName = '';
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = JSON.stringify(error);
|
||||
@ -216,7 +203,7 @@ export default {
|
||||
api
|
||||
.updateWorkerTag(tagId, updatedTagData)
|
||||
.then(() => {
|
||||
console.log("Tag updated successfully");
|
||||
console.log('Tag updated successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = JSON.stringify(error);
|
||||
|
@ -1,16 +1,23 @@
|
||||
<template>
|
||||
<div class="col col-workers-list">
|
||||
<workers-table ref="workersTable" :activeWorkerID="workerID" @tableRowClicked="onTableWorkerClicked" />
|
||||
<workers-table
|
||||
ref="workersTable"
|
||||
:activeWorkerID="workerID"
|
||||
@tableRowClicked="onTableWorkerClicked" />
|
||||
</div>
|
||||
<div class="col col-workers-details">
|
||||
<worker-details :workerData="workers.activeWorker" />
|
||||
</div>
|
||||
<footer class="app-footer">
|
||||
<notification-bar />
|
||||
<update-listener ref="updateListener"
|
||||
mainSubscription="allWorkers" extraSubscription="allWorkerTags"
|
||||
@workerUpdate="onSIOWorkerUpdate" @workerTagUpdate="onSIOWorkerTagsUpdate"
|
||||
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
||||
<update-listener
|
||||
ref="updateListener"
|
||||
mainSubscription="allWorkers"
|
||||
extraSubscription="allWorkerTags"
|
||||
@workerUpdate="onSIOWorkerUpdate"
|
||||
@workerTagUpdate="onSIOWorkerTagsUpdate"
|
||||
@sioReconnected="onSIOReconnected"
|
||||
@sioDisconnected="onSIODisconnected" />
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@ -26,18 +33,18 @@
|
||||
|
||||
<script>
|
||||
import { WorkerMgtApi } from '@/manager-api';
|
||||
import { useNotifs } from '@/stores/notifications'
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
import { useWorkers } from '@/stores/workers';
|
||||
import { getAPIClient } from "@/api-client";
|
||||
import { getAPIClient } from '@/api-client';
|
||||
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue'
|
||||
import UpdateListener from '@/components/UpdateListener.vue'
|
||||
import WorkerDetails from '@/components/workers/WorkerDetails.vue'
|
||||
import WorkersTable from '@/components/workers/WorkersTable.vue'
|
||||
import NotificationBar from '@/components/footer/NotificationBar.vue';
|
||||
import UpdateListener from '@/components/UpdateListener.vue';
|
||||
import WorkerDetails from '@/components/workers/WorkerDetails.vue';
|
||||
import WorkersTable from '@/components/workers/WorkersTable.vue';
|
||||
|
||||
export default {
|
||||
name: 'WorkersView',
|
||||
props: ["workerID"], // provided by Vue Router.
|
||||
props: ['workerID'], // provided by Vue Router.
|
||||
components: {
|
||||
NotificationBar,
|
||||
UpdateListener,
|
||||
@ -69,27 +76,24 @@ export default {
|
||||
this.$refs.workersTable.onReconnected();
|
||||
this._fetchWorker(this.workerID);
|
||||
},
|
||||
onSIODisconnected(reason) {
|
||||
},
|
||||
onSIODisconnected(reason) {},
|
||||
onSIOWorkerUpdate(workerUpdate) {
|
||||
this.notifs.addWorkerUpdate(workerUpdate);
|
||||
|
||||
if (this.$refs.workersTable) {
|
||||
this.$refs.workersTable.processWorkerUpdate(workerUpdate);
|
||||
}
|
||||
if (this.workerID != workerUpdate.id)
|
||||
return;
|
||||
if (this.workerID != workerUpdate.id) return;
|
||||
|
||||
if (workerUpdate.deleted_at) {
|
||||
this._routeToWorker("");
|
||||
this._routeToWorker('');
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchWorker(this.workerID);
|
||||
},
|
||||
onSIOWorkerTagsUpdate(workerTagsUpdate) {
|
||||
this.workers.refreshTags()
|
||||
.then(() => this._fetchWorker(this.workerID));
|
||||
this.workers.refreshTags().then(() => this._fetchWorker(this.workerID));
|
||||
},
|
||||
|
||||
onTableWorkerClicked(rowData) {
|
||||
@ -115,9 +119,8 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.api.fetchWorker(workerID)
|
||||
.then((worker) => this.workers.setActiveWorker(worker));
|
||||
return this.api.fetchWorker(workerID).then((worker) => this.workers.setActiveWorker(worker));
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { fileURLToPath, URL } from 'url'
|
||||
import { fileURLToPath, URL } from 'url';
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user