changeset 1559:5d84dcb79a54

layout importqueue
author Thomas Junk <thomas.junk@intevation.de>
date Wed, 12 Dec 2018 09:48:37 +0100
parents 0ded4c56978e
children 70421380142d
files client/src/components/Importqueue.vue client/src/components/Importqueuedetail.vue client/src/components/importqueue/Importqueue.vue client/src/components/importqueue/Importqueuedetail.vue client/src/router.js
diffstat 5 files changed, 635 insertions(+), 638 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/Importqueue.vue	Wed Dec 12 09:22:20 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,362 +0,0 @@
-<template>
-  <div class="d-flex flex-row">
-    <div :class="spacerStyle"></div>
-    <div class="mt-3 importqueuecard flex-grow-1">
-      <div class="card shadow-xs">
-        <h6
-          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-        >
-          <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon>
-          <translate class="headline">Importqueue</translate>
-        </h6>
-        <div class="card-body importcardbody">
-          <div class="card-body importcardbody">
-            <div class="searchandfilter d-flex flex-row">
-              <div class="searchgroup input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text" id="search">
-                    <font-awesome-icon icon="search"></font-awesome-icon>
-                  </span>
-                </div>
-                <input
-                  v-model="searchQuery"
-                  type="text"
-                  class="form-control"
-                  placeholder
-                  aria-label="Search"
-                  aria-describedby="search"
-                />
-              </div>
-              <div class="filters">
-                <button
-                  @click="setFilter('successful')"
-                  :class="successfulStyle"
-                >
-                  <translate>Successful</translate>
-                </button>
-                <button @click="setFilter('failed')" :class="failedStyle">
-                  <translate>Failed</translate>
-                </button>
-                <button @click="setFilter('pending')" :class="pendingStyle">
-                  <translate>Pending</translate>
-                </button>
-                <button @click="setFilter('rejected')" :class="rejectedStyle">
-                  <translate>Rejected</translate>
-                </button>
-                <button @click="setFilter('accepted')" :class="acceptedStyle">
-                  <translate>Accepted</translate>
-                </button>
-              </div>
-            </div>
-            <div class="text-left d-flex flex-row w-50 border-bottom">
-              <div class="header py-1 jobid mr-2">
-                <translate>Id</translate>
-              </div>
-              <div class="header py-1 enqueued mr-2">
-                <translate>Enqueued</translate>
-              </div>
-              <div class="header py-1 kind mr-2">
-                <translate>Kind</translate>
-              </div>
-              <div class="header py-1 user mr-2">
-                <translate>User</translate>
-              </div>
-              <div class="header py-1 signer mr-2">
-                <translate>Signer</translate>
-              </div>
-              <div class="header py-1 state mr-2">
-                <translate>State</translate>
-              </div>
-            </div>
-            <div class="text-left" v-for="job in filteredImports" :key="job.id">
-              <Importqueuedetail :job="job"></Importqueuedetail>
-            </div>
-            <div>
-              <button @click="refresh" class="btn btn-info refresh">
-                <translate>Refresh</translate>
-              </button>
-            </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):
- * Markus Kottländer <markus@intevation.de>
- */
-import { displayError } from "../lib/errors.js";
-import { mapState } from "vuex";
-import { HTTP } from "../lib/http.js";
-import Importqueuedetail from "./Importqueuedetail";
-
-export default {
-  name: "importqueue",
-  components: {
-    Importqueuedetail
-  },
-  data() {
-    return {
-      searchQuery: "",
-      successful: false,
-      failed: false,
-      pending: false,
-      rejected: false,
-      accepted: false
-    };
-  },
-  mounted() {
-    this.loadQueue();
-  },
-  methods: {
-    setFilter(name) {
-      this[name] = !this[name];
-      const allSet =
-        this.successful &&
-        this.failed &&
-        this.pending &&
-        this.accepted &&
-        this.rejected;
-      if (allSet) {
-        this.successful = false;
-        this.failed = false;
-        this.pending = false;
-        this.accepted = false;
-        this.rejected = false;
-      }
-    },
-    loadQueue() {
-      this.$store.dispatch("imports/getImports").catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: this.$gettext("Backend Error"),
-          message: `${status}: ${data.message || data}`
-        });
-      });
-    },
-    refresh() {
-      this.loadQueue();
-    },
-    showDetails(id) {
-      HTTP.get("/imports/" + id, {
-        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-      })
-        .then(response => {
-          const { entries } = response.data;
-          this.entries = entries;
-          this.$modal.show("details");
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    close() {
-      this.$modal.hide("details");
-    }
-  },
-  computed: {
-    ...mapState("imports", ["imports"]),
-    ...mapState("application", ["showSidebar"]),
-    sortIcon() {
-      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
-    },
-    filteredImports() {
-      const filtered = this.imports
-        .filter(element => {
-          if (!this.searchQuery) return true;
-          return [(element.kind, element.user, element.enqueued)].some(x => {
-            return x.toLowerCase().includes(this.searchQuery.toLowerCase());
-          });
-        })
-        .filter(y => {
-          if (
-            !this.successful &&
-            !this.failed &&
-            !this.pending &&
-            !this.accepted &&
-            !this.rejected
-          )
-            return true;
-          let filterCriteria = [];
-          if (this.successful) filterCriteria.push("successful");
-          if (this.failed) filterCriteria.push("failed");
-          if (this.pending) filterCriteria.push("pending");
-          if (this.accepted) filterCriteria.push("accepted");
-          if (this.rejected) filterCriteria.push("rejected");
-          const result = filterCriteria.map(selectedState => {
-            return y.state === selectedState;
-          });
-          return result.some(x => x);
-        });
-      return filtered;
-    },
-    spacerStyle() {
-      return [
-        "spacer ml-3",
-        {
-          "spacer-expanded": this.showSidebar,
-          "spacer-collapsed": !this.showSidebar
-        }
-      ];
-    },
-    successfulStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.successful,
-        "btn-dark": this.successful
-      };
-    },
-    pendingStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.pending,
-        "btn-dark": this.pending
-      };
-    },
-    failedStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.failed,
-        "btn-dark": this.failed
-      };
-    },
-    rejectedStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.rejected,
-        "btn-dark": this.rejected
-      };
-    },
-    acceptedStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.accepted,
-        "btn-dark": this.accepted
-      };
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.jobid {
-  width: 80px;
-}
-
-.enqueued {
-  width: 120px;
-}
-
-.user {
-  width: 80px;
-}
-
-.signer {
-  width: 80px;
-}
-
-.kind {
-  width: 80px;
-}
-
-.state {
-  width: 80px;
-}
-
-.header {
-  font-weight: bold;
-  font-size: 0.9em;
-}
-
-.details thead {
-  display: block;
-}
-.details tbody {
-  display: block;
-}
-
-.details tbody {
-  height: 260px;
-  overflow-y: auto;
-  overflow-x: hidden;
-}
-
-.closebutton {
-  top: $small-offset;
-}
-
-.refresh {
-  position: absolute;
-  right: $offset;
-  bottom: $offset;
-}
-
-.spacer {
-  height: 100vh;
-}
-
-.spacer-collapsed {
-  min-width: $icon-width + $offset;
-  transition: $transition-fast;
-}
-
-.spacer-expanded {
-  min-width: $sidebar-width;
-}
-
-.importqueuecard {
-  width: 97%;
-  margin-left: $offset;
-  margin-right: $offset;
-  min-height: 20rem;
-}
-
-.card-body {
-  width: 100%;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.searchandfilter {
-  position: relative;
-  margin-bottom: $xx-large-offset;
-}
-
-.filters {
-  position: absolute;
-  right: 0;
-}
-
-.filters button {
-  margin-right: $small-offset;
-}
-
-.table td,
-.table th {
-  border-top: 0 !important;
-  text-align: left;
-  padding: $small-offset !important;
-}
-
-.searchgroup {
-  position: absolute;
-  left: 0;
-  width: 45%;
-}
-</style>
--- a/client/src/components/Importqueuedetail.vue	Wed Dec 12 09:22:20 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,275 +0,0 @@
-<template>
-  <div class="entry d-flex flex-column py-1 border-bottom w-50">
-    <div class="d-flex flex-row position-relative">
-      <div @click="showDetails(job.id)" class="jobid ml-2 mt-2 mr-2">
-        {{ job.id }}
-      </div>
-      <div @click="showDetails(job.id)" class="enqueued mt-2  mr-2">
-        {{ formatDate(job.enqueued) }}
-      </div>
-      <div @click="showDetails(job.id)" class="kind mt-2 mr-2">
-        {{ job.kind }}
-      </div>
-      <div @click="showDetails(job.id)" class="user mt-2 mr-2">
-        {{ job.user }}
-      </div>
-      <div @click="showDetails(job.id)" class="signer mt-2 mr-2">
-        {{ job.signer }}
-      </div>
-      <div @click="showDetails(job.id)" class="state mt-2 mr-2">
-        {{ job.state }}
-      </div>
-      <div
-        @click="showDetails(job.id)"
-        class="btn btn-sm h-100 rounded-0 btn-info detailsbutton"
-      >
-        <font-awesome-icon
-          v-if="show"
-          icon="angle-up"
-          fixed-width
-        ></font-awesome-icon>
-        <font-awesome-icon
-          v-else
-          icon="angle-down"
-          fixed-width
-        ></font-awesome-icon>
-      </div>
-    </div>
-    <div class="detailstable d-flex flex-row">
-      <div :class="collapse">
-        <table class="table table-responsive">
-          <thead>
-            <tr>
-              <th class="first pb-0">
-                <small class="condensed"><translate>Kind</translate></small>
-              </th>
-              <th class="second  pb-0">
-                <a href="#" @click="sortAsc = !sortAsc" class="sort-link"
-                  ><small class="condensed"><translate>Date</translate></small>
-                  <small class="condensed"
-                    ><font-awesome-icon
-                      :icon="sortIcon"
-                      class="ml-1"
-                    ></font-awesome-icon></small
-                ></a>
-              </th>
-              <th class="third pb-0">
-                <small class="condensed"><translate>Message</translate></small>
-              </th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr
-              v-for="(entry, index) in sortedEntries"
-              :key="index"
-              class="detailsrow"
-            >
-              <td class="first">
-                <span class="condensed">{{ entry.kind }}</span>
-              </td>
-              <td class="second">
-                <span class="condensed">{{ formatDate(entry.time) }}</span>
-              </td>
-              <td class="third">
-                <span class="condensed">{{ entry.message }}</span>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </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 { HTTP } from "../lib/http.js";
-import { displayError } from "../lib/errors.js";
-import locale2 from "locale2";
-
-export default {
-  name: "importqueuedetail",
-  props: ["job"],
-  data() {
-    return {
-      show: false,
-      entries: [],
-      sortAsc: true
-    };
-  },
-  methods: {
-    formatDate(date) {
-      return date
-        ? new Date(date).toLocaleDateString(locale2, {
-            day: "2-digit",
-            month: "2-digit",
-            year: "numeric"
-          })
-        : "";
-    },
-    showDetails(id) {
-      if (this.show) {
-        this.show = false;
-        return;
-      }
-      if (this.entries.length === 0) {
-        HTTP.get("/imports/" + id, {
-          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-        })
-          .then(response => {
-            const { entries } = response.data;
-            this.entries = entries;
-            this.show = true;
-          })
-          .catch(error => {
-            const { status, data } = error.response;
-            displayError({
-              title: this.$gettext("Backend Error"),
-              message: `${status}: ${data.message || data}`
-            });
-          });
-      } else {
-        this.show = true;
-      }
-    }
-  },
-  computed: {
-    sortedEntries() {
-      let sorted = this.entries.slice();
-      sorted.sort((r1, r2) => {
-        let d1 = new Date(r1.time);
-        let d2 = new Date(r2.time);
-        if (d2 < d1) {
-          return !this.sortAsc ? -1 : 1;
-        }
-        if (d2 > d1) {
-          return !this.sortAsc ? 1 : -1;
-        }
-        return 0;
-      });
-      return sorted;
-    },
-    sortIcon() {
-      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
-    },
-    icon() {
-      return {
-        "angle-up": !this.show,
-        "angle-down": this.show
-      };
-    },
-    collapse() {
-      return {
-        details: true,
-        collapse: true,
-        show: this.show,
-        "w-100": true
-      };
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.condensed {
-  font-stretch: condensed;
-}
-
-.entry {
-  background-color: white;
-  cursor: pointer;
-}
-
-.entry:hover {
-  background-color: #f0f0f0;
-  transition: 1s;
-}
-
-.detailstable {
-  margin-left: $offset;
-  margin-right: $large-offset;
-}
-
-.detailsbutton {
-  position: absolute;
-  top: 0;
-  right: 0;
-  height: 100%;
-}
-.jobid {
-  width: 80px;
-}
-
-.enqueued {
-  width: 120px;
-}
-
-.user {
-  width: 80px;
-}
-
-.signer {
-  width: 80px;
-}
-
-.kind {
-  width: 80px;
-}
-
-.state {
-  width: 80px;
-}
-
-.details {
-  width: 50%;
-}
-
-.detailsrow {
-  line-height: 0.1em;
-}
-
-.first {
-  width: 65px;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
-}
-
-.second {
-  width: 100px;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
-}
-
-.third {
-  width: 600px;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
-}
-
-thead,
-tbody {
-  display: block;
-}
-
-tbody {
-  height: 150px;
-  overflow-y: auto;
-  overflow-x: hidden;
-}
-</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/importqueue/Importqueue.vue	Wed Dec 12 09:48:37 2018 +0100
@@ -0,0 +1,362 @@
+<template>
+  <div class="d-flex flex-row">
+    <div :class="spacerStyle"></div>
+    <div class="mt-3 importqueuecard flex-grow-1">
+      <div class="card shadow-xs">
+        <h6
+          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+        >
+          <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon>
+          <translate class="headline">Importqueue</translate>
+        </h6>
+        <div class="card-body importcardbody">
+          <div class="card-body importcardbody">
+            <div class="searchandfilter d-flex flex-row">
+              <div class="searchgroup input-group">
+                <div class="input-group-prepend">
+                  <span class="input-group-text" id="search">
+                    <font-awesome-icon icon="search"></font-awesome-icon>
+                  </span>
+                </div>
+                <input
+                  v-model="searchQuery"
+                  type="text"
+                  class="form-control"
+                  placeholder
+                  aria-label="Search"
+                  aria-describedby="search"
+                />
+              </div>
+              <div class="filters">
+                <button
+                  @click="setFilter('successful')"
+                  :class="successfulStyle"
+                >
+                  <translate>Successful</translate>
+                </button>
+                <button @click="setFilter('failed')" :class="failedStyle">
+                  <translate>Failed</translate>
+                </button>
+                <button @click="setFilter('pending')" :class="pendingStyle">
+                  <translate>Pending</translate>
+                </button>
+                <button @click="setFilter('rejected')" :class="rejectedStyle">
+                  <translate>Rejected</translate>
+                </button>
+                <button @click="setFilter('accepted')" :class="acceptedStyle">
+                  <translate>Accepted</translate>
+                </button>
+              </div>
+            </div>
+            <div class="text-left d-flex flex-row w-50 border-bottom">
+              <div class="header py-1 jobid mr-2">
+                <translate>Id</translate>
+              </div>
+              <div class="header py-1 enqueued mr-2">
+                <translate>Enqueued</translate>
+              </div>
+              <div class="header py-1 kind mr-2">
+                <translate>Kind</translate>
+              </div>
+              <div class="header py-1 user mr-2">
+                <translate>User</translate>
+              </div>
+              <div class="header py-1 signer mr-2">
+                <translate>Signer</translate>
+              </div>
+              <div class="header py-1 state mr-2">
+                <translate>State</translate>
+              </div>
+            </div>
+            <div class="text-left" v-for="job in filteredImports" :key="job.id">
+              <Importqueuedetail :job="job"></Importqueuedetail>
+            </div>
+            <div>
+              <button @click="refresh" class="btn btn-info refresh">
+                <translate>Refresh</translate>
+              </button>
+            </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):
+ * Markus Kottländer <markus@intevation.de>
+ */
+import { displayError } from "../../lib/errors.js";
+import { mapState } from "vuex";
+import { HTTP } from "../../lib/http.js";
+import Importqueuedetail from "./Importqueuedetail";
+
+export default {
+  name: "importqueue",
+  components: {
+    Importqueuedetail
+  },
+  data() {
+    return {
+      searchQuery: "",
+      successful: false,
+      failed: false,
+      pending: false,
+      rejected: false,
+      accepted: false
+    };
+  },
+  mounted() {
+    this.loadQueue();
+  },
+  methods: {
+    setFilter(name) {
+      this[name] = !this[name];
+      const allSet =
+        this.successful &&
+        this.failed &&
+        this.pending &&
+        this.accepted &&
+        this.rejected;
+      if (allSet) {
+        this.successful = false;
+        this.failed = false;
+        this.pending = false;
+        this.accepted = false;
+        this.rejected = false;
+      }
+    },
+    loadQueue() {
+      this.$store.dispatch("imports/getImports").catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+    },
+    refresh() {
+      this.loadQueue();
+    },
+    showDetails(id) {
+      HTTP.get("/imports/" + id, {
+        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+      })
+        .then(response => {
+          const { entries } = response.data;
+          this.entries = entries;
+          this.$modal.show("details");
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    },
+    close() {
+      this.$modal.hide("details");
+    }
+  },
+  computed: {
+    ...mapState("imports", ["imports"]),
+    ...mapState("application", ["showSidebar"]),
+    sortIcon() {
+      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
+    },
+    filteredImports() {
+      const filtered = this.imports
+        .filter(element => {
+          if (!this.searchQuery) return true;
+          return [(element.kind, element.user, element.enqueued)].some(x => {
+            return x.toLowerCase().includes(this.searchQuery.toLowerCase());
+          });
+        })
+        .filter(y => {
+          if (
+            !this.successful &&
+            !this.failed &&
+            !this.pending &&
+            !this.accepted &&
+            !this.rejected
+          )
+            return true;
+          let filterCriteria = [];
+          if (this.successful) filterCriteria.push("successful");
+          if (this.failed) filterCriteria.push("failed");
+          if (this.pending) filterCriteria.push("pending");
+          if (this.accepted) filterCriteria.push("accepted");
+          if (this.rejected) filterCriteria.push("rejected");
+          const result = filterCriteria.map(selectedState => {
+            return y.state === selectedState;
+          });
+          return result.some(x => x);
+        });
+      return filtered;
+    },
+    spacerStyle() {
+      return [
+        "spacer ml-3",
+        {
+          "spacer-expanded": this.showSidebar,
+          "spacer-collapsed": !this.showSidebar
+        }
+      ];
+    },
+    successfulStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.successful,
+        "btn-dark": this.successful
+      };
+    },
+    pendingStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.pending,
+        "btn-dark": this.pending
+      };
+    },
+    failedStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.failed,
+        "btn-dark": this.failed
+      };
+    },
+    rejectedStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.rejected,
+        "btn-dark": this.rejected
+      };
+    },
+    acceptedStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.accepted,
+        "btn-dark": this.accepted
+      };
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.jobid {
+  width: 80px;
+}
+
+.enqueued {
+  width: 120px;
+}
+
+.user {
+  width: 80px;
+}
+
+.signer {
+  width: 80px;
+}
+
+.kind {
+  width: 80px;
+}
+
+.state {
+  width: 80px;
+}
+
+.header {
+  font-weight: bold;
+  font-size: 0.9em;
+}
+
+.details thead {
+  display: block;
+}
+.details tbody {
+  display: block;
+}
+
+.details tbody {
+  height: 260px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.closebutton {
+  top: $small-offset;
+}
+
+.refresh {
+  position: absolute;
+  right: $offset;
+  bottom: $offset;
+}
+
+.spacer {
+  height: 100vh;
+}
+
+.spacer-collapsed {
+  min-width: $icon-width + $offset;
+  transition: $transition-fast;
+}
+
+.spacer-expanded {
+  min-width: $sidebar-width;
+}
+
+.importqueuecard {
+  width: 97%;
+  margin-left: $offset;
+  margin-right: $offset;
+  min-height: 20rem;
+}
+
+.card-body {
+  width: 100%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.searchandfilter {
+  position: relative;
+  margin-bottom: $xx-large-offset;
+}
+
+.filters {
+  position: absolute;
+  right: 0;
+}
+
+.filters button {
+  margin-right: $small-offset;
+}
+
+.table td,
+.table th {
+  border-top: 0 !important;
+  text-align: left;
+  padding: $small-offset !important;
+}
+
+.searchgroup {
+  position: absolute;
+  left: 0;
+  width: 45%;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/importqueue/Importqueuedetail.vue	Wed Dec 12 09:48:37 2018 +0100
@@ -0,0 +1,272 @@
+<template>
+  <div class="entry d-flex flex-column py-1 border-bottom w-50">
+    <div class="d-flex flex-row position-relative">
+      <div @click="showDetails(job.id)" class="jobid ml-2 mt-1 mr-2">
+        {{ job.id }}
+      </div>
+      <div @click="showDetails(job.id)" class="enqueued mt-1  mr-2">
+        {{ formatDate(job.enqueued) }}
+      </div>
+      <div @click="showDetails(job.id)" class="kind mt-1 mr-2">
+        {{ job.kind }}
+      </div>
+      <div @click="showDetails(job.id)" class="user mt-1 mr-2">
+        {{ job.user }}
+      </div>
+      <div @click="showDetails(job.id)" class="signer mt-1 mr-2">
+        {{ job.signer }}
+      </div>
+      <div @click="showDetails(job.id)" class="state mt-1 mr-2">
+        {{ job.state }}
+      </div>
+      <div @click="showDetails(job.id)" class="mt-1 text-info detailsbutton">
+        <font-awesome-icon
+          v-if="show"
+          icon="angle-up"
+          fixed-width
+        ></font-awesome-icon>
+        <font-awesome-icon
+          v-else
+          icon="angle-down"
+          fixed-width
+        ></font-awesome-icon>
+      </div>
+    </div>
+    <div class="detailstable d-flex flex-row">
+      <div :class="collapse">
+        <table class="table table-responsive">
+          <thead>
+            <tr>
+              <th class="first pb-0">
+                <small class="condensed"><translate>Kind</translate></small>
+              </th>
+              <th class="second  pb-0">
+                <a href="#" @click="sortAsc = !sortAsc" class="sort-link"
+                  ><small class="condensed"><translate>Date</translate></small>
+                  <small class="condensed"
+                    ><font-awesome-icon
+                      :icon="sortIcon"
+                      class="ml-1"
+                    ></font-awesome-icon></small
+                ></a>
+              </th>
+              <th class="third pb-0">
+                <small class="condensed"><translate>Message</translate></small>
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr
+              v-for="(entry, index) in sortedEntries"
+              :key="index"
+              class="detailsrow"
+            >
+              <td class="first">
+                <span class="condensed">{{ entry.kind }}</span>
+              </td>
+              <td class="second">
+                <span class="condensed">{{ formatDate(entry.time) }}</span>
+              </td>
+              <td class="third">
+                <span class="condensed">{{ entry.message }}</span>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </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 { HTTP } from "../../lib/http.js";
+import { displayError } from "../../lib/errors.js";
+import locale2 from "locale2";
+
+export default {
+  name: "importqueuedetail",
+  props: ["job"],
+  data() {
+    return {
+      show: false,
+      entries: [],
+      sortAsc: true
+    };
+  },
+  methods: {
+    formatDate(date) {
+      return date
+        ? new Date(date).toLocaleDateString(locale2, {
+            day: "2-digit",
+            month: "2-digit",
+            year: "numeric"
+          })
+        : "";
+    },
+    showDetails(id) {
+      if (this.show) {
+        this.show = false;
+        return;
+      }
+      if (this.entries.length === 0) {
+        HTTP.get("/imports/" + id, {
+          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+        })
+          .then(response => {
+            const { entries } = response.data;
+            this.entries = entries;
+            this.show = true;
+          })
+          .catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      } else {
+        this.show = true;
+      }
+    }
+  },
+  computed: {
+    sortedEntries() {
+      let sorted = this.entries.slice();
+      sorted.sort((r1, r2) => {
+        let d1 = new Date(r1.time);
+        let d2 = new Date(r2.time);
+        if (d2 < d1) {
+          return !this.sortAsc ? -1 : 1;
+        }
+        if (d2 > d1) {
+          return !this.sortAsc ? 1 : -1;
+        }
+        return 0;
+      });
+      return sorted;
+    },
+    sortIcon() {
+      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
+    },
+    icon() {
+      return {
+        "angle-up": !this.show,
+        "angle-down": this.show
+      };
+    },
+    collapse() {
+      return {
+        details: true,
+        collapse: true,
+        show: this.show,
+        "w-100": true
+      };
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.condensed {
+  font-stretch: condensed;
+}
+
+.entry {
+  background-color: white;
+  cursor: pointer;
+}
+
+.entry:hover {
+  background-color: #efefef;
+  transition: 1.5s;
+}
+
+.detailstable {
+  margin-left: $offset;
+  margin-right: $large-offset;
+}
+
+.detailsbutton {
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+}
+.jobid {
+  width: 80px;
+}
+
+.enqueued {
+  width: 120px;
+}
+
+.user {
+  width: 80px;
+}
+
+.signer {
+  width: 80px;
+}
+
+.kind {
+  width: 80px;
+}
+
+.state {
+  width: 80px;
+}
+
+.details {
+  width: 50%;
+}
+
+.detailsrow {
+  line-height: 0.1em;
+}
+
+.first {
+  width: 65px;
+  padding-left: 0px;
+  border-top: 0px;
+  padding-bottom: $small-offset;
+}
+
+.second {
+  width: 100px;
+  padding-left: 0px;
+  border-top: 0px;
+  padding-bottom: $small-offset;
+}
+
+.third {
+  width: 600px;
+  padding-left: 0px;
+  border-top: 0px;
+  padding-bottom: $small-offset;
+}
+
+thead,
+tbody {
+  display: block;
+}
+
+tbody {
+  height: 150px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+</style>
--- a/client/src/router.js	Wed Dec 12 09:22:20 2018 +0100
+++ b/client/src/router.js	Wed Dec 12 09:48:37 2018 +0100
@@ -24,7 +24,7 @@
 const Usermanagement = () =>
   import("./components/usermanagement/Usermanagement.vue");
 const Logs = () => import("./components/Logs.vue");
-const Importqueue = () => import("./components/Importqueue.vue");
+const Importqueue = () => import("./components/importqueue/Importqueue.vue");
 const Importschedule = () =>
   import("./components/importschedule/Importschedule.vue");
 const Systemconfiguration = () =>