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