Webapp: avoid browser JS errors about forbidden 'User-Agent' header

Brave (and maybe other browseres) refuse to set the 'User-Agent' header
in XMLHTTPRequests, and are vocal about this in the debug log. Since the
OpenAPI code generator always outputs a custom 'User-Agent' header, I've
added some JS code to strip that off when constructing an API client.
This commit is contained in:
Sybren A. Stüvel 2023-02-21 11:08:48 +01:00
parent a5cfa9959b
commit 1add6bfc8a
22 changed files with 171 additions and 194 deletions

View File

@ -26,7 +26,7 @@
<script> <script>
import * as API from '@/manager-api'; import * as API from '@/manager-api';
import { apiClient } from '@/stores/api-query-count'; import { getAPIClient } from "@/api-client";
import { backendURL } from '@/urls'; import { backendURL } from '@/urls';
import ApiSpinner from '@/components/ApiSpinner.vue' import ApiSpinner from '@/components/ApiSpinner.vue'
@ -51,7 +51,7 @@ export default {
methods: { methods: {
// TODO: also call this when SocketIO reconnects. // TODO: also call this when SocketIO reconnects.
fetchManagerInfo() { fetchManagerInfo() {
const metaAPI = new API.MetaApi(apiClient); const metaAPI = new API.MetaApi(getAPIClient());
metaAPI.getVersion().then((version) => { metaAPI.getVersion().then((version) => {
this.flamencoName = version.name; this.flamencoName = version.name;
this.flamencoVersion = version.version; this.flamencoVersion = version.version;

View File

@ -17,7 +17,7 @@ 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 { apiClient } from '@/stores/api-query-count'; import { getAPIClient } from "@/api-client";
export default { export default {
name: 'SetupAssistant', name: 'SetupAssistant',
@ -35,7 +35,7 @@ export default {
methods: { methods: {
// TODO: also call this when SocketIO reconnects. // TODO: also call this when SocketIO reconnects.
fetchManagerInfo() { fetchManagerInfo() {
const metaAPI = new MetaApi(apiClient); const metaAPI = new MetaApi(getAPIClient());
metaAPI.getVersion().then((version) => { metaAPI.getVersion().then((version) => {
this.flamencoName = version.name; this.flamencoName = version.name;
this.flamencoVersion = version.version; this.flamencoVersion = version.version;

39
web/app/src/api-client.js Normal file
View File

@ -0,0 +1,39 @@
import { ApiClient } from "@/manager-api";
import { CountingApiClient } from "@/stores/api-query-count";
import { api as apiURL } from '@/urls'
/**
* Scrub the custom User-Agent header from the API client, for those webbrowsers
* who do not want to have it set.
*
* It's actually scrubbed for all webbrowsers, as those privacy-first
* webbrowsers also make it hard to fingerprint which browser you're using (for
* good reason).
*
* @param {ApiClient} apiClient
*/
export function scrubAPIClient(apiClient) {
delete apiClient.defaultHeaders['User-Agent'];
}
/**
* @returns {ApiClient} Bare API client that is not connected to the UI in any way.
*/
export function newBareAPIClient() {
const apiClient = new ApiClient(apiURL());
scrubAPIClient(apiClient);
return apiClient;
}
let apiClient = null;
/**
* @returns {ApiClient} API client that updates the UI to show long-running queries.
*/
export function getAPIClient() {
if (apiClient == null) {
apiClient = new CountingApiClient(apiURL());
scrubAPIClient(apiClient);
}
return apiClient;
}

View File

@ -26,7 +26,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useNotifs } from '@/stores/notifications'; import { useNotifs } from '@/stores/notifications';
import { apiClient } from '@/stores/api-query-count'; import { getAPIClient } from "@/api-client";
import { JobsApi, JobPriorityChange } from '@/manager-api'; import { JobsApi, JobPriorityChange } from '@/manager-api';
const props = defineProps({ const props = defineProps({
@ -45,7 +45,7 @@ const errorMessage = ref('');
// Methods // Methods
function updateJobPriority() { function updateJobPriority() {
const jobPriorityChange = new JobPriorityChange(priorityState.value); const jobPriorityChange = new JobPriorityChange(priorityState.value);
const jobsAPI = new JobsApi(apiClient); 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}`)

View File

@ -3,7 +3,7 @@ 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 { apiClient } from '@/stores/api-query-count'; import { getAPIClient } from "@/api-client";
import { JobsApi } from '@/manager-api'; import { JobsApi } from '@/manager-api';
const taskLog = useTaskLog(); const taskLog = useTaskLog();
@ -59,7 +59,7 @@ function _fetchLogTail(taskID) {
if (!taskID) return; if (!taskID) return;
const jobsAPI = new JobsApi(apiClient); const jobsAPI = new JobsApi(getAPIClient());
return jobsAPI.fetchTaskLogTail(taskID) return jobsAPI.fetchTaskLogTail(taskID)
.then((logTail) => { .then((logTail) => {
taskLog.addChunk(logTail); taskLog.addChunk(logTail);

View File

@ -26,7 +26,7 @@
</template> </template>
<script setup> <script setup>
import { apiClient } from '@/stores/api-query-count'; 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'
@ -35,7 +35,7 @@ import { watch, onMounted, inject, ref, nextTick } from 'vue'
const props = defineProps(['jobID']); const props = defineProps(['jobID']);
const emit = defineEmits(['reshuffled']) const emit = defineEmits(['reshuffled'])
const jobsApi = new JobsApi(apiClient); 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("");

View File

@ -18,7 +18,7 @@
<script> <script>
import { useJobs } from '@/stores/jobs'; import { useJobs } from '@/stores/jobs';
import { useNotifs } from '@/stores/notifications'; import { useNotifs } from '@/stores/notifications';
import { apiClient } from '@/stores/api-query-count'; 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';
@ -31,7 +31,7 @@ export default {
data: () => ({ data: () => ({
jobs: useJobs(), jobs: useJobs(),
notifs: useNotifs(), notifs: useNotifs(),
jobsAPI: new JobsApi(apiClient), jobsAPI: new JobsApi(getAPIClient()),
deleteInfo: null, deleteInfo: null,
}), }),

View File

@ -39,7 +39,7 @@
<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>
<dt class="field-priority" title="Priority">Priority</dt> <dt class="field-priority" title="Priority">Priority</dt>
<dd> <dd>
@ -76,7 +76,7 @@
<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 { apiClient } from '@/stores/api-query-count'; 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'
@ -103,7 +103,7 @@ export default {
datetime: datetime, // So that the template can access it. datetime: datetime, // So that the template can access it.
copyElementText: copyElementText, copyElementText: copyElementText,
simpleSettings: null, // Object with filtered job settings, or null if there is no job. simpleSettings: null, // Object with filtered job settings, or null if there is no job.
jobsApi: new API.JobsApi(apiClient), jobsApi: new API.JobsApi(getAPIClient()),
jobType: null, // API.AvailableJobType object for the current job type. jobType: null, // API.AvailableJobType object for the current job type.
jobTypeSettings: null, // Mapping from setting key to its definition in the job type. jobTypeSettings: null, // Mapping from setting key to its definition in the job type.
showAllSettings: false, showAllSettings: false,

View File

@ -17,7 +17,7 @@ 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 { apiClient } from '@/stores/api-query-count'; 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'
@ -130,7 +130,7 @@ export default {
this.fetchAllJobs(); this.fetchAllJobs();
}, },
fetchAllJobs() { fetchAllJobs() {
const jobsApi = new API.JobsApi(apiClient); const jobsApi = new API.JobsApi(getAPIClient());
const jobsQuery = {}; const jobsQuery = {};
this.jobs.isJobless = false; this.jobs.isJobless = false;
jobsApi.queryJobs(jobsQuery).then(this.onJobsFetched, function (error) { jobsApi.queryJobs(jobsQuery).then(this.onJobsFetched, function (error) {

View File

@ -9,7 +9,7 @@
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 { apiClient } from '@/stores/api-query-count'; 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
@ -27,7 +27,7 @@ const cssClasses = reactive({
'nothing-rendered-yet': true, 'nothing-rendered-yet': true,
}) })
const jobsApi = new JobsApi(apiClient); const jobsApi = new JobsApi(getAPIClient());
/** /**
* Fetches the last-rendered info for the given job, then updates the <img> tag for it. * Fetches the last-rendered info for the given job, then updates the <img> tag for it.

View File

@ -73,7 +73,7 @@
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 { apiClient } from '@/stores/api-query-count'; 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';
@ -90,7 +90,7 @@ export default {
return { return {
datetime: datetime, // So that the template can access it. datetime: datetime, // So that the template can access it.
copyElementText: copyElementText, copyElementText: copyElementText,
jobsApi: new JobsApi(apiClient), jobsApi: new JobsApi(getAPIClient()),
notifs: useNotifs(), notifs: useNotifs(),
}; };
}, },

View File

@ -17,7 +17,7 @@ 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 { apiClient } from '@/stores/api-query-count'; 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'
@ -132,7 +132,7 @@ export default {
return; return;
} }
const jobsApi = new API.JobsApi(apiClient); 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.

View File

@ -6,11 +6,7 @@
</option> </option>
<option v-for="(action, key) in WORKER_ACTIONS" :value="key">{{ action.label }}</option> <option v-for="(action, key) in WORKER_ACTIONS" :value="key">{{ action.label }}</option>
</select> </select>
<button <button :disabled="!canPerformAction" class="btn" @click.prevent="performWorkerAction">Apply</button>
:disabled="!canPerformAction"
class="btn"
@click.prevent="performWorkerAction"
>Apply</button>
</template> </template>
<script setup> <script setup>
@ -18,46 +14,46 @@ 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 { apiClient } from '@/stores/api-query-count'; 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. */
const WORKER_ACTIONS = Object.freeze({ 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,
}, },
offline_immediate: { offline_immediate: {
label: 'Shut Down (immediately)', label: 'Shut Down (immediately)',
icon: '✝!', icon: '✝!',
title: 'Immediately shut down the worker. It may automatically restart.', title: 'Immediately shut down the worker. It may automatically restart.',
target_status: 'offline', target_status: 'offline',
lazy: false, lazy: false,
}, },
asleep_lazy: { asleep_lazy: {
label: 'Send to Sleep (after task is finished)', label: 'Send to Sleep (after task is finished)',
icon: '😴', icon: '😴',
title: 'Let the worker sleep after finishing this task.', title: 'Let the worker sleep after finishing this task.',
target_status: 'asleep', target_status: 'asleep',
lazy: true, lazy: true,
}, },
asleep_immediate: { asleep_immediate: {
label: 'Send to Sleep (immediately)', label: 'Send to Sleep (immediately)',
icon: '😴!', icon: '😴!',
title: 'Let the worker sleep immediately.', title: 'Let the worker sleep immediately.',
target_status: 'asleep', target_status: 'asleep',
lazy: false, lazy: false,
}, },
wakeup: { wakeup: {
label: 'Wake Up', label: 'Wake Up',
icon: '😃', icon: '😃',
title: 'Wake the worker up. A sleeping worker can take a minute to respond.', title: 'Wake the worker up. A sleeping worker can take a minute to respond.',
target_status: 'awake', target_status: 'awake',
lazy: false, lazy: false,
}, },
}); });
const selectedAction = ref(''); const selectedAction = ref('');
@ -73,7 +69,7 @@ function performWorkerAction() {
return; return;
} }
const api = new WorkerMgtApi(apiClient); 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);

View File

@ -123,7 +123,7 @@ import { useNotifs } from '@/stores/notifications'
import * as datetime from "@/datetime"; import * as datetime from "@/datetime";
import { WorkerMgtApi, WorkerSleepSchedule } from '@/manager-api'; import { WorkerMgtApi, WorkerSleepSchedule } from '@/manager-api';
import { apiClient } from '@/stores/api-query-count'; 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';
@ -140,7 +140,7 @@ export default {
data() { data() {
return { return {
datetime: datetime, // So that the template can access it. datetime: datetime, // So that the template can access it.
api: new WorkerMgtApi(apiClient), api: new WorkerMgtApi(getAPIClient()),
workerStatusHTML: "", workerStatusHTML: "",
workerSleepSchedule: this.defaultWorkerSleepSchedule(), workerSleepSchedule: this.defaultWorkerSleepSchedule(),
isScheduleEditing: false, isScheduleEditing: false,

View File

@ -19,7 +19,7 @@
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 { apiClient } from '@/stores/api-query-count'; 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'
@ -117,7 +117,7 @@ export default {
this.fetchAllWorkers(); this.fetchAllWorkers();
}, },
fetchAllWorkers() { fetchAllWorkers() {
const api = new WorkerMgtApi(apiClient); const api = new WorkerMgtApi(getAPIClient());
api.fetchWorkers().then(this.onWorkersFetched, function (error) { api.fetchWorkers().then(this.onWorkersFetched, function (error) {
// TODO: error handling. // TODO: error handling.
console.error(error); console.error(error);

View File

@ -7,7 +7,8 @@ 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 { ApiClient, MetaApi } from "@/manager-api"; import { MetaApi } from "@/manager-api";
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
@ -42,7 +43,7 @@ function setupAssistantMode() {
/* 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
* require Pinia, which is unavailable until the app is actually started. And to * require Pinia, which is unavailable until the app is actually started. And to
* 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 = new ApiClient(urls.api());; const apiClient = newBareAPIClient();
const metaAPI = new MetaApi(apiClient); const metaAPI = new MetaApi(apiClient);
metaAPI.getConfiguration() metaAPI.getConfiguration()
.then((config) => { .then((config) => {

View File

@ -1,6 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ApiClient } from "@/manager-api"; import { ApiClient } from "@/manager-api";
import * as urls from '@/urls'
/** /**
* Keep track of running API queries. * Keep track of running API queries.
@ -42,5 +41,3 @@ export class CountingApiClient extends ApiClient {
}); });
} }
} }
export const apiClient = new CountingApiClient(urls.api());;

View File

@ -1,10 +1,10 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import * as API from '@/manager-api'; import * as API from '@/manager-api';
import { apiClient } from '@/stores/api-query-count'; import { getAPIClient } from "@/api-client";
const jobsAPI = new API.JobsApi(apiClient); const jobsAPI = new API.JobsApi(getAPIClient());
// '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/

View File

@ -1,10 +1,10 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import * as API from '@/manager-api'; import * as API from '@/manager-api';
import { apiClient } from '@/stores/api-query-count'; import { getAPIClient } from "@/api-client";
const jobsAPI = new API.JobsApi(apiClient); const jobsAPI = new API.JobsApi(getAPIClient());
// '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/

View File

@ -39,7 +39,7 @@ 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 { apiClient } from '@/stores/api-query-count'; 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'
@ -127,7 +127,7 @@ export default {
return; return;
} }
const jobsAPI = new API.JobsApi(apiClient); 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);
@ -225,7 +225,7 @@ export default {
return; return;
} }
const jobsAPI = new API.JobsApi(apiClient); 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);
@ -254,7 +254,7 @@ export default {
return; return;
} }
const jobsAPI = new API.JobsApi(apiClient); 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);

View File

@ -2,38 +2,28 @@
<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 <li v-for="step in totalSetupSteps" :key="step" @click="jumpToStep(step)" :class="{
v-for="step in totalSetupSteps" :key="step" current: step == currentSetupStep,
@click="jumpToStep(step)" done: step < overallSetupStep,
:class="{ done_previously: (step < overallSetupStep && currentSetupStep > step),
current: step == currentSetupStep, done_and_current: step == currentSetupStep && (step < overallSetupStep || step == 1),
done: step < overallSetupStep, disabled: step > overallSetupStep,
done_previously: (step < overallSetupStep && currentSetupStep > step), }">
done_and_current: step == currentSetupStep && (step < overallSetupStep || step == 1),
disabled: step > overallSetupStep,
}"
>
<span></span> <span></span>
</li> </li>
<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 will be up
and running in a few minutes! 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>
<img src="@/assets/architecture.png" alt="Flamenco architecture"/> <img src="@/assets/architecture.png" alt="Flamenco architecture" />
<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>
@ -41,26 +31,24 @@
<strong>Manager</strong>: This application. It coordinates all the activity. <strong>Manager</strong>: This application. It coordinates all the activity.
</li> </li>
<li> <li>
<strong>Worker</strong>: A workstation or dedicated rendering machine. It executes the tasks assigned by the Manager. <strong>Worker</strong>: A workstation or dedicated rendering machine. It executes the tasks assigned by the
Manager.
</li> </li>
<li> <li>
<strong>Shared Storage</strong>: A location accessible by the Manager and the Workers, where the files to be rendered should be saved. <strong>Shared Storage</strong>: A location accessible by the Manager and the Workers, where the files to be
rendered should be saved.
</li> </li>
<li> <li>
<strong>Blender Add-on</strong>: This is needed to connect to the Manager and submit a job from Blender. <strong>Blender Add-on</strong>: This is needed to connect to the Manager and submit a job from Blender.
</li> </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>.</p> <a href="https://flamenco.blender.org">flamenco.blender.org</a>.
</p>
</step-item> </step-item>
<step-item <step-item v-show="currentSetupStep == 2" @next-clicked="nextStepAfterCheckSharedStoragePath"
v-show="currentSetupStep == 2" @back-clicked="prevStep" :is-next-clickable="sharedStoragePath.length > 0" title="Shared Storage">
@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 Workers.
@ -79,37 +67,26 @@
<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 <input v-model="sharedStoragePath" @keyup.enter="nextStepAfterCheckSharedStoragePath" type="text"
v-model="sharedStoragePath" placeholder="Path to shared storage" :class="{
@keyup.enter="nextStepAfterCheckSharedStoragePath"
type="text"
placeholder="Path to shared storage"
:class="{
'is-invalid': (sharedStorageCheckResult != null) && !sharedStorageCheckResult.is_usable 'is-invalid': (sharedStorageCheckResult != null) && !sharedStorageCheckResult.is_usable
}"
>
<p v-if="sharedStorageCheckResult != null"
:class="{
'check-ok': sharedStorageCheckResult.is_usable,
'check-failed': !sharedStorageCheckResult.is_usable
}"> }">
<p v-if="sharedStorageCheckResult != null" :class="{
'check-ok': sharedStorageCheckResult.is_usable,
'check-failed': !sharedStorageCheckResult.is_usable
}">
{{ sharedStorageCheckResult.cause }} {{ sharedStorageCheckResult.cause }}
</p> </p>
</step-item> </step-item>
<step-item <step-item v-show="currentSetupStep == 3" @next-clicked="nextStepAfterCheckBlenderExePath" @back-clicked="prevStep"
v-show="currentSetupStep == 3" :is-next-clickable="selectedBlender != null || customBlenderExe != (null || '')" title="Blender">
@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 currently using,
you can manually set up the other paths later. you can manually set up the other paths later.
</p> </p>
@ -119,23 +96,16 @@
<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 <input v-model="selectedBlender" :value="autoFoundBlenderPathEnvvar" id="blender-path_envvar" name="blender"
v-model="selectedBlender"
:value="autoFoundBlenderPathEnvvar"
id="blender-path_envvar"
name="blender"
type="radio"> 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 <span aria-label="Console output when running with --version" class="command-preview"
aria-label="Console output when running with --version" data-microtip-position="top" role="tooltip">
class="command-preview"
data-microtip-position="top"
role="tooltip">
{{ autoFoundBlenderPathEnvvar.cause }} {{ autoFoundBlenderPathEnvvar.cause }}
</span> </span>
</div> </div>
@ -143,23 +113,16 @@
<label v-if="autoFoundBlenderFileAssociation" for="blender-file_association"> <label v-if="autoFoundBlenderFileAssociation" for="blender-file_association">
<div> <div>
<input <input v-model="selectedBlender" :value="autoFoundBlenderFileAssociation" id="blender-file_association"
v-model="selectedBlender" name="blender" type="radio">
:value="autoFoundBlenderFileAssociation" {{ sourceLabels[autoFoundBlenderFileAssociation.source] }}
id="blender-file_association"
name="blender"
type="radio">
{{ 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 <span aria-label="Console output when running with --version" class="command-preview"
aria-label="Console output when running with --version" data-microtip-position="top" role="tooltip">
class="command-preview"
data-microtip-position="top"
role="tooltip">
{{ autoFoundBlenderFileAssociation.cause }} {{ autoFoundBlenderFileAssociation.cause }}
</span> </span>
</div> </div>
@ -167,24 +130,15 @@
<label for="blender-input_path"> <label for="blender-input_path">
<div> <div>
<input <input type="radio" v-model="selectedBlender" name="blender" :value="blenderFromInputPath"
type="radio" id="blender-input_path">
v-model="selectedBlender"
name="blender"
:value="blenderFromInputPath"
id="blender-input_path"
>
{{ sourceLabels['input_path'] }} {{ sourceLabels['input_path'] }}
</div> </div>
<div> <div>
<input <input v-model="customBlenderExe" @keyup.enter="nextStepAfterCheckBlenderExePath"
v-model="customBlenderExe"
@keyup.enter="nextStepAfterCheckBlenderExePath"
@focus="selectedBlender = null" @focus="selectedBlender = null"
:class="{'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable}" :class="{ 'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable }" type="text"
type="text" placeholder="Path to Blender">
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 v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
{{ blenderExeCheckResult.cause }}</p> {{ blenderExeCheckResult.cause }}</p>
@ -193,13 +147,9 @@
</fieldset> </fieldset>
<div v-if="autoFoundBlenders.length === 0"> <div v-if="autoFoundBlenders.length === 0">
<input <input v-model="customBlenderExe" @keyup.enter="nextStepAfterCheckBlenderExePath"
v-model="customBlenderExe" :class="{ 'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable }" type="text"
@keyup.enter="nextStepAfterCheckBlenderExePath" placeholder="Path to Blender executable">
: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 v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
@ -207,14 +157,8 @@
</div> </div>
</step-item> </step-item>
<step-item <step-item v-show="currentSetupStep == 4" @next-clicked="confirmSetupAssistant" @back-clicked="prevStep"
v-show="currentSetupStep == 4" next-label="Confirm" title="Review" :is-next-clickable="setupConfirmIsClickable">
@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>
@ -253,7 +197,7 @@ 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 { apiClient } from '@/stores/api-query-count'; import { getAPIClient } from "@/api-client";
export default { export default {
name: 'SetupAssistantView', name: 'SetupAssistantView',
@ -265,7 +209,7 @@ export default {
data: () => ({ data: () => ({
sharedStoragePath: "", sharedStoragePath: "",
sharedStorageCheckResult: null, // api.PathCheckResult sharedStorageCheckResult: null, // api.PathCheckResult
metaAPI: new MetaApi(apiClient), metaAPI: new MetaApi(getAPIClient()),
allBlenders: [], // combination of autoFoundBlenders and blenderExeCheckResult. allBlenders: [], // combination of autoFoundBlenders and blenderExeCheckResult.
@ -458,7 +402,6 @@ export default {
} }
</script> </script>
<style> <style>
.step-welcome ul { .step-welcome ul {
padding-left: var(--spacer-xl); padding-left: var(--spacer-xl);
margin-bottom: var(--spacer-xl); margin-bottom: var(--spacer-xl);
@ -517,7 +460,8 @@ export default {
} }
.progress-bar { .progress-bar {
--width-each-segment: calc(100% / calc(v-bind('totalSetupSteps') - 1)); /* Substract 1 because the first step has no progress. */ --width-each-segment: calc(100% / calc(v-bind('totalSetupSteps') - 1));
/* Substract 1 because the first step has no progress. */
--progress-bar-width-at-current-step: calc(var(--width-each-segment) * calc(v-bind('currentSetupStep') - 1)); --progress-bar-width-at-current-step: calc(var(--width-each-segment) * calc(v-bind('currentSetupStep') - 1));
position: absolute; position: absolute;

View File

@ -7,8 +7,7 @@
</div> </div>
<footer class="app-footer"> <footer class="app-footer">
<notification-bar /> <notification-bar />
<update-listener ref="updateListener" mainSubscription="allWorkers" <update-listener ref="updateListener" mainSubscription="allWorkers" @workerUpdate="onSIOWorkerUpdate"
@workerUpdate="onSIOWorkerUpdate"
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" /> @sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
</footer> </footer>
</template> </template>
@ -17,6 +16,7 @@
.col-workers-list { .col-workers-list {
grid-area: col-1; grid-area: col-1;
} }
.col-workers-2 { .col-workers-2 {
grid-area: col-2; grid-area: col-2;
} }
@ -26,7 +26,7 @@
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 { apiClient } from '@/stores/api-query-count'; 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'
@ -45,7 +45,7 @@ export default {
data: () => ({ data: () => ({
workers: useWorkers(), workers: useWorkers(),
notifs: useNotifs(), notifs: useNotifs(),
api: new WorkerMgtApi(apiClient), api: new WorkerMgtApi(getAPIClient()),
}), }),
mounted() { mounted() {
window.workersView = this; window.workersView = this;