Mercurial > gemma
view client/src/components/importoverview/ImportOverview.vue @ 5095:e21cbb9768a2
Prevent duplicate fairway areas
In principal, there can be only one or no fairway area at each point
on the map. Since polygons from real data will often be topologically
inexact, just disallow equal geometries. This will also help to
avoid importing duplicates with concurrent imports, once the history
of fairway dimensions will be preserved.
author | Tom Gottfried <tom@intevation.de> |
---|---|
date | Wed, 25 Mar 2020 18:10:02 +0100 |
parents | 9f0830a1845d |
children | 03f28c5d2a88 |
line wrap: on
line source
<template> <div class="overview"> <UIBoxHeader icon="clipboard-check" :title="importReviewLabel" :closeCallback="$parent.close" :actions="[ { callback: saveImportsView, icon: 'download' }, { callback: loadUpdatedLogs, icon: 'sync' } ]" /> <div class="position-relative"> <UISpinnerOverlay v-if="loading" /> <div class="border-bottom p-2 d-flex justify-content-between"> <Filters></Filters> <button class="btn btn-xs btn-info" :disabled="!reviewed.length" @click="save" > <translate>Commit</translate> {{ reviewed.length }} </button> </div> <div class="p-2 d-flex align-items-center justify-content-between border-bottom" > <button :disabled="!this.prev" @click="earlier" class="btn btn-xs btn-outline-secondary" > <font-awesome-icon icon="angle-left" fixed-width /> <translate>Earlier</translate> </button> <div class="d-flex align-items-center small"> {{ interval[0] | dateTime(selectedInterval !== $options.LAST_HOUR) }} <template v-if="selectedInterval !== $options.TODAY"> <span class="mx-2">–</span> {{ interval[1] | dateTime(selectedInterval !== $options.LAST_HOUR) }} </template> <select style="width: 75px; height: 24px" class="form-control form-control-sm small ml-2" v-model="selectedInterval" > <option :value="$options.LAST_HOUR"> <translate>Hour</translate> </option> <option :value="$options.TODAY"><translate>Day</translate></option> <option :value="$options.LAST_7_DAYS"> <translate>7 days</translate> </option> <option :value="$options.LAST_30_DAYS"> <translate>30 Days</translate> </option> </select> </div> <div class="btn-group"> <button :disabled="!this.next" @click="later" class="btn btn-xs btn-outline-secondary" > <translate>Later</translate> <font-awesome-icon icon="angle-right" fixed-width /> </button> <button :disabled="!this.next" @click="now" class="btn btn-xs btn-outline-secondary" > <font-awesome-icon icon="angle-double-right" fixed-width /> </button> </div> </div> <UITableHeader :columns="[ { id: 'id', title: `${idLabel}`, width: '70px' }, { id: 'kind', title: `${kindLabel.replace('fm_').toUpperCase()}`, width: '125px' }, { id: 'enqueued', title: `${enqueuedLabel}`, width: '135px' }, { id: 'user', title: `${ownerLabel}`, width: '80px' }, { id: 'country', title: `${countryLabel}`, width: '50px' }, { id: 'signer', title: `${signerLabel}`, width: '80px' }, { id: 'state', title: `${statusLabel}`, width: '72px' }, { id: 'changed', title: `${changedLabel}`, width: '138px' }, { id: 'warnings', icon: 'exclamation-triangle', width: '44px' } ]" /> <!-- For server-side sorting, etc simply don't use the sortTable filter. Instead you could just pass a function that loads the imports, like: :data="loadImports(sortColumn, sortDirection)" --> <UITableBody :data="filteredImports() | sortTable(sortColumn, sortDirection)" :isActive="item => item.id === this.show" maxHeight="70vh" > <template v-slot:row="{ item: entry }"> <LogEntry :entry="entry"></LogEntry> </template> <template v-slot:expand="{ item: entry }"> <LogDetail :entry="entry"></LogDetail> </template> </UITableBody> </div> </div> </template> <style lang="sass" scoped> .spinner-overlay top: 110px </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> * Markus Kottländer <markus.kottlaender@intevation.de> */ import { mapState, mapGetters } from "vuex"; import { displayError, displayInfo } from "@/lib/errors"; import { STATES } from "@/store/imports"; import { sortTable } from "@/lib/mixins"; import { HTTP } from "@/lib/http"; import app from "@/main"; import { saveAs } from "file-saver"; import { startOfDay, startOfHour, endOfHour, endOfDay, addDays, subDays, format } from "date-fns"; export default { components: { Filters: () => import("./Filters"), LogEntry: () => import("./LogEntry"), LogDetail: () => import("./LogDetail") }, mixins: [sortTable], LAST_HOUR: "lasthour", TODAY: "today", LAST_7_DAYS: "lastsevendays", LAST_30_DAYS: "lastthirtydays", data() { return { loading: false, selectedInterval: this.$options.LAST_HOUR }; }, computed: { ...mapState("application", ["searchQuery"]), ...mapState("imports", [ "show", "imports", "reviewed", "startDate", "endDate", "prev", "next" ]), ...mapGetters("usermanagement", ["userCountries"]), ...mapGetters("imports", ["filters"]), countryLabel() { return this.$gettext("Country"); }, importReviewLabel() { return this.$gettext("Import review"); }, idLabel() { return this.$gettext("Id"); }, kindLabel() { return this.$gettext("Kind"); }, enqueuedLabel() { return this.$gettext("Enqueued"); }, changedLabel() { return this.$gettext("Changed"); }, ownerLabel() { return this.$gettext("Owner"); }, signerLabel() { return this.$gettext("Signer"); }, statusLabel() { return this.$gettext("Status"); }, interval() { return [this.startDate, this.endDate]; } }, watch: { $route() { const { id } = this.$route.params; if (id) this.showSingleRessource(id); }, selectedInterval() { this.loadUpdatedLogs(); }, imports() { if (this.imports.length == 0) { if (this.next) { const [start, end] = this.determineInterval(this.next); this.$store.commit("imports/setStartDate", start); this.$store.commit("imports/setEndDate", end); this.loadLogs(); } else if (this.prev) { const [start, end] = this.determineInterval(this.prev); this.$store.commit("imports/setStartDate", start); this.$store.commit("imports/setEndDate", end); this.loadLogs(); } } }, filters() { this.loadLogs(); } }, methods: { saveImportsView() { const content = ""; this.$store.commit("application/popup", { icon: "clipboard-check", title: this.$gettext("Export logs"), padding: false, big: true, content: content, confirm: { icon: "check", callback: dates => { const { from, to } = dates; displayInfo({ title: "Generating CSV", message: `${from} - ${to}`, options: { timeout: 0, buttons: [{ text: "Ok", action: null, bold: true }] } }); HTTP.get( `/imports/export?from=${encodeURIComponent( format(startOfDay(new Date(from)), "YYYY-MM-DDTHH:mm:ssZ") )}&to=${encodeURIComponent( format(endOfDay(new Date(to)), "YYYY-MM-DDTHH:mm:ssZ") )}&query=`, { headers: { "X-Gemma-Auth": localStorage.getItem("token") } } ) .then(response => { const imports = response.data; app.$snotify.clear(); if (!imports) return; const csvFile = new Blob([new TextEncoder().encode(imports)], { type: "text/csv" }); saveAs(csvFile, "log.csv"); }) .catch(error => { console.log(error); const { status, data } = error.response; app.$snotify.clear(); displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` }); }); } }, cancel: { label: this.$gettext("Cancel"), icon: "times" }, dateSelection: true }); }, showSingleRessource(id) { id = id * 1; this.loadDetails(id) .then(response => { this.$store.commit("imports/setCurrentDetails", response.data); const { enqueued } = response.data; this.$store.commit("imports/setStartDate", startOfHour(enqueued)); this.$store.commit("imports/setEndDate", endOfHour(enqueued)); this.$store.commit("imports/showDetailsFor", id); this.loadLogs(); }) .catch(error => { this.loading = false; this.$store.commit("imports/setCurrentDetails", {}); const { status, data } = error.response; displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` }); }); }, loadDetails(id) { return new Promise((resolve, reject) => { HTTP.get("/imports/" + id, { headers: { "X-Gemma-Auth": localStorage.getItem("token") } }) .then(response => { resolve(response); }) .catch(error => { reject(error); }); }); }, determineInterval(pointInTime) { let start, end; switch (this.selectedInterval) { case this.$options.LAST_HOUR: start = startOfHour(pointInTime); end = endOfHour(pointInTime); break; case this.$options.TODAY: start = startOfDay(pointInTime); end = endOfDay(pointInTime); break; case this.$options.LAST_7_DAYS: start = startOfDay(pointInTime); end = endOfDay(addDays(start, 7)); break; case this.$options.LAST_30_DAYS: start = startOfDay(pointInTime); end = endOfDay(addDays(start, 30)); break; } return [start, end]; }, earlier() { if (!this.prev) return; const [start, end] = this.determineInterval(this.prev); this.$store.commit("imports/setStartDate", start); this.$store.commit("imports/setEndDate", end); this.loadLogs(); }, later() { if (!this.next) return; const [start, end] = this.determineInterval(this.next); this.$store.commit("imports/setStartDate", start); this.$store.commit("imports/setEndDate", end); this.loadLogs(); }, now() { if (!this.next) return; const [start, end] = this.determineInterval(new Date()); this.$store.commit("imports/setStartDate", start); this.$store.commit("imports/setEndDate", end); this.loadLogs(); }, filteredImports() { return this.imports.map(x => { x["country"] = this.userCountries[x.user]; return x; }); }, loadUpdatedLogs() { const now = new Date(); switch (this.selectedInterval) { case this.$options.LAST_HOUR: this.$store.commit("imports/setStartDate", startOfHour(now)); this.$store.commit("imports/setEndDate", now); break; case this.$options.TODAY: this.$store.commit("imports/setStartDate", startOfDay(now)); this.$store.commit("imports/setEndDate", now); break; case this.$options.LAST_7_DAYS: this.$store.commit( "imports/setStartDate", subDays(startOfDay(now), 7) ); this.$store.commit("imports/setEndDate", now); break; case this.$options.LAST_30_DAYS: this.$store.commit( "imports/setStartDate", subDays(startOfDay(now), 30) ); this.$store.commit("imports/setEndDate", now); break; } this.loadLogs(); }, loadLogs() { this.loading = true; this.$store .dispatch("imports/getImports", { filter: this.filters, from: encodeURIComponent( format(this.startDate, "YYYY-MM-DDTHH:mm:ssZ") ), to: encodeURIComponent(format(this.endDate, "YYYY-MM-DDTHH:mm:ssZ")), query: this.searchQuery }) .then(() => { if (this.show) { this.loadDetails(this.show) .then(response => { this.$store.commit("imports/setCurrentDetails", response.data); this.loading = false; }) .catch(error => { this.loading = false; this.$store.commit("imports/setCurrentDetails", {}); const { status, data } = error.response; displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` }); }); } else { this.loading = false; } }) .catch(error => { this.loading = false; let message = "Backend not reachable"; if (error.response) { const { status, data } = error.response; message = `${status}: ${data.message || data}`; } displayError({ title: this.$gettext("Backend Error"), message: message }); }); }, 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 approved = STATES.APPROVED === r.status; popupContent += `<tr> <td>${r.id}</td> <td>${r.kind.toUpperCase()}</td> <td>${this.$options.filters.dateTime(r.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", []); this.$store.commit("map/startRefreshLayers"); this.$store.commit("gauges/deleteNashSutcliffeCache"); this.$store.dispatch("map/refreshLayers"); this.$store.dispatch("imports/loadStagingNotifications"); this.$store.dispatch("imports/loadStretches"); this.$store.dispatch("imports/loadSections"); this.$store.commit("map/reviewActive", false); this.$nextTick(() => { this.$store.commit("map/finishRefreshLayers"); }); 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 => { let message = "Backend not reachable"; if (error.response) { const { status, data } = error.response; message = `${status}: ${data.message || data}`; } displayError({ title: this.$gettext("Backend Error"), message: message }); }); } }, cancel: { label: this.$gettext("Cancel"), icon: "times" } }); } }, mounted() { this.loadUpdatedLogs(); this.$store.dispatch("usermanagement/loadUsers").catch(error => { let message = "Backend not reachable"; if (error.response) { const { status, data } = error.response; message = `${status}: ${data.message || data}`; } displayError({ title: this.$gettext("Backend Error"), message: message }); }); const { id } = this.$route.params; if (id) { this.showSingleRessource(id); } else { this.$store.commit("application/searchQuery", ""); this.loadLogs(); } }, activated() { this.loadUpdatedLogs(); } }; </script>