Web: introduce VueRouter and split up into more components
Most of the code moved from `App.vue` to `views/JobsView.vue`. Notification bar has its own component, and there are placeholder "views" for Workers and Settings pages. There is still some clunky handling of updates via SocketIO, as those are a mix of job-specific and global (like SocketIO reconnection events). The advantage of the current approach is that SocketIO connections are closed when you leave the Jobs page, and reopened when you enter the Workers page. My gut feeling says this is nice because it ensures that all SocketIO connection-specific things are cleaned up when you navigate.
This commit is contained in:
parent
d673da7a0c
commit
af39414a06
@ -3,9 +3,15 @@
|
|||||||
<a href="/" class="navbar-brand">{{ flamencoName }}</a>
|
<a href="/" class="navbar-brand">{{ flamencoName }}</a>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Jobs</a></li>
|
<li>
|
||||||
<li><a href="/">Workers</a></li>
|
<router-link to="/">Jobs</router-link>
|
||||||
<li><a href="/">Settings</a></li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link to="/workers">Workers</router-link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link to="/settings">Settings</router-link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<api-spinner />
|
<api-spinner />
|
||||||
@ -13,39 +19,14 @@
|
|||||||
version: {{ flamencoVersion }}
|
version: {{ flamencoVersion }}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<div class="col col-1">
|
<router-view></router-view>
|
||||||
<jobs-table ref="jobsTable" @selectedJobChange="onSelectedJobChanged" />
|
|
||||||
</div>
|
|
||||||
<div class="col col-2">
|
|
||||||
<job-details :jobData="jobs.activeJob" />
|
|
||||||
<tasks-table v-if="jobs.activeJobID" ref="tasksTable" :jobID="jobs.activeJobID"
|
|
||||||
@selectedTaskChange="onSelectedTaskChanged" />
|
|
||||||
</div>
|
|
||||||
<div class="col col-3">
|
|
||||||
<task-details :taskData="tasks.activeTask" />
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<span class='notifications' v-if="notifs.last">{{ notifs.last.msg }}</span>
|
|
||||||
<update-listener ref="updateListener" :websocketURL="websocketURL" :subscribedJob="jobs.activeJobID"
|
|
||||||
@jobUpdate="onSioJobUpdate" @taskUpdate="onSioTaskUpdate" @message="onChatMessage"
|
|
||||||
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
|
||||||
</footer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as urls from '@/urls'
|
|
||||||
import * as API from '@/manager-api';
|
import * as API from '@/manager-api';
|
||||||
import { useJobs } from '@/stores/jobs';
|
|
||||||
import { useTasks } from '@/stores/tasks';
|
|
||||||
import { useNotifs } from '@/stores/notifications';
|
|
||||||
import { apiClient } from '@/stores/api-query-count';
|
import { apiClient } from '@/stores/api-query-count';
|
||||||
|
|
||||||
import ApiSpinner from '@/components/ApiSpinner.vue'
|
import ApiSpinner from '@/components/ApiSpinner.vue'
|
||||||
import JobsTable from '@/components/JobsTable.vue'
|
|
||||||
import JobDetails from '@/components/JobDetails.vue'
|
|
||||||
import TaskDetails from '@/components/TaskDetails.vue'
|
|
||||||
import TasksTable from '@/components/TasksTable.vue'
|
|
||||||
import UpdateListener from '@/components/UpdateListener.vue'
|
|
||||||
|
|
||||||
const DEFAULT_FLAMENCO_NAME = "Flamenco";
|
const DEFAULT_FLAMENCO_NAME = "Flamenco";
|
||||||
const DEFAULT_FLAMENCO_VERSION = "unknown";
|
const DEFAULT_FLAMENCO_VERSION = "unknown";
|
||||||
@ -53,16 +34,9 @@ const DEFAULT_FLAMENCO_VERSION = "unknown";
|
|||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
ApiSpinner, JobsTable, JobDetails, TaskDetails, TasksTable, UpdateListener,
|
ApiSpinner,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
websocketURL: urls.ws(),
|
|
||||||
messages: [],
|
|
||||||
|
|
||||||
jobs: useJobs(),
|
|
||||||
tasks: useTasks(),
|
|
||||||
notifs: useNotifs(),
|
|
||||||
|
|
||||||
flamencoName: DEFAULT_FLAMENCO_NAME,
|
flamencoName: DEFAULT_FLAMENCO_NAME,
|
||||||
flamencoVersion: DEFAULT_FLAMENCO_VERSION,
|
flamencoVersion: DEFAULT_FLAMENCO_VERSION,
|
||||||
}),
|
}),
|
||||||
@ -71,99 +45,7 @@ export default {
|
|||||||
this.fetchManagerInfo();
|
this.fetchManagerInfo();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// onSelectedJobChanged is called whenever the selected job changes; this is
|
// TODO: also call this when SocketIO reconnects.
|
||||||
// both when another job is selected and when the selected job itself gets
|
|
||||||
// updated.
|
|
||||||
onSelectedJobChanged(jobSummary) {
|
|
||||||
if (!jobSummary) { // There is no selected job.
|
|
||||||
this.jobs.deselectAllJobs();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobsAPI = new API.JobsApi(apiClient);
|
|
||||||
jobsAPI.fetchJob(jobSummary.id)
|
|
||||||
.then((job) => {
|
|
||||||
this.jobs.setSelectedJob(job);
|
|
||||||
// Forward the full job to Tabulator, so that that gets updated too.
|
|
||||||
this.$refs.jobsTable.processJobUpdate(job);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSelectedTaskChanged(taskSummary) {
|
|
||||||
if (!taskSummary) { // There is no selected task.
|
|
||||||
this.tasks.deselectAllTasks();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("selected task changed:", taskSummary);
|
|
||||||
const jobsAPI = new API.JobsApi(apiClient);
|
|
||||||
jobsAPI.fetchTask(taskSummary.id)
|
|
||||||
.then((task) => {
|
|
||||||
this.tasks.setSelectedTask(task);
|
|
||||||
// Forward the full task to Tabulator, so that that gets updated too.
|
|
||||||
if (this.$refs.tasksTable)
|
|
||||||
this.$refs.tasksTable.processTaskUpdate(task);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
sendMessage(message) {
|
|
||||||
this.$refs.jobsListener.sendBroadcastMessage("typer", message);
|
|
||||||
},
|
|
||||||
|
|
||||||
// SocketIO data event handlers:
|
|
||||||
onSioJobUpdate(jobUpdate) {
|
|
||||||
if (!jobUpdate.previous_status)
|
|
||||||
return this.onJobNew(jobUpdate);
|
|
||||||
return this.onJobUpdate(jobUpdate);
|
|
||||||
},
|
|
||||||
onJobUpdate(jobUpdate) {
|
|
||||||
// this.messages.push(`Job update: ${jobUpdate.id} (${jobUpdate.previous_status} → ${jobUpdate.status})`);
|
|
||||||
if (this.$refs.jobsTable) {
|
|
||||||
this.$refs.jobsTable.processJobUpdate(jobUpdate);
|
|
||||||
} else {
|
|
||||||
console.warn("App: this.$refs.jobsTable is", this.$refs.jobsTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.jobs.activeJobID == jobUpdate.id) {
|
|
||||||
this.onSelectedJobChanged(jobUpdate);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJobNew(jobUpdate) {
|
|
||||||
if (!this.$refs.jobsTable) {
|
|
||||||
console.warn("App: this.$refs.jobsTable is", this.$refs.jobsTable);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this.messages.push(`New job: ${jobUpdate.id} (${jobUpdate.status})`);
|
|
||||||
this.$refs.jobsTable.processNewJob(jobUpdate);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event handler for SocketIO task updates.
|
|
||||||
* @param {API.SocketIOTaskUpdate} taskUpdate
|
|
||||||
*/
|
|
||||||
onSioTaskUpdate(taskUpdate) {
|
|
||||||
if (this.$refs.tasksTable)
|
|
||||||
this.$refs.tasksTable.processTaskUpdate(taskUpdate);
|
|
||||||
if (this.tasks.activeTaskID == taskUpdate.id)
|
|
||||||
this.onSelectedTaskChanged(taskUpdate);
|
|
||||||
},
|
|
||||||
|
|
||||||
onChatMessage(message) {
|
|
||||||
console.log("chat message received:", message);
|
|
||||||
this.messages.push(`${message.text}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// SocketIO connection event handlers:
|
|
||||||
onSIOReconnected() {
|
|
||||||
this.$refs.jobsTable.onReconnected();
|
|
||||||
if (this.$refs.tasksTable)
|
|
||||||
this.$refs.tasksTable.onReconnected();
|
|
||||||
this.fetchManagerInfo();
|
|
||||||
},
|
|
||||||
onSIODisconnected(reason) {
|
|
||||||
this.flamencoName = DEFAULT_FLAMENCO_NAME;
|
|
||||||
this.flamencoVersion = DEFAULT_FLAMENCO_VERSION;
|
|
||||||
this.jobs.deselectAllJobs();
|
|
||||||
},
|
|
||||||
fetchManagerInfo() {
|
fetchManagerInfo() {
|
||||||
const metaAPI = new API.MetaApi(apiClient);
|
const metaAPI = new API.MetaApi(apiClient);
|
||||||
metaAPI.getVersion().then((version) => {
|
metaAPI.getVersion().then((version) => {
|
||||||
|
14
web/app/src/components/NotificationBar.vue
Normal file
14
web/app/src/components/NotificationBar.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<span class='notifications' v-if="notifs.last">{{ notifs.last.msg }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useNotifs } from '@/stores/notifications';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NotificationBar',
|
||||||
|
data: () => ({
|
||||||
|
notifs: useNotifs(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
</script>
|
@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
|
import router from '@/router/index'
|
||||||
|
|
||||||
// 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.
|
||||||
@ -15,4 +16,5 @@ const app = createApp(App)
|
|||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
27
web/app/src/router/index.js
Normal file
27
web/app/src/router/index.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'jobs',
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (ViewName.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import('../views/JobsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/workers',
|
||||||
|
name: 'workers',
|
||||||
|
component: () => import('../views/WorkersView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
component: () => import('../views/SettingsView.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
let url = new URL(window.location.href);
|
let url = new URL(window.location.href);
|
||||||
url.port = "8080";
|
url.port = "8080";
|
||||||
|
url.pathname = "/";
|
||||||
const flamencoAPIURL = url.href;
|
const flamencoAPIURL = url.href;
|
||||||
|
|
||||||
url.protocol = "ws:";
|
url.protocol = "ws:";
|
||||||
|
139
web/app/src/views/JobsView.vue
Normal file
139
web/app/src/views/JobsView.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="col col-1">
|
||||||
|
<jobs-table ref="jobsTable" @selectedJobChange="onSelectedJobChanged" />
|
||||||
|
</div>
|
||||||
|
<div class="col col-2">
|
||||||
|
<job-details :jobData="jobs.activeJob" />
|
||||||
|
<tasks-table v-if="jobs.activeJobID" ref="tasksTable" :jobID="jobs.activeJobID"
|
||||||
|
@selectedTaskChange="onSelectedTaskChanged" />
|
||||||
|
</div>
|
||||||
|
<div class="col col-3">
|
||||||
|
<task-details :taskData="tasks.activeTask" />
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<notification-bar />
|
||||||
|
<update-listener ref="updateListener" :websocketURL="websocketURL" :subscribedJob="jobs.activeJobID"
|
||||||
|
@jobUpdate="onSioJobUpdate" @taskUpdate="onSioTaskUpdate" @message="onChatMessage"
|
||||||
|
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as urls from '@/urls'
|
||||||
|
import * as API from '@/manager-api';
|
||||||
|
import { useJobs } from '@/stores/jobs';
|
||||||
|
import { useTasks } from '@/stores/tasks';
|
||||||
|
import { apiClient } from '@/stores/api-query-count';
|
||||||
|
|
||||||
|
import JobsTable from '@/components/JobsTable.vue'
|
||||||
|
import JobDetails from '@/components/JobDetails.vue'
|
||||||
|
import TaskDetails from '@/components/TaskDetails.vue'
|
||||||
|
import TasksTable from '@/components/TasksTable.vue'
|
||||||
|
import NotificationBar from '@/components/NotificationBar.vue'
|
||||||
|
import UpdateListener from '@/components/UpdateListener.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'JobsView',
|
||||||
|
components: {
|
||||||
|
JobsTable, JobDetails, TaskDetails, TasksTable, NotificationBar, UpdateListener,
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
websocketURL: urls.ws(),
|
||||||
|
messages: [],
|
||||||
|
|
||||||
|
jobs: useJobs(),
|
||||||
|
tasks: useTasks(),
|
||||||
|
}),
|
||||||
|
mounted() {
|
||||||
|
window.jobsView = this;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// onSelectedJobChanged is called whenever the selected job changes; this is
|
||||||
|
// both when another job is selected and when the selected job itself gets
|
||||||
|
// updated.
|
||||||
|
onSelectedJobChanged(jobSummary) {
|
||||||
|
if (!jobSummary) { // There is no selected job.
|
||||||
|
this.jobs.deselectAllJobs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobsAPI = new API.JobsApi(apiClient);
|
||||||
|
jobsAPI.fetchJob(jobSummary.id)
|
||||||
|
.then((job) => {
|
||||||
|
this.jobs.setSelectedJob(job);
|
||||||
|
// Forward the full job to Tabulator, so that that gets updated too.
|
||||||
|
this.$refs.jobsTable.processJobUpdate(job);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSelectedTaskChanged(taskSummary) {
|
||||||
|
if (!taskSummary) { // There is no selected task.
|
||||||
|
this.tasks.deselectAllTasks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("selected task changed:", taskSummary);
|
||||||
|
const jobsAPI = new API.JobsApi(apiClient);
|
||||||
|
jobsAPI.fetchTask(taskSummary.id)
|
||||||
|
.then((task) => {
|
||||||
|
this.tasks.setSelectedTask(task);
|
||||||
|
// Forward the full task to Tabulator, so that that gets updated too.
|
||||||
|
if (this.$refs.tasksTable)
|
||||||
|
this.$refs.tasksTable.processTaskUpdate(task);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// SocketIO data event handlers:
|
||||||
|
onSioJobUpdate(jobUpdate) {
|
||||||
|
if (!jobUpdate.previous_status)
|
||||||
|
return this.onJobNew(jobUpdate);
|
||||||
|
return this.onJobUpdate(jobUpdate);
|
||||||
|
},
|
||||||
|
onJobUpdate(jobUpdate) {
|
||||||
|
// this.messages.push(`Job update: ${jobUpdate.id} (${jobUpdate.previous_status} → ${jobUpdate.status})`);
|
||||||
|
if (this.$refs.jobsTable) {
|
||||||
|
this.$refs.jobsTable.processJobUpdate(jobUpdate);
|
||||||
|
} else {
|
||||||
|
console.warn("App: this.$refs.jobsTable is", this.$refs.jobsTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.jobs.activeJobID == jobUpdate.id) {
|
||||||
|
this.onSelectedJobChanged(jobUpdate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onJobNew(jobUpdate) {
|
||||||
|
if (!this.$refs.jobsTable) {
|
||||||
|
console.warn("App: this.$refs.jobsTable is", this.$refs.jobsTable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.messages.push(`New job: ${jobUpdate.id} (${jobUpdate.status})`);
|
||||||
|
this.$refs.jobsTable.processNewJob(jobUpdate);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for SocketIO task updates.
|
||||||
|
* @param {API.SocketIOTaskUpdate} taskUpdate
|
||||||
|
*/
|
||||||
|
onSioTaskUpdate(taskUpdate) {
|
||||||
|
if (this.$refs.tasksTable)
|
||||||
|
this.$refs.tasksTable.processTaskUpdate(taskUpdate);
|
||||||
|
if (this.tasks.activeTaskID == taskUpdate.id)
|
||||||
|
this.onSelectedTaskChanged(taskUpdate);
|
||||||
|
},
|
||||||
|
|
||||||
|
onChatMessage(message) {
|
||||||
|
console.log("chat message received:", message);
|
||||||
|
this.messages.push(`${message.text}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// SocketIO connection event handlers:
|
||||||
|
onSIOReconnected() {
|
||||||
|
this.$refs.jobsTable.onReconnected();
|
||||||
|
if (this.$refs.tasksTable)
|
||||||
|
this.$refs.tasksTable.onReconnected();
|
||||||
|
},
|
||||||
|
onSIODisconnected(reason) {
|
||||||
|
this.jobs.deselectAllJobs();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
28
web/app/src/views/SettingsView.vue
Normal file
28
web/app/src/views/SettingsView.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div class="col col-1">
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
<div class="col col-2">
|
||||||
|
More settings
|
||||||
|
</div>
|
||||||
|
<div class="col col-3">
|
||||||
|
Completely different settings
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
No footer here.
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SettingsView',
|
||||||
|
components: {},
|
||||||
|
data: () => ({
|
||||||
|
}),
|
||||||
|
mounted() {
|
||||||
|
window.settingsView = this;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
45
web/app/src/views/WorkersView.vue
Normal file
45
web/app/src/views/WorkersView.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="col col-1">
|
||||||
|
Workers
|
||||||
|
</div>
|
||||||
|
<div class="col col-2">
|
||||||
|
Worker Details
|
||||||
|
</div>
|
||||||
|
<div class="col col-3">
|
||||||
|
Column 3
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<notification-bar />
|
||||||
|
<update-listener ref="updateListener" :websocketURL="websocketURL" @sioReconnected="onSIOReconnected"
|
||||||
|
@sioDisconnected="onSIODisconnected" />
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as urls from '@/urls'
|
||||||
|
import * as API from '@/manager-api';
|
||||||
|
import { apiClient } from '@/stores/api-query-count';
|
||||||
|
|
||||||
|
import NotificationBar from '@/components/NotificationBar.vue'
|
||||||
|
import UpdateListener from '@/components/UpdateListener.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WorkersView',
|
||||||
|
components: {
|
||||||
|
NotificationBar, UpdateListener,
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
websocketURL: urls.ws(),
|
||||||
|
}),
|
||||||
|
mounted() {
|
||||||
|
window.workersView = this;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// SocketIO connection event handlers:
|
||||||
|
onSIOReconnected() {
|
||||||
|
},
|
||||||
|
onSIODisconnected(reason) {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user