flamenco/web/app/src/views/FirstTimeWizardView.vue
2022-07-22 14:57:11 +02:00

553 lines
17 KiB
Vue

<template>
<div class="setup-container">
<h1>Flamenco Setup Assistant</h1>
<div class="setup-step">
<ul class="progress">
<li
v-for="step in 4" :key="step"
@click="jumpToStep(step)"
:class="{
current: step == currentSetupStep,
done: step < currentSetupStep,
disabled: step > overallSetupStep,
}"
>
<span></span>
</li>
</ul>
<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="architecture.png" />
<p>The illustration shows the key components of Flamenco, and how they interact together. In particular:</p>
<ul>
<li><strong>Manager</strong>: This application. It coordinates all the activity.</li>
<li><strong>Worker</strong>: An 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 files, logs and internal previews can 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 flamenco.blender.org.</p>
</step-item>
<step-item
v-show="currentSetupStep == 2"
@next-clicked="nextStep"
@back-clicked="prevStep"
:is-next-clickable="sharedStorageCheckResult !=null && sharedStorageCheckResult.is_usable"
title="Shared Storage"
>
<p>Please specify a storage path (or drive), where you want to store your Flamenco data.
The location of the shared storage should be accessible by Flamenco Manager and by the Workers.
This could be:
</p>
<ul>
<li>A NAS in your network</li>
<li>A local drive or folder, if you are working alone</li>
<li>Some other file sharing server</li>
</ul>
<p>Using a service like Syncthing, ownCloud, or Dropbox for
this is not recommended, as Flamenco can't coordinate data synchronization.</p>
<input
v-model="sharedStoragePath"
@input="checkSharedStoragePath"
@keyup.enter="nextStepAfterStoragePath"
type="text"
placeholder="Shared Storage Path"
class="path-input"
>
<p v-if="sharedStorageCheckResult != null"
:class="{
'check-ok': sharedStorageCheckResult.is_usable,
'check-failed': !sharedStorageCheckResult.is_usable
}">
{{ sharedStorageCheckResult.cause }}
</p>
<p v-else></p>
</step-item>
<step-item
v-show="currentSetupStep == 3"
@next-clicked="nextStep"
@back-clicked="prevStep"
:is-next-clickable="selectedBlender != null && selectedBlender.is_usable"
title="Blender"
>
<div v-if="isBlenderExeFinding" class="is-in-progress">Looking for Blender installs...</div>
<fieldset v-if="allBlenders.length > 1">
<legend>Choose which Blender to use:</legend>
<div v-for="(blender, index) in allBlenders">
<label :for="'blender-'+index">
<input type="radio" v-model="selectedBlender" name="blender" :value="blender.path" :id="'blender-'+index">
{{ blender.cause }}
<span
:aria-label="blender.path"
data-microtip-position="top"
role="tooltip">
[Path]
</span>
<span
:aria-label="sourceLabels[blender.source]"
data-microtip-position="top"
role="tooltip">
[Source]
</span>
</label>
</div>
</fieldset>
<p v-if="allBlenders.length <= 1">
Provide a path to Blender. This path should be accessible by all Workers. If your rendering
setup features operating systems different form the one you are currently using, you can
manually set up the other paths later.
</p>
<p v-else>Or provide an alternative command to try.</p>
<input
@input="checkBlenderExePath"
v-model="customBlenderExe"
type="text"
placeholder="Blender Path"
class="path-input"
>
<div v-if="isBlenderExeChecking" class="is-in-progress">Checking...</div>
<p v-if="blenderExeCheckResult != null && blenderExeCheckResult.is_usable" class="check-ok">
Found something, it is selected above.</p>
<p v-if="blenderExeCheckResult != null && !blenderExeCheckResult.is_usable" class="check-failed">
{{ blenderExeCheckResult.cause }}</p>
</step-item>
<step-item
v-show="currentSetupStep == 4"
@next-clicked="confirmWizard"
@back-clicked="prevStep"
next-label="Confirm"
title="Review"
:is-next-clickable="isConfigComplete"
>
<div v-if="isConfigComplete">
<p>This is the configuration that will be used by Flamenco:</p>
<dl>
<dt>Storage</dt>
<dd>{{ sharedStorageCheckResult.path }}</dd>
<dt>Blender</dt>
<dd v-if="selectedBlender.source == 'file_association'">
Whatever Blender is associated with .blend files
(currently "<code>{{ selectedBlender.path }}</code>")
</dd>
<dd v-if="selectedBlender.source == 'path_envvar'">
The command "<code>{{ selectedBlender.input }}</code>" as found on <code>$PATH</code>
(currently "<code>{{ selectedBlender.path }}</code>")
</dd>
<dd v-if="selectedBlender.source == 'input_path'">
The command you provided:
"<code>{{ selectedBlender.path }}</code>"
</dd>
</dl>
</div>
<p v-if="isConfirmed" class="check-ok">Configuration has been saved, Flamenco will restart.</p>
</step-item>
</div>
</div>
<footer class="app-footer">
<notification-bar />
</footer>
<update-listener ref="updateListener" @sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
</template>
<script>
import microtip from 'microtip/microtip.css'
import NotificationBar from '@/components/footer/NotificationBar.vue'
import UpdateListener from '@/components/UpdateListener.vue'
import StepItem from '@/components/steps/StepItem.vue';
import { MetaApi, PathCheckInput, WizardConfig } from "@/manager-api";
import { apiClient } from '@/stores/api-query-count';
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
export default {
name: 'FirstTimeWizardView',
components: {
NotificationBar,
UpdateListener,
StepItem,
},
data: () => ({
sharedStoragePath: "",
sharedStorageCheckResult: null, // api.PathCheckResult
metaAPI: new MetaApi(apiClient),
allBlenders: [], // combination of autoFoundBlenders and blenderExeCheckResult.
autoFoundBlenders: [], // list of api.BlenderPathCheckResult
isBlenderExeFinding: false,
selectedBlender: null, // the chosen api.BlenderPathCheckResult
customBlenderExe: "",
isBlenderExeChecking: false,
blenderExeCheckResult: null, // api.BlenderPathCheckResult
sourceLabels: {
file_association: "This Blender runs when you double-click a .blend file.",
path_envvar: "This Blender was found on the $PATH environment.",
input_path: "You pointed Flamenco to this executable.",
},
isConfirming: false,
isConfirmed: false,
currentSetupStep: 1,
overallSetupStep: 1,
}),
computed: {
cleanSharedStoragePath() {
return this.sharedStoragePath.trim();
},
cleanCustomBlenderExe() {
return this.customBlenderExe.trim();
},
isSharedStorageValid() {
return this.sharedStorageCheckResult != null && this.sharedStorageCheckResult.is_usable;
},
isSelectedBlenderValid() {
return this.selectedBlender != null && this.selectedBlender.is_usable;
},
isConfigComplete() {
return this.isSharedStorageValid && this.isSelectedBlenderValid;
},
},
mounted() {
this.findBlenderExePath();
document.body.classList.add('is-first-time-wizard');
},
methods: {
// SocketIO connection event handlers:
onSIOReconnected() {
},
onSIODisconnected(reason) {
},
checkSharedStoragePath() {
const pathCheck = new PathCheckInput(this.cleanSharedStoragePath);
console.log("requesting path check:", pathCheck);
this.metaAPI.checkSharedStoragePath({ pathCheckInput: pathCheck })
.then((result) => {
console.log("Storage path check result:", result);
this.sharedStorageCheckResult = result;
})
.catch((error) => {
console.log("Error checking storage path:", error);
})
},
findBlenderExePath() {
this.isBlenderExeFinding = true;
this.autoFoundBlenders = [];
console.log("Finding Blender");
this.metaAPI.findBlenderExePath()
.then((result) => {
console.log("Result of finding Blender:", result);
this.autoFoundBlenders = result;
this._refreshAllBlenders();
})
.catch((error) => {
console.log("Error finding Blender:", error);
})
.finally(() => {
this.isBlenderExeFinding = false;
})
},
checkBlenderExePath() {
const exeToTry = this.cleanCustomBlenderExe;
if (exeToTry == "") {
// Just erase any previously-found custom Blender executable.
this.isBlenderExeChecking = false;
this.blenderExeCheckResult = null;
this._refreshAllBlenders();
return;
}
this.isBlenderExeChecking = true;
this.blenderExeCheckResult = null;
const pathCheck = new PathCheckInput(exeToTry);
console.log("requesting path check:", pathCheck);
this.metaAPI.checkBlenderExePath({ pathCheckInput: pathCheck })
.then((result) => {
console.log("Blender exe path check result:", result);
this.blenderExeCheckResult = result;
if (result.is_usable) {
this.selectedBlender = result;
}
this._refreshAllBlenders();
})
.catch((error) => {
console.log("Error checking storage path:", error);
})
.finally(() => {
this.isBlenderExeChecking = false;
})
},
_refreshAllBlenders() {
if (this.blenderExeCheckResult == null || !this.blenderExeCheckResult.is_usable) {
this.allBlenders = this.autoFoundBlenders;
} else {
this.allBlenders = this.autoFoundBlenders.concat([this.blenderExeCheckResult]);
}
},
nextStepAfterStoragePath() {
if (this.isSharedStorageValid) {
this.nextStep();
}
},
nextStep() {
if (this.overallSetupStep <= this.currentSetupStep) {
this.overallSetupStep = this.currentSetupStep + 1;
}
this.currentSetupStep++;
},
prevStep() {
this.currentSetupStep--;
},
jumpToStep(step) {
if (step <= this.overallSetupStep) {
this.currentSetupStep = step;
}
},
confirmWizard() {
const wizardConfig = new WizardConfig(
this.sharedStorageCheckResult.path,
this.selectedBlender,
);
console.log("saving configuration:", wizardConfig);
this.isConfirming = true;
this.isConfirmed = false;
this.metaAPI.saveWizardConfig({ wizardConfig: wizardConfig })
.then((result) => {
console.log("Wizard config saved, reload the page");
this.isConfirmed = true;
// Give the Manager some time to restart.
window.setTimeout(() => { window.location.reload() }, 2000);
})
.catch((error) => {
console.log("Error saving wizard config:", error);
// Only clear this flag on an error.
this.isConfirming = false;
})
},
},
created() {
this.checkSharedStoragePath = debounce(this.checkSharedStoragePath, 200)
this.checkBlenderExePath = debounce(this.checkBlenderExePath, 200)
}
}
</script>
<style>
.progress {
--wiz-progress-indicator-size: 8px;
--wiz-progress-indicator-border-width: 2px;
--wiz-progress-indicator-color: var(--color-text-hint);
--wiz-progress-indicator-color-current: var(--color-accent);
display: flex;
justify-content: space-between;
list-style: none;
margin-bottom: 2rem;
padding: 0;
position: relative;
}
.progress li {
cursor: pointer;
}
/* Progress indicator dot. */
.progress li span {
background-color: var(--color-background-column);
border-radius: 50%;
border: var(--wiz-progress-indicator-border-width) solid var(--color-background-column);
box-shadow: 0 0 0 var(--wiz-progress-indicator-border-width) var(--wiz-progress-indicator-color);
content: '';
cursor: pointer;
display: block;
height: var(--wiz-progress-indicator-size);
position: relative;
width: var(--wiz-progress-indicator-size);
}
.progress li.disabled span {
cursor: not-allowed;
}
.progress li.done span {
background-color: var(--wiz-progress-indicator-color-current);
box-shadow: 0 0 0 var(--wiz-progress-indicator-border-width) var(--wiz-progress-indicator-color-current);
}
.progress li.current span {
background-color: var(--color-background-column);
box-shadow: 0 0 0 var(--wiz-progress-indicator-border-width) var(--wiz-progress-indicator-color-current);
}
.progress li.current span {
box-shadow: 0 0 0 var(--wiz-progress-indicator-border-width) var(--wiz-progress-indicator-color-current);
}
/* Progress indicator line between dots. */
.progress:before {
background-color: var(--wiz-progress-indicator-color);
content: '';
display: block;
height: var(--wiz-progress-indicator-border-width);
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
}
.setup-step {
background-color: var(--color-background-column);
border-radius: var(--border-radius);
padding: var(--spacer) var(--spacer-lg);
}
body.is-first-time-wizard #app {
grid-template-areas:
"header"
"col-full-width"
"footer";
grid-template-columns: 1fr;
}
@media (max-width: 1280px) {
body.is-first-time-wizard #app {
grid-template-areas:
"header"
"col-full-width"
"footer";
grid-template-columns: 1fr;
grid-template-rows: var(--header-height) 1fr var(--footer-height);
}
}
.btn-bar-wide .btn:last-child {
margin-left: auto;
}
.setup-container {
--color-check-failed: var(--color-status-failed);
--color-check-ok: var(--color-status-completed);
max-width: 640px;
margin: 10vh auto auto;
width: 100%;
}
.setup-container h1 {
font-size: xx-large;
text-align: center;
}
.setup-container section {
font-size: larger;
}
.setup-container img {
max-width: 100%;
border-radius: var(--border-radius);
}
.setup-container p.hint {
color: var(--color-text-hint);
font-size: smaller;
}
.setup-container .check-ok {
color: var(--color-check-ok);
}
.setup-container .check-failed {
color: var(--color-check-failed);
}
.setup-container .check-ok::before {
content: "✔ ";
}
.setup-container .check-failed::before {
content: "❌ ";
}
.setup-container .blender-selector {
padding: 0.5em;
outline: thin solid var(--color-border);
border-radius: var(--border-radius);
}
.setup-container .blender-selector.selected-blender {
color: var(--color-accent-text);
background-color: var(--color-accent-background);
outline-width: var(--border-width);
}
.setup-container .blender-selector button {
display: block;
margin-left: auto;
}
.is-in-progress {
animation: is-in-progress 3s infinite linear;
background-image: linear-gradient(to left, var(--color-text-muted), rgba(255, 255, 255, 0.25), var(--color-text-muted));
background-size: 200px;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
@keyframes is-in-progress {
0% {
background-position: 0px;
}
100% {
background-position: 200px;
}
}
</style>