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

View File

@ -17,7 +17,7 @@ const DEFAULT_FLAMENCO_NAME = "Flamenco";
const DEFAULT_FLAMENCO_VERSION = "unknown";
import ApiSpinner from '@/components/ApiSpinner.vue'
import { MetaApi } from "@/manager-api";
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
export default {
name: 'SetupAssistant',
@ -35,7 +35,7 @@ export default {
methods: {
// TODO: also call this when SocketIO reconnects.
fetchManagerInfo() {
const metaAPI = new MetaApi(apiClient);
const metaAPI = new MetaApi(getAPIClient());
metaAPI.getVersion().then((version) => {
this.flamencoName = version.name;
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 { useNotifs } from '@/stores/notifications';
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { JobsApi, JobPriorityChange } from '@/manager-api';
const props = defineProps({
@ -45,7 +45,7 @@ const errorMessage = ref('');
// Methods
function updateJobPriority() {
const jobPriorityChange = new JobPriorityChange(priorityState.value);
const jobsAPI = new JobsApi(apiClient);
const jobsAPI = new JobsApi(getAPIClient());
return jobsAPI.setJobPriority(props.jobId, jobPriorityChange)
.then(() => {
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 { useTaskLog } from '@/stores/tasklog'
import { useTasks } from '@/stores/tasks'
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { JobsApi } from '@/manager-api';
const taskLog = useTaskLog();
@ -59,7 +59,7 @@ function _fetchLogTail(taskID) {
if (!taskID) return;
const jobsAPI = new JobsApi(apiClient);
const jobsAPI = new JobsApi(getAPIClient());
return jobsAPI.fetchTaskLogTail(taskID)
.then((logTail) => {
taskLog.addChunk(logTail);

View File

@ -26,7 +26,7 @@
</template>
<script setup>
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { JobsApi } from '@/manager-api';
import LinkWorker from '@/components/LinkWorker.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 emit = defineEmits(['reshuffled'])
const jobsApi = new JobsApi(apiClient);
const jobsApi = new JobsApi(getAPIClient());
const isVisible = inject("isVisible");
const isFetching = ref(false);
const errorMsg = ref("");

View File

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

View File

@ -39,7 +39,7 @@
<dd class="field-status-label" :class="'status-' + jobData.status">{{ jobData.status }}</dd>
<dt class="field-type" title="Type">Type</dt>
<dd>{{ jobType? jobType.label : jobData.type }}</dd>
<dd>{{ jobType ? jobType.label : jobData.type }}</dd>
<dt class="field-priority" title="Priority">Priority</dt>
<dd>
@ -76,7 +76,7 @@
<script>
import * as datetime from "@/datetime";
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 Blocklist from './Blocklist.vue'
import TabItem from '@/components/TabItem.vue'
@ -103,7 +103,7 @@ export default {
datetime: datetime, // So that the template can access it.
copyElementText: copyElementText,
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.
jobTypeSettings: null, // Mapping from setting key to its definition in the job type.
showAllSettings: false,

View File

@ -17,7 +17,7 @@ import { TabulatorFull as Tabulator } from 'tabulator-tables';
import * as datetime from "@/datetime";
import * as API from '@/manager-api'
import { indicator } from '@/statusindicator';
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { useJobs } from '@/stores/jobs';
import JobActionsBar from '@/components/jobs/JobActionsBar.vue'
@ -130,7 +130,7 @@ export default {
this.fetchAllJobs();
},
fetchAllJobs() {
const jobsApi = new API.JobsApi(apiClient);
const jobsApi = new API.JobsApi(getAPIClient());
const jobsQuery = {};
this.jobs.isJobless = false;
jobsApi.queryJobs(jobsQuery).then(this.onJobsFetched, function (error) {

View File

@ -9,7 +9,7 @@
import { reactive, ref, watch } from 'vue'
import { api } from '@/urls';
import { JobsApi, JobLastRenderedImageInfo, SocketIOLastRenderedUpdate } from '@/manager-api';
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
const props = defineProps([
/* 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,
})
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.

View File

@ -73,7 +73,7 @@
import * as datetime from "@/datetime";
import { JobsApi } from '@/manager-api';
import { backendURL } from '@/urls';
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { useNotifs } from "@/stores/notifications";
import LinkWorker from '@/components/LinkWorker.vue';
import { copyElementText } from '@/clipboard';
@ -90,7 +90,7 @@ export default {
return {
datetime: datetime, // So that the template can access it.
copyElementText: copyElementText,
jobsApi: new JobsApi(apiClient),
jobsApi: new JobsApi(getAPIClient()),
notifs: useNotifs(),
};
},

View File

@ -17,7 +17,7 @@ import { TabulatorFull as Tabulator } from 'tabulator-tables';
import * as datetime from "@/datetime";
import * as API from '@/manager-api'
import { indicator } from '@/statusindicator';
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { useTasks } from '@/stores/tasks';
import TaskActionsBar from '@/components/jobs/TaskActionsBar.vue'
@ -132,7 +132,7 @@ export default {
return;
}
const jobsApi = new API.JobsApi(apiClient);
const jobsApi = new API.JobsApi(getAPIClient());
jobsApi.fetchJobTasks(this.jobID)
.then(this.onTasksFetched, function (error) {
// TODO: error handling.

View File

@ -6,11 +6,7 @@
</option>
<option v-for="(action, key) in WORKER_ACTIONS" :value="key">{{ action.label }}</option>
</select>
<button
:disabled="!canPerformAction"
class="btn"
@click.prevent="performWorkerAction"
>Apply</button>
<button :disabled="!canPerformAction" class="btn" @click.prevent="performWorkerAction">Apply</button>
</template>
<script setup>
@ -18,46 +14,46 @@ import { computed, ref } from 'vue'
import { useWorkers } from '@/stores/workers';
import { useNotifs } from '@/stores/notifications';
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.
* We don't need it to be tracked, as it won't be changed anyway. */
const WORKER_ACTIONS = Object.freeze({
offline_lazy: {
label: 'Shut Down (after task is finished)',
icon: '✝',
title: 'Shut down the worker after the current task finishes. The worker may automatically restart.',
target_status: 'offline',
lazy: true,
},
offline_immediate: {
label: 'Shut Down (immediately)',
icon: '✝!',
title: 'Immediately shut down the worker. It may automatically restart.',
target_status: 'offline',
lazy: false,
},
asleep_lazy: {
label: 'Send to Sleep (after task is finished)',
icon: '😴',
title: 'Let the worker sleep after finishing this task.',
target_status: 'asleep',
lazy: true,
},
asleep_immediate: {
label: 'Send to Sleep (immediately)',
icon: '😴!',
title: 'Let the worker sleep immediately.',
target_status: 'asleep',
lazy: false,
},
wakeup: {
label: 'Wake Up',
icon: '😃',
title: 'Wake the worker up. A sleeping worker can take a minute to respond.',
target_status: 'awake',
lazy: false,
},
offline_lazy: {
label: 'Shut Down (after task is finished)',
icon: '✝',
title: 'Shut down the worker after the current task finishes. The worker may automatically restart.',
target_status: 'offline',
lazy: true,
},
offline_immediate: {
label: 'Shut Down (immediately)',
icon: '✝!',
title: 'Immediately shut down the worker. It may automatically restart.',
target_status: 'offline',
lazy: false,
},
asleep_lazy: {
label: 'Send to Sleep (after task is finished)',
icon: '😴',
title: 'Let the worker sleep after finishing this task.',
target_status: 'asleep',
lazy: true,
},
asleep_immediate: {
label: 'Send to Sleep (immediately)',
icon: '😴!',
title: 'Let the worker sleep immediately.',
target_status: 'asleep',
lazy: false,
},
wakeup: {
label: 'Wake Up',
icon: '😃',
title: 'Wake the worker up. A sleeping worker can take a minute to respond.',
target_status: 'awake',
lazy: false,
},
});
const selectedAction = ref('');
@ -73,7 +69,7 @@ function performWorkerAction() {
return;
}
const api = new WorkerMgtApi(apiClient);
const api = new WorkerMgtApi(getAPIClient());
const action = WORKER_ACTIONS[selectedAction.value];
const statuschange = new WorkerStatusChangeRequest(action.target_status, action.lazy);
console.log("Requesting worker status change", statuschange);

View File

@ -123,7 +123,7 @@ import { useNotifs } from '@/stores/notifications'
import * as datetime from "@/datetime";
import { WorkerMgtApi, WorkerSleepSchedule } from '@/manager-api';
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { workerStatus } from "../../statusindicator";
import LinkWorkerTask from '@/components/LinkWorkerTask.vue';
import SwitchCheckbox from '@/components/SwitchCheckbox.vue';
@ -140,7 +140,7 @@ export default {
data() {
return {
datetime: datetime, // So that the template can access it.
api: new WorkerMgtApi(apiClient),
api: new WorkerMgtApi(getAPIClient()),
workerStatusHTML: "",
workerSleepSchedule: this.defaultWorkerSleepSchedule(),
isScheduleEditing: false,

View File

@ -19,7 +19,7 @@
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import { WorkerMgtApi } from '@/manager-api'
import { indicator, workerStatus } from '@/statusindicator';
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
import { useWorkers } from '@/stores/workers';
import StatusFilterBar from '@/components/StatusFilterBar.vue'
@ -117,7 +117,7 @@ export default {
this.fetchAllWorkers();
},
fetchAllWorkers() {
const api = new WorkerMgtApi(apiClient);
const api = new WorkerMgtApi(getAPIClient());
api.fetchWorkers().then(this.onWorkersFetched, function (error) {
// TODO: error handling.
console.error(error);

View File

@ -7,7 +7,8 @@ import SetupAssistant from '@/SetupAssistant.vue'
import autoreload from '@/autoreloader'
import router from '@/router/index'
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'
// 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
* 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. */
const apiClient = new ApiClient(urls.api());;
const apiClient = newBareAPIClient();
const metaAPI = new MetaApi(apiClient);
metaAPI.getConfiguration()
.then((config) => {

View File

@ -1,6 +1,5 @@
import { defineStore } from "pinia";
import { ApiClient } from "@/manager-api";
import * as urls from '@/urls'
/**
* 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 * 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.
// See https://pinia.vuejs.org/core-concepts/

View File

@ -1,10 +1,10 @@
import { defineStore } from 'pinia'
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.
// See https://pinia.vuejs.org/core-concepts/

View File

@ -39,7 +39,7 @@ import { useJobs } from '@/stores/jobs';
import { useTasks } from '@/stores/tasks';
import { useNotifs } from '@/stores/notifications'
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 GetTheAddon from '@/components/GetTheAddon.vue'
@ -127,7 +127,7 @@ export default {
return;
}
const jobsAPI = new API.JobsApi(apiClient);
const jobsAPI = new API.JobsApi(getAPIClient());
jobsAPI.fetchTask(taskSummary.id)
.then((task) => {
this.tasks.setActiveTask(task);
@ -225,7 +225,7 @@ export default {
return;
}
const jobsAPI = new API.JobsApi(apiClient);
const jobsAPI = new API.JobsApi(getAPIClient());
return jobsAPI.fetchJob(jobID)
.then((job) => {
this.jobs.setActiveJob(job);
@ -254,7 +254,7 @@ export default {
return;
}
const jobsAPI = new API.JobsApi(apiClient);
const jobsAPI = new API.JobsApi(getAPIClient());
return jobsAPI.fetchTask(taskID)
.then((task) => {
this.tasks.setActiveTask(task);

View File

@ -2,38 +2,28 @@
<div class="setup-container">
<h1>Flamenco Setup Assistant</h1>
<ul class="progress">
<li
v-for="step in totalSetupSteps" :key="step"
@click="jumpToStep(step)"
:class="{
current: step == currentSetupStep,
done: step < overallSetupStep,
done_previously: (step < overallSetupStep && currentSetupStep > step),
done_and_current: step == currentSetupStep && (step < overallSetupStep || step == 1),
disabled: step > overallSetupStep,
}"
>
<li v-for="step in totalSetupSteps" :key="step" @click="jumpToStep(step)" :class="{
current: step == currentSetupStep,
done: step < overallSetupStep,
done_previously: (step < overallSetupStep && currentSetupStep > step),
done_and_current: step == currentSetupStep && (step < overallSetupStep || step == 1),
disabled: step > overallSetupStep,
}">
<span></span>
</li>
<div class="progress-bar"></div>
</ul>
<div class="setup-step step-welcome">
<step-item
v-show="currentSetupStep == 1"
@next-clicked="nextStep"
:is-next-clickable="true"
:is-back-visible="false"
title="Welcome!"
next-label="Let's go"
>
<step-item v-show="currentSetupStep == 1" @next-clicked="nextStep" :is-next-clickable="true"
:is-back-visible="false" title="Welcome!" next-label="Let's go">
<p>
This setup assistant will guide you through the initial configuration of Flamenco. You will be up
and running in a few minutes!
</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>
<ul>
@ -41,26 +31,24 @@
<strong>Manager</strong>: This application. It coordinates all the activity.
</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>
<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>
<strong>Blender Add-on</strong>: This is needed to connect to the Manager and submit a job from Blender.
</li>
</ul>
<p>More information is available on the online documentation at
<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
v-show="currentSetupStep == 2"
@next-clicked="nextStepAfterCheckSharedStoragePath"
@back-clicked="prevStep"
:is-next-clickable="sharedStoragePath.length > 0"
title="Shared Storage"
>
<step-item v-show="currentSetupStep == 2" @next-clicked="nextStepAfterCheckSharedStoragePath"
@back-clicked="prevStep" :is-next-clickable="sharedStoragePath.length > 0" title="Shared Storage">
<p>Specify a path (or drive) where you want to store your Flamenco data.</p>
<p>
The location of the shared storage should be accessible by Flamenco Manager and by the Workers.
@ -79,37 +67,26 @@
<a href="https://flamenco.blender.org/usage/shared-storage/">Learn more</a>.
</p>
<input
v-model="sharedStoragePath"
@keyup.enter="nextStepAfterCheckSharedStoragePath"
type="text"
placeholder="Path to shared storage"
:class="{
<input v-model="sharedStoragePath" @keyup.enter="nextStepAfterCheckSharedStoragePath" type="text"
placeholder="Path to shared storage" :class="{
'is-invalid': (sharedStorageCheckResult != null) && !sharedStorageCheckResult.is_usable
}"
>
<p v-if="sharedStorageCheckResult != null"
:class="{
'check-ok': sharedStorageCheckResult.is_usable,
'check-failed': !sharedStorageCheckResult.is_usable
}">
<p v-if="sharedStorageCheckResult != null" :class="{
'check-ok': sharedStorageCheckResult.is_usable,
'check-failed': !sharedStorageCheckResult.is_usable
}">
{{ sharedStorageCheckResult.cause }}
</p>
</step-item>
<step-item
v-show="currentSetupStep == 3"
@next-clicked="nextStepAfterCheckBlenderExePath"
@back-clicked="prevStep"
:is-next-clickable="selectedBlender != null || customBlenderExe != (null || '')"
title="Blender"
>
<step-item v-show="currentSetupStep == 3" @next-clicked="nextStepAfterCheckBlenderExePath" @back-clicked="prevStep"
:is-next-clickable="selectedBlender != null || customBlenderExe != (null || '')" title="Blender">
<div v-if="isBlenderExeFinding" class="is-in-progress">Looking for Blender installs...</div>
<p v-if="autoFoundBlenders.length === 0">
Provide a path to a Blender executable accessible by all Workers.
<br/><br/>
<br /><br />
If your rendering setup features operating systems different from the one you are currently using,
you can manually set up the other paths later.
</p>
@ -119,23 +96,16 @@
<fieldset v-if="autoFoundBlenders.length >= 1">
<label v-if="autoFoundBlenderPathEnvvar" for="blender-path_envvar">
<div>
<input
v-model="selectedBlender"
:value="autoFoundBlenderPathEnvvar"
id="blender-path_envvar"
name="blender"
<input v-model="selectedBlender" :value="autoFoundBlenderPathEnvvar" id="blender-path_envvar" name="blender"
type="radio">
{{ sourceLabels[autoFoundBlenderPathEnvvar.source] }}
{{ sourceLabels[autoFoundBlenderPathEnvvar.source] }}
</div>
<div class="setup-path-command">
<span class="path">
{{autoFoundBlenderPathEnvvar.path}}
{{ autoFoundBlenderPathEnvvar.path }}
</span>
<span
aria-label="Console output when running with --version"
class="command-preview"
data-microtip-position="top"
role="tooltip">
<span aria-label="Console output when running with --version" class="command-preview"
data-microtip-position="top" role="tooltip">
{{ autoFoundBlenderPathEnvvar.cause }}
</span>
</div>
@ -143,23 +113,16 @@
<label v-if="autoFoundBlenderFileAssociation" for="blender-file_association">
<div>
<input
v-model="selectedBlender"
:value="autoFoundBlenderFileAssociation"
id="blender-file_association"
name="blender"
type="radio">
{{ sourceLabels[autoFoundBlenderFileAssociation.source] }}
<input v-model="selectedBlender" :value="autoFoundBlenderFileAssociation" id="blender-file_association"
name="blender" type="radio">
{{ sourceLabels[autoFoundBlenderFileAssociation.source] }}
</div>
<div class="setup-path-command">
<span class="path">
{{autoFoundBlenderFileAssociation.path}}
{{ autoFoundBlenderFileAssociation.path }}
</span>
<span
aria-label="Console output when running with --version"
class="command-preview"
data-microtip-position="top"
role="tooltip">
<span aria-label="Console output when running with --version" class="command-preview"
data-microtip-position="top" role="tooltip">
{{ autoFoundBlenderFileAssociation.cause }}
</span>
</div>
@ -167,24 +130,15 @@
<label for="blender-input_path">
<div>
<input
type="radio"
v-model="selectedBlender"
name="blender"
:value="blenderFromInputPath"
id="blender-input_path"
>
<input type="radio" v-model="selectedBlender" name="blender" :value="blenderFromInputPath"
id="blender-input_path">
{{ sourceLabels['input_path'] }}
</div>
<div>
<input
v-model="customBlenderExe"
@keyup.enter="nextStepAfterCheckBlenderExePath"
<input v-model="customBlenderExe" @keyup.enter="nextStepAfterCheckBlenderExePath"
@focus="selectedBlender = null"
:class="{'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable}"
type="text"
placeholder="Path to Blender"
>
:class="{ 'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable }" type="text"
placeholder="Path to Blender">
<p v-if="isBlenderExeChecking" class="is-in-progress">Checking...</p>
<p v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
{{ blenderExeCheckResult.cause }}</p>
@ -193,13 +147,9 @@
</fieldset>
<div v-if="autoFoundBlenders.length === 0">
<input
v-model="customBlenderExe"
@keyup.enter="nextStepAfterCheckBlenderExePath"
:class="{'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable}"
type="text"
placeholder="Path to Blender executable"
>
<input v-model="customBlenderExe" @keyup.enter="nextStepAfterCheckBlenderExePath"
:class="{ 'is-invalid': blenderExeCheckResult != null && !blenderExeCheckResult.is_usable }" type="text"
placeholder="Path to Blender executable">
<p v-if="isBlenderExeChecking" class="is-in-progress">Checking...</p>
<p v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
@ -207,14 +157,8 @@
</div>
</step-item>
<step-item
v-show="currentSetupStep == 4"
@next-clicked="confirmSetupAssistant"
@back-clicked="prevStep"
next-label="Confirm"
title="Review"
:is-next-clickable="setupConfirmIsClickable"
>
<step-item v-show="currentSetupStep == 4" @next-clicked="confirmSetupAssistant" @back-clicked="prevStep"
next-label="Confirm" title="Review" :is-next-clickable="setupConfirmIsClickable">
<div v-if="isConfigComplete">
<p>This is the configuration that will be used by Flamenco:</p>
<dl>
@ -253,7 +197,7 @@ import NotificationBar from '@/components/footer/NotificationBar.vue'
import UpdateListener from '@/components/UpdateListener.vue'
import StepItem from '@/components/steps/StepItem.vue';
import { MetaApi, PathCheckInput, SetupAssistantConfig } from "@/manager-api";
import { apiClient } from '@/stores/api-query-count';
import { getAPIClient } from "@/api-client";
export default {
name: 'SetupAssistantView',
@ -265,7 +209,7 @@ export default {
data: () => ({
sharedStoragePath: "",
sharedStorageCheckResult: null, // api.PathCheckResult
metaAPI: new MetaApi(apiClient),
metaAPI: new MetaApi(getAPIClient()),
allBlenders: [], // combination of autoFoundBlenders and blenderExeCheckResult.
@ -458,7 +402,6 @@ export default {
}
</script>
<style>
.step-welcome ul {
padding-left: var(--spacer-xl);
margin-bottom: var(--spacer-xl);
@ -517,7 +460,8 @@ export default {
}
.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));
position: absolute;

View File

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