view client/src/components/importoverview/ImportOverview.vue @ 2799:e19fac818aab

import_overview: specifying single imports via URL should open the overview with all logentries of the according hour and open the details for the specified import
author Thomas Junk <thomas.junk@intevation.de>
date Mon, 25 Mar 2019 16:16:58 +0100
parents 718ec88fbeed
children fb7cc59f5e12
line wrap: on
line source

<template>
  <div class="overview">
    <UIBoxHeader
      icon="clipboard-check"
      :title="importReviewLabel"
      :closeCallback="$parent.close"
      :actions="[{ callback: loadUpdatedLogs, icon: 'redo' }]"
    />
    <div class="position-relative">
      <transition name="fade">
        <div class="loading" v-if="loading">
          <font-awesome-icon icon="spinner" spin />
        </div>
      </transition>
      <div class="p-2 mb-1 d-flex flex-row flex-fill justify-content-between">
        <Filters></Filters>
        <div>
          <button
            class="btn btn-sm btn-info"
            :disabled="!reviewed.length"
            @click="save"
          >
            <translate>Commit</translate> {{ reviewed.length }}
          </button>
        </div>
      </div>
      <div
        class="ml-2 mr-2 mb-2 datefilter d-flex flex-row justify-content-between"
      >
        <div class="mr-3 my-auto pointer">
          <button
            :disabled="!this.prev"
            @click="earlier"
            class="btn btn-sm btn-outline-light text-dark"
          >
            <translate>Earlier</translate>
            <font-awesome-icon class="ml-2" icon="angle-left" />
          </button>
        </div>
        <div class="selected-interval my-auto">
          <span class="date">{{ interval[0] | dateTime }}</span>
          <span class="ml-3 mr-3">-</span>
          <span class="date">{{ interval[1] | dateTime }}</span>
        </div>
        <div class="ml-3 my-auto pointer">
          <button
            :disabled="!this.next"
            @click="later"
            class="btn btn-sm btn-outline-light text-dark"
          >
            <font-awesome-icon class="mr-2" icon="angle-right" /><translate
              >Later</translate
            >
          </button>
        </div>
        <div class="d-flex flex-row">
          <select
            class="my-auto btn btn-outline-light text-dark form-control interval-select"
            v-model="selectedInterval"
          >
            <option
              :selected="selectedInterval === $options.LAST_HOUR"
              :value="$options.LAST_HOUR"
              ><translate>Hour</translate></option
            >
            <option
              :selected="selectedInterval === $options.TODAY"
              :value="$options.TODAY"
              ><translate>Day</translate></option
            >
            <option
              :selected="selectedInterval === $options.LAST_7_DAYS"
              :value="$options.LAST_7_DAYS"
              ><translate>7 days</translate></option
            >
            <option
              :selected="selectedInterval === $options.LAST_30_DAYS"
              :value="$options.LAST_30_DAYS"
              ><translate>30 Days</translate></option
            >
          </select>
        </div>
      </div>
      <UITableHeader
        :columns="[
          { id: 'id', title: `${idLabel}`, width: '79px' },
          { id: 'kind', title: `${kindLabel}`, width: '53px' },
          { id: 'enqueued', title: `${enqueuedLabel}`, width: '138px' },
          { id: 'user', title: `${userLabel}`, width: '105px' },
          { id: 'signer', title: `${signerLabel}`, width: '105px' },
          { id: 'state', title: `${statusLabel}`, width: '72px' },
          { 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)"
        maxHeight="80vh"
        v-slot="{ item: entry }"
      >
        <LogEntry :entry="entry"></LogEntry>
      </UITableBody>
    </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.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.js";

import {
  startOfDay,
  startOfHour,
  endOfHour,
  endOfDay,
  addDays,
  subDays,
  format
} from "date-fns";

