Mercurial > gemma
changeset 2421:e61ca8310dc9
merging with staging_consolidation
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Thu, 28 Feb 2019 14:04:05 +0100 |
parents | 4625ae70f076 (current diff) cef666846f6b (diff) |
children | 77baf4f0ee1e |
files | client/src/components/importqueue/Importqueue.vue client/src/components/importqueue/Importqueuedetail.vue client/src/components/staging/Staging.vue client/src/components/staging/StagingDetail.vue |
diffstat | 12 files changed, 784 insertions(+), 1524 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/App.vue Thu Feb 28 12:45:40 2019 +0100 +++ b/client/src/components/App.vue Thu Feb 28 14:04:05 2019 +0100 @@ -90,7 +90,9 @@ ...mapState("user", ["isAuthenticated"]), ...mapState("application", ["contextBoxContent", "showSearchbar"]), isMapVisible() { - return /stretches|review|bottlenecks|mainview/.test(this.routeName); + return /importoverview|stretches|review|bottlenecks|mainview/.test( + this.routeName + ); }, routeName() { const routeName = this.$route.name;
--- a/client/src/components/Contextbox.vue Thu Feb 28 12:45:40 2019 +0100 +++ b/client/src/components/Contextbox.vue Thu Feb 28 14:04:05 2019 +0100 @@ -3,6 +3,9 @@ <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks> <Staging v-if="contextBoxContent === 'staging'"></Staging> <Stretches v-if="contextBoxContent === 'stretches'"></Stretches> + <ImportOverview + v-if="contextBoxContent === 'importoverview'" + ></ImportOverview> </div> </template> @@ -25,9 +28,10 @@ export default { name: "contextbox", components: { - Bottlenecks: () => import("./Bottlenecks"), - Staging: () => import("./staging/Staging.vue"), - Stretches: () => import("./ImportStretches.vue") + Bottlenecks: () => import("@/components/Bottlenecks"), + Stretches: () => import("@/components/ImportStretches.vue"), + ImportOverview: () => + import("@/components/importoverview/ImportOverview.vue") }, computed: { ...mapState("application", [
--- a/client/src/components/Sidebar.vue Thu Feb 28 12:45:40 2019 +0100 +++ b/client/src/components/Sidebar.vue Thu Feb 28 14:04:05 2019 +0100 @@ -28,7 +28,7 @@ <span class="fix-trans-space" v-translate>Bottlenecks</span> </router-link> <div v-if="isWaterwayAdmin"> - <router-link to="/review" class="position-relative"> + <router-link to="/imports/overview" class="position-relative"> <font-awesome-icon class="fa-fw mr-2" fixed-width @@ -126,16 +126,6 @@ <span class="fix-trans-space" v-translate>Logs</span> </router-link> </div> - <div v-if="isWaterwayAdmin"> - <router-link to="/importqueue"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="tasks" - ></font-awesome-icon> - <span class="fix-trans-space" v-translate>Importqueue</span> - </router-link> - </div> <hr class="m-0" /> <a @click="logoff" href="#" class="logout"> <font-awesome-icon
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/ImportOverview.vue Thu Feb 28 14:04:05 2019 +0100 @@ -0,0 +1,117 @@ +<template> + <div> + <UIBoxHeader + icon="clipboard-check" + title="Staging Area" + :closeCallback="$parent.close" + /> + <div class="d-flex flex-row w-100 justify-content-end"> + <button class="btn btn-dark align-self-start" @click="refresh"> + <translate>Refresh</translate> + </button> + </div> + <div class="d-flex flex-row w-100 border-bottom"> + <font-awesome-icon + class="pointer" + @click="toggleStaging()" + v-if="stagingVisible" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + @click="toggleStaging()" + v-if="!stagingVisible" + icon="angle-down" + fixed-width + ></font-awesome-icon> + <Staging v-if="stagingVisible"></Staging> + <div v-else><h5>Staging</h5></div> + </div> + <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"></Logs> + <div v-else><h5>Logs</h5></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 { displayError } from "@/lib/errors.js"; +import { mapState } from "vuex"; + +export default { + name: "importoverview", + components: { + Staging: () => import("./staging/Staging.vue"), + Logs: () => import("./logs/Logs.vue") + }, + computed: { + ...mapState("imports", ["stagingVisible", "logsVisible"]) + }, + methods: { + toggleStaging() { + this.$store.commit("imports/setStagingVisibility", !this.stagingVisible); + }, + toggleLogs() { + this.$store.commit("imports/setLogsVisibility", !this.logsVisible); + }, + refresh() { + this.loadImportQueue(); + this.loadLogs(); + }, + loadImportQueue() { + this.$store + .dispatch("imports/getImports") + .then(() => {}) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + loadLogs() { + this.$store.dispatch("imports/getStaging").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + this.refresh(); + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/staging/Staging.vue Thu Feb 28 14:04:05 2019 +0100 @@ -0,0 +1,94 @@ +<template> + <div class="w-100"> + <div class="d-flex justify-content-between flex-row w-100 border-bottom"> + <h2>Staging</h2> + <button class="btn btn-info align-self-end" @click="save"> + <translate>Confirm</translate> + </button> + </div> + <StagingDetail + class="mb-3 border-bottom" + :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/getImports") + .then(() => {}) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("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>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/staging/StagingDetail.vue Thu Feb 28 14:04:05 2019 +0100 @@ -0,0 +1,518 @@ +<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"> + {{ formatSurveyDate(data.summary.date) }} + </div> + <div v-else class="mt-auto mb-auto small text-left date">-</div> + <div class="mt-auto mb-auto small text-left imported"> + {{ formatSurveyDate(data.enqueued.split("T")[0]) }} + </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-sm': true, + 'btn-outline-success': needsApproval(data) || isRejected(data), + 'btn-success': isApproved(data) + }" + @click="toggleApproval(data.id, $options.STATES.APPROVED)" + > + <font-awesome-icon icon="check"></font-awesome-icon> + </button> + </div> + <div> + <button + :class="{ + 'mr-3': true, + btn: true, + 'btn-sm': true, + 'btn-outline-danger': needsApproval(data) || isApproved(data), + 'btn-danger': isRejected(data) + }" + @click="toggleApproval(data.id, $options.STATES.REJECTED)" + > + <font-awesome-icon icon="times" class="pointer"></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="condensed text-left">{{ info }}</td> + <td class="condensed pl-3 text-left"> + {{ bottleneck.properties[info] }} + </td> + </tr> + </table> + </div> + </div> + </div> + </div> + <div v-if="showAGMDetails"> + <div class="pl-3 d-flex flex-row"> + <span class="condensed agmcode text-left" + ><small><translate>ISRS Code</translate></small></span + > + <span class="condensed 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="condensed agmcode text-left" + ><small + >{{ result["fk-gauge-id"] }} + <translate>( New )</translate></small + ></span + > + <span + v-if="result.versions.length == 2" + class="condensed agmcode text-left" + ><small>{{ result["fk-gauge-id"] }}</small></span + > + <span class="condensed agmdetail text-left" + ><small>{{ formatDateTime(result["measure-date"]) }}</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 condensed 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 condensed 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 { formatSurveyDate, formatDateTime } from "@/lib/date.js"; +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 [ + "pb-2", + "pt-2", + "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"; + }, + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + formatDateTime(date) { + return formatDateTime(date); + }, + 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></style>
--- a/client/src/components/importqueue/Importqueue.vue Thu Feb 28 12:45:40 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,351 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <Spacer></Spacer> - <div class="mt-3 importqueuecard flex-grow-1"> - <div class="card shadow-xs"> - <UIBoxHeader icon="tasks" title="Importqueue" /> - <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"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <input - v-model="searchQuery" - type="text" - class="form-control" - placeholder - aria-label="Search" - aria-describedby="search" - /> - </div> - <div class="filters"> - <button @click="setFilter('failed')" :class="failedStyle"> - <translate>Failed</translate> - </button> - <button @click="setFilter('pending')" :class="pendingStyle"> - <translate>Pending</translate> - </button> - <button @click="setFilter('rejected')" :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> - <div class="text-left d-flex flex-row border-bottom entries"> - <div class="header py-1 jobid mr-2"> - <translate>Id</translate> - </div> - <div class="header py-1 enqueued mr-2"> - <translate>Enqueued</translate> - </div> - <div class="header py-1 kind mr-2"> - <translate>Kind</translate> - </div> - <div class="header py-1 user mr-2"> - <translate>User</translate> - </div> - <div class="header py-1 signer mr-2"> - <translate>Signer</translate> - </div> - <div class="header py-1 state mr-2"> - <translate>State</translate> - </div> - </div> - <div class="importqueuedetail"> - <div - class="text-left" - v-for="job in filteredImports" - :key="job.id" - > - <Importqueuedetail - :reload="reload" - :job="job" - ></Importqueuedetail> - </div> - </div> - <div> - <button @click="refresh" class="btn btn-info refresh"> - <translate>Refresh</translate> - </button> - </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): - * Markus Kottländer <markus@intevation.de> - */ -import { displayError } from "@/lib/errors.js"; -import { mapState } from "vuex"; -import { HTTP } from "@/lib/http.js"; - -export default { - name: "importqueue", - components: { - Importqueuedetail: () => import("./Importqueuedetail"), - Spacer: () => import("@/components/Spacer") - }, - data() { - return { - reload: false, - searchQuery: "", - successful: false, - failed: false, - pending: false, - rejected: false, - accepted: false, - warning: false - }; - }, - mounted() { - this.loadQueue(); - }, - methods: { - setFilter(name) { - this[name] = !this[name]; - const allSet = - this.failed && - this.pending && - this.accepted && - this.rejected && - this.warning; - if (allSet) { - this.warning = false; - this.successful = false; - this.failed = false; - this.pending = false; - this.accepted = false; - this.rejected = false; - } - }, - loadQueue() { - this.reload = true; - this.$store - .dispatch("imports/getImports") - .then(() => { - this.reload = false; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - refresh() { - this.loadQueue(); - }, - showDetails(id) { - HTTP.get("/imports/" + id, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - const { entries } = response.data; - this.entries = entries; - this.$modal.show("details"); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - close() { - this.$modal.hide("details"); - } - }, - computed: { - ...mapState("imports", ["imports"]), - ...mapState("application", ["showSidebar"]), - sortIcon() { - return this.sortAsc ? "sort-amount-down" : "sort-amount-up"; - }, - filteredImports() { - const filtered = this.imports - .filter(element => { - if (!this.searchQuery) return true; - return [(element.kind, element.user, element.enqueued)].some(x => { - return x.toLowerCase().includes(this.searchQuery.toLowerCase()); - }); - }) - .filter(y => { - if ( - !this.failed && - !this.pending && - !this.accepted && - !this.rejected && - !this.warning - ) - return true; - let filterCriteria = []; - if (this.failed) filterCriteria.push("failed"); - if (this.pending) filterCriteria.push("pending"); - if (this.accepted) filterCriteria.push("accepted"); - if (this.rejected) filterCriteria.push("declined"); - const result = filterCriteria.map(selectedState => { - return y.state === selectedState; - }); - if (this.warning) return result.some(x => x) || y.warnings; - return result.some(x => x); - }); - return filtered; - }, - pendingStyle() { - return { - btn: true, - "btn-light": !this.pending, - "btn-dark": this.pending - }; - }, - failedStyle() { - return { - btn: true, - "btn-light": !this.failed, - "btn-dark": this.failed - }; - }, - rejectedStyle() { - return { - btn: true, - "btn-light": !this.rejected, - "btn-dark": this.rejected - }; - }, - acceptedStyle() { - return { - btn: true, - "btn-light": !this.accepted, - "btn-dark": this.accepted - }; - }, - warningStyle() { - return { - btn: true, - "btn-light": !this.warning, - "btn-dark": this.warning - }; - } - } -}; -</script> - -<style lang="scss" scoped> -.importqueuedetail { - margin-bottom: 3rem; -} -.entries { - width: 100%; -} - -.jobid { - width: 15%; -} - -.enqueued { - width: 15%; -} - -.user { - width: 15%; -} - -.signer { - width: 15%; -} - -.kind { - width: 10%; -} - -.state { - width: 15%; -} - -.details thead { - display: block; -} -.details tbody { - display: block; -} - -.details tbody { - height: 260px; - overflow-y: auto; - overflow-x: hidden; -} - -.closebutton { - top: $small-offset; -} - -.refresh { - position: absolute; - right: $offset; - bottom: $offset; -} - -.importqueuecard { - width: 97%; - margin-right: $offset; - 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: 45%; -} -</style>
--- a/client/src/components/importqueue/Importqueuedetail.vue Thu Feb 28 12:45:40 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,359 +0,0 @@ -<template> - <div class="entry d-flex flex-column py-1 border-bottom"> - <div class="d-flex flex-row position-relative"> - <div @click="showDetails(job.id)" class="jobid ml-2 mt-1 mr-2"> - {{ job.id }} - </div> - <div @click="showDetails(job.id)" class="enqueued mt-1 mr-2"> - {{ formatDateTime(job.enqueued) }} - </div> - <div @click="showDetails(job.id)" class="kind mt-1 mr-2"> - {{ job.kind.toUpperCase() }} - </div> - <div @click="showDetails(job.id)" class="user mt-1 mr-2"> - {{ job.user }} - </div> - <div @click="showDetails(job.id)" class="signer mt-1 mr-2"> - {{ job.signer }} - </div> - <div @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> - </div> - <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"> - <table class="table table-responsive"> - <thead> - <tr> - <th class="type pb-0"> - <small class="condensed"><translate>Kind</translate></small> - </th> - <th class="datetime pb-0"> - <a href="#" @click="sortAsc = !sortAsc" class="sort-link" - ><small class="condensed"><translate>Date</translate></small> - <small class="condensed" - ><font-awesome-icon - :icon="sortIcon" - class="ml-1" - ></font-awesome-icon></small - ></a> - </th> - <th class="message pb-0"> - <small class="condensed"><translate>Message</translate></small> - </th> - </tr> - </thead> - <tbody> - <tr - v-for="(entry, index) in sortedEntries" - :key="index" - class="detailsrow" - > - <td class="type"> - <span - :class="[ - 'condensed', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ entry.kind.toUpperCase() }}</span - > - </td> - <td class="datetime"> - <span - :class="[ - 'condensed', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ formatDateTime(entry.time) }}</span - > - </td> - <td class="message"> - <span - :class="[ - 'condensed', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ entry.message }}</span - > - </td> - </tr> - </tbody> - </table> - </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> -.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: 15%; -} - -.enqueued { - width: 15%; -} - -.user { - width: 15%; -} - -.signer { - width: 15%; -} - -.kind { - width: 10%; -} - -.state { - width: 15%; -} - -.details { - width: 50%; -} - -.detailsrow { - line-height: 0.1em; -} - -.type { - width: 65px; - white-space: nowrap; - padding-left: 0px; - border-top: 0px; - padding-bottom: $small-offset; -} - -.datetime { - width: 200px; - white-space: nowrap; - padding-left: 0px; - border-top: 0px; - padding-bottom: $small-offset; -} - -.message { - min-width: 700px; - white-space: nowrap; - padding-left: 0px; - border-top: 0px; - padding-bottom: $small-offset; -} - -thead, -tbody { - display: block; -} - -tbody { - height: 150px; - overflow-y: auto; - overflow-x: auto; -} -</style>
--- a/client/src/components/staging/Staging.vue Thu Feb 28 12:45:40 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,193 +0,0 @@ -<template> - <div class="w-90 stagingcard"> - <UIBoxHeader - icon="clipboard-check" - title="Staging Area" - :closeCallback="$parent.close" - /> - <div class="mt-3 pl-3 pr-3"> - <div class="mt-3 text-left flex-row d-flex border-bottom"> - <div class="header text-left name"><translate>Name</translate></div> - <div class="header text-left type"><translate>Type</translate></div> - <div class="header text-left date"><translate>Date</translate></div> - <div class="header text-left imported"> - <translate>Imported</translate> - </div> - <div class="header text-left username"> - <translate>Username</translate> - </div> - <div class="ml-3 controls"></div> - </div> - <div class="mt-3 stagingdetails details" v-if="filteredData.length > 0"> - <StagingDetail - class="mb-3 border-bottom" - :key="data.id" - v-for="data in filteredData" - :data="data" - ></StagingDetail> - </div> - </div> - <div class="mt-3 p-3" v-if="filteredData.length > 0"> - <button @click="confirmReview" class="confirm-button btn btn-info"> - <translate>Confirm</translate> - </button> - </div> - <div v-else class="mr-auto ml-auto"><translate>No results.</translate></div> - <div class="mt-1 p-3"> - <button @click="loadData" class="refresh btn btn-dark">Refresh</button> - </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> - * Markus Kottländer <markus@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "@/lib/http.js"; -import { displayError, displayInfo } from "@/lib/errors.js"; -import { STATES } from "@/store/imports.js"; - -export default { - data() { - return {}; - }, - components: { - StagingDetail: () => import("./StagingDetail") - }, - mounted() { - this.loadData(); - }, - computed: { - ...mapState("application", ["searchQuery"]), - ...mapState("imports", ["staging"]), - filteredData() { - return this.staging.filter(data => { - const result = [data.id + "", data.enqueued, data.kind, data.user].some( - x => x.toLowerCase().includes(this.searchQuery.toLowerCase()) - ); - return result; - }); - } - }, - methods: { - loadData() { - this.$store.dispatch("imports/getStaging").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - confirmReview() { - const reviewResults = this.staging - .filter(x => x.status !== STATES.NEEDSAPPROVAL) - .map(r => { - return { - id: r.id, - state: r.status - }; - }); - if (!reviewResults.length) return; - HTTP.patch("/imports", reviewResults, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - 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 }] - } - }); - this.loadData(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - STATES: STATES -}; -</script> - -<style lang="scss" scoped> -.stagingdetails { - overflow-y: auto; - max-height: 250px; -} -.name { - width: 180px; -} - -.date { - width: 90px; -} - -.type { - width: 40px; -} - -.imported { - width: 90px; -} - -.username { - width: 150px; -} - -.controls { - width: 60px; -} - -.refresh { - position: absolute; - left: $offset; - bottom: $offset; -} -.table th, -td { - font-size: 0.9rem; - border-top: 0px !important; - border-bottom-width: 1px; - text-align: left; - padding: 0.5rem !important; -} - -.stagingcard { - position: relative; - min-height: 150px; -} - -.confirm-button { - position: absolute; - right: $offset; - bottom: $offset; -} -</style>
--- a/client/src/components/staging/StagingDetail.vue Thu Feb 28 12:45:40 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,583 +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"> - {{ formatSurveyDate(data.summary.date) }} - </div> - <div v-else class="mt-auto mb-auto small text-left date">-</div> - <div class="mt-auto mb-auto small text-left imported"> - {{ formatSurveyDate(data.enqueued.split("T")[0]) }} - </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-sm': true, - 'btn-outline-success': needsApproval(data) || isRejected(data), - 'btn-success': isApproved(data) - }" - @click="toggleApproval(data.id, $options.STATES.APPROVED)" - > - <font-awesome-icon icon="check"></font-awesome-icon> - </button> - </div> - <div> - <button - :class="{ - 'mr-3': true, - btn: true, - 'btn-sm': true, - 'btn-outline-danger': needsApproval(data) || isApproved(data), - 'btn-danger': isRejected(data) - }" - @click="toggleApproval(data.id, $options.STATES.REJECTED)" - > - <font-awesome-icon icon="times" class="pointer"></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="condensed text-left">{{ info }}</td> - <td class="condensed pl-3 text-left"> - {{ bottleneck.properties[info] }} - </td> - </tr> - </table> - </div> - </div> - </div> - </div> - <div v-if="showAGMDetails"> - <div class="pl-3 d-flex flex-row"> - <span class="condensed agmcode text-left" - ><small><translate>ISRS Code</translate></small></span - > - <span class="condensed 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="condensed agmcode text-left" - ><small - >{{ result["fk-gauge-id"] }} - <translate>( New )</translate></small - ></span - > - <span - v-if="result.versions.length == 2" - class="condensed agmcode text-left" - ><small>{{ result["fk-gauge-id"] }}</small></span - > - <span class="condensed agmdetail text-left" - ><small>{{ formatDateTime(result["measure-date"]) }}</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 condensed 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 condensed 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 { formatSurveyDate, formatDateTime } from "@/lib/date.js"; -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 [ - "pb-2", - "pt-2", - "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"; - }, - formatSurveyDate(date) { - return formatSurveyDate(date); - }, - formatDateTime(date) { - return formatDateTime(date); - }, - 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> -.expander { - margin-right: 1.25rem; -} - -.bottlenecksdetails { - overflow-y: auto; - max-height: 250px; -} -.diffs { - overflow-y: auto; - max-height: 250px; -} - -.highlight { - background-color: #f9f9f9; -} - -.condensed { - font-stretch: condensed; -} - -.empty { - margin-right: 20px; -} - -.name { - width: 180px; -} - -.date { - width: 90px; -} - -.type { - width: 40px; -} - -.imported { - width: 90px; -} - -.username { - width: 150px; -} - -.controls { - width: 60px; -} - -.agmcode { - width: 200px; -} - -.agmdate { - width: 100px; -} - -.agmdetailskeys { - width: 130px; -} - -.agmdetailsvalues { - width: 200px; -} -</style>
--- a/client/src/router.js Thu Feb 28 12:45:40 2019 +0100 +++ b/client/src/router.js Thu Feb 28 14:04:05 2019 +0100 @@ -81,22 +81,6 @@ } }, { - path: "/importqueue/:id?", - name: "importqueue", - component: () => import("./components/importqueue/Importqueue.vue"), - meta: { - requiresAuth: true - }, - beforeEnter: (to, from, next) => { - const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; - if (!isWaterwayAdmin) { - next("/"); - } else { - next(); - } - } - }, - { path: "/importsoundingresults", name: "importsoundingresults", component: () => import("./components/ImportSoundingresults.vue"), @@ -191,8 +175,8 @@ } }, { - path: "/review/:id?", - name: "review", + path: "/imports/overview/:id?", + name: "importoverview", component: Main, meta: { requiresAuth: true @@ -202,9 +186,8 @@ if (!isWaterwayAdmin) { next("/"); } else { - store.commit("application/searchQuery", ""); store.commit("application/showContextBox", true); - store.commit("application/contextBoxContent", "staging"); + store.commit("application/contextBoxContent", "importoverview"); store.commit("application/showSearchbar", true); next(); }
--- a/client/src/store/imports.js Thu Feb 28 12:45:40 2019 +0100 +++ b/client/src/store/imports.js Thu Feb 28 14:04:05 2019 +0100 @@ -30,7 +30,9 @@ stretches: [], imports: [], staging: [], - importToReview: null + importToReview: null, + stagingVisible: true, + logsVisible: true }; }; @@ -67,6 +69,18 @@ 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: { setStretches: (state, stretches) => { state.stretches = stretches; @@ -74,6 +88,12 @@ setImports: (state, imports) => { 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 }; @@ -138,9 +158,11 @@ }); }); }, - getImports({ commit }) { + getImports({ commit }, filter) { + let queryParams = ""; + if (filter) queryParams = "?states=" + filter.join(","); return new Promise((resolve, reject) => { - HTTP.get("/imports", { + HTTP.get("/imports" + queryParams, { headers: { "X-Gemma-Auth": localStorage.getItem("token") } }) .then(response => { @@ -165,6 +187,22 @@ reject(error); }); }); + }, + confirmReview({ state }, reviewResults) { + return new Promise((resolve, reject) => { + HTTP.patch("/imports", reviewResults, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); } } };