Mercurial > gemma
changeset 2651:9f3856337f55
import_overview: new unified interface as default
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Thu, 14 Mar 2019 14:53:17 +0100 |
parents | a308baa7e7af |
children | a2dd96c305be |
files | client/src/components/Contextbox.vue client/src/components/ImportStretches.vue client/src/components/Sidebar.vue client/src/components/importoverview/ImportOverview.vue client/src/components/importoverview/ImportOverviewAlt.vue client/src/components/importoverview/importlogs/LogDetail.vue client/src/components/importoverview/importlogs/Logs.vue client/src/components/importoverview/staging/Staging.vue client/src/components/importoverview/staging/StagingDetail.vue client/src/router.js client/src/store/imports.js |
diffstat | 11 files changed, 153 insertions(+), 1497 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/Contextbox.vue Thu Mar 14 14:50:20 2019 +0100 +++ b/client/src/components/Contextbox.vue Thu Mar 14 14:53:17 2019 +0100 @@ -6,9 +6,6 @@ <ImportOverview v-if="contextBoxContent === 'importoverview'" ></ImportOverview> - <ImportOverviewAlt - v-if="contextBoxContent === 'importoverview2'" - ></ImportOverviewAlt> </div> </template> @@ -34,9 +31,7 @@ Bottlenecks: () => import("@/components/Bottlenecks"), Stretches: () => import("@/components/ImportStretches.vue"), ImportOverview: () => - import("@/components/importoverview/ImportOverview.vue"), - ImportOverviewAlt: () => - import("@/components/importoverview/ImportOverviewAlt.vue") + import("@/components/importoverview/ImportOverview.vue") }, computed: { ...mapState("application", [
--- a/client/src/components/ImportStretches.vue Thu Mar 14 14:50:20 2019 +0100 +++ b/client/src/components/ImportStretches.vue Thu Mar 14 14:53:17 2019 +0100 @@ -281,11 +281,13 @@ import { mapState, mapGetters } from "vuex"; import { displayError, displayInfo } from "@/lib/errors.js"; import { LAYERS } from "@/store/map.js"; +import { HTTP } from "@/lib/http"; export default { name: "importstretches", data() { return { + staging: [], edit: false, editExistingStretch: false, id: "", @@ -346,9 +348,12 @@ }, loadStagingData() { return new Promise((resolve, reject) => { - this.$store - .dispatch("imports/getStaging") + HTTP.get("/imports?states=pending", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) .then(response => { + const { imports } = response.data; + this.staging = imports; resolve(response); }) .catch(error => { @@ -544,7 +549,7 @@ computed: { ...mapState("map", ["identifiedFeatures", "currentMeasurement"]), ...mapGetters("user", ["isSysAdmin"]), - ...mapState("imports", ["stretches", "staging"]), + ...mapState("imports", ["stretches"]), stretchesInStaging() { const result = []; for (let stretch of this.stretches) {
--- a/client/src/components/Sidebar.vue Thu Mar 14 14:50:20 2019 +0100 +++ b/client/src/components/Sidebar.vue Thu Mar 14 14:53:17 2019 +0100 @@ -1,7 +1,7 @@ <template> <div class="position-relative"> - <span class="indicator" v-if="!showSidebar && staging.length"> - {{ staging.length }} + <span class="indicator" v-if="!showSidebar && stagingNotifications"> + {{ stagingNotifications }} </span> <div :class="sidebarStyle"> <div @@ -35,8 +35,8 @@ icon="clipboard-check" ></font-awesome-icon> <span class="fix-trans-space" v-translate>Staging area</span> - <span class="indicator" v-if="showSidebar && staging.length"> - {{ staging.length }} + <span class="indicator" v-if="showSidebar && stagingNotifications"> + {{ stagingNotifications }} </span> </router-link> </div> @@ -154,14 +154,19 @@ import { mapGetters, mapState } from "vuex"; import { logOff } from "@/lib/session.js"; import { displayError } from "@/lib/errors"; +import { HTTP } from "@/lib/http"; export default { name: "sidebar", props: ["routeName"], + data() { + return { + stagingNotifications: null + }; + }, computed: { ...mapGetters("user", ["isSysAdmin", "isWaterwayAdmin"]), ...mapState("user", ["user"]), - ...mapState("imports", ["staging"]), ...mapState("application", [ "showSidebar", "showSearchbarLastState", @@ -197,13 +202,21 @@ ); }, updateIndicators() { - this.$store.dispatch("imports/getStaging").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` + this.$store; + HTTP.get("/imports?states=pending", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { imports } = response.data; + this.stagingNotifications = imports.length; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); }); - }); } }, mounted() {
--- a/client/src/components/importoverview/ImportOverview.vue Thu Mar 14 14:50:20 2019 +0100 +++ b/client/src/components/importoverview/ImportOverview.vue Thu Mar 14 14:53:17 2019 +0100 @@ -4,68 +4,50 @@ icon="clipboard-check" title="Staging Area" :closeCallback="$parent.close" + :actions="[{ callback: loadLogs, icon: 'redo' }]" /> - <div class="d-flex flex-row w-100 justify-content-end"> - <button - class="btn btn-sm btn-outline-info align-self-start mr-3" - @click="refresh" - > - <font-awesome-icon icon="redo"></font-awesome-icon> - </button> - </div> - <div class="d-flex flex-row w-100 border-bottom"> - <font-awesome-icon - class="pointer" - @click="toggleStaging()" - v-if="stagingVisible && staging.length > 0" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - @click="toggleStaging()" - v-if="!stagingVisible && staging.length > 0" - icon="angle-down" - fixed-width - ></font-awesome-icon> - <span style="width:1.25em;" v-if="!(staging.length > 0)"></span> - <Staging v-if="stagingVisible && staging.length > 0"></Staging> - <div v-else class="d-flex flex-row"> - <h6> - <small><translate>Review</translate></small> - </h6> - <small class="ml-3" v-if="!(staging.length > 0)" - ><translate>Nothing to review</translate></small + <div class="position-relative"> + <transition name="fade"> + <div + class="loading d-flex justify-content-center align-items-center" + v-if="loading" > - </div> - </div> - <div class="mt-2"> - <div class="d-flex flex-row"> - <font-awesome-icon - class="pointer" - @click="toggleLogs()" - v-if="logsVisible" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - @click="toggleLogs()" - v-if="!logsVisible" - icon="angle-down" - fixed-width - ></font-awesome-icon> - <Logs v-if="logsVisible" :reload="reload"></Logs> - <div v-else> - <h6> - <small><translate>Logs</translate></small> - </h6> + <font-awesome-icon icon="spinner" spin /> + </div> + </transition> + <div class="p-2 d-flex flex-row flex-fill justify-content-between"> + <Filters></Filters> + <div> + <button + class="btn btn-sm btn-info" + :disabled="!reviewed.length" + @click="save" + > + <translate>Commit</translate> {{ reviewed.length }} + </button> </div> </div> + <LogEntry + class="border-top d-flex-flex-column w-100" + :entry="entry" + v-for="entry in imports" + :key="entry.id" + ></LogEntry> </div> </div> </template> +<style lang="sass" scoped> +.loading + background: rgba(255, 255, 255, 0.9) + position: absolute + z-index: 99 + top: 0 + right: 0 + bottom: 0 + left: 0 +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -80,54 +62,32 @@ * Author(s): * Thomas Junk <thomas.junk@intevation.de> */ -import { displayError } from "@/lib/errors.js"; + import { mapState } from "vuex"; +import { displayError, displayInfo } from "@/lib/errors.js"; +import { STATES } from "@/store/imports.js"; export default { - name: "importoverview", + name: "importoverviewalt", + components: { + Filters: () => import("./Filters.vue"), + LogEntry: () => import("./LogEntry.vue") + }, data() { return { - reload: false + loading: false }; }, - components: { - Staging: () => import("./staging/Staging.vue"), - Logs: () => import("./importlogs/Logs.vue") - }, computed: { - ...mapState("imports", ["stagingVisible", "logsVisible", "staging"]) + ...mapState("imports", ["imports", "filters", "reviewed"]) }, methods: { - toggleStaging() { - this.$store.commit("imports/setStagingVisibility", !this.stagingVisible); - }, - toggleLogs() { - this.$store.commit("imports/setLogsVisibility", !this.logsVisible); - }, - refresh() { - this.reload = true; - this.loadImportQueue(); - this.loadLogs(); - }, - loadImportQueue() { - this.$store - .dispatch("imports/getStaging") - .then(() => { - this.reload = false; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, loadLogs() { + this.loading = true; this.$store .dispatch("imports/getImports") .then(() => { - this.reload = false; + this.loading = false; }) .catch(error => { const { status, data } = error.response; @@ -136,17 +96,81 @@ message: `${status}: ${data.message || data}` }); }); + }, + save() { + if (!this.reviewed.length) return; + + let popupContent = `<table class="table table-sm small mb-0 border-0" style="margin-top: -1px;">`; + this.reviewed.forEach(r => { + let imp = this.imports.find(i => i.id === r.id); + let approved = STATES.APPROVED === r.status; + popupContent += `<tr> + <td>${imp.id}</td> + <td>${imp.kind.toUpperCase()}</td> + <td>${this.$options.filters.dateTime(imp.enqueued)}</td> + <td class="text-${approved ? "success" : "danger"}"> + ${this.$gettext(approved ? "approved" : "declined")} + </td> + </tr>`; + }); + popupContent += "</table>"; + + this.$store.commit("application/popup", { + icon: "clipboard-check", + title: this.$gettext("Finish Review"), + padding: false, + big: true, + content: popupContent, + confirm: { + icon: "check", + callback: () => { + let data = this.reviewed.map(r => ({ + id: r.id, + state: r.status + })); + this.$store + .dispatch("imports/confirmReview", data) + .then(response => { + this.loadLogs(); + this.$store.commit("imports/setReviewed", []); + const messages = response.data + .map(x => { + if (x.message) return x.message; + if (x.error) return x.error; + }) + .join("\n\n"); + displayInfo({ + title: "Staging Area", + message: messages, + options: { + timeout: 0, + buttons: [{ text: "Ok", action: null, bold: true }] + } + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } + }); + } + }, + watch: { + filters() { + this.$store.dispatch("imports/getImports", this.filters); } }, mounted() { - this.refresh(); + this.loadLogs(); } }; </script> - -<style lang="scss" scoped> -.overview { - max-height: 850px; - overflow-y: auto; -} -</style>
--- a/client/src/components/importoverview/ImportOverviewAlt.vue Thu Mar 14 14:50:20 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,176 +0,0 @@ -<template> - <div class="overview"> - <UIBoxHeader - icon="clipboard-check" - title="Staging Area" - :closeCallback="$parent.close" - :actions="[{ callback: loadLogs, icon: 'redo' }]" - /> - <div class="position-relative"> - <transition name="fade"> - <div - class="loading d-flex justify-content-center align-items-center" - v-if="loading" - > - <font-awesome-icon icon="spinner" spin /> - </div> - </transition> - <div class="p-2 d-flex flex-row flex-fill justify-content-between"> - <Filters></Filters> - <div> - <button - class="btn btn-sm btn-info" - :disabled="!reviewed.length" - @click="save" - > - <translate>Commit</translate> {{ reviewed.length }} - </button> - </div> - </div> - <LogEntry - class="border-top d-flex-flex-column w-100" - :entry="entry" - v-for="entry in imports" - :key="entry.id" - ></LogEntry> - </div> - </div> -</template> - -<style lang="sass" scoped> -.loading - background: rgba(255, 255, 255, 0.9) - position: absolute - z-index: 99 - top: 0 - right: 0 - bottom: 0 - left: 0 -</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 { displayError, displayInfo } from "@/lib/errors.js"; -import { STATES } from "@/store/imports.js"; - -export default { - name: "importoverviewalt", - components: { - Filters: () => import("./Filters.vue"), - LogEntry: () => import("./LogEntry.vue") - }, - data() { - return { - loading: false - }; - }, - computed: { - ...mapState("imports", ["imports", "filters", "reviewed"]) - }, - methods: { - loadLogs() { - this.loading = true; - this.$store - .dispatch("imports/getImports") - .then(() => { - this.loading = false; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - save() { - if (!this.reviewed.length) return; - - let popupContent = `<table class="table table-sm small mb-0 border-0" style="margin-top: -1px;">`; - this.reviewed.forEach(r => { - let imp = this.imports.find(i => i.id === r.id); - let approved = STATES.APPROVED === r.status; - popupContent += `<tr> - <td>${imp.id}</td> - <td>${imp.kind.toUpperCase()}</td> - <td>${this.$options.filters.dateTime(imp.enqueued)}</td> - <td class="text-${approved ? "success" : "danger"}"> - ${this.$gettext(approved ? "approved" : "declined")} - </td> - </tr>`; - }); - popupContent += "</table>"; - - this.$store.commit("application/popup", { - icon: "clipboard-check", - title: this.$gettext("Finish Review"), - padding: false, - big: true, - content: popupContent, - confirm: { - icon: "check", - callback: () => { - let data = this.reviewed.map(r => ({ - id: r.id, - state: r.status - })); - this.$store - .dispatch("imports/confirmReview", data) - .then(response => { - this.loadLogs(); - this.$store.commit("imports/setReviewed", []); - const messages = response.data - .map(x => { - if (x.message) return x.message; - if (x.error) return x.error; - }) - .join("\n\n"); - displayInfo({ - title: "Staging Area", - message: messages, - options: { - timeout: 0, - buttons: [{ text: "Ok", action: null, bold: true }] - } - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - cancel: { - label: this.$gettext("Cancel"), - icon: "times" - } - }); - } - }, - watch: { - filters() { - this.$store.dispatch("imports/getImports", this.filters); - } - }, - mounted() { - this.loadLogs(); - } -}; -</script>
--- a/client/src/components/importoverview/importlogs/LogDetail.vue Thu Mar 14 14:50:20 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,329 +0,0 @@ -<template> - <div class="entry d-flex flex-column py-1 border-bottom"> - <div class="d-flex flex-row position-relative"> - <small @click="showDetails(job.id)" class="jobid ml-2 mt-1 mr-2"> - {{ job.id }} - </small> - <small @click="showDetails(job.id)" class="enqueued mt-1 mr-2"> - {{ formatDateTime(job.enqueued) }} - </small> - <small @click="showDetails(job.id)" class="kind mt-1 mr-2"> - {{ job.kind.toUpperCase() }} - </small> - <small @click="showDetails(job.id)" class="user mt-1 mr-2"> - {{ job.user }} - </small> - <small @click="showDetails(job.id)" class="signer mt-1 mr-2"> - {{ job.signer }} - </small> - <small @click="showDetails(job.id)" class="state mt-1 mr-2"> - <span :class="{ 'text-danger': job.state.toUpperCase() == 'FAILED' }" - >{{ job.state - }}<font-awesome-icon - v-if="job.warnings" - class="ml-1 text-warning" - icon="exclamation-triangle" - fixed-width - ></font-awesome-icon> - </span> - <span v-if="!job.warnings" style="margin-right: 1.6em;"></span> - </small> - <div @click="showDetails(job.id)" class="mt-1 text-info detailsbutton"> - <font-awesome-icon - class="pointer" - v-if="show" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="loading" - icon="spinner" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="!show && !loading" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - <div class="detailstable d-flex flex-row"> - <div :class="collapse"> - <div class="text-left"> - <small style="margin-right:10px" class="type condensed" - ><translate>Kind</translate></small - > - <a - href="#" - @click="sortAsc = !sortAsc" - style="margin-right:58px" - class="datetime sort-link" - ><small class="condensed"><translate>Date</translate></small> - <small class="message condensed" - ><font-awesome-icon - :icon="sortIcon" - class="ml-1" - ></font-awesome-icon></small - ></a> - <small class="condensed"><translate>Message</translate></small> - </div> - <div class="logentries"> - <div - v-for="(entry, index) in sortedEntries" - :key="index" - class="detailsrow text-left" - > - <small - :class="[ - 'condensed type', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ entry.kind.toUpperCase() }}</small - > - <small - :class="[ - 'condensed datetime', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ formatDateTime(entry.time) }}</small - > - <small - :class="[ - 'condensed message', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ entry.message }}</small - > - </div> - </div> - </div> - </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): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { HTTP } from "@/lib/http.js"; -import { displayError } from "@/lib/errors.js"; -import locale2 from "locale2"; - -export default { - name: "importqueuedetail", - props: ["job", "reload"], - data() { - return { - loading: false, - show: false, - entries: [], - sortAsc: true - }; - }, - mounted() { - this.openSpecificDetail(); - }, - watch: { - $route() { - this.openSpecificDetail(); - }, - reload() { - if (this.reload) { - this.entries = []; - this.show = false; - } - } - }, - methods: { - openSpecificDetail() { - const { id } = this.$route.params; - if (id == this.job.id) { - this.showDetails(id); - } else { - this.show = false; - } - }, - formatDate(date) { - return date - ? new Date(date).toLocaleDateString(locale2, { - day: "2-digit", - month: "2-digit", - year: "numeric" - }) - : ""; - }, - formatDateTime(date) { - if (!date) return ""; - const d = new Date(date); - return ( - d.toLocaleDateString(locale2, { - day: "2-digit", - month: "2-digit", - year: "numeric" - }) + - " - " + - d.toLocaleTimeString(locale2, { - hour12: false - }) - ); - }, - showDetails(id) { - if (this.show) { - this.show = false; - return; - } - if (this.entries.length === 0) { - this.loading = true; - HTTP.get("/imports/" + id, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - const { entries } = response.data; - this.entries = entries; - this.show = true; - this.loading = false; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } else { - this.show = true; - } - } - }, - computed: { - sortedEntries() { - let sorted = this.entries.slice(); - sorted.sort((r1, r2) => { - let d1 = new Date(r1.time); - let d2 = new Date(r2.time); - if (d2 < d1) { - return !this.sortAsc ? -1 : 1; - } - if (d2 > d1) { - return !this.sortAsc ? 1 : -1; - } - return 0; - }); - return sorted; - }, - sortIcon() { - return this.sortAsc ? "sort-amount-down" : "sort-amount-up"; - }, - icon() { - return { - "angle-up": !this.show, - "angle-down": this.show - }; - }, - collapse() { - return { - details: true, - collapse: true, - show: this.show, - "w-100": true - }; - } - } -}; -</script> - -<style lang="scss" scoped> -.logentries { - overflow: auto; -} - -.condensed { - font-stretch: condensed; -} - -.entry { - background-color: white; - cursor: pointer; - width: 100%; -} - -.entry:hover { - background-color: #efefef; - transition: 1.6s; -} - -.detailstable { - margin-left: $offset; - margin-right: $large-offset; -} - -.detailsbutton { - position: absolute; - top: 0; - right: 0; - height: 100%; -} -.jobid { - width: 5%; -} - -.user { - width: 15%; -} - -.signer { - width: 15%; -} - -.kind { - width: 10%; -} - -.state { - width: 15%; -} - -.details { - width: 50%; -} - -.detailsrow { - line-height: 0.7rem; -} - -.type { - white-space: nowrap; -} - -.datetime { - white-space: nowrap; - padding-left: 10px; - padding-right: 10px; -} - -.message { - white-space: nowrap; -} -</style>
--- a/client/src/components/importoverview/importlogs/Logs.vue Thu Mar 14 14:50:20 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,171 +0,0 @@ -<template> - <div class="w-95"> - <div class="text-left"> - <h6><translate>Logs</translate></h6> - </div> - <div class="d-flex justify-content-between flex-row"> - <button @click="setFilter('failed')" :class="failedStyle"> - <translate>Failed</translate> - </button> - <button @click="setFilter('pending')" :class="pendingStyle"> - <translate>Pending</translate> - </button> - <button @click="setFilter('declined')" :class="rejectedStyle"> - <translate>Rejected</translate> - </button> - <button @click="setFilter('accepted')" :class="acceptedStyle"> - <translate>Accepted</translate> - </button> - <button @click="setFilter('warning')" :class="warningStyle"> - <translate>Warning</translate> - </button> - </div> - <div class="mt-3 logdetails"> - <div v-for="job in imports" :key="job.id" class="d-flex flex-row"> - <LogDetail :job="job"></LogDetail> - </div> - </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): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { mapState } from "vuex"; -import { displayError } from "@/lib/errors.js"; - -export default { - name: "logsection", - components: { - LogDetail: () => import("./LogDetail.vue") - }, - props: ["reload"], - data() { - return { - loading: false, - failed: false, - pending: false, - declined: false, - accepted: false, - warning: false - }; - }, - computed: { - ...mapState("imports", ["imports"]), - pendingStyle() { - return { - btn: true, - "btn-sm": true, - "btn-outline-info": !this.pending, - "btn-info": this.pending - }; - }, - failedStyle() { - return { - btn: true, - "btn-sm": true, - "btn-outline-info": !this.failed, - "btn-info": this.failed - }; - }, - rejectedStyle() { - return { - btn: true, - "btn-sm": true, - "btn-outline-info": !this.declined, - "btn-info": this.declined - }; - }, - acceptedStyle() { - return { - btn: true, - "btn-sm": true, - "btn-outline-info": !this.accepted, - "btn-info": this.accepted - }; - }, - warningStyle() { - return { - btn: true, - "btn-sm": true, - "btn-outline-info": !this.warning, - "btn-info": this.warning - }; - } - }, - watch: { - reload() { - if (!this.reload) return; - this.warning = false; - this.successful = false; - this.failed = false; - this.pending = false; - this.accepted = false; - this.declined = false; - } - }, - methods: { - setFilter(name) { - if (this.loading) return; - this[name] = !this[name]; - const allSet = - this.failed && - this.pending && - this.accepted && - this.declined && - this.warning; - if (allSet) { - this.warning = false; - this.successful = false; - this.failed = false; - this.pending = false; - this.accepted = false; - this.declined = false; - } - this.loadFiltered(); - }, - loadFiltered() { - this.loading = true; - const filter = [ - "failed", - "pending", - "accepted", - "declined", - "warning" - ].filter(x => this[x]); - this.$store - .dispatch("imports/getImports", filter) - .then(() => { - this.loading = false; - }) - .catch(error => { - this.loading = false; - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - } -}; -</script> - -<style lang="scss" scoped> -.logdetails { - overflow-y: auto; - max-height: 650px; -} -</style>
--- a/client/src/components/importoverview/staging/Staging.vue Thu Mar 14 14:50:20 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -<template> - <div class="w-100"> - <div class="d-flex justify-content-between flex-row w-100 border-bottom"> - <h6><translate>Review</translate></h6> - <button class="btn btn-sm btn-info align-self-end" @click="save"> - <translate>Confirm</translate> - </button> - </div> - <StagingDetail - :key="data.id" - v-for="data in filteredData" - :data="data" - ></StagingDetail> - </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): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; -import { displayError, displayInfo } from "@/lib/errors.js"; - -export default { - name: "stagingsection", - computed: { - ...mapState("imports", ["staging"]), - ...mapGetters("imports", ["processedReviews"]), - filteredData() { - return this.staging; - } - }, - methods: { - loadImportQueue() { - this.$store.dispatch("imports/getStaging").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - save() { - if (!this.processedReviews.length) return; - this.$store - .dispatch("imports/confirmReview", this.processedReviews) - .then(response => { - this.loadImportQueue(); - const messages = response.data - .map(x => { - if (x.message) return x.message; - if (x.error) return x.error; - }) - .join("\n\n"); - displayInfo({ - title: "Staging Area", - message: messages, - options: { - timeout: 0, - buttons: [{ text: "Ok", action: null, bold: true }] - } - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - components: { - StagingDetail: () => import("./StagingDetail.vue") - } -}; -</script> - -<style></style>
--- a/client/src/components/importoverview/staging/StagingDetail.vue Thu Mar 14 14:50:20 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,552 +0,0 @@ -<template> - <div :class="detail"> - <div class="d-flex flex-row"> - <div class="mt-auto d-flex flex-row mb-auto small name text-left"> - <a - v-if="isSoundingResult(data.kind.toUpperCase())" - class="text-left" - @click="zoomTo()" - href="#" - >{{ data.summary.bottleneck }}</a - > - <span v-if="isBottleneck(data.kind.toUpperCase())" class="text-left" - ><translate>Bottlenecks</translate> ({{ - data.summary.bottlenecks.length - }})</span - > - <a - v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())" - class="text-left" - ><translate>Approved Gauge Measurements</translate> ({{ - data.summary.length - }})</a - > - <span - class="text-left" - v-if="isFairwayDimension(data.kind.toUpperCase())" - >{{ data.summary["source-organization"] }} (LOS: - {{ data.summary.los }})</span - > - <a - href="#" - class="text-left" - @click="zoomToStretch(data.summary.stretch)" - v-if="isStretch(data.kind.toUpperCase())" - >{{ data.summary.stretch }}</a - > - </div> - <div class="mt-auto mb-auto small text-left type"> - {{ data.kind.toUpperCase() }} - </div> - <div v-if="data.summary" class="mt-auto mb-auto small text-left date"> - {{ data.summary.date | surveyDate }} - </div> - <div v-else class="mt-auto mb-auto small text-left date">-</div> - <div class="mt-auto mb-auto small text-left imported"> - {{ data.enqueued.split("T")[0] | surveyDate }} - </div> - <div class="mt-auto mb-auto small text-left username"> - {{ data.user }} - </div> - <div class="controls d-flex flex-row justify-content-end"> - <div> - <button - :class="{ - 'ml-3': true, - 'mr-3': true, - btn: true, - 'btn-outline-success': needsApproval(data) || isRejected(data), - 'btn-success': isApproved(data), - actions: true - }" - @click="toggleApproval(data.id, $options.STATES.APPROVED)" - > - <font-awesome-icon - class="small pointer mb-2" - icon="check" - ></font-awesome-icon> - </button> - </div> - <div> - <button - :class="{ - 'mr-3': true, - btn: true, - 'btn-outline-danger': needsApproval(data) || isApproved(data), - 'btn-danger': isRejected(data), - actions: true - }" - @click="toggleApproval(data.id, $options.STATES.REJECTED)" - > - <font-awesome-icon - icon="times" - class="small pointer mb-2" - ></font-awesome-icon> - </button> - </div> - <div - v-if=" - !isBottleneck(data.kind.toUpperCase()) || - isApprovedGaugeMeasurement(data.kind.toUpperCase()) - " - class="expander" - ></div> - <div v-if="isBottleneck(data.kind.toUpperCase())"> - <div class="mt-auto mb-auto text-info text-left"> - <font-awesome-icon - class="pointer" - @click="showDetails()" - v-if="show" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - @click="showDetails()" - v-if="loading" - icon="spinner" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - @click="showDetails()" - class="pointer" - v-if="!show && !loading" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - <div v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())"> - <div - @click="showAGMDetails = !showAGMDetails" - class="mt-auto mb-auto text-info text-left" - > - <font-awesome-icon - class="pointer" - v-if="showAGMDetails" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="!showAGMDetails" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - <div v-else class="empty"></div> - </div> - </div> - <div v-if="show && bottlenecks.length > 0" class="bottlenecksdetails"> - <div - v-for="(bottleneck, index) in bottlenecks" - :key="index" - class="d-flex flex-row" - > - <div class="d-flex flex-column"> - <div class="d-flex flex-row"> - <a @click="moveToBottleneck(index)" class="small" href="#">{{ - bottleneck.properties.objnam - }}</a> - <div - @click="showBottleneckDetails(index)" - class="small mt-auto mb-auto text-info text-left" - > - <font-awesome-icon - class="pointer" - v-if="showBottleneckDetail === index" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="!(showBottleneckDetail === index)" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - - <div class="d-flex flex-row" v-if="showBottleneckDetail === index"> - <table> - <tr - v-for="(info, index) in Object.keys(bottleneck.properties)" - :key="index" - class="mr-1 small text-muted" - > - <td class="text-left">{{ info }}</td> - <td class="pl-3 text-left"> - {{ bottleneck.properties[info] }} - </td> - </tr> - </table> - </div> - </div> - </div> - </div> - <div class="agmdetails" v-if="showAGMDetails"> - <div class="pl-3 d-flex flex-row"> - <span class="agmcode text-left" - ><small><translate>ISRS Code</translate></small></span - > - <span class="agmdetail text-left" - ><small><translate>Date of measurement</translate></small></span - > - </div> - <div class="diffs"> - <div v-for="(result, index) in data.summary" :key="index"> - <div class="pl-3 d-flex flex-row"> - <span v-if="result.versions.length == 1" class="agmcode text-left" - ><small - >{{ result["fk-gauge-id"] }} - <translate>( New )</translate></small - ></span - > - <span v-if="result.versions.length == 2" class="agmcode text-left" - ><small>{{ result["fk-gauge-id"] }}</small></span - > - <span class="agmdetail text-left" - ><small>{{ result["measure-date"] | dateTime }}</small></span - > - <div - @click="toggleDiff(index)" - class="small ml-auto mt-auto mb-auto text-info text-left" - > - <font-awesome-icon - class="pointer" - v-if="showDiff == index" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="showDiff != index" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - <div v-if="showDiff == index" class="pl-3 d-flex flex-row"> - <div> - <div class="d-flex flex-row pl-3 text-left"> - <div class="header border-bottom agmdetailskeys"> - <small><translate>Value</translate></small> - </div> - <div - v-if="result.versions.length == 2" - class="header border-bottom agmdetailsvalues" - > - <small><translate>Old</translate></small> - </div> - <div class="header border-bottom agmdetailsvalues"> - <small><translate>New</translate></small> - </div> - </div> - <div - class="d-flex flex-row pl-3 text-left" - v-for="(entry, index) in Object.keys(result.versions[0])" - :key="index" - > - <div - v-if=" - result.versions.length == 1 || - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailskeys" - > - <small>{{ entry }}</small> - </div> - <div - v-if=" - result.versions.length == 1 || - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailsvalues" - > - <small>{{ result.versions[0][entry] }}</small> - </div> - <div - v-if=" - result.versions.length == 2 && - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailsvalues" - > - <small>{{ result.versions[1][entry] }}</small> - </div> - </div> - </div> - </div> - </div> - </div> - </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): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { STATES } from "@/store/imports.js"; -import { HTTP } from "@/lib/http"; -import { WFS } from "ol/format.js"; -import { or as orFilter, equalTo as equalToFilter } from "ol/format/filter.js"; -import { displayError } from "@/lib/errors.js"; -import { mapState } from "vuex"; -import { LAYERS } from "@/store/map.js"; - -const NO_DIFF = -1; -const NO_BOTTLENECK = -1; - -export default { - name: "stagingdetail", - props: ["data"], - data() { - return { - showDiff: NO_DIFF, - showAGMDetails: false, - showBottleneckDetail: NO_BOTTLENECK, - show: false, - loading: false, - bottlenecks: [] - }; - }, - mounted() { - this.bottlenecks = []; - const { id } = this.$route.params; - this.$store.commit("imports/setImportToReview", id); - if (this.open) this.showDetails(); - }, - computed: { - ...mapState("imports", ["importToReview"]), - open() { - return this.importToReview == this.data.id; - }, - detail() { - return [ - "staging", - "d-flex", - "flex-column", - "w-100", - { - highlight: this.open && this.needsApproval(this.data) - } - ]; - } - }, - watch: { - showAGMDetails() { - if (!this.showAGMDetails) this.showDiff = NO_DIFF; - }, - open() { - this.show = this.open; - }, - $route() { - const { id } = this.$route.params; - this.$store.commit("imports/setImportToReview", id); - if (this.open) this.showDetails(); - } - }, - methods: { - showBottleneckDetails(index) { - if (index == this.showBottleneckDetail) { - this.showBottleneckDetail = NO_BOTTLENECK; - return; - } - this.showBottleneckDetail = index; - }, - toggleDiff(number) { - if (this.showDiff !== number || this.showDiff == -1) { - this.showDiff = number; - } else { - this.showDiff = -1; - } - }, - zoomToStretch(name) { - this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES); - this.$store - .dispatch("imports/loadStretch", name) - .then(response => { - if (response.data.features.length < 1) - throw new Error("no feaures found for: " + name); - this.moveToExtent(response.data.features[0]); - }) - .catch(error => { - console.log(error); - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - showDetails() { - if (!this.isBottleneck(this.data.kind.toUpperCase())) return; - if (this.show) { - this.show = false; - return; - } - if (this.bottlenecks.length > 0) { - this.show = true; - return; - } - this.loading = true; - const generateFilter = () => { - const { bottlenecks } = this.data.summary; - if (bottlenecks.length === 1) - return equalToFilter("bottleneck_id", bottlenecks[0]); - const orExpressions = bottlenecks.map(x => { - return equalToFilter("bottleneck_id", x); - }); - return orFilter(...orExpressions); - }; - const filterExpression = generateFilter(); - const bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({ - srsName: "EPSG:4326", - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["bottlenecks_geoserver"], - outputFormat: "application/json", - filter: filterExpression - }); - HTTP.post( - "/internal/wfs", - new XMLSerializer().serializeToString( - bottleneckFeatureCollectionRequest - ), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(response => { - this.bottlenecks = response.data.features; - this.show = true; - this.loading = false; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - isFairwayDimension(kind) { - return kind === "FD"; - }, - isApprovedGaugeMeasurement(kind) { - return kind === "AGM"; - }, - isBottleneck(kind) { - return kind === "BN" || kind === "UBN"; - }, - isStretch(kind) { - return kind === "ST"; - }, - isSoundingResult(kind) { - return kind === "SR"; - }, - needsApproval(item) { - return item.status === STATES.NEEDSAPPROVAL; - }, - isRejected(item) { - return item.status === STATES.REJECTED; - }, - isApproved(item) { - return item.status === STATES.APPROVED; - }, - moveToBottleneck(index) { - this.$store.commit("map/setLayerVisible", LAYERS.BOTTLENECKS); - this.moveToExtent(this.bottlenecks[index]); - }, - moveToExtent(feature) { - this.$store.commit("map/moveToExtent", { - feature: feature, - zoom: 17, - preventZoomOut: true - }); - }, - moveMap(coordinates) { - this.$store.commit("map/moveMap", { - coordinates: coordinates, - zoom: 17, - preventZoomOut: true - }); - }, - zoomTo() { - const { lat, lon, bottleneck, date } = this.data.summary; - const coordinates = [lat, lon]; - this.moveMap(coordinates); - this.$store - .dispatch("bottlenecks/setSelectedBottleneck", bottleneck) - .then(() => { - this.$store.commit("bottlenecks/setSelectedSurveyByDate", date); - }); - }, - toggleApproval(id, newStatus) { - this.$store.commit("imports/toggleApproval", { - id: id, - newStatus: newStatus - }); - } - }, - STATES: STATES -}; -</script> - -<style lang="scss" scoped> -.name { - width: 230px; -} - -.type { - width: 40px; -} - -.date { - width: 100px; -} - -.imported { - width: 100px; -} - -.username { - width: 50px; -} - -.actions { - padding: 3px; - width: 21px; - height: 21px; -} - -.bottlenecksdetails { - max-height: 300px; - overflow-y: auto; - font-stretch: condensed; - line-height: 0.9rem; -} - -.agmdetails { - max-height: 300px; - overflow-y: auto; - font-stretch: condensed; - line-height: 0.9rem; -} -</style>
--- a/client/src/router.js Thu Mar 14 14:50:20 2019 +0100 +++ b/client/src/router.js Thu Mar 14 14:53:17 2019 +0100 @@ -194,25 +194,6 @@ } }, { - path: "/imports/overview2/:id?", - name: "importoverview2", - component: Main, - meta: { - requiresAuth: true - }, - beforeEnter: (to, from, next) => { - const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; - if (!isWaterwayAdmin) { - next("/"); - } else { - store.commit("application/showContextBox", true); - store.commit("application/contextBoxContent", "importoverview2"); - store.commit("application/showSearchbar", true); - next(); - } - } - }, - { path: "/stretches", name: "stretches", component: Main,
--- a/client/src/store/imports.js Thu Mar 14 14:50:20 2019 +0100 +++ b/client/src/store/imports.js Thu Mar 14 14:53:17 2019 +0100 @@ -32,11 +32,7 @@ filters: [], stretches: [], imports: [], - staging: [], reviewed: [], - importToReview: null, - stagingVisible: true, - logsVisible: true, show: NODETAILS, showAdditional: NODETAILS, showLogs: NODETAILS @@ -76,18 +72,6 @@ init, namespaced: true, state: init(), - getters: { - processedReviews: state => { - return state.staging - .filter(x => x.status !== STATES.NEEDSAPPROVAL) - .map(r => { - return { - id: r.id, - state: r.status - }; - }); - } - }, mutations: { setFilters: (state, filters) => { state.filters = filters; @@ -120,18 +104,6 @@ }); state.imports = imports; }, - setStagingVisibility: (state, visibility) => { - state.stagingVisible = visibility; - }, - setLogsVisibility: (state, visibility) => { - state.logsVisible = visibility; - }, - setStaging: (state, staging) => { - const enriched = staging.map(x => { - return { ...x, status: STATES.NEEDSAPPROVAL }; - }); - state.staging = enriched; - }, showDetailsFor: (state, id) => { state.show = id; }, @@ -150,11 +122,6 @@ hideAdditionalLogs: state => { state.showLogs = NODETAILS; }, - setImportToReview: (state, id) => { - if (!isNaN(parseFloat(id)) && isFinite(id)) { - state.importToReview = id; - } - }, toggleApprove: (state, change) => { const { id, newStatus } = change; const stagedResult = state.imports.find(e => { @@ -172,17 +139,6 @@ state.reviewed.push({ id: stagedResult.id, status: newStatus }); } } - }, - toggleApproval: (state, change) => { - const { id, newStatus } = change; - const stagedResult = state.staging.find(e => { - return e.id === id; - }); - if (stagedResult.status === newStatus) { - stagedResult.status = STATES.NEEDSAPPROVAL; - } else { - stagedResult.status = newStatus; - } } }, actions: {