export default {
  name: "importoverview",
  mixins: [sortTable],
  components: {
    Filters: () => import("./Filters.vue"),
    LogEntry: () => import("./LogEntry.vue")
  },
  data() {
    return {
      loading: false,
      selectedInterval: this.$options.LAST_HOUR
    };
  },
  LAST_HOUR: "lasthour",
  TODAY: "today",
  LAST_7_DAYS: "lastsevendays",
  LAST_30_DAYS: "lastthirtydays",
  computed: {
    ...mapState("application", ["searchQuery"]),
    ...mapState("imports", [
      "show",
      "imports",
      "reviewed",
      "startDate",
      "endDate",
      "prev",
      "next"
    ]),
    ...mapGetters("imports", ["filters"]),
    importReviewLabel() {
      return this.$gettext("Import review");
    },
    idLabel() {
      return this.$gettext("Id");
    },
    kindLabel() {
      return this.$gettext("Kind");
    },
    enqueuedLabel() {
      return this.$gettext("Enqueued");
    },
    userLabel() {
      return this.$gettext("User");
    },
    signerLabel() {
      return this.$gettext("Signer");
    },
    statusLabel() {
      return this.$gettext("Status");
    },
    interval() {
      return [this.startDate, this.endDate];
    }
  },
  methods: {
    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(7, start));
          break;
        case this.$options.LAST_30_DAYS:
          start = startOfDay(pointInTime);
          end = endOfDay(addDays(30, start));
          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();
    },
    filteredImports() {
      return this.imports;
    },
    loadUpdatedLogs() {
      const [start, end] = this.determineInterval(new Date());
      this.$store.commit("imports/setStartDate", start);
      this.$store.commit("imports/setEndDate", end);
      this.loadLogs();
    },
    loadLogs() {
      this.loading = true;
      this.$store
        .dispatch("imports/getImports", {
          filter: this.filters,
          from: format(this.startDate, "YYYY-MM-DDTHH:mm:ss.SSS"),
          to: format(this.endDate, "YYYY-MM-DDTHH:mm:ss.SSS"),
          query: this.searchQuery
        })
        .then(() => {
          if (this.show != -1) {
            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 => {
          const { status, data } = error.response;
          this.loading = false;
          displayError({
            title: this.$gettext("Backend Error"),
            message: `${status}: ${data.message || data}`
          });
        });
    },
    save() {
      if (!this.reviewed.length) return;

      let popupContent = `<table class="table table-sm small mb-0 border-0" style="margin-top: -1px;">`;
      this.reviewed.forEach(r => {
        let imp = this.imports.find(i => i.id === r.id);
        let approved = STATES.APPROVED === r.status;
        popupContent += `<tr>
          <td>${imp.id}</td>
          <td>${imp.kind.toUpperCase()}</td>
          <td>${this.$options.filters.dateTime(imp.enqueued)}</td>
          <td class="text-${approved ? "success" : "danger"}">
            ${this.$gettext(approved ? "approved" : "declined")}
          </td>
        </tr>`;
      });
      popupContent += "</table>";

      this.$store.commit("application/popup", {
        icon: "clipboard-check",
        title: this.$gettext("Finish Review"),
        padding: false,
        big: true,
        content: popupContent,
        confirm: {
          icon: "check",
          callback: () => {
            let data = this.reviewed.map(r => ({
              id: r.id,
              state: r.status
            }));
            this.$store
              .dispatch("imports/confirmReview", data)
              .then(response => {
                this.loadLogs();
                this.$store.commit("imports/setReviewed", []);
                const messages = response.data
                  .map(x => {
                    if (x.message) return x.message;
                    if (x.error) return x.error;
                  })
                  .join("\n\n");
                displayInfo({
                  title: "Staging Area",
                  message: messages,
                  options: {
                    timeout: 0,
                    buttons: [{ text: "Ok", action: null, bold: true }]
                  }
                });
              })
              .catch(error => {
                const { status, data } = error.response;
                displayError({
                  title: "Backend Error",
                  message: `${status}: ${data.message || data}`
                });
              });
          }
        },
        cancel: {
          label: this.$gettext("Cancel"),
          icon: "times"
        }
      });
    }
  },
  watch: {
    $route() {
      const { id } = this.$route.params;
      if (id) this.showSingleRessource(id);
    },
    selectedInterval() {
      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();
    },
    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();
    }
  },
  mounted() {
    const { id } = this.$route.params;
    if (!id) {
      this.$store.commit("application/searchQuery", "");
      this.loadLogs();
    } else {
      this.showSingleRessource(id);
    }
  }
};
</script>

<style lang="scss" scoped>
.overview {
  max-height: 90vh;
}
.date {
  font-stretch: condensed;
}
.interval-select {
  padding: 0px;
  margin: 0px;
  font-size: 80%;
}
</style>