Mercurial > gemma
changeset 1276:aec9ed491dad
more cleanup in client/src
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Thu, 22 Nov 2018 07:40:23 +0100 |
parents | 9e23a2b02b32 |
children | 10b01a298745 |
files | client/src/components/App.vue client/src/components/admin/Importqueue.vue client/src/components/admin/Logs.vue client/src/components/admin/Systemconfiguration.vue client/src/components/admin/logs.vue client/src/components/admin/systemconfiguration.vue client/src/components/map/Bottlenecks.vue client/src/components/map/Contextbox.vue client/src/components/map/Staging.vue client/src/components/map/contextbox/Bottlenecks.vue client/src/components/map/contextbox/Contextbox.vue client/src/components/map/contextbox/Staging.vue client/src/components/map/contextbox/imports/Imports.vue client/src/components/map/imports/Importqueue.vue client/src/components/map/imports/Imports.vue client/src/router.js |
diffstat | 16 files changed, 1206 insertions(+), 1206 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/App.vue Thu Nov 22 07:28:21 2018 +0100 +++ b/client/src/components/App.vue Thu Nov 22 07:40:23 2018 +0100 @@ -89,7 +89,7 @@ Layers: () => import("./map/layers/Layers"), Sidebar: () => import("./Sidebar"), Search: () => import("./map/Search"), - Contextbox: () => import("./map/Contextbox"), + Contextbox: () => import("./map/contextbox/Contextbox"), Toolbar: () => import("./map/toolbar/Toolbar") } };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/admin/Importqueue.vue Thu Nov 22 07:40:23 2018 +0100 @@ -0,0 +1,170 @@ +<template> + <div class="d-flex flex-row"> + <div :class="spacerStyle"></div> + <div class="mt-3 mx-auto"> + <div class="card importqueuecard"> + <div class="card-header shadow-sm text-white bg-info mb-3">Importqueue</div> + <div class="card-body importcardbody"> + <div class="card-body importcardbody"> + <div class="searchandfilter d-flex flex-row"> + <div class="searchgroup input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" id="search"> + <i class="fa fa-search"></i> + </span> + </div> + <input + type="text" + class="form-control" + placeholder="" + aria-label="Search" + aria-describedby="search" + > + </div> + <div class="filters"> + <button + @click="setFilter('successful')" + :class="successfulStyle" + >Successful</button> + <button @click="setFilter('failed')" :class="failedStyle">Failed</button> + <button @click="setFilter('pending')" :class="pendingStyle">Pending</button> + </div> + </div> + <table class="table"> + <thead> + <tr> + <th>Enqueued</th> + <th>Kind</th> + <th>User</th> + <th>State</th> + </tr> + </thead> + <tbody> + <tr v-for="job in imports" :key="job.id"> + <td>{{job.enqueued}}</td> + <td>{{job.kind}}</td> + <td>{{job.user}}</td> + <td>{{job.state}}</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> +import { displayError } from "../../lib/errors.js"; +import { mapState } from "vuex"; + +export default { + name: "importqueue", + data() { + return { + successful: false, + failed: false, + pending: false + }; + }, + methods: { + setFilter(name) { + this[name] = !this[name]; + const allSet = this.successful && this.failed && this.pending; + if (allSet) { + this.all = false; + this.successful = false; + this.failed = false; + this.pending = false; + } + } + }, + computed: { + ...mapState("imports", ["imports"]), + ...mapState("application", ["showSidebar"]), + spacerStyle() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + }, + successfulStyle() { + return { + btn: true, + "btn-light": !this.successful, + "btn-dark": this.successful + }; + }, + pendingStyle() { + return { + btn: true, + "btn-light": !this.pending, + "btn-dark": this.pending + }; + }, + failedStyle() { + return { + btn: true, + "btn-light": !this.failed, + "btn-dark": this.failed + }; + } + }, + mounted() { + this.$store.dispatch("imports/getImports").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } +}; +</script> + +<style lang="sass" scoped> +.spacer + height: 100vh + +.spacer-collapsed + min-width: $icon-width + $offset + transition: $transition-fast + +.spacer-expanded + min-width: $sidebar-width + $offset + +.importqueuecard + width: 80vw + min-height: 20rem + +.card-body + width: 100% + margin-left: auto + margin-right: auto + +.searchandfilter + position: relative + margin-bottom: $xx-large-offset + +.filters + position: absolute + right: 0 + +.filters button + margin-right: $small-offset + +.table td, +.table th + border-top: 0 !important + text-align: left + padding: $small-offset !important + +.searchgroup + position: absolute + left: 0 + width: 50% +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/admin/Logs.vue Thu Nov 22 07:40:23 2018 +0100 @@ -0,0 +1,158 @@ +<template> + <div class="main d-flex flex-column"> + <div class="d-flex flex-row"> + <div :class="spacer"></div> + <div class="logoutput text-left bg-white shadow mt-3 mx-3"> + <pre id="code" v-highlightjs="logs"><code class="bash hljs hljs-string"></code></pre> + </div> + </div> + <div class="d-flex flex-row logmenu"> + <div class="d-flex align-self-center"> + <ul class="nav nav-pills"> + <li class="nav-item"> + <a + @click="fetch('system/log/apache2/access.log', 'accesslog')" + :class="accesslogStyle" + href="#" + >Accesslog</a> + </li> + <li class="nav-item"> + <a + @click="fetch('system/log/apache2/error.log', 'errorlog')" + :class="errorlogStyle" + href="#" + >Errorlog</a> + </li> + </ul> + </div> + <div class="statuscontainer d-flex flex-row"> + <div class="statusline ml-3 mt-1 align-self-center"> + <h3>Last refresh: {{refreshed}}</h3> + </div> + <div class="refresh"> + <button class="btn btn-dark" @click="fetch(currentFile, currentLog)">Refresh</button> + </div> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +.statuscontainer + width: 87% + position: relative + +.logmenu + margin-left: 5rem + min-width: 60vw + +#code + overflow: auto + +.refresh + position: absolute + right: 0 + +.logoutput + width: 95% + height: 85vh + overflow: auto + transition: $transition-fast + +.spacer + height: 90vh + +.spacer-collapsed + min-width: $icon-width + $offset + transition: $transition-fast + +.spacer-expanded + min-width: $sidebar-width + $offset + +.statusline + position: absolute + right: 0 + margin-right: 7rem +</style> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "../../lib/http.js"; +import "../../../node_modules/highlight.js/styles/paraiso-dark.css"; +import Vue from "vue"; +import VueHighlightJS from "vue-highlightjs"; +Vue.use(VueHighlightJS); + +const ACCESSLOG = "accesslog"; +const ERRORLOG = "errorlog"; + +export default { + name: "logs", + mounted() { + this.fetch("system/log/apache2/access.log", ACCESSLOG); + }, + data() { + return { + logs: null, + currentLog: null, + currentFile: null, + refreshed: null + }; + }, + methods: { + fetch(file, type) { + HTTP.get(file, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.logs = response.data.content; + this.currentLog = type; + this.refreshed = new Date().toLocaleString(); + this.currentFile = file; + }) + .catch(); + }, + disallow(e) { + e.target.blur(); + } + }, + computed: { + ...mapState("application", ["showSidebar"]), + accesslogStyle() { + return { + active: this.currentLog == ACCESSLOG, + "nav-link": true + }; + }, + errorlogStyle() { + return { + active: this.currentLog == ERRORLOG, + "nav-link": true + }; + }, + spacer() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/admin/Systemconfiguration.vue Thu Nov 22 07:40:23 2018 +0100 @@ -0,0 +1,144 @@ +<template> + <div class="d-flex flex-row"> + <div class="card sysconfig mt-3 mx-auto"> + <div class="card-header shadow-sm text-white bg-info mb-6"> + Systemconfiguration + </div> + <div class="card-body config"> + <section class="configsection"> + <h4 class="card-title">Bottleneck Areas stroke-color</h4> + <compact-picker v-model="strokeColor" /> + </section> + <section> + <h4 class="card-title">Bottleneck Areas fill-color</h4> + <chrome-picker v-model="fillColor" /> + </section> + <div class="sendbutton"> + <a @click.prevent="submit" class="btn btn-info">Send</a> + </div> + </div> <!-- card-body --> + </div> + </div> +</template> + +<style scoped lang="sass"> +.config + text-align: left + +.configsection + margin-bottom: $large-offset + +.sendbutton + position: absolute + right: $offset + bottom: $offset + +.inputs + margin-left: auto + margin-right: auto + +.sysconfig + width: 30vw +</style> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { Chrome } from "vue-color"; +import { Compact } from "vue-color"; + +import { HTTP } from "../../lib/http"; +import { displayError } from "../../lib/errors.js"; +export default { + name: "systemconfiguration", + data() { + return { + sent: false, + strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, + fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, + currentConfig: null + }; + }, + components: { "chrome-picker": Chrome, "compact-picker": Compact }, + methods: { + submit() { + HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.strokeColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.fillColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } +}; +</script>
--- a/client/src/components/admin/logs.vue Thu Nov 22 07:28:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,158 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <div class="d-flex flex-row"> - <div :class="spacer"></div> - <div class="logoutput text-left bg-white shadow mt-3 mx-3"> - <pre id="code" v-highlightjs="logs"><code class="bash hljs hljs-string"></code></pre> - </div> - </div> - <div class="d-flex flex-row logmenu"> - <div class="d-flex align-self-center"> - <ul class="nav nav-pills"> - <li class="nav-item"> - <a - @click="fetch('system/log/apache2/access.log', 'accesslog')" - :class="accesslogStyle" - href="#" - >Accesslog</a> - </li> - <li class="nav-item"> - <a - @click="fetch('system/log/apache2/error.log', 'errorlog')" - :class="errorlogStyle" - href="#" - >Errorlog</a> - </li> - </ul> - </div> - <div class="statuscontainer d-flex flex-row"> - <div class="statusline ml-3 mt-1 align-self-center"> - <h3>Last refresh: {{refreshed}}</h3> - </div> - <div class="refresh"> - <button class="btn btn-dark" @click="fetch(currentFile, currentLog)">Refresh</button> - </div> - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> -.statuscontainer - width: 87% - position: relative - -.logmenu - margin-left: 5rem - min-width: 60vw - -#code - overflow: auto - -.refresh - position: absolute - right: 0 - -.logoutput - width: 95% - height: 85vh - overflow: auto - transition: $transition-fast - -.spacer - height: 90vh - -.spacer-collapsed - min-width: $icon-width + $offset - transition: $transition-fast - -.spacer-expanded - min-width: $sidebar-width + $offset - -.statusline - position: absolute - right: 0 - margin-right: 7rem -</style> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../lib/http.js"; -import "../../../node_modules/highlight.js/styles/paraiso-dark.css"; -import Vue from "vue"; -import VueHighlightJS from "vue-highlightjs"; -Vue.use(VueHighlightJS); - -const ACCESSLOG = "accesslog"; -const ERRORLOG = "errorlog"; - -export default { - name: "logs", - mounted() { - this.fetch("system/log/apache2/access.log", ACCESSLOG); - }, - data() { - return { - logs: null, - currentLog: null, - currentFile: null, - refreshed: null - }; - }, - methods: { - fetch(file, type) { - HTTP.get(file, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.logs = response.data.content; - this.currentLog = type; - this.refreshed = new Date().toLocaleString(); - this.currentFile = file; - }) - .catch(); - }, - disallow(e) { - e.target.blur(); - } - }, - computed: { - ...mapState("application", ["showSidebar"]), - accesslogStyle() { - return { - active: this.currentLog == ACCESSLOG, - "nav-link": true - }; - }, - errorlogStyle() { - return { - active: this.currentLog == ERRORLOG, - "nav-link": true - }; - }, - spacer() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - } -}; -</script>
--- a/client/src/components/admin/systemconfiguration.vue Thu Nov 22 07:28:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,144 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div class="card sysconfig mt-3 mx-auto"> - <div class="card-header shadow-sm text-white bg-info mb-6"> - Systemconfiguration - </div> - <div class="card-body config"> - <section class="configsection"> - <h4 class="card-title">Bottleneck Areas stroke-color</h4> - <compact-picker v-model="strokeColor" /> - </section> - <section> - <h4 class="card-title">Bottleneck Areas fill-color</h4> - <chrome-picker v-model="fillColor" /> - </section> - <div class="sendbutton"> - <a @click.prevent="submit" class="btn btn-info">Send</a> - </div> - </div> <!-- card-body --> - </div> - </div> -</template> - -<style scoped lang="sass"> -.config - text-align: left - -.configsection - margin-bottom: $large-offset - -.sendbutton - position: absolute - right: $offset - bottom: $offset - -.inputs - margin-left: auto - margin-right: auto - -.sysconfig - width: 30vw -</style> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { Chrome } from "vue-color"; -import { Compact } from "vue-color"; - -import { HTTP } from "../../lib/http"; -import { displayError } from "../../lib/errors.js"; -export default { - name: "systemconfiguration", - data() { - return { - sent: false, - strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, - fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, - currentConfig: null - }; - }, - components: { "chrome-picker": Chrome, "compact-picker": Compact }, - methods: { - submit() { - HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.strokeColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.fillColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script>
--- a/client/src/components/map/Bottlenecks.vue Thu Nov 22 07:28:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,233 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <i class="fa fa-ship mr-2"></i> - Bottlenecks - </h6> - <div class="row p-2 text-left small"> - <div class="col-5"> - <a href="#" @click="sortBy('name')" class="sort-link">Name</a> - <i :class="sortClass" v-if="sortColumn === 'name'"></i> - </div> - <div class="col-2"> - <a - href="#" - @click="sortBy('latestMeasurement')" - class="sort-link" - >Latest Measurement</a> - <i :class="sortClass" v-if="sortColumn === 'latestMeasurement'"></i> - </div> - <div class="col-3"> - <a href="#" @click="sortBy('chainage')" class="sort-link">Chainage</a> - <i :class="sortClass" v-if="sortColumn === 'chainage'"></i> - </div> - <div class="col-2"></div> - </div> - <div class="bottleneck-list small text-left" v-if="filteredAndSortedBottlenecks().length"> - <div - v-for="bottleneck in filteredAndSortedBottlenecks()" - :key="bottleneck.properties.name" - class="border-top row mx-0 py-2" - > - <div class="col-5 text-left"> - <a - href="#" - class="d-block" - @click="moveToBottleneck(bottleneck)" - >{{ bottleneck.properties.name }}</a> - </div> - <div class="col-2">{{ displayCurrentSurvey(bottleneck.properties.current) }}</div> - <div - class="col-3" - >{{ displayCurrentChainage(bottleneck.properties.from, bottleneck.properties.from) }}</div> - <div class="col-2 text-right"> - <button - type="button" - class="btn btn-sm btn-outline-secondary" - @click="toggleBottleneck(bottleneck.properties.name)" - > - <i class="fa fa-angle-down"></i> - </button> - </div> - <div - :class="['col-12', 'surveys', {open: openBottleneck === bottleneck.properties.name}]" - > - <a - href="#" - class="d-block p-2" - v-for="(survey, index) in openBottleneckSurveys" - :key="index" - @click="selectSurvey(survey, bottleneck)" - >{{ survey.date_info }}</a> - </div> - </div> - </div> - <div v-else class="small text-center py-3 border-top"> - No results. - </div> - </div> -</template> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../lib/http"; -import { displayError } from "../../lib/errors.js"; - -export default { - name: "bottlenecks", - data() { - return { - sortColumn: "name", - sortDirection: "ASC", - openBottleneck: null, - openBottleneckSurveys: null - }; - }, - computed: { - ...mapState("application", ["searchQuery", "showSearchbarLastState"]), - ...mapState("bottlenecks", ["bottlenecks"]), - sortClass() { - return [ - "fa ml-1", - { - "fa-sort-amount-asc": this.sortDirection === "ASC", - "fa-sort-amount-desc": this.sortDirection === "DESC" - } - ]; - } - }, - methods: { - filteredAndSortedBottlenecks() { - return this.bottlenecks - .filter(bn => { - return bn.properties.name - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }) - .sort((bnA, bnB) => { - switch (this.sortColumn) { - case "name": - if ( - bnA.properties.name.toLowerCase() < - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - bnA.properties.name.toLowerCase() > - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - case "latestMeasurement": { - if ( - (bnA.properties.current || "") < (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - (bnA.properties.current || "") > (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - } - - case "chainage": - if (bnA.properties.from < bnB.properties.from) - return this.sortDirection === "ASC" ? -1 : 1; - if (bnA.properties.from > bnB.properties.from) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - default: - return 0; - } - }); - }, - selectSurvey(survey, bottleneck) { - this.$store.dispatch( - "bottlenecks/setSelectedBottleneck", - bottleneck.properties.name - ); - this.$store.commit("bottlenecks/setSelectedSurvey", survey); - this.moveToBottleneck(bottleneck); - }, - moveToBottleneck(bottleneck) { - this.$store.commit("map/moveMap", { - coordinates: bottleneck.geometry.coordinates, - zoom: 17, - preventZoomOut: true - }); - }, - sortBy(column) { - this.sortColumn = column; - this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; - }, - toggleBottleneck(name) { - this.openBottleneckSurveys = null; - if (name === this.openBottleneck) { - this.openBottleneck = null; - } else { - this.openBottleneck = name; - - HTTP.get("/surveys/" + name, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) - .then(response => { - this.openBottleneckSurveys = response.data.surveys; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - displayCurrentSurvey(current) { - return current ? current.substr(0, current.length - 1) : ""; - }, - displayCurrentChainage(from, to) { - return from / 10 + " - " + to / 10; - } - }, - mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); - } -}; -</script> - -<style lang="sass" scoped> -.bottleneck-list - overflow-y: auto - max-height: 500px - -.surveys - max-height: 0 - overflow: hidden - transition: max-height 0.3s ease - -.surveys.open - max-height: 999px - -.sort-link - color: #444 - font-weight: bold -</style>
--- a/client/src/components/map/Contextbox.vue Thu Nov 22 07:28:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -<template> - <div :class="style"> - <div @click="close" class="ui-element close-contextbox text-muted"> - <i class="fa fa-close"></i> - </div> - <Bottlenecks v-if="showInContextBox === 'bottlenecks'"></Bottlenecks> - <Imports v-if="showInContextBox === 'imports'"></Imports> - <Staging v-if="showInContextBox === 'staging'"></Staging> - </div> -</template> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "contextbox", - components: { - Bottlenecks: () => import("./Bottlenecks"), - Imports: () => import("./imports/Imports.vue"), - Staging: () => import("./Staging.vue") - }, - computed: { - ...mapState("application", ["showSearchbarLastState", "showInContextBox"]), - style() { - return [ - "ui-element shadow-xs contextbox ml-3", - { - contextboxcollapsed: !this.showInContextBox, - contextboxextended: this.showInContextBox, - "rounded-bottom": this.showInContextBox !== "imports", - rounded: this.showInContextBox === "imports" - } - ]; - } - }, - methods: { - close() { - this.$store.commit("application/showInContextBox", null); - this.$store.commit( - "application/showSearchbar", - this.showSearchbarLastState - ); - } - } -}; -</script> - -<style lang="sass" scoped> -.contextbox - position: relative - background-color: #ffffff - opacity: $slight-transparent - transition: left 0.3s ease - overflow: hidden - background: #fff - -.contextboxcollapsed - width: 0 - height: 0 - transition: $transition-fast - -.contextboxextended - min-width: 600px - -.close-contextbox - position: absolute - z-index: 2 - right: 0 - top: 7px - height: $icon-width - width: $icon-height - display: none - -.contextboxextended .close-contextbox - display: block -</style>
--- a/client/src/components/map/Staging.vue Thu Nov 22 07:28:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <i class="fa fa-list-ol mr-2"></i> - Staging Area - </h6> - <table class="table mb-0"> - <thead> - <tr> - <th>Name</th> - <th>Datatype</th> - <th>Importdate</th> - <th>ImportID</th> - <th> </th> - </tr> - </thead> - <tbody v-if="filteredData.length"> - <tr v-for="data in filteredData" :key="data.id"> - <td> - <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a> - </td> - <td>{{ data.type }}</td> - <td>{{ data.date }}</td> - <td>{{ data.importID }}</td> - <td> - <button class="btn btn-outline-info"> - <i class="fa fa-thumbs-up"></i> - </button> - - <button class="btn btn-outline-info"> - <i class="fa fa-thumbs-down"></i> - </button> - </td> - </tr> - </tbody> - <tbody v-else> - <tr> - <td class="text-center" colspan="6">No results.</td> - </tr> - </tbody> - </table> - <div class="p-3" v-if="filteredData.length"> - <button class="btn btn-info">Confirm</button> - </div> - </div> -</template> - -<script> -import { mapState } from "vuex"; - -const demodata = [ - { - id: 1, - name: "B1", - date: "2018-11-19 10:23", - location: [16.5364, 48.1471], - status: "Not approved", - importID: "123456789", - type: "bottleneck" - }, - { - id: 2, - name: "B2", - date: "2018-11-19 10:24", - location: [16.5364, 48.1472], - status: "Not approved", - importID: "123456789", - type: "bottleneck" - }, - { - id: 3, - name: "s1", - date: "2018-11-13 10:25", - location: [16.5364, 48.1473], - status: "Not approved", - importID: "987654321", - type: "soundingresult" - }, - { - id: 4, - name: "s2", - date: "2018-11-13 10:26", - location: [16.5364, 48.1474], - status: "Not approved", - importID: "987654321", - type: "soundingresult" - } -]; - -export default { - computed: { - ...mapState("application", ["searchQuery"]), - filteredData() { - return demodata.filter(data => { - const nameFound = data.name - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const dateFound = data.date - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const locationFound = data.location.find(coord => { - return coord - .toString() - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }); - const statusFound = data.status - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const importIDFound = data.importID - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const typeFound = data.type - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - - return ( - nameFound || - dateFound || - locationFound || - statusFound || - importIDFound || - typeFound - ); - }); - } - }, - methods: { - zoomTo(coordinates) { - this.$store.commit("map/moveMap", { - coordinates: coordinates, - zoom: 17, - preventZoomOut: true - }); - } - } -}; -</script> - -<style lang="sass" scoped> -.table th, -td - font-size: 0.9rem - border-top: 0px !important - border-bottom-width: 1px - text-align: left - padding: 0.5rem !important -</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/contextbox/Bottlenecks.vue Thu Nov 22 07:40:23 2018 +0100 @@ -0,0 +1,233 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <i class="fa fa-ship mr-2"></i> + Bottlenecks + </h6> + <div class="row p-2 text-left small"> + <div class="col-5"> + <a href="#" @click="sortBy('name')" class="sort-link">Name</a> + <i :class="sortClass" v-if="sortColumn === 'name'"></i> + </div> + <div class="col-2"> + <a + href="#" + @click="sortBy('latestMeasurement')" + class="sort-link" + >Latest Measurement</a> + <i :class="sortClass" v-if="sortColumn === 'latestMeasurement'"></i> + </div> + <div class="col-3"> + <a href="#" @click="sortBy('chainage')" class="sort-link">Chainage</a> + <i :class="sortClass" v-if="sortColumn === 'chainage'"></i> + </div> + <div class="col-2"></div> + </div> + <div class="bottleneck-list small text-left" v-if="filteredAndSortedBottlenecks().length"> + <div + v-for="bottleneck in filteredAndSortedBottlenecks()" + :key="bottleneck.properties.name" + class="border-top row mx-0 py-2" + > + <div class="col-5 text-left"> + <a + href="#" + class="d-block" + @click="moveToBottleneck(bottleneck)" + >{{ bottleneck.properties.name }}</a> + </div> + <div class="col-2">{{ displayCurrentSurvey(bottleneck.properties.current) }}</div> + <div + class="col-3" + >{{ displayCurrentChainage(bottleneck.properties.from, bottleneck.properties.from) }}</div> + <div class="col-2 text-right"> + <button + type="button" + class="btn btn-sm btn-outline-secondary" + @click="toggleBottleneck(bottleneck.properties.name)" + > + <i class="fa fa-angle-down"></i> + </button> + </div> + <div + :class="['col-12', 'surveys', {open: openBottleneck === bottleneck.properties.name}]" + > + <a + href="#" + class="d-block p-2" + v-for="(survey, index) in openBottleneckSurveys" + :key="index" + @click="selectSurvey(survey, bottleneck)" + >{{ survey.date_info }}</a> + </div> + </div> + </div> + <div v-else class="small text-center py-3 border-top"> + No results. + </div> + </div> +</template> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "../../../lib/http"; +import { displayError } from "../../../lib/errors.js"; + +export default { + name: "bottlenecks", + data() { + return { + sortColumn: "name", + sortDirection: "ASC", + openBottleneck: null, + openBottleneckSurveys: null + }; + }, + computed: { + ...mapState("application", ["searchQuery", "showSearchbarLastState"]), + ...mapState("bottlenecks", ["bottlenecks"]), + sortClass() { + return [ + "fa ml-1", + { + "fa-sort-amount-asc": this.sortDirection === "ASC", + "fa-sort-amount-desc": this.sortDirection === "DESC" + } + ]; + } + }, + methods: { + filteredAndSortedBottlenecks() { + return this.bottlenecks + .filter(bn => { + return bn.properties.name + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }) + .sort((bnA, bnB) => { + switch (this.sortColumn) { + case "name": + if ( + bnA.properties.name.toLowerCase() < + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + bnA.properties.name.toLowerCase() > + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + case "latestMeasurement": { + if ( + (bnA.properties.current || "") < (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + (bnA.properties.current || "") > (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + } + + case "chainage": + if (bnA.properties.from < bnB.properties.from) + return this.sortDirection === "ASC" ? -1 : 1; + if (bnA.properties.from > bnB.properties.from) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + default: + return 0; + } + }); + }, + selectSurvey(survey, bottleneck) { + this.$store.dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.properties.name + ); + this.$store.commit("bottlenecks/setSelectedSurvey", survey); + this.moveToBottleneck(bottleneck); + }, + moveToBottleneck(bottleneck) { + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + }, + sortBy(column) { + this.sortColumn = column; + this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; + }, + toggleBottleneck(name) { + this.openBottleneckSurveys = null; + if (name === this.openBottleneck) { + this.openBottleneck = null; + } else { + this.openBottleneck = name; + + HTTP.get("/surveys/" + name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.openBottleneckSurveys = response.data.surveys; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + displayCurrentSurvey(current) { + return current ? current.substr(0, current.length - 1) : ""; + }, + displayCurrentChainage(from, to) { + return from / 10 + " - " + to / 10; + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + } +}; +</script> + +<style lang="sass" scoped> +.bottleneck-list + overflow-y: auto + max-height: 500px + +.surveys + max-height: 0 + overflow: hidden + transition: max-height 0.3s ease + +.surveys.open + max-height: 999px + +.sort-link + color: #444 + font-weight: bold +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/contextbox/Contextbox.vue Thu Nov 22 07:40:23 2018 +0100 @@ -0,0 +1,90 @@ +<template> + <div :class="style"> + <div @click="close" class="ui-element close-contextbox text-muted"> + <i class="fa fa-close"></i> + </div> + <Bottlenecks v-if="showInContextBox === 'bottlenecks'"></Bottlenecks> + <Imports v-if="showInContextBox === 'imports'"></Imports> + <Staging v-if="showInContextBox === 'staging'"></Staging> + </div> +</template> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "contextbox", + components: { + Bottlenecks: () => import("./Bottlenecks"), + Imports: () => import("./imports/Imports.vue"), + Staging: () => import("./Staging.vue") + }, + computed: { + ...mapState("application", ["showSearchbarLastState", "showInContextBox"]), + style() { + return [ + "ui-element shadow-xs contextbox ml-3", + { + contextboxcollapsed: !this.showInContextBox, + contextboxextended: this.showInContextBox, + "rounded-bottom": this.showInContextBox !== "imports", + rounded: this.showInContextBox === "imports" + } + ]; + } + }, + methods: { + close() { + this.$store.commit("application/showInContextBox", null); + this.$store.commit( + "application/showSearchbar", + this.showSearchbarLastState + ); + } + } +}; +</script> + +<style lang="sass" scoped> +.contextbox + position: relative + background-color: #ffffff + opacity: $slight-transparent + transition: left 0.3s ease + overflow: hidden + background: #fff + +.contextboxcollapsed + width: 0 + height: 0 + transition: $transition-fast + +.contextboxextended + min-width: 600px + +.close-contextbox + position: absolute + z-index: 2 + right: 0 + top: 7px + height: $icon-width + width: $icon-height + display: none + +.contextboxextended .close-contextbox + display: block +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/contextbox/Staging.vue Thu Nov 22 07:40:23 2018 +0100 @@ -0,0 +1,148 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <i class="fa fa-list-ol mr-2"></i> + Staging Area + </h6> + <table class="table mb-0"> + <thead> + <tr> + <th>Name</th> + <th>Datatype</th> + <th>Importdate</th> + <th>ImportID</th> + <th> </th> + </tr> + </thead> + <tbody v-if="filteredData.length"> + <tr v-for="data in filteredData" :key="data.id"> + <td> + <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a> + </td> + <td>{{ data.type }}</td> + <td>{{ data.date }}</td> + <td>{{ data.importID }}</td> + <td> + <button class="btn btn-outline-info"> + <i class="fa fa-thumbs-up"></i> + </button> + + <button class="btn btn-outline-info"> + <i class="fa fa-thumbs-down"></i> + </button> + </td> + </tr> + </tbody> + <tbody v-else> + <tr> + <td class="text-center" colspan="6">No results.</td> + </tr> + </tbody> + </table> + <div class="p-3" v-if="filteredData.length"> + <button class="btn btn-info">Confirm</button> + </div> + </div> +</template> + +<script> +import { mapState } from "vuex"; + +const demodata = [ + { + id: 1, + name: "B1", + date: "2018-11-19 10:23", + location: [16.5364, 48.1471], + status: "Not approved", + importID: "123456789", + type: "bottleneck" + }, + { + id: 2, + name: "B2", + date: "2018-11-19 10:24", + location: [16.5364, 48.1472], + status: "Not approved", + importID: "123456789", + type: "bottleneck" + }, + { + id: 3, + name: "s1", + date: "2018-11-13 10:25", + location: [16.5364, 48.1473], + status: "Not approved", + importID: "987654321", + type: "soundingresult" + }, + { + id: 4, + name: "s2", + date: "2018-11-13 10:26", + location: [16.5364, 48.1474], + status: "Not approved", + importID: "987654321", + type: "soundingresult" + } +]; + +export default { + computed: { + ...mapState("application", ["searchQuery"]), + filteredData() { + return demodata.filter(data => { + const nameFound = data.name + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const dateFound = data.date + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const locationFound = data.location.find(coord => { + return coord + .toString() + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }); + const statusFound = data.status + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const importIDFound = data.importID + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const typeFound = data.type + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + + return ( + nameFound || + dateFound || + locationFound || + statusFound || + importIDFound || + typeFound + ); + }); + } + }, + methods: { + zoomTo(coordinates) { + this.$store.commit("map/moveMap", { + coordinates: coordinates, + zoom: 17, + preventZoomOut: true + }); + } + } +}; +</script> + +<style lang="sass" scoped> +.table th, +td + font-size: 0.9rem + border-top: 0px !important + border-bottom-width: 1px + text-align: left + padding: 0.5rem !important +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/contextbox/imports/Imports.vue Thu Nov 22 07:40:23 2018 +0100 @@ -0,0 +1,259 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <i class="fa fa-upload mr-2"></i> + Import Soundingresults + </h6> + <div v-if="editState" class="ml-auto mr-auto mt-4 w-90"> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label> + </div> + <input + id="bottleneck" + type="text" + class="form-control" + placeholder="Name of Bottleneck" + aria-label="bottleneck" + aria-describedby="bottlenecklabel" + v-model="bottleneck" + > + </div> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label class="label-text" for="importdate" id="importdatelabel">Date</label> + </div> + <input + id="importdate" + type="date" + class="form-control" + placeholder="Date of import" + aria-label="bottleneck" + aria-describedby="bottlenecklabel" + v-model="importDate" + > + </div> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label class="label-text" for="depthreference">Depth reference</label> + </div> + <select v-model="depthReference" class="custom-select" id="depthreference"> + <option + v-for="option in this.$options.depthReferenceOptions" + :key="option" + >{{option}}</option> + </select> + </div> + </div> + <div class="w-90 ml-auto mr-auto mt-4 mb-4"> + <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> + <div class="custom-file"> + <input + type="file" + @change="fileSelected" + class="custom-file-input" + id="uploadFile" + > + <label class="custom-file-label" for="uploadFile">{{uploadLabel}}</label> + </div> + </div> + <div class="buttons text-right"> + <a + v-if="editState" + download="meta.json" + :href="dataLink " + class="btn btn-outline-info pull-left" + >Download Meta.json</a> + <button + v-if="editState" + @click="deleteTempData" + class="btn btn-danger" + type="button" + >Cancel Upload</button> + <button + :disabled="disableUpload" + @click="submit" + class="btn btn-info" + type="button" + >{{uploadState?"Upload":"Confirm"}}</button> + </div> + </div> + </div> +</template> + +<script> +import { HTTP } from "../../../../lib/http"; +import { displayError, displayInfo } from "../../../../lib/errors.js"; + +const defaultLabel = "Choose .zip-file"; +const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; + +export default { + name: "imports", + data() { + return { + importState: IMPORTSTATE.UPLOAD, + depthReference: "", + bottleneck: "", + importDate: "", + uploadLabel: defaultLabel, + uploadFile: null, + disableUpload: false, + token: null + }; + }, + methods: { + initialState() { + this.importState = IMPORTSTATE.UPLOAD; + this.depthReference = ""; + this.bottleneck = ""; + this.importDate = ""; + this.uploadLabel = defaultLabel; + this.uploadFile = null; + this.disableUpload = false; + this.token = null; + }, + fileSelected(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files) return; + this.uploadLabel = files[0].name; + this.uploadFile = files[0]; + }, + deleteTempData() { + HTTP.delete("/imports/soundingresult-upload/" + this.token, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }) + .then(() => { + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + submit() { + if (!this.uploadFile || this.disableUpload) return; + if (this.importState === IMPORTSTATE.UPLOAD) { + this.upload(); + } else { + this.confirm(); + } + }, + upload() { + let formData = new FormData(); + formData.append("soundingresult", this.uploadFile); + HTTP.post("/imports/soundingresult-upload", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(response => { + const { bottleneck, date } = response.data.meta; + const depthReference = response.data.meta["depth-reference"]; + this.importState = IMPORTSTATE.EDIT; + this.bottleneck = bottleneck; + this.depthReference = depthReference; + this.importDate = new Date(date).toISOString().split("T")[0]; + this.token = response.data.token; + }) + .catch(error => { + const { status, data } = error.response; + const messages = data.messages ? data.messages.join(", ") : ""; + displayError({ + title: "Backend Error", + message: `${status}: ${messages}` + }); + }); + }, + confirm() { + let formData = new FormData(); + formData.append("token", this.token); + ["bottleneck", "importDate", "depthReference"].forEach(x => { + if (this[x]) formData.append(x, this[x]); + }); + HTTP.post("/imports/soundingresult", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(() => { + displayInfo({ + title: "Import", + message: "Starting import for " + this.bottleneck + }); + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + computed: { + editState() { + return this.importState === IMPORTSTATE.EDIT; + }, + uploadState() { + return this.importState === IMPORTSTATE.UPLOAD; + }, + dataLink() { + return ( + "data:text/json;charset=utf-8," + + encodeURIComponent( + JSON.stringify({ + depthReference: this.depthReference, + bottleneck: this.bottleneck, + date: this.importDate + }) + ) + ); + } + }, + depthReferenceOptions: [ + "", + "NAP", + "KP", + "FZP", + "ADR", + "TAW", + "PUL", + "NGM", + "ETRS", + "POT", + "LDC", + "HDC", + "ZPG", + "GLW", + "HSW", + "LNW", + "HNW", + "IGN", + "WGS", + "RN", + "HBO" + ] +}; +</script> + +<style lang="sass" scoped> +.offset-r + margin-right: $large-offset + +.buttons button + margin-left: $offset !important + +.label-text + width: 10rem + text-align: left + line-height: 2.25rem +</style>
--- a/client/src/components/map/imports/Importqueue.vue Thu Nov 22 07:28:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,170 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="mt-3 mx-auto"> - <div class="card importqueuecard"> - <div class="card-header shadow-sm text-white bg-info mb-3">Importqueue</div> - <div class="card-body importcardbody"> - <div class="card-body importcardbody"> - <div class="searchandfilter d-flex flex-row"> - <div class="searchgroup input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="search"> - <i class="fa fa-search"></i> - </span> - </div> - <input - type="text" - class="form-control" - placeholder="" - aria-label="Search" - aria-describedby="search" - > - </div> - <div class="filters"> - <button - @click="setFilter('successful')" - :class="successfulStyle" - >Successful</button> - <button @click="setFilter('failed')" :class="failedStyle">Failed</button> - <button @click="setFilter('pending')" :class="pendingStyle">Pending</button> - </div> - </div> - <table class="table"> - <thead> - <tr> - <th>Enqueued</th> - <th>Kind</th> - <th>User</th> - <th>State</th> - </tr> - </thead> - <tbody> - <tr v-for="job in imports" :key="job.id"> - <td>{{job.enqueued}}</td> - <td>{{job.kind}}</td> - <td>{{job.user}}</td> - <td>{{job.state}}</td> - </tr> - </tbody> - </table> - </div> - </div> - </div> - </div> - </div> -</template> - -<script> -import { displayError } from "../../../lib/errors.js"; -import { mapState } from "vuex"; - -export default { - name: "importqueue", - data() { - return { - successful: false, - failed: false, - pending: false - }; - }, - methods: { - setFilter(name) { - this[name] = !this[name]; - const allSet = this.successful && this.failed && this.pending; - if (allSet) { - this.all = false; - this.successful = false; - this.failed = false; - this.pending = false; - } - } - }, - computed: { - ...mapState("imports", ["imports"]), - ...mapState("application", ["showSidebar"]), - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - }, - successfulStyle() { - return { - btn: true, - "btn-light": !this.successful, - "btn-dark": this.successful - }; - }, - pendingStyle() { - return { - btn: true, - "btn-light": !this.pending, - "btn-dark": this.pending - }; - }, - failedStyle() { - return { - btn: true, - "btn-light": !this.failed, - "btn-dark": this.failed - }; - } - }, - mounted() { - this.$store.dispatch("imports/getImports").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script> - -<style lang="sass" scoped> -.spacer - height: 100vh - -.spacer-collapsed - min-width: $icon-width + $offset - transition: $transition-fast - -.spacer-expanded - min-width: $sidebar-width + $offset - -.importqueuecard - width: 80vw - min-height: 20rem - -.card-body - width: 100% - margin-left: auto - margin-right: auto - -.searchandfilter - position: relative - margin-bottom: $xx-large-offset - -.filters - position: absolute - right: 0 - -.filters button - margin-right: $small-offset - -.table td, -.table th - border-top: 0 !important - text-align: left - padding: $small-offset !important - -.searchgroup - position: absolute - left: 0 - width: 50% -</style>
--- a/client/src/components/map/imports/Imports.vue Thu Nov 22 07:28:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,259 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <i class="fa fa-upload mr-2"></i> - Import Soundingresults - </h6> - <div v-if="editState" class="ml-auto mr-auto mt-4 w-90"> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label> - </div> - <input - id="bottleneck" - type="text" - class="form-control" - placeholder="Name of Bottleneck" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="bottleneck" - > - </div> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label class="label-text" for="importdate" id="importdatelabel">Date</label> - </div> - <input - id="importdate" - type="date" - class="form-control" - placeholder="Date of import" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="importDate" - > - </div> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label class="label-text" for="depthreference">Depth reference</label> - </div> - <select v-model="depthReference" class="custom-select" id="depthreference"> - <option - v-for="option in this.$options.depthReferenceOptions" - :key="option" - >{{option}}</option> - </select> - </div> - </div> - <div class="w-90 ml-auto mr-auto mt-4 mb-4"> - <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> - <div class="custom-file"> - <input - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - > - <label class="custom-file-label" for="uploadFile">{{uploadLabel}}</label> - </div> - </div> - <div class="buttons text-right"> - <a - v-if="editState" - download="meta.json" - :href="dataLink " - class="btn btn-outline-info pull-left" - >Download Meta.json</a> - <button - v-if="editState" - @click="deleteTempData" - class="btn btn-danger" - type="button" - >Cancel Upload</button> - <button - :disabled="disableUpload" - @click="submit" - class="btn btn-info" - type="button" - >{{uploadState?"Upload":"Confirm"}}</button> - </div> - </div> - </div> -</template> - -<script> -import { HTTP } from "../../../lib/http"; -import { displayError, displayInfo } from "../../../lib/errors.js"; - -const defaultLabel = "Choose .zip-file"; -const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; - -export default { - name: "imports", - data() { - return { - importState: IMPORTSTATE.UPLOAD, - depthReference: "", - bottleneck: "", - importDate: "", - uploadLabel: defaultLabel, - uploadFile: null, - disableUpload: false, - token: null - }; - }, - methods: { - initialState() { - this.importState = IMPORTSTATE.UPLOAD; - this.depthReference = ""; - this.bottleneck = ""; - this.importDate = ""; - this.uploadLabel = defaultLabel; - this.uploadFile = null; - this.disableUpload = false; - this.token = null; - }, - fileSelected(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files) return; - this.uploadLabel = files[0].name; - this.uploadFile = files[0]; - }, - deleteTempData() { - HTTP.delete("/imports/soundingresult-upload/" + this.token, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - } - }) - .then(() => { - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - submit() { - if (!this.uploadFile || this.disableUpload) return; - if (this.importState === IMPORTSTATE.UPLOAD) { - this.upload(); - } else { - this.confirm(); - } - }, - upload() { - let formData = new FormData(); - formData.append("soundingresult", this.uploadFile); - HTTP.post("/imports/soundingresult-upload", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(response => { - const { bottleneck, date } = response.data.meta; - const depthReference = response.data.meta["depth-reference"]; - this.importState = IMPORTSTATE.EDIT; - this.bottleneck = bottleneck; - this.depthReference = depthReference; - this.importDate = new Date(date).toISOString().split("T")[0]; - this.token = response.data.token; - }) - .catch(error => { - const { status, data } = error.response; - const messages = data.messages ? data.messages.join(", ") : ""; - displayError({ - title: "Backend Error", - message: `${status}: ${messages}` - }); - }); - }, - confirm() { - let formData = new FormData(); - formData.append("token", this.token); - ["bottleneck", "importDate", "depthReference"].forEach(x => { - if (this[x]) formData.append(x, this[x]); - }); - HTTP.post("/imports/soundingresult", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(() => { - displayInfo({ - title: "Import", - message: "Starting import for " + this.bottleneck - }); - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - computed: { - editState() { - return this.importState === IMPORTSTATE.EDIT; - }, - uploadState() { - return this.importState === IMPORTSTATE.UPLOAD; - }, - dataLink() { - return ( - "data:text/json;charset=utf-8," + - encodeURIComponent( - JSON.stringify({ - depthReference: this.depthReference, - bottleneck: this.bottleneck, - date: this.importDate - }) - ) - ); - } - }, - depthReferenceOptions: [ - "", - "NAP", - "KP", - "FZP", - "ADR", - "TAW", - "PUL", - "NGM", - "ETRS", - "POT", - "LDC", - "HDC", - "ZPG", - "GLW", - "HSW", - "LNW", - "HNW", - "IGN", - "WGS", - "RN", - "HBO" - ] -}; -</script> - -<style lang="sass" scoped> -.offset-r - margin-right: $large-offset - -.buttons button - margin-left: $offset !important - -.label-text - width: 10rem - text-align: left - line-height: 2.25rem -</style>
--- a/client/src/router.js Thu Nov 22 07:28:21 2018 +0100 +++ b/client/src/router.js Thu Nov 22 07:40:23 2018 +0100 @@ -22,10 +22,10 @@ const Main = () => import("./components/map/Main.vue"); const Usermanagement = () => import("./components/admin/usermanagement/Usermanagement.vue"); -const Logs = () => import("./components/admin/logs.vue"); -const Importqueue = () => import("./components/map/imports/Importqueue.vue"); +const Logs = () => import("./components/admin/Logs.vue"); +const Importqueue = () => import("./components/admin/Importqueue.vue"); const Systemconfiguration = () => - import("./components/admin/systemconfiguration.vue"); + import("./components/admin/Systemconfiguration.vue"); Vue.use(Router);