changeset 2969:b92a8d088d8a unified_import

merge with default
author Thomas Junk <thomas.junk@intevation.de>
date Tue, 02 Apr 2019 10:07:48 +0200
parents 8b32574bed09 (current diff) 6f7b8755eb07 (diff)
children 149a8f81f99e
files client/src/components/ui/SpinnerOverlay.vue
diffstat 18 files changed, 328 insertions(+), 350 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/assets/application.scss	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/assets/application.scss	Tue Apr 02 10:07:48 2019 +0200
@@ -197,12 +197,12 @@
 
 select.form-control-sm.small {
   padding: 0.25rem 0.1rem;
-  font-size: 80%;
+  font-size: 0.75rem;
 }
 
 input.form-control-sm.small {
   padding: 0.25rem 0.2rem;
-  font-size: 80%;
+  font-size: 0.75rem;
 }
 
 .empty {
--- a/client/src/components/Bottlenecks.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/Bottlenecks.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -24,7 +24,7 @@
     <UITableBody
       :data="filteredBottlenecks() | sortTable(sortColumn, sortDirection)"
       :maxHeight="(showSplitscreen ? 18 : 35) + 'rem'"
-      :active="openBottleneck"
+      :isActive="item => item === this.openBottleneck"
     >
       <template v-slot:row="{ item: bottleneck }">
         <div class="table-cell truncate text-left" style="width: 230px">
--- a/client/src/components/importoverview/AdditionalLog.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importoverview/AdditionalLog.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -1,18 +1,12 @@
 <template>
   <div
     :class="[
-      'additionallog',
-      'd-flex',
-      'flex-column',
-      'text-left',
-      {
-        full: showAdditional === $options.NODETAILS,
-        split: showAdditional !== $options.NODETAILS
-      }
+      'additionallog d-flex flex-column text-left',
+      { split: showAdditional }
     ]"
   >
     <div
-      class="d-flex flex-row"
+      class="d-flex flex-row px-2 border-top"
       v-for="(line, index) in details.entries"
       :key="index"
     >
@@ -36,7 +30,7 @@
             'font-weight-bold': /warn|error/.test(line.kind)
           }
         ]"
-        >{{ line.time }}</span
+        >{{ line.time | dateTime }}</span
       >
       <span
         :class="[
@@ -53,6 +47,31 @@
   </div>
 </template>
 
+<style lang="sass" scoped>
+.additionallog
+  max-height: 70vh
+  overflow-y: auto
+  &.split
+    max-height: 35vh
+
+  > div
+    &:not(:first-child)
+      border-top-style: dashed !important
+
+    &:hover
+      background-color: #fcfcfc
+
+  .kind
+    width: 9%
+
+  .time
+    width: 26%
+
+  .message
+    width: 65%
+    word-wrap: break-word
+</style>
+
 <script>
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
@@ -73,32 +92,6 @@
   name: "additionallogs",
   computed: {
     ...mapState("imports", ["showAdditional", "details"])
-  },
-  NODETAILS: -1
+  }
 };
 </script>
-
-<style lang="scss" scoped>
-.additionallog {
-  overflow-y: auto;
-}
-
-.split {
-  max-height: 35vh;
-}
-
-.full {
-  max-height: 70vh;
-}
-
-.kind {
-  width: 9%;
-}
-.time {
-  width: 26%;
-}
-.message {
-  width: 65%;
-  word-wrap: break-word;
-}
-</style>
--- a/client/src/components/importoverview/ApprovedGaugeMeasurementDetail.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importoverview/ApprovedGaugeMeasurementDetail.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -2,8 +2,8 @@
   <div
     :class="{
       diffs: true,
-      full: showLogs === $options.NODETAILS,
-      split: showLogs !== $options.NODETAILS
+      full: !showLogs,
+      split: showLogs
     }"
   >
     <div v-for="(result, index) in details.summary" :key="index">
@@ -92,6 +92,28 @@
   </div>
 </template>
 
+<style lang="sass" scoped>
+.diffs
+  width: 100%
+  max-height: 20vh
+  overflow-y: auto
+
+.agmcode
+  width: 35%
+
+.agmdetailskeys
+  width: 33%
+
+.agmdetailsvalues
+  width: 33%
+
+.split
+  max-height: 35vh
+
+.full
+  max-height: 70vh
+</style>
+
 <script>
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
@@ -108,14 +130,12 @@
  */
 import { mapState } from "vuex";
 
