mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:27:07 +00:00
Initial commit
This commit is contained in:
70
lib/www/client/source/src/App.vue
Normal file
70
lib/www/client/source/src/App.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<v-app id="dougal">
|
||||
<dougal-navigation></dougal-navigation>
|
||||
<v-main>
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
<dougal-footer></dougal-footer>
|
||||
<v-snackbar v-model="snack"
|
||||
:color="snackColour"
|
||||
:timeout="6000"
|
||||
>
|
||||
{{ snackText }}
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn
|
||||
text
|
||||
v-bind="attrs"
|
||||
@click="snack = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '../node_modules/typeface-roboto/index.css'
|
||||
@import '../node_modules/@mdi/font/css/materialdesignicons.css'
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import DougalNavigation from './components/navigation';
|
||||
import DougalFooter from './components/footer';
|
||||
|
||||
export default {
|
||||
name: 'Dougal',
|
||||
|
||||
components: {
|
||||
DougalNavigation,
|
||||
DougalFooter
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
snack: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
snackText () { return this.$store.state.snack.snackText },
|
||||
snackColour () { return this.$store.state.snack.snackColour }
|
||||
},
|
||||
|
||||
watch: {
|
||||
"$vuetify.theme.dark": {
|
||||
handler (newValue) {
|
||||
localStorage.setItem("darkTheme", newValue);
|
||||
}
|
||||
},
|
||||
|
||||
snackText (newVal) {
|
||||
this.snack = !!newVal;
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
// Local Storage values are always strings
|
||||
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
BIN
lib/www/client/source/src/assets/logo.png
Normal file
BIN
lib/www/client/source/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
447
lib/www/client/source/src/assets/logo.svg
Normal file
447
lib/www/client/source/src/assets/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 30 KiB |
13
lib/www/client/source/src/assets/zoom-fit-best.svg
Normal file
13
lib/www/client/source/src/assets/zoom-fit-best.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="M 2 2 L 2 5 L 3 5 L 3 3 L 5 3 L 5 2 L 3 2 L 2 2 z M 8 2 L 6 4 L 10 4 L 8 2 z M 11 2 L 11 3 L 13 3 L 13 5 L 14 5 L 14 2 L 13 2 L 11 2 z M 5 5 L 5 11 L 11 11 L 11 5 L 5 5 z M 4 6 L 2 8 L 4 10 L 4 6 z M 6 6 L 10 6 L 10 10 L 6 10 L 6 6 z M 12 6 L 12 10 L 14 8 L 12 6 z M 2 11 L 2 14 L 3 14 L 5 14 L 5 13 L 3 13 L 3 11 L 2 11 z M 13 11 L 13 13 L 11 13 L 11 14 L 14 14 L 14 13 L 14 11 L 13 11 z M 6 12 L 8 14 L 10 12 L 6 12 z "
|
||||
class="ColorScheme-Text"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 758 B |
39
lib/www/client/source/src/components/footer.vue
Normal file
39
lib/www/client/source/src/components/footer.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<!-- kate: replace-tabs on; indent-width 2; -->
|
||||
<template>
|
||||
<v-footer app>
|
||||
<small>© {{year}} <a href="https://aaltronav.eu/" target="_blank" class="brand">Aaltronav</a></small>
|
||||
<v-spacer></v-spacer>
|
||||
<div title="Night mode">
|
||||
<v-switch
|
||||
class="ma-auto"
|
||||
flat
|
||||
hide-details
|
||||
v-model="$vuetify.theme.dark"
|
||||
append-icon="mdi-weather-night"
|
||||
></v-switch>
|
||||
</div>
|
||||
</v-footer>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Bank Gothic Medium";
|
||||
src: local("Bank Gothic Medium"), url("/fonts/bank-gothic-medium.woff");
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-family: "Bank Gothic Medium";
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DougalFooter',
|
||||
computed: {
|
||||
year () {
|
||||
const date = new Date();
|
||||
return date.getUTCFullYear();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
110
lib/www/client/source/src/components/navigation.vue
Normal file
110
lib/www/client/source/src/components/navigation.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
|
||||
<v-app-bar
|
||||
app
|
||||
clipped-left
|
||||
>
|
||||
<v-img src="https://aaltronav.eu/media/aaltronav-logo.svg"
|
||||
contain
|
||||
max-height="32px" max-width="32px"
|
||||
></v-img>
|
||||
|
||||
<v-toolbar-title class="mx-2" @click="$router.push('/')" style="cursor: pointer;">Dougal</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-breadcrumbs :items="path"></v-breadcrumbs>
|
||||
|
||||
<template v-if="$route.name != 'Login'">
|
||||
<v-btn text link to="/login" v-if="!$root.user">Log in</v-btn>
|
||||
<template v-else>
|
||||
<v-btn title="Edit profile" disabled>
|
||||
{{$root.user.name}}
|
||||
</v-btn>
|
||||
<v-btn class="ml-2" title="Log out" link to="/?logout=1">
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</template>
|
||||
<template v-slot:extension v-if="$route.matched.find(i => i.name == 'Project')">
|
||||
<v-tabs :value="tab" show-arrows align-with-title>
|
||||
<v-tab v-for="tab, index in tabs" :key="index" link :to="tabLink(tab.href)" v-text="tab.text"></v-tab>
|
||||
</v-tabs>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'DougalNavigation',
|
||||
data() {
|
||||
return {
|
||||
drawer: false,
|
||||
tabs: [
|
||||
{ href: "summary", text: "Summary" },
|
||||
{ href: "lines", text: "Lines" },
|
||||
{ href: "sequences", text: "Sequences" },
|
||||
{ href: "calendar", text: "Calendar" },
|
||||
{ href: "log", text: "Log" },
|
||||
{ href: "map", text: "Map" }
|
||||
],
|
||||
path: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
tab () {
|
||||
return this.tabs.findIndex(t => t.href == this.$route.path.split(/\/+/)[3]);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
"$route" (newVal, oldVal) {
|
||||
if (newVal.matched != oldVal.matched) {
|
||||
this.breadcrumbs();
|
||||
}
|
||||
},
|
||||
|
||||
"$store.state.project.projectId" () {
|
||||
this.breadcrumbs();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
tabLink (href) {
|
||||
return `/projects/${this.$route.params.project}/${href}`;
|
||||
},
|
||||
|
||||
breadcrumbs () {
|
||||
this.path = this.$route.matched
|
||||
.map(i => {
|
||||
return {
|
||||
breadcrumbs: i.meta && i.meta.breadcrumbs,
|
||||
ctx: i.instances.default
|
||||
}
|
||||
})
|
||||
.filter(i => i.breadcrumbs)
|
||||
.flat()
|
||||
.map(i => {
|
||||
|
||||
return i.breadcrumbs.map( breadcrumb => {
|
||||
const o = {};
|
||||
for (const key of Object.keys(breadcrumb)) {
|
||||
o[key] = typeof breadcrumb[key] === 'function'
|
||||
? i.ctx && breadcrumb[key](i.ctx)
|
||||
: breadcrumb[key];
|
||||
}
|
||||
return o;
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.breadcrumbs();
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
11
lib/www/client/source/src/lib/FormatTimestamp.js
Normal file
11
lib/www/client/source/src/lib/FormatTimestamp.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
export default function FormatTimestamp (str) {
|
||||
const d = new Date(str);
|
||||
if (isNaN(d)) {
|
||||
return str;
|
||||
} else {
|
||||
// Get rid of milliseconds
|
||||
return d.toISOString().substring(0,19)+"Z";
|
||||
}
|
||||
}
|
||||
42
lib/www/client/source/src/main.js
Normal file
42
lib/www/client/source/src/main.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
data () {
|
||||
return {
|
||||
apiUrl: "/api",
|
||||
snack: false,
|
||||
snackText: null,
|
||||
snackColour: null,
|
||||
|
||||
user: null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
markdown (value) {
|
||||
return typeof value == "string"
|
||||
? marked(value)
|
||||
: value;
|
||||
},
|
||||
|
||||
showSnack(text, colour = "primary") {
|
||||
console.log("showSnack", text, colour);
|
||||
this.snackColour = colour;
|
||||
this.snackText = text;
|
||||
this.snack = true;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
7
lib/www/client/source/src/plugins/vuetify.js
Normal file
7
lib/www/client/source/src/plugins/vuetify.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import Vuetify from 'vuetify/lib';
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
export default new Vuetify({
|
||||
});
|
||||
115
lib/www/client/source/src/router/index.js
Normal file
115
lib/www/client/source/src/router/index.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Project from '../views/Project.vue'
|
||||
import ProjectList from '../views/ProjectList.vue'
|
||||
import ProjectSummary from '../views/ProjectSummary.vue'
|
||||
import LineList from '../views/LineList.vue'
|
||||
import LineSummary from '../views/LineSummary.vue'
|
||||
import SequenceList from '../views/SequenceList.vue'
|
||||
import SequenceSummary from '../views/SequenceSummary.vue'
|
||||
import Calendar from '../views/Calendar.vue'
|
||||
import Log from '../views/Log.vue'
|
||||
import Map from '../views/Map.vue'
|
||||
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/projects",
|
||||
redirect: "/projects/"
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/projects/",
|
||||
component: ProjectList,
|
||||
meta: {
|
||||
breadcrumbs: [
|
||||
{ text: "Projects", href: "/projects", disabled: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/projects/:project",
|
||||
redirect: "/projects/:project/"
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/projects/:project/",
|
||||
name: "Project",
|
||||
component: Project,
|
||||
meta: {
|
||||
breadcrumbs: [
|
||||
{ text: "Projects", href: "/projects" },
|
||||
{
|
||||
text: (ctx) => ctx.$store.state.project.projectName || "…",
|
||||
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`
|
||||
}
|
||||
]
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
redirect: "summary"
|
||||
},
|
||||
{
|
||||
path: "summary",
|
||||
component: ProjectSummary
|
||||
},
|
||||
{
|
||||
path: "lines/",
|
||||
name: "LineList",
|
||||
component: LineList
|
||||
},
|
||||
{
|
||||
path: "lines/:line",
|
||||
name: "Line",
|
||||
component: LineSummary
|
||||
},
|
||||
{
|
||||
path: "sequences",
|
||||
component: SequenceList
|
||||
},
|
||||
{
|
||||
path: "sequences/:sequence",
|
||||
component: SequenceSummary
|
||||
},
|
||||
{
|
||||
path: "calendar",
|
||||
component: Calendar
|
||||
},
|
||||
{
|
||||
path: "log",
|
||||
component: Log
|
||||
},
|
||||
{
|
||||
path: "map",
|
||||
component: Map
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
16
lib/www/client/source/src/store/index.js
Normal file
16
lib/www/client/source/src/store/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import api from './modules/api'
|
||||
import snack from './modules/snack'
|
||||
import project from './modules/project'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
api,
|
||||
snack,
|
||||
project
|
||||
}
|
||||
})
|
||||
18
lib/www/client/source/src/store/modules/api/actions.js
Normal file
18
lib/www/client/source/src/store/modules/api/actions.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
async function api ({state, commit, dispatch}, [resource, init = {}]) {
|
||||
try {
|
||||
// this.loading = true;
|
||||
const res = await fetch(`${state.apiUrl}${resource}`, init);
|
||||
if (res.ok) {
|
||||
return await res.json();
|
||||
} else {
|
||||
await dispatch('showSnack', [res.statusText, "warning"]);
|
||||
}
|
||||
} catch (err) {
|
||||
await dispatch('showSnack', [err, "error"]);
|
||||
} finally {
|
||||
// this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default { api };
|
||||
6
lib/www/client/source/src/store/modules/api/index.js
Normal file
6
lib/www/client/source/src/store/modules/api/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import state from './state'
|
||||
import getters from './getters'
|
||||
import actions from './actions'
|
||||
import mutations from './mutations'
|
||||
|
||||
export default { state, getters, actions, mutations };
|
||||
5
lib/www/client/source/src/store/modules/api/state.js
Normal file
5
lib/www/client/source/src/store/modules/api/state.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const state = () => ({
|
||||
apiUrl: "/api"
|
||||
});
|
||||
|
||||
export default state;
|
||||
13
lib/www/client/source/src/store/modules/project/actions.js
Normal file
13
lib/www/client/source/src/store/modules/project/actions.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
async function getProject ({commit, dispatch}, projectId) {
|
||||
const res = await dispatch('api', [`/project/${String(projectId).toLowerCase()}`]);
|
||||
if (res) {
|
||||
commit('setProjectName', res.name);
|
||||
commit('setProjectId', res.pid);
|
||||
const recentProjects = JSON.parse(localStorage.getItem("recentProjects") || "[]")
|
||||
recentProjects.unshift(res);
|
||||
localStorage.setItem("recentProjects", JSON.stringify(recentProjects.slice(0, 3)));
|
||||
}
|
||||
}
|
||||
|
||||
export default { getProject };
|
||||
6
lib/www/client/source/src/store/modules/project/index.js
Normal file
6
lib/www/client/source/src/store/modules/project/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import state from './state'
|
||||
import getters from './getters'
|
||||
import actions from './actions'
|
||||
import mutations from './mutations'
|
||||
|
||||
export default { state, getters, actions, mutations };
|
||||
10
lib/www/client/source/src/store/modules/project/mutations.js
Normal file
10
lib/www/client/source/src/store/modules/project/mutations.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
function setProjectId (state, pid) {
|
||||
state.projectId = pid;
|
||||
}
|
||||
|
||||
function setProjectName (state, name) {
|
||||
state.projectName = name;
|
||||
}
|
||||
|
||||
export default { setProjectId, setProjectName };
|
||||
6
lib/www/client/source/src/store/modules/project/state.js
Normal file
6
lib/www/client/source/src/store/modules/project/state.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const state = () => ({
|
||||
projectId: null,
|
||||
projectName: null
|
||||
});
|
||||
|
||||
export default state;
|
||||
7
lib/www/client/source/src/store/modules/snack/actions.js
Normal file
7
lib/www/client/source/src/store/modules/snack/actions.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
function showSnack({commit}, [text, colour]) {
|
||||
commit('setSnackColour', colour || 'primary');
|
||||
commit('setSnackText', text);
|
||||
}
|
||||
|
||||
export default { showSnack };
|
||||
6
lib/www/client/source/src/store/modules/snack/index.js
Normal file
6
lib/www/client/source/src/store/modules/snack/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import state from './state'
|
||||
import getters from './getters'
|
||||
import actions from './actions'
|
||||
import mutations from './mutations'
|
||||
|
||||
export default { state, getters, actions, mutations };
|
||||
10
lib/www/client/source/src/store/modules/snack/mutations.js
Normal file
10
lib/www/client/source/src/store/modules/snack/mutations.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
function setSnackText (state, text) {
|
||||
state.snackText = text;
|
||||
}
|
||||
|
||||
function setSnackColour (state, colour) {
|
||||
state.snackColour = colour;
|
||||
}
|
||||
|
||||
export default { setSnackText, setSnackColour };
|
||||
6
lib/www/client/source/src/store/modules/snack/state.js
Normal file
6
lib/www/client/source/src/store/modules/snack/state.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const state = () => ({
|
||||
snackText: null,
|
||||
snackColour: null
|
||||
});
|
||||
|
||||
export default state;
|
||||
5
lib/www/client/source/src/views/About.vue
Normal file
5
lib/www/client/source/src/views/About.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
177
lib/www/client/source/src/views/Calendar.vue
Normal file
177
lib/www/client/source/src/views/Calendar.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-sheet height="64">
|
||||
<v-toolbar flat color="white">
|
||||
|
||||
<v-menu bottom right>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
outlined
|
||||
color="grey darken-2"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<span>Go to</span>
|
||||
<v-icon right>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="setFirst">
|
||||
<v-list-item-title>First sequence</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setLast">
|
||||
<v-list-item-title>Last sequence</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setToday">
|
||||
<v-list-item-title>Today</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
|
||||
<v-btn fab text small color="grey darken-2" @click="prev">
|
||||
<v-icon small>mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
<v-btn fab text small color="grey darken-2" @click="next">
|
||||
<v-icon small>mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title v-if="$refs.calendar">
|
||||
{{ $refs.calendar.title }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu bottom right>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
outlined
|
||||
color="grey darken-2"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<span>{{ typeLabel }}</span>
|
||||
<v-icon right>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="type = 'day'">
|
||||
<v-list-item-title>Day</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="type = 'week'">
|
||||
<v-list-item-title>Week</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="type = 'month'">
|
||||
<v-list-item-title>Month</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="type = '4day'">
|
||||
<v-list-item-title>4 days</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar>
|
||||
</v-sheet>
|
||||
<v-sheet>
|
||||
<v-calendar
|
||||
ref="calendar"
|
||||
v-model="focus"
|
||||
:events="events"
|
||||
:event-color="getEventColour"
|
||||
color="primary"
|
||||
:type="type"
|
||||
:locale-first-day-of-year="4"
|
||||
:weekdays="weekdays"
|
||||
:show-week="true"
|
||||
></v-calendar>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: "Calendar",
|
||||
|
||||
data () {
|
||||
return {
|
||||
weekdays: [1, 2, 3, 4, 5, 6, 0],
|
||||
type: "week",
|
||||
focus: "",
|
||||
events: [
|
||||
],
|
||||
loading: false,
|
||||
options: {
|
||||
sortBy: "sequence"
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
typeLabel () {
|
||||
const labels = {
|
||||
month: 'Month',
|
||||
week: 'Week',
|
||||
day: 'Day',
|
||||
'4day': '4 Days',
|
||||
};
|
||||
|
||||
return labels[this.type];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async getEvents () {
|
||||
this.loading = true;
|
||||
const query = new URLSearchParams(this.options);
|
||||
const url = `/project/${this.$route.params.project}/sequence?${query.toString()}`;
|
||||
|
||||
const finalSequences = await this.api([url]) || [];
|
||||
this.events = finalSequences.map(s => {
|
||||
const e = {};
|
||||
//e.start = s.ts0.substring(0,10)+" "+s.ts0.substring(11,19)
|
||||
//e.end = s.ts1.substring(0,10)+" "+s.ts1.substring(11,19)
|
||||
e.start = new Date(s.ts0);
|
||||
e.end = new Date(s.ts1);
|
||||
e.timed = true;
|
||||
e.colour = "orange";
|
||||
e.name = `Sequence ${s.sequence}`;
|
||||
return e;
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
getEventColour (event) {
|
||||
return event.colour;
|
||||
},
|
||||
|
||||
setToday () {
|
||||
this.focus = "";
|
||||
},
|
||||
|
||||
setFirst () {
|
||||
this.focus = this.events[0].start;
|
||||
},
|
||||
|
||||
setLast () {
|
||||
this.focus = this.events[this.events.length-1].start;
|
||||
},
|
||||
|
||||
prev () {
|
||||
this.$refs.calendar.prev()
|
||||
},
|
||||
|
||||
next () {
|
||||
this.$refs.calendar.next()
|
||||
},
|
||||
|
||||
|
||||
...mapActions(["api"])
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.getEvents();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
39
lib/www/client/source/src/views/Home.vue
Normal file
39
lib/www/client/source/src/views/Home.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card class="mx-auto" max-width="600" tile>
|
||||
<v-card-title style="word-break: normal;">Dougal – Seismic Production & Data Analysis Tool</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list two-line subheader>
|
||||
<v-subheader>Recent projects</v-subheader>
|
||||
<v-list-item-group>
|
||||
<v-list-item link to="/projects/eq20221">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>EQ20221</v-list-item-title>
|
||||
<v-list-item-subtitle>Snorre PRM</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" to="/projects/">All projects</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="success">New project</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
118
lib/www/client/source/src/views/LineList.vue
Normal file
118
lib/www/client/source/src/views/LineList.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:server-items-length="num_lines"
|
||||
:options.sync="options"
|
||||
:loading="loading"
|
||||
:fixed-header="true"
|
||||
>
|
||||
|
||||
<template v-slot:item.length="props">
|
||||
<span>{{ Math.round(props.value) }} m</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.azimuth="props">
|
||||
<span>{{ props.value.toFixed(1) }} °</span>
|
||||
</template>
|
||||
|
||||
</v-data-table>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "LineList",
|
||||
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
value: "line",
|
||||
text: "Line"
|
||||
},
|
||||
{
|
||||
value: "status",
|
||||
text: "Status"
|
||||
},
|
||||
{
|
||||
value: "fsp",
|
||||
text: "FSP",
|
||||
align: "end"
|
||||
},
|
||||
{
|
||||
value: "lsp",
|
||||
text: "LSP",
|
||||
align: "end"
|
||||
},
|
||||
{
|
||||
value: "num_points",
|
||||
text: "Num. points",
|
||||
align: "end"
|
||||
},
|
||||
{
|
||||
value: "length",
|
||||
text: "Length",
|
||||
align: "end"
|
||||
},
|
||||
{
|
||||
value: "azimuth",
|
||||
text: "Azimuth",
|
||||
align: "end"
|
||||
},
|
||||
{
|
||||
value: "remarks",
|
||||
text: "Remarks"
|
||||
}
|
||||
],
|
||||
items: [],
|
||||
options: {},
|
||||
loading: false,
|
||||
num_lines: null
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
options: {
|
||||
handler () {
|
||||
this.getLines();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async getNumLines () {
|
||||
const projectInfo = await this.api([`/project/${this.$route.params.project}`]);
|
||||
this.num_lines = projectInfo.lines;
|
||||
},
|
||||
|
||||
async getLines () {
|
||||
this.loading = true;
|
||||
|
||||
const query = new URLSearchParams(this.options);
|
||||
if (this.options.itemsPerPage < 0) {
|
||||
query.delete("itemsPerPage");
|
||||
}
|
||||
const url = `/project/${this.$route.params.project}/line?${query.toString()}`;
|
||||
|
||||
this.items = await this.api([url]) || [];
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.getLines();
|
||||
this.getNumLines();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
13
lib/www/client/source/src/views/LineSummary.vue
Normal file
13
lib/www/client/source/src/views/LineSummary.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
LineSummary
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "LineSummary"
|
||||
}
|
||||
|
||||
</script>
|
||||
13
lib/www/client/source/src/views/Log.vue
Normal file
13
lib/www/client/source/src/views/Log.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
Log
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "Log"
|
||||
}
|
||||
|
||||
</script>
|
||||
276
lib/www/client/source/src/views/Map.vue
Normal file
276
lib/www/client/source/src/views/Map.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div id="map">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
@import '../../node_modules/leaflet/dist/leaflet.css'
|
||||
</style>
|
||||
|
||||
<style>
|
||||
#map {
|
||||
height: 100%;
|
||||
background-color: #dae4f7;
|
||||
}
|
||||
|
||||
.theme--dark #map {
|
||||
background-color: #111;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import L from 'leaflet'
|
||||
import { mapActions } from 'vuex'
|
||||
import ftstamp from '@/lib/FormatTimestamp'
|
||||
import zoomFitIcon from '@/assets/zoom-fit-best.svg'
|
||||
|
||||
var map;
|
||||
|
||||
const tileMaps = {
|
||||
|
||||
"No background": L.tileLayer("/nullmap"), // FIXME
|
||||
|
||||
"Norwegian nautical charts": L.tileLayer('https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}', {
|
||||
attribution: '<a href="http://www.kartverket.no/" target="_blank">Kartverket</a>'
|
||||
}),
|
||||
|
||||
"GEBCO": L.tileLayer.wms("https://www.gebco.net/data_and_products/gebco_web_services/2019/mapserv?", {layers: "GEBCO_2019_Grid_2", attribution: "<a href='https://www.gebco.net/' target='_blank' title='General Bathymetric Chart of the Oceans'>GEBCO</a>"}),
|
||||
|
||||
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}),
|
||||
|
||||
"MODIS Satellite": L.tileLayer('https://map1.vis.earthdata.nasa.gov/wmts-webmerc/MODIS_Terra_CorrectedReflectance_TrueColor/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', {
|
||||
attribution: 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (<a href="https://earthdata.nasa.gov">ESDIS</a>) with funding provided by NASA/HQ.',
|
||||
bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
|
||||
minZoom: 1,
|
||||
maxZoom: 9,
|
||||
format: 'jpg',
|
||||
time: '',
|
||||
tilematrixset: 'GoogleMapsCompatible_Level'
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
const layers = {
|
||||
"OpenSeaMap": L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
|
||||
attribution: 'Map data: © <a href="http://www.openseamap.org">OpenSeaMap</a> contributors'
|
||||
}),
|
||||
"Preplots": L.geoJSON(null, {
|
||||
pointToLayer (point, latlng) {
|
||||
return L.circle(latlng, {
|
||||
radius: 1,
|
||||
color: "#3388ff",
|
||||
stroke: false,
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
},
|
||||
onEachFeature (feature, layer) {
|
||||
const popup = feature.geometry.type == "Point"
|
||||
? `Preplot<br/>Point <b>${feature.properties.line} / ${feature.properties.point}</b>`
|
||||
: `Preplot<br/>Line <b>${feature.properties.line}</b>`;
|
||||
layer.bindTooltip(popup, {sticky: true});
|
||||
},
|
||||
}),
|
||||
"Raw lines": L.geoJSON(null, {
|
||||
pointToLayer (point, latlng) {
|
||||
return L.circle(latlng, {
|
||||
radius: 3,
|
||||
color: "red",
|
||||
stroke: false,
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
},
|
||||
style (feature) {
|
||||
return {
|
||||
color: "red"
|
||||
}
|
||||
},
|
||||
onEachFeature (feature, layer) {
|
||||
const popup = feature.geometry.type == "Point"
|
||||
? `Raw sequence ${feature.properties.sequence}<br/>Point <b>${feature.properties.line} / ${feature.properties.point}</b><br/>${feature.properties.objref}<br/>${feature.properties.tstamp}`
|
||||
: `Raw sequence ${feature.properties.sequence}<br/>
|
||||
Line <b>${feature.properties.line}</b><br/>
|
||||
${feature.properties.num_points} points, ???? m<br/>
|
||||
????<br/>
|
||||
<table><tr><td>????</td><td>${ftstamp(feature.properties.ts0)}</td></tr><tr><td>????</td><td>${ftstamp(feature.properties.ts1)}</td></tr></table>`;
|
||||
layer.bindTooltip(popup, {sticky: true});
|
||||
}
|
||||
}),
|
||||
"Final lines": L.geoJSON(null, {
|
||||
pointToLayer (point, latlng) {
|
||||
return L.circle(latlng, {
|
||||
radius: 3,
|
||||
color: "orange",
|
||||
stroke: false,
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
},
|
||||
style (feature) {
|
||||
return {
|
||||
color: "orange"
|
||||
}
|
||||
},
|
||||
onEachFeature (feature, layer) {
|
||||
const p = feature.properties;
|
||||
const popup = feature.geometry.type == "Point"
|
||||
? `Final sequence ${p.sequence}<br/>Point <b>${p.line} / ${p.point}</b><br/>${p.objref}<br/>${p.tstamp}`
|
||||
: `Final sequence ${p.sequence}<br/>
|
||||
Line <b>${p.line}</b><br/>
|
||||
${p.num_points} points${p.missing_shots ? " <i>("+p.missing_shots+" missing)</i>" : ""}<br/>
|
||||
${Math.round(p.length)} m ${p.azimuth.toFixed(1)}°<br/>
|
||||
${p.duration}<br/>
|
||||
<table><tr><td><b>${p.fsp}</b></td><td>@ ${ftstamp(p.ts0)}</td></tr><tr><td><b>${p.lsp}</b></td><td>@ ${ftstamp(p.ts1)}</td></tr></table>`;
|
||||
// : `Final sequence ${feature.properties.sequence}<br/>Line <b>${feature.properties.line}</b><br/>${feature.properties.num_points} points<br/>From ${feature.properties.ts0}<br/>until ${feature.properties.ts0}`;
|
||||
layer.bindTooltip(popup, {sticky: true});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "Map",
|
||||
|
||||
data () {
|
||||
return {
|
||||
//map: null,
|
||||
loading: false,
|
||||
layerRefreshConfig: [
|
||||
{
|
||||
layer: layers.Preplots,
|
||||
url: (query = "") => {
|
||||
return map.getZoom() < 18
|
||||
? `/project/${this.$route.params.project}/gis/preplot/line`
|
||||
: `/project/${this.$route.params.project}/gis/preplot/point?${query.toString()}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
layer: layers["Raw lines"],
|
||||
url: (query = "") => {
|
||||
return map.getZoom() < 17
|
||||
? `/project/${this.$route.params.project}/gis/raw/line`
|
||||
: `/project/${this.$route.params.project}/gis/raw/point?${query.toString()}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
layer: layers["Final lines"],
|
||||
url: (query = "") => {
|
||||
return map.getZoom() < 17
|
||||
? `/project/${this.$route.params.project}/gis/final/line`
|
||||
: `/project/${this.$route.params.project}/gis/final/point?${query.toString()}`;
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async fitProjectBounds () {
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await fetch(`${this.$root.apiUrl}/project/${this.$route.params.project}/gis`);
|
||||
if (res.ok) {
|
||||
const bbox = new L.GeoJSON(await res.json());
|
||||
map.fitBounds(bbox.getBounds());
|
||||
} else {
|
||||
this.$root.showSnack(res.statusText, "warning");
|
||||
}
|
||||
} catch (err) {
|
||||
this.$root.showSnack(err, "error")
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async refreshLayers (layerset) {
|
||||
|
||||
const bounds = map.getBounds().pad(0.3);
|
||||
const bboxScale = map.getZoom()/5;
|
||||
const bbox = [
|
||||
bounds._southWest.lng,
|
||||
bounds._southWest.lat,
|
||||
bounds._northEast.lng,
|
||||
bounds._northEast.lat
|
||||
].map(i => i.toFixed(bboxScale)).join(",");
|
||||
const limit = 10000;
|
||||
|
||||
const query = new URLSearchParams({bbox, limit});
|
||||
|
||||
for (const l of this.layerRefreshConfig.filter(i => !layerset || layerset.includes(i.layer))) {
|
||||
if (map.hasLayer(l.layer)) {
|
||||
// Firing all refresh events asynchronously, which is OK provided
|
||||
// we don't have hundreds of layers to be refreshed.
|
||||
await this.api([l.url(query)]).then( (layer) => {
|
||||
l.layer.clearLayers();
|
||||
if ((layer.features && layer.features.length < limit) || ("length" in layer && layer.length < limit)) {
|
||||
l.layer.addData(layer);
|
||||
} else {
|
||||
console.log("Too much data from", l.url(query));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
map = L.map('map', {maxZoom: 22});
|
||||
|
||||
tileMaps["No background"].addTo(map);
|
||||
layers.OpenSeaMap.addTo(map);
|
||||
layers.Preplots.addTo(map);
|
||||
|
||||
const layerControl = L.control.layers(tileMaps, layers).addTo(map);
|
||||
map.setView([0, 0], 3);
|
||||
|
||||
let moveStart = map.getBounds().pad(0.3);
|
||||
let zoomStart = map.getZoom();
|
||||
console.log("MAP", map);
|
||||
map.on('movestart', () => {
|
||||
moveStart = map.getBounds().pad(0.3);
|
||||
zoomStart = map.getZoom();
|
||||
});
|
||||
map.on('moveend', () => {
|
||||
console.log("Contained", moveStart.contains(map.getBounds()), map.getZoom() != zoomStart);
|
||||
if (!moveStart.contains(map.getBounds()) || map.getZoom() != zoomStart) {
|
||||
this.refreshLayers();
|
||||
}
|
||||
});
|
||||
|
||||
this.layerRefreshConfig.forEach( l => {
|
||||
l.layer.on('add', ({target}) => this.refreshLayers([target]));
|
||||
});
|
||||
|
||||
this.fitProjectBounds();
|
||||
|
||||
|
||||
// /usr/share/icons/breeze/actions/16/zoom-fit-best.svg
|
||||
const fitProjectBounds = this.fitProjectBounds;
|
||||
const FitToBoundsControl = L.Control.extend({
|
||||
onAdd (map) {
|
||||
const widget = L.DomUtil.create('div');
|
||||
widget.className = "leaflet-touch leaflet-bar leaflet-control";
|
||||
//widget.appendChild(document.getElementById("zoom-fit-best-icon"));
|
||||
widget.innerHTML = `<a href="#"><img src="${zoomFitIcon}""></a>`;
|
||||
widget.setAttribute("title", "Fit to bounds");
|
||||
widget.addEventListener("click", fitProjectBounds);
|
||||
return widget;
|
||||
},
|
||||
|
||||
onRemove (map) {
|
||||
this.removeEventListener("click", fitProjectBounds);
|
||||
}
|
||||
});
|
||||
|
||||
function fitToBoundsControl (opts) {
|
||||
return new FitToBoundsControl(opts);
|
||||
}
|
||||
|
||||
fitToBoundsControl({position: "topleft"}).addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
36
lib/www/client/source/src/views/Project.vue
Normal file
36
lib/www/client/source/src/views/Project.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<v-container fluid fill-height class="ma-0 pa-0">
|
||||
<v-row no-gutters align="stretch" class="fill-height">
|
||||
<v-col cols="12">
|
||||
<!-- Show component here according to selected route -->
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
//
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(["getProject"])
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
await this.getProject(this.$route.params.project);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
111
lib/www/client/source/src/views/ProjectList.vue
Normal file
111
lib/www/client/source/src/views/ProjectList.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:options.sync="options"
|
||||
:loading="loading"
|
||||
>
|
||||
|
||||
<template v-slot:item.pid="props">
|
||||
<a :href="`/projects/${props.item.pid}`">{{props.value}}</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.progress="props">
|
||||
{{ props.item.preplot_points ? (props.item.unique_shots/props.item.preplot_points*100).toFixed(1)+'%' : "" }}
|
||||
<v-progress-linear v-if="props.item.preplot_points"
|
||||
height="2"
|
||||
:value="(props.item.unique_shots/props.item.preplot_points*100)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
</v-data-table>
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
td p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "ProjectList",
|
||||
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
value: "pid",
|
||||
text: "Project ID"
|
||||
},
|
||||
{
|
||||
value: "name",
|
||||
text: "Name"
|
||||
},
|
||||
{
|
||||
value: "lines",
|
||||
text: "Lines"
|
||||
},
|
||||
{
|
||||
value: "sequences",
|
||||
text: "Sequences"
|
||||
},
|
||||
{
|
||||
value: "unique_shots",
|
||||
text: "Shots"
|
||||
},
|
||||
{
|
||||
value: "progress",
|
||||
text: "% Complete"
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
options: {},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async list () {
|
||||
this.items = await this.api(["/project/"]) || [];
|
||||
},
|
||||
|
||||
async summary (item) {
|
||||
const details = await this.api([`/project/${item.pid}/summary`]);
|
||||
if (details) {
|
||||
return Object.assign({}, details, item);
|
||||
}
|
||||
},
|
||||
|
||||
async load () {
|
||||
this.loading = true;
|
||||
await this.list();
|
||||
const promises = [];
|
||||
for (const key in this.items) {
|
||||
const item = this.items[key];
|
||||
const promise = this.summary(item)
|
||||
.then( expanded => {
|
||||
if (expanded) {
|
||||
this.$set(this.items, key, expanded);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
Promise.all(promises).finally( () => this.loading = false);
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
13
lib/www/client/source/src/views/ProjectSummary.vue
Normal file
13
lib/www/client/source/src/views/ProjectSummary.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
ProjectSummary
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "ProjectSummary"
|
||||
}
|
||||
|
||||
</script>
|
||||
13
lib/www/client/source/src/views/SequenceList.vue
Normal file
13
lib/www/client/source/src/views/SequenceList.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
SequenceList
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "SequenceList"
|
||||
}
|
||||
|
||||
</script>
|
||||
13
lib/www/client/source/src/views/SequenceSummary.vue
Normal file
13
lib/www/client/source/src/views/SequenceSummary.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
SequenceSummary
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "SequenceSummary"
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user