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>