-const NODIFF = -1;
-
 export default {
   name: "agmdetails",
   props: ["entry"],
   data() {
     return {
-      showDiff: NODIFF
+      showDiff: false
     };
   },
   computed: {
@@ -123,41 +143,12 @@
   },
   methods: {
     toggleDiff(number) {
-      if (this.showDiff !== number || this.showDiff == NODIFF) {
+      if (this.showDiff !== number) {
         this.showDiff = number;
       } else {
-        this.showDiff = NODIFF;
+        this.showDiff = false;
       }
     }
-  },
-  NODETAILS: -1
+  }
 };
 </script>
-
-<style lang="scss" scoped>
-.diffs {
-  width: 615px;
-  max-height: 20vh;
-  overflow-y: auto;
-}
-
-.agmcode {
-  width: 35%;
-}
-
-.agmdetailskeys {
-  width: 33%;
-}
-
-.agmdetailsvalues {
-  width: 33%;
-}
-
-.split {
-  max-height: 35vh;
-}
-
-.full {
-  max-height: 70vh;
-}
-</style>
--- a/client/src/components/importoverview/BottleneckDetail.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importoverview/BottleneckDetail.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -2,8 +2,8 @@
   <div
     :class="{
       bottleneckdetails: true,
-      full: showLogs === $options.NODETAILS,
-      split: showLogs !== $options.NODETAILS
+      full: !showLogs,
+      split: showLogs
     }"
   >
     <div
@@ -30,9 +30,9 @@
               fixed-width
             ></font-awesome-icon>
           </div>
-          <a @click="moveToBottleneck(index)" class="" href="#">{{
-            bottleneck.properties.objnam
-          }}</a>
+          <a @click="moveToBottleneck(index)" href="#">
+            {{ bottleneck.properties.objnam }}
+          </a>
         </div>
 
         <div class="ml-3 d-flex flex-row" v-if="showBottleneckDetail === index">
@@ -54,6 +54,18 @@
   </div>
 </template>
 
+<style lang="sass" scoped>
+.bottleneckdetails
+  width: 100%
+  overflow-y: auto
+
+.split
+  max-height: 35vh
+
+.full
+  max-height: 70vh
+</style>
+
 <script>
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
@@ -149,27 +161,11 @@
     },
     showBottleneckDetails(index) {
       if (index == this.showBottleneckDetail) {
-        this.showBottleneckDetail = NO_BOTTLENECK;
+        this.showBottleneckDetail = false;
         return;
       }
       this.showBottleneckDetail = index;
     }
-  },
-  NODETAILS: -1
+  }
 };
 </script>
-
-<style lang="scss" scoped>
-.bottleneckdetails {
-  width: 615px;
-  overflow-y: auto;
-}
-
-.split {
-  max-height: 35vh;
-}
-
-.full {
-  max-height: 70vh;
-}
-</style>
--- a/client/src/components/importoverview/Filters.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importoverview/Filters.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -1,18 +1,33 @@
 <template>
   <div>
-    <button @click="setFilter('pending')" :class="pendingStyle">
+    <button
+      @click="setFilter('pending')"
+      :class="'mr-1 btn btn-xs btn-' + (this.pending ? 'secondary' : 'light')"
+    >
       <translate>pending</translate>
     </button>
-    <button @click="setFilter('failed')" :class="failedStyle">
+    <button
+      @click="setFilter('failed')"
+      :class="'mr-1 btn btn-xs btn-' + (this.failed ? 'secondary' : 'light')"
+    >
       <translate>failed</translate>
     </button>
-    <button @click="setFilter('accepted')" :class="acceptedStyle">
+    <button
+      @click="setFilter('accepted')"
+      :class="'mr-1 btn btn-xs btn-' + (this.accepted ? 'secondary' : 'light')"
+    >
       <translate>accepted</translate>
     </button>
-    <button @click="setFilter('declined')" :class="declinedStyle">
+    <button
+      @click="setFilter('declined')"
+      :class="'mr-1 btn btn-xs btn-' + (this.declined ? 'secondary' : 'light')"
+    >
       <translate>declined</translate>
     </button>
-    <button @click="setFilter('warning')" :class="warningStyle">
+    <button
+      @click="setFilter('warning')"
+      :class="'btn btn-xs btn-' + (this.warning ? 'secondary' : 'light')"
+    >
       <translate>warning</translate>
     </button>
   </div>
@@ -49,53 +64,7 @@
       "accepted",
       "warning",
       "declined"
-    ]),
-    pendingStyle() {
-      return {
-        btn: true,
-        "btn-sm": true,
-        "btn-light": !this.pending,
-        "btn-secondary": this.pending
-      };
-    },
-    failedStyle() {
-      return {
-        "ml-2": true,
-        btn: true,
-        "btn-sm": true,
-        "btn-light": !this.failed,
-        "btn-secondary": this.failed
-      };
-    },
-    declinedStyle() {
-      return {
-        "ml-2": true,
-        btn: true,
-        "btn-sm": true,
-        "btn-light": !this.declined,
-        "btn-secondary": this.declined
-      };
-    },
-    acceptedStyle() {
-      return {
-        "ml-2": true,
-        btn: true,
-        "btn-sm": true,
-        "btn-light": !this.accepted,
-        "btn-secondary": this.accepted
-      };
-    },
-    warningStyle() {
-      return {
-        "ml-2": true,
-        btn: true,
-        "btn-sm": true,
-        "btn-light": !this.warning,
-        "btn-secondary": this.warning
-      };
-    }
+    ])
   }
 };
 </script>
-
-<style lang="scss" scoped></style>
--- a/client/src/components/importoverview/ImportOverview.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importoverview/ImportOverview.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -8,64 +8,56 @@
     />
     <div class="position-relative">
       <SpinnerOverlay v-if="loading" />
-      <div class="p-2 mb-1 d-flex flex-row flex-fill justify-content-between">
+      <div class="border-bottom p-2 d-flex 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>
+        <button
+          class="btn btn-xs btn-info"
+          :disabled="!reviewed.length"
+          @click="save"
+        >
+          <translate>Commit</translate> {{ reviewed.length }}
+        </button>
       </div>
       <div
-        class="ml-2 mr-2 mb-2 datefilter d-flex flex-row justify-content-between"
+        class="p-2 d-flex align-items-center justify-content-between border-bottom"
       >
-        <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">
+        <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 }}
+          <span class="mx-2">&ndash;</span>
+          {{ interval[1] | dateTime }}
           <select
-            class="my-auto btn btn-outline-light text-dark form-control interval-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.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_7_DAYS">
+              <translate>7 days</translate>
+            </option>
             <option :value="$options.LAST_30_DAYS">
               <translate>30 Days</translate>
             </option>
           </select>
         </div>
+        <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>
       </div>
       <UITableHeader
         :columns="[
@@ -85,16 +77,25 @@
      -->
       <UITableBody
         :data="filteredImports() | sortTable(sortColumn, sortDirection)"
+        :isActive="item => item.id === this.show"
         maxHeight="73vh"
       >
         <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.
@@ -129,7 +130,8 @@
 export default {
   components: {
     Filters: () => import("./Filters.vue"),
-    LogEntry: () => import("./LogEntry.vue")
+    LogEntry: () => import("./LogEntry.vue"),
+    LogDetail: () => import("./LogDetail.vue")
   },
   mixins: [sortTable],
   LAST_HOUR: "lasthour",
@@ -321,7 +323,7 @@
           query: this.searchQuery
         })
         .then(() => {
-          if (this.show != -1) {
+          if (this.show) {
             this.loadDetails(this.show)
               .then(response => {
                 this.$store.commit("imports/setCurrentDetails", response.data);
@@ -418,23 +420,12 @@
   },
   mounted() {
     const { id } = this.$route.params;
-    if (!id) {
+    if (id) {
+      this.showSingleRessource(id);
+    } else {
       this.$store.commit("application/searchQuery", "");
       this.loadLogs();
-    } else {
-      this.showSingleRessource(id);
     }
   }
 };
 </script>
-
-<style lang="scss" scoped>
-.date {
-  font-stretch: condensed;
-}
-.interval-select {
-  padding: 0px;
-  margin: 0px;
-  font-size: 80%;
-}
-</style>
--- a/client/src/components/importoverview/LogDetail.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importoverview/LogDetail.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -1,5 +1,5 @@
 <template>
-  <div class="border-top">
+  <div>
     <div
       class="d-flex fex-row"
       style="padding-left: 3px;"
@@ -71,7 +71,7 @@
     </div>
     <AdditionalLog
       v-if="entry.id === showLogs"
-      class="mx-4 pb-1 d-flex flex-row"
+      class="d-flex flex-row"
     ></AdditionalLog>
   </div>
 </template>
--- a/client/src/components/importoverview/LogEntry.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importoverview/LogEntry.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -1,81 +1,71 @@
 <template>
-  <div>
-    <div class="row no-gutters text-left">
-      <div
-        style="width: 79px;"
-        class="table-cell d-flex justify-content-between"
+  <div class="row w-100 no-gutters text-left">
+    <div style="width: 79px;" class="table-cell d-flex justify-content-between">
+      <font-awesome-icon
+        @click="toggleDetails"
+        :class="'pointer ' + (entry.id === show ? 'text-white' : 'text-info')"
+        :icon="entry.id === show ? 'angle-down' : 'angle-right'"
+        fixed-width
+      ></font-awesome-icon>
+      {{ entry.id }}
+    </div>
+    <div style="width: 53px;" class="table-cell center">
+      {{ entry.kind.toUpperCase() }}
+    </div>
+    <div style="width: 138px;" class="table-cell center">
+      {{ entry.enqueued | dateTime }}
+    </div>
+    <div style="width: 105px;" class="table-cell truncate">
+      {{ entry.user }}
+    </div>
+    <div style="width: 105px;" class="table-cell truncate">
+      {{ entry.signer }}
+    </div>
+    <div style="width: 72px;" class="table-cell center">
+      <span v-if="entry.state === 'failed'" class="text-danger">
+        {{ entry.state }}
+      </span>
+      <span v-else>{{ entry.state }}</span>
+    </div>
+    <div style="width: 44px;" class="table-cell center">
+      <font-awesome-icon
+        v-if="entry.warnings"
+        class="text-warning"
+        icon="exclamation-triangle"
+        fixed-width
+      ></font-awesome-icon>
+    </div>
+    <div style="flex-grow: 1; padding: 0;" class="table-cell text-right">
+      <button
+        :class="['action approved', { active: isApproved }]"
+        @click="toggleApproval($options.STATES.APPROVED)"
+        v-if="entry.state === 'pending'"
       >
         <font-awesome-icon
-          @click="toggleDetails"
-          class="my-auto text-info pointer"
-          :icon="entry.id === show ? 'angle-down' : 'angle-right'"
-          fixed-width
+          class="small pointer"
+          icon="check"
         ></font-awesome-icon>
-        {{ entry.id }}
-      </div>
-      <div style="width: 53px;" class="table-cell text-center">
-        {{ entry.kind.toUpperCase() }}
-      </div>
-      <div style="width: 138px;" class="table-cell text-center">
-        {{ entry.enqueued | dateTime }}
-      </div>
-      <div style="width: 105px;" class="table-cell truncate">
-        {{ entry.user }}
-      </div>
-      <div style="width: 105px;" class="table-cell truncate">
-        {{ entry.signer }}
-      </div>
-      <div style="width: 72px;" class="table-cell text-center">
-        <span v-if="entry.state === 'failed'" class="text-danger">{{
-          entry.state
-        }}</span>
-        <span v-else>{{ entry.state }}</span>
-      </div>
-      <div style="width: 44px;" class="table-cell text-center">
+      </button>
+      <button
+        :class="['action rejected', { active: isRejected }]"
+        @click="toggleApproval($options.STATES.REJECTED)"
+        v-if="entry.state === 'pending'"
+      >
         <font-awesome-icon
-          v-if="entry.warnings"
-          class="text-warning"
-          icon="exclamation-triangle"
-          fixed-width
+          icon="times"
+          class="small pointer"
         ></font-awesome-icon>
-      </div>
-      <div style="flex-grow: 1; padding: 0;" class="table-cell text-right">
-        <div v-if="entry.state === 'pending'">
-          <button
-            :class="['actions approved', { active: isApproved }]"
-            @click="toggleApproval($options.STATES.APPROVED)"
-          >
-            <font-awesome-icon
-              class="small pointer"
-              icon="check"
-            ></font-awesome-icon>
-          </button>
-          <button
-            :class="['actions rejected', { active: isRejected }]"
-            @click="toggleApproval($options.STATES.REJECTED)"
-          >
-            <font-awesome-icon
-              icon="times"
-              class="small pointer"
-            ></font-awesome-icon>
-          </button>
-        </div>
-      </div>
+      </button>
     </div>
-    <LogDetail
-      :entry="entry"
-      :details="details"
-      v-if="show === entry.id"
-    ></LogDetail>
   </div>
 </template>
 
 <style lang="sass" scoped>
-.actions
+.action
   height: 100%
   width: 50%
   border: 0
-  background: transparent
+  background: white
   outline: none
   &.approved
     color: green
@@ -90,6 +80,11 @@
     &:hover
       color: white
       background: red
+.active
+  .action
+    background-color: #d2eaee
+    &.rejected
+      border-left: solid 1px rgba(255, 255, 255, 0.3)
 </style>
 
 <script>
@@ -112,9 +107,7 @@
 import { HTTP } from "@/lib/http.js";
 
 export default {
-  components: {
-    LogDetail: () => import("./LogDetail.vue")
-  },
+  STATES,
   props: ["entry"],
   computed: {
     ...mapState("imports", ["show"]),
--- a/client/src/components/importschedule/Importschedule.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/importschedule/Importschedule.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -32,13 +32,16 @@
         />
         <UITableBody
           :data="filteredSchedules() | sortTable(sortColumn, sortDirection)"
+          :isActive="item => currentSchedule && item.id === currentSchedule.id"
         >
           <template v-slot:row="{ item: schedule }">
-            <div class="py-1 col-1">{{ schedule.id }}</div>
-            <div class="py-1 col-2">{{ schedule.kind.toUpperCase() }}</div>
-            <div class="py-1 col-2">{{ schedule.user }}</div>
-            <div class="py-1 col-2">{{ schedule.config.cron }}</div>
-            <div class="py-1 col-2 text-center">
+            <div class="table-cell col-1">{{ schedule.id }}</div>
+            <div class="table-cell col-2">
+              {{ schedule.kind.toUpperCase() }}
+            </div>
+            <div class="table-cell col-2">{{ schedule.user }}</div>
+            <div class="table-cell col-2">{{ schedule.config.cron }}</div>
+            <div class="table-cell col-2 text-center">
               <font-awesome-icon
                 v-if="schedule.config['send-email']"
                 class="fa-fw mr-2"
@@ -46,7 +49,7 @@
                 icon="check"
               ></font-awesome-icon>
             </div>
-            <div class="py-1 col text-right">
+            <div class="table-cell col justify-content-end">
               <button
                 @click="editSchedule(schedule.id)"
                 class="btn btn-xs btn-dark mr-1"
@@ -141,7 +144,11 @@
   },
   computed: {
     ...mapState("application", ["showSidebar"]),
-    ...mapState("importschedule", ["schedules", "importScheduleDetailVisible"]),
+    ...mapState("importschedule", [
+      "schedules",
+      "currentSchedule",
+      "importScheduleDetailVisible"
+    ]),
     importScheduleLabel() {
       return this.$gettext("Import Schedule");
     },
--- a/client/src/components/ui/SpinnerOverlay.vue	Mon Apr 01 15:12:49 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<template>
-  <transition name="fade">
-    <div class="spinner-overlay">
-      <font-awesome-icon icon="spinner" spin />
-    </div>
-  </transition>
-</template>
-
-<style lang="sass">
-.spinner-overlay
-  background: rgba(255, 255, 255, 0.9)
-  position: absolute
-  z-index: 99
-  top: 0
-  right: 0
-  bottom: 0
-  left: 0
-  display: flex
-  align-items: center
-  justify-content: center
-  color: #888
-</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/ui/UISpinnerOverlay.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -0,0 +1,38 @@
+<template>
+  <transition name="fade">
+    <div class="spinner-overlay">
+      <font-awesome-icon icon="spinner" spin />
+    </div>
+  </transition>
+</template>
+
+<style lang="sass">
+.spinner-overlay
+  background: rgba(255, 255, 255, 0.9)
+  position: absolute
+  z-index: 99
+  top: 0
+  right: 0
+  bottom: 0
+  left: 0
+  display: flex
+  align-items: center
+  justify-content: center
+  color: #888
+</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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+</script>
--- a/client/src/components/ui/UITableBody.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/ui/UITableBody.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -7,12 +7,12 @@
     <div
       v-for="(item, index) in data"
       :key="key(index)"
-      :class="['row-container border-top', { active: active === item }]"
+      :class="['row-container border-top', { active: isActive(item) }]"
     >
       <div class="row mx-0">
         <slot :item="item" :index="index" name="row"></slot>
       </div>
-      <div class="expand" v-if="active === item">
+      <div class="expand" v-if="isActive(item)">
         <slot :item="item" :index="index" name="expand"></slot>
       </div>
     </div>
@@ -27,14 +27,12 @@
   .row-container
     > .row
       &:hover
-        background-color: #fafafa
+        background-color: #fcfcfc
       .table-cell
         display: flex
         align-items: center
         padding: 1.5px 3px
         border-right: solid 1px #dee2e6
-        &:hover
-          background-color: #f2f2f2
         &:last-child
           border-right: none
         &.center
@@ -77,8 +75,9 @@
       type: String,
       default: "18rem"
     },
-    active: {
-      type: [Object, Array]
+    isActive: {
+      type: Function,
+      default: () => false
     }
   },
   methods: {
--- a/client/src/components/usermanagement/Usermanagement.vue	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/components/usermanagement/Usermanagement.vue	Tue Apr 02 10:07:48 2019 +0200
@@ -15,8 +15,8 @@
           />
           <UITableBody
             :data="users | sortTable(sortColumn, sortDirection, page, pageSize)"
+            :isActive="item => item === currentUser"
             maxHeight="47rem"
-            :active="currentUser"
           >
             <template v-slot:row="{ item: user }">
               <div
--- a/client/src/main.js	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/main.js	Tue Apr 02 10:07:48 2019 +0200
@@ -33,7 +33,7 @@
 import UIBoxHeader from "@/components/ui/UIBoxHeader";
 import UITableHeader from "@/components/ui/UITableHeader";
 import UITableBody from "@/components/ui/UITableBody";
-import SpinnerOverlay from "@/components/ui/SpinnerOverlay";
+import UISpinnerOverlay from "@/components/ui/UISpinnerOverlay";
 
 // styles
 import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
@@ -179,7 +179,7 @@
 Vue.component("UIBoxHeader", UIBoxHeader);
 Vue.component("UITableHeader", UITableHeader);
 Vue.component("UITableBody", UITableBody);
-Vue.component("SpinnerOverlay", SpinnerOverlay);
+Vue.component("UISpinnerOverlay", UISpinnerOverlay);
 
 // register global filters
 for (let name in filters) Vue.filter(name, filters[name]);
--- a/client/src/store/imports.js	Mon Apr 01 15:12:49 2019 +0200
+++ b/client/src/store/imports.js	Tue Apr 02 10:07:48 2019 +0200
@@ -17,16 +17,12 @@
 import { equalTo as equalToFilter } from "ol/format/filter.js";
 import { startOfHour, endOfHour } from "date-fns";
 
-/* eslint-disable no-unused-vars */
-/* eslint-disable no-unreachable */
 const STATES = {
   NEEDSAPPROVAL: "pending",
   APPROVED: "accepted",
   REJECTED: "declined"
 };
 
-const NODETAILS = -1;
-
 // initial state
 const init = () => {
   return {
@@ -38,9 +34,9 @@
     stretches: [],
     imports: [],
     reviewed: [],
-    show: NODETAILS,
-    showAdditional: NODETAILS,
-    showLogs: NODETAILS,
+    show: null,
+    showAdditional: null,
+    showLogs: null,
     details: [],
     startDate: startOfHour(new Date()),
     endDate: endOfHour(new Date()),
@@ -158,19 +154,19 @@
       state.show = id;
     },
     hideDetails: state => {
-      state.show = NODETAILS;
+      state.show = null;
     },
     showAdditionalInfoFor: (state, id) => {
       state.showAdditional = id;
     },
     hideAdditionalInfo: state => {
-      state.showAdditional = NODETAILS;
+      state.showAdditional = false;
     },
     showAdditionalLogsFor: (state, id) => {
       state.showLogs = id;
     },
     hideAdditionalLogs: state => {
-      state.showLogs = NODETAILS;
+      state.showLogs = false;
     },
     toggleApprove: (state, change) => {
       const { id, newStatus } = change;
@@ -192,7 +188,7 @@
     }
   },
   actions: {
-    loadStretch({ commit }, name) {
+    loadStretch(context, name) {
       return new Promise((resolve, reject) => {
         getStretchFromWFS(equalToFilter("name", name))
           .then(response => {
@@ -219,7 +215,7 @@
           });
       });
     },
-    saveStretch({ commit }, stretch) {
+    saveStretch(context, stretch) {
       return new Promise((resolve, reject) => {
         HTTP.post("/imports/st", stretch, {
           headers: { "X-Gemma-Auth": localStorage.getItem("token") }
@@ -271,7 +267,7 @@
           });
       });
     },
-    confirmReview({ state }, reviewResults) {
+    confirmReview(context, reviewResults) {
       return new Promise((resolve, reject) => {
         HTTP.patch("/imports", reviewResults, {
           headers: {
--- a/pkg/common/nashsutcliffe.go	Mon Apr 01 15:12:49 2019 +0200
+++ b/pkg/common/nashsutcliffe.go	Tue Apr 02 10:07:48 2019 +0200
@@ -14,6 +14,7 @@
 package common
 
 import (
+	"math"
 	"sort"
 	"time"
 )
@@ -24,6 +25,10 @@
 	Observed  float64
 }
 
+func (m NSMeasurement) Valid() bool {
+	return !m.When.IsZero() && !math.IsNaN(m.Predicted) && !math.IsNaN(m.Observed)
+}
+
 func NashSutcliffeSort(measurements []NSMeasurement) {
 	sort.Slice(measurements, func(i, j int) bool {
 		return measurements[i].When.Before(measurements[j].When)
--- a/pkg/controllers/gauges.go	Mon Apr 01 15:12:49 2019 +0200
+++ b/pkg/controllers/gauges.go	Tue Apr 02 10:07:48 2019 +0200
@@ -18,6 +18,7 @@
 	"encoding/csv"
 	"fmt"
 	"log"
+	"math"
 	"net/http"
 	"sort"
 	"strconv"
@@ -36,13 +37,10 @@
 const (
 	selectPredictedObserveredSQL = `
 SELECT
-  a.measure_date AS measure_date,
-  a.water_level  AS predicted,
-  b.water_level  AS observed
-FROM waterway.gauge_measurements a JOIN waterway.gauge_measurements b
-  ON a.fk_gauge_id  = b.fk_gauge_id AND
-     a.measure_date = b.measure_date AND
-     a.predicted AND NOT b.predicted
+  measure_date,
+  predicted,
+  water_level
+FROM waterway.gauge_measurements
 WHERE
   a.fk_gauge_id = (
     $1::char(2),
@@ -51,11 +49,10 @@
     $4::char(5),
     $5::int
   ) AND
-  a.measure_date BETWEEN
-    $6::timestamp AND $6::timestamp - '72hours'::interval
-ORDER BY a.measure_date
+  measure_date BETWEEN
+    $6::timestamp - '72hours'::interval AND $6::timestamp
+ORDER BY measure_date, date_issue
 `
-
 	selectWaterlevelsSQL = `
 SELECT
   measure_date,
@@ -457,21 +454,46 @@
 
 	var measurements []common.NSMeasurement
 
+	invalid := common.NSMeasurement{
+		Predicted: math.NaN(),
+		Observed:  math.NaN(),
+	}
+	current := invalid
+
 	for rows.Next() {
-		var m common.NSMeasurement
+		var (
+			when      time.Time
+			predicted bool
+			value     float64
+		)
 		if err = rows.Scan(
-			&m.When,
-			&m.Predicted,
-			&m.Observed,
+			&when,
+			&predicted,
+			&value,
 		); err != nil {
 			return
 		}
-		measurements = append(measurements, m)
+		if !when.Equal(current.When) {
+			if current.Valid() {
+				measurements = append(measurements, current)
+			}
+			current = invalid
+		}
+		if predicted {
+			current.Predicted = value
+		} else {
+			current.Observed = value
+		}
 	}
+
 	if err = rows.Err(); err != nil {
 		return
 	}
 
+	if current.Valid() {
+		measurements = append(measurements, current)
+	}
+
 	type coeff struct {
 		Value   float64 `json:"value"`
 		Samples int     `json:"samples"`