view client/src/components/importoverview/ImportOverview.vue @ 2748:c6695d6e9334

import_overview: filtering date interval via backend
author Thomas Junk <thomas.junk@intevation.de>
date Thu, 21 Mar 2019 11:34:48 +0100
parents d2896bb852e7
children cd789302b3e2
line wrap: on
line source

<template>
  <div class="overview">
    <UIBoxHeader
      icon="clipboard-check"
      title="Import review"
      :closeCallback="$parent.close"
      :actions="[{ callback: loadLogs, 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
            @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="!laterPossible"
            @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: 'Id', width: '79px' },
          { id: 'kind', title: 'Kind', width: '53px' },
          { id: 'enqueued', title: 'Enqueued', width: '138px' },
          { id: 'user', title: 'User', width: '105px' },
          { id: 'signer', title: 'Signer', width: '105px' },
          { id: 'state', title: 'Status', 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 {
  startOfDay,
  startOfHour,
  endOfHour,
  addHours,
  subHours,
  endOfDay,
  addDays,
  subDays,
  isFuture,
  format
} from "date-fns";

export default {
  name: "importoverview",
  mixins: [sortTable],
  components: {
    Filters: () => import("./Filters.vue"),
    LogEntry: () => import("./LogEntry.vue")
  },
  data() {
    return {
      loading: false,
      startDate: subHours(new Date(), 1),
      endDate: new Date(),
      selectedInterval: this.$options.LAST_HOUR,
      laterPossible: false
    };
  },
  LAST_HOUR: "lasthour",
  TODAY: "today",
  LAST_7_DAYS: "lastsevendays",
  LAST_30_DAYS: "lastthirtydays",
  computed: {
    ...mapState("application", ["searchQuery"]),
    ...mapState("imports", ["imports", "reviewed"]),
    ...mapGetters("imports", ["filters"]),
    interval() {
      return [this.startDate, this.endDate];
    }
  },
  methods: {
    earlier() {
      switch (this.selectedInterval) {
        case this.$options.LAST_HOUR:
          this.startDate = subHours(startOfHour(this.startDate), 1);
          this.endDate = endOfHour(this.startDate);
          this.laterPossible = true;
          break;
        case this.$options.TODAY:
          this.startDate = subDays(startOfDay(this.startDate), 1);
          this.endDate = endOfDay(this.startDate);
          this.laterPossible = true;
          break;
        case this.$options.LAST_7_DAYS:
          this.startDate = subDays(startOfDay(this.startDate), 7);
          this.endDate = endOfDay(addDays(this.startDate, 7));
          this.laterPossible = true;
          break;
        case this.$options.LAST_30_DAYS:
          this.startDate = subDays(startOfDay(this.startDate), 30);
          this.endDate = endOfDay(addDays(this.startDate, 30));
          this.laterPossible = true;
          break;
      }
      this.loadLogs();
    },
    later() {
      let start, end;
      const now = new Date();
      switch (this.selectedInterval) {
        case this.$options.LAST_HOUR:
          start = addHours(startOfHour(this.startDate), 1);
          end = endOfHour(start);
          this.laterPossible = !isFuture(end);
          if (isFuture(subHours(end, 1))) {
            return;
          }
          this.startDate = start;
          this.endDate = isFuture(end) ? now : end;
          break;
        case this.$options.TODAY:
          start = addDays(startOfDay(this.startDate), 1);
          end = endOfDay(start);
          this.laterPossible = !isFuture(end);
          if (isFuture(subDays(end, 1))) {
            return;
          }
          this.startDate = start;
          this.endDate = isFuture(end) ? now : end;
          break;
        case this.$options.LAST_7_DAYS:
          start = addDays(startOfDay(this.startDate), 7);
          end = endOfDay(addDays(start, 7));
          this.laterPossible = !isFuture(end);
          if (isFuture(subDays(end, 7))) {
            return;
          }
          this.startDate = start;
          this.endDate = isFuture(end) ? now : end;
          break;
        case this.$options.LAST_30_DAYS:
          start = addDays(startOfDay(this.startDate), 30);
          end = endOfDay(addDays(start, 30));
          this.laterPossible = !isFuture(end);
          if (isFuture(subDays(end, 7))) return;
          this.startDate = start;
          this.endDate = isFuture(end) ? now : end;
          break;
      }
      this.loadLogs();
    },
    filteredImports() {
      return this.imports.filter(i => {
        return (i.kind + i.id)
          .toLowerCase()
          .includes(this.searchQuery.toLowerCase());
      });
    },
    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")
        })
        .then(() => {
          this.loading = false;
        })
        .catch(error => {
          const { status, data } = error.response;
          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: {
    selectedInterval() {
      const now = new Date();
      switch (this.selectedInterval) {
        case this.$options.LAST_HOUR:
          this.startDate = subHours(now, 1);
          this.endDate = now;
          break;
        case this.$options.TODAY:
          this.startDate = startOfDay(now);
          this.endDate = now;
          break;
        case this.$options.LAST_7_DAYS:
          this.startDate = subDays(startOfDay(now), 7);
          this.endDate = now;
          break;
        case this.$options.LAST_30_DAYS:
          this.startDate = subDays(startOfDay(now), 30);
          this.endDate = now;
          break;
      }
      this.loadLogs();
    },
    filters() {
      this.loadLogs();
    }
  },
  mounted() {
    this.loadLogs();
  }
};
</script>

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