Mercurial > gemma
changeset 2403:a4f36c481f4b staging_consolidation
wip
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Wed, 27 Feb 2019 16:21:45 +0100 |
parents | 7600bb49e158 |
children | 228387d5f2c5 |
files | client/src/components/importoverview/ImportOverview.vue client/src/components/importoverview/staging/Staging.vue client/src/components/importoverview/staging/StagingDetail.vue |
diffstat | 3 files changed, 716 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/ImportOverview.vue Wed Feb 27 16:21:45 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 Wed Feb 27 16:21:45 2019 +0100 @@ -0,0 +1,81 @@ +<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: { + save() { + if (!this.processedReviews.length) return; + this.$store + .dispatch("imports/confirmReview", this.processedReviews) + .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 }] + } + }); + }) + .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 Wed Feb 27 16:21:45 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>