changeset 3649:fb8a53c7c6d3

merged configuration branch into default
author Markus Kottlaender <markus@intevation.de>
date Thu, 13 Jun 2019 11:20:17 +0200
parents 0ec5c8ec1e44 (current diff) 6bb8def12f20 (diff)
children 2a079d0a71c1
files client/src/components/map/layers.js
diffstat 11 files changed, 300 insertions(+), 104 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/assets/application.scss	Wed Jun 12 18:26:26 2019 +0200
+++ b/client/src/assets/application.scss	Thu Jun 13 11:20:17 2019 +0200
@@ -222,3 +222,8 @@
   width: 50% !important;
   height: 50% !important;
 }
+
+.custom-control-input:checked~.custom-control-label::before {
+    border-color: $color-info;
+    background-color: $color-info;
+}
--- a/client/src/components/map/layers.js	Wed Jun 12 18:26:26 2019 +0200
+++ b/client/src/components/map/layers.js	Thu Jun 13 11:20:17 2019 +0200
@@ -237,9 +237,9 @@
         visible: true,
         source: new ImageSource({
           preload: 1,
-          url: "https://service.d4d-portal.info/wms/",
+          url: store.state.application.config.ecdis_wms_url,
           crossOrigin: "anonymous",
-          params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true }
+          params: JSON.parse(store.state.application.config.ecdis_wms_params)
         })
       }),
       new ImageLayer({
--- a/client/src/components/systemconfiguration/DataAccuracy.vue	Wed Jun 12 18:26:26 2019 +0200
+++ b/client/src/components/systemconfiguration/DataAccuracy.vue	Thu Jun 13 11:20:17 2019 +0200
@@ -330,7 +330,17 @@
   },
   methods: {
     submit() {
-      this.$store.commit("application/config", this.config);
+      this.$store.dispatch("application/saveConfig", {
+        bn_revtime_multiplier: this.config.bn_revtime_multiplier,
+        gm_latest_hours: this.config.gm_latest_hours,
+        gm_min_values_14d: this.config.gm_min_values_14d,
+        gm_forecast_offset_24h: this.config.gm_forecast_offset_24h,
+        gm_forecast_offset_72h: this.config.gm_forecast_offset_72h,
+        gm_forecast_vs_reality_nsc_24h: this.config
+          .gm_forecast_vs_reality_nsc_24h,
+        gm_forecast_vs_reality_nsc_72h: this.config
+          .gm_forecast_vs_reality_nsc_72h
+      });
     }
   }
 };
--- a/client/src/components/systemconfiguration/MapLayers.vue	Wed Jun 12 18:26:26 2019 +0200
+++ b/client/src/components/systemconfiguration/MapLayers.vue	Thu Jun 13 11:20:17 2019 +0200
@@ -5,14 +5,50 @@
       <div class="row">
         <div class="col-sm-6">
           <div class="form-group">
-            <label for="ecdis-url" class="font-weight-bold">ECDIS URL</label>
+            <label for="ecdis-url" class="font-weight-bold">
+              ECDIS WMS URL
+            </label>
             <input
               type="url"
               class="form-control"
               placeholder="https://..."
-              v-model="config.ecdis_url"
+              @input="lookupWMSCapabilities()"
+              v-model="config.ecdis_wms_url"
             />
           </div>
+          <label for="ecdis-layers">
+            <translate>Layers</translate>
+            <transition name="fade"
+              ><font-awesome-icon
+                icon="spinner"
+                spin
+                v-if="availableWMSLayersLoading"
+                class="ml-2"
+            /></transition>
+          </label>
+          <div class="container-fluid">
+            <div class="row">
+              <div
+                class="custom-control custom-checkbox col-sm-4"
+                v-for="layer in availableWMSLayers"
+                :key="'layer-' + layer"
+              >
+                <input
+                  type="checkbox"
+                  class="custom-control-input"
+                  v-model="selectedWMSLayers"
+                  :id="'layer-' + layer"
+                  :value="layer"
+                />
+                <label
+                  class="custom-control-label text-break"
+                  :for="'layer-' + layer"
+                >
+                  {{ layer }}
+                </label>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
@@ -39,15 +75,84 @@
  * Markus Kottländer <markus@intevation.de>
  */
 import { mapState } from "vuex";
+import WMSCapabilities from "ol/format/WMSCapabilities";
+import { HTTP } from "@/lib/http";
+
+const WMSCapabilitiesParser = new WMSCapabilities();
 
 export default {
+  data() {
+    return {
+      selectedWMSLayers: [],
+      availableWMSLayers: [],
+      availableWMSLayersLoading: false,
+      lookupWMSCapabilitiesTimeout: null,
+      wmsVersion: ""
+    };
+  },
   computed: {
     ...mapState("application", ["config"])
   },
   methods: {
+    lookupWMSCapabilities() {
+      if (this.lookupWMSCapabilitiesTimeout) {
+        clearTimeout(this.lookupWMSCapabilitiesTimeout);
+      }
+      this.lookupWMSCapabilitiesTimeout = setTimeout(() => {
+        let url;
+        try {
+          let urlParts = new URL(this.config.ecdis_wms_url);
+          url =
+            urlParts.protocol +
+            "//" +
+            urlParts.host +
+            urlParts.pathname.trim("/") +
+            "/";
+        } catch (e) {
+          url = this.config.ecdis_wms_url;
+        }
+        this.availableWMSLayersLoading = true;
+        HTTP.get(url + "?request=GetCapabilities&service=WMS")
+          .then(response => {
+            let capabilities = WMSCapabilitiesParser.read(response.data);
+            this.wmsVersion = capabilities.version;
+            this.availableWMSLayers = [];
+            this.getLayersRecursive(capabilities.Capability.Layer.Layer);
+          })
+          .catch(() => {
+            this.availableWMSLayers = [];
+          })
+          .finally(() => (this.availableWMSLayersLoading = false));
+      }, 500);
+    },
+    getLayersRecursive(layers) {
+      layers.forEach(l => {
+        if (l.hasOwnProperty("Layer")) {
+          this.getLayersRecursive(l.Layer);
+        } else {
+          this.availableWMSLayers.push(l.Name);
+        }
+      });
+    },
     submit() {
-      this.$store.commit("application/config", this.config);
+      this.$store.dispatch("application/saveConfig", {
+        ecdis_wms_url: this.config.ecdis_wms_url,
+        ecdis_wms_params: JSON.stringify({
+          LAYERS: this.selectedWMSLayers
+            .filter(l => this.availableWMSLayers.find(al => al === l))
+            .join(","),
+          VERSION: this.wmsVersion,
+          TILED: true
+        })
+      });
     }
+  },
+  mounted() {
+    let ecdisWmsParams = JSON.parse(this.config.ecdis_wms_params);
+    if (ecdisWmsParams.LAYERS) {
+      this.selectedWMSLayers = ecdisWmsParams.LAYERS.split(",");
+    }
+    this.lookupWMSCapabilities();
   }
 };
 </script>
--- a/client/src/components/systemconfiguration/MorphologyClassbreaks.vue	Wed Jun 12 18:26:26 2019 +0200
+++ b/client/src/components/systemconfiguration/MorphologyClassbreaks.vue	Thu Jun 13 11:20:17 2019 +0200
@@ -8,11 +8,11 @@
       <div class="d-flex flex-wrap">
         <div
           class="input-group mb-3 mr-2 classbreak"
-          v-for="(value, i) in config.morphology_classbreaks"
+          v-for="(value, i) in morphologyClassbreaks"
           :key="i"
         >
           <input
-            v-model="config.morphology_classbreaks[i]"
+            v-model="morphologyClassbreaks[i]"
             type="number"
             min="0"
             step="0.1"
@@ -22,7 +22,7 @@
             <button
               class="btn btn-sm btn-outline-secondary"
               type="button"
-              @click="config.morphology_classbreaks.splice(i, 1)"
+              @click="morphologyClassbreaks.splice(i, 1)"
             >
               <font-awesome-icon icon="times" />
             </button>
@@ -31,11 +31,9 @@
         <button
           class="btn btn-sm btn-success mb-3"
           @click="
-            config.morphology_classbreaks.push(
-              config.morphology_classbreaks.length
-                ? config.morphology_classbreaks[
-                    config.morphology_classbreaks.length - 1
-                  ]
+            morphologyClassbreaks.push(
+              morphologyClassbreaks.length
+                ? morphologyClassbreaks[morphologyClassbreaks.length - 1]
                 : 1
             )
           "
@@ -51,11 +49,11 @@
       <div class="d-flex flex-wrap">
         <div
           class="input-group mb-3 mr-2 classbreak"
-          v-for="(value, i) in config.morphology_classbreaks_compare"
+          v-for="(value, i) in morphologyClassbreaksCompare"
           :key="i"
         >
           <input
-            v-model="config.morphology_classbreaks_compare[i]"
+            v-model="morphologyClassbreaksCompare[i]"
             type="number"
             step="0.1"
             class="form-control form-control-sm"
@@ -64,7 +62,7 @@
             <button
               class="btn btn-sm btn-outline-secondary"
               type="button"
-              @click="config.morphology_classbreaks_compare.splice(i, 1)"
+              @click="morphologyClassbreaksCompare.splice(i, 1)"
             >
               <font-awesome-icon icon="times" />
             </button>
@@ -73,10 +71,10 @@
         <button
           class="btn btn-sm btn-success mb-3"
           @click="
-            config.morphology_classbreaks_compare.push(
-              config.morphology_classbreaks_compare.length
-                ? config.morphology_classbreaks_compare[
-                    config.morphology_classbreaks_compare.length - 1
+            morphologyClassbreaksCompare.push(
+              morphologyClassbreaksCompare.length
+                ? morphologyClassbreaksCompare[
+                    morphologyClassbreaksCompare.length - 1
                   ]
                 : 1
             )
@@ -122,13 +120,32 @@
 import { mapState } from "vuex";
 
 export default {
+  data() {
+    return {
+      morphologyClassbreaks: [],
+      morphologyClassbreaksCompare: []
+    };
+  },
   computed: {
     ...mapState("application", ["config"])
   },
   methods: {
     submit() {
-      this.$store.commit("application/config", this.config);
+      this.$store.dispatch("application/saveConfig", {
+        morphology_classbreaks: this.morphologyClassbreaks.join(","),
+        morphology_classbreaks_compare: this.morphologyClassbreaksCompare.join(
+          ","
+        )
+      });
     }
+  },
+  mounted() {
+    this.morphologyClassbreaks = this.config.morphology_classbreaks
+      .split(",")
+      .map(n => Number(n));
+    this.morphologyClassbreaksCompare = this.config.morphology_classbreaks_compare
+      .split(",")
+      .map(n => Number(n));
   }
 };
 </script>
--- a/client/src/components/systemconfiguration/Systemconfiguration.vue	Wed Jun 12 18:26:26 2019 +0200
+++ b/client/src/components/systemconfiguration/Systemconfiguration.vue	Thu Jun 13 11:20:17 2019 +0200
@@ -6,6 +6,9 @@
       <div class="text-left flex-fill" style="overflow: auto">
         <PDFTemplates />
         <ColorSettings v-if="isSysAdmin" />
+        <MapLayers v-if="isSysAdmin" />
+        <DataAccuracy v-if="isSysAdmin" />
+        <MorphologyClassbreaks v-if="isSysAdmin" />
       </div>
       <!-- card-body -->
     </div>
@@ -35,7 +38,10 @@
   components: {
     Spacer: () => import("../Spacer"),
     PDFTemplates: () => import("./PDFTemplates"),
-    ColorSettings: () => import("./ColorSettings")
+    ColorSettings: () => import("./ColorSettings"),
+    MapLayers: () => import("./MapLayers"),
+    DataAccuracy: () => import("./DataAccuracy"),
+    MorphologyClassbreaks: () => import("./MorphologyClassbreaks")
   },
   computed: {
     ...mapGetters("user", ["isSysAdmin"]),
--- a/client/src/store/application.js	Wed Jun 12 18:26:26 2019 +0200
+++ b/client/src/store/application.js	Thu Jun 13 11:20:17 2019 +0200
@@ -14,6 +14,8 @@
  *   Bernhard E. Reiter <bernhard.reiter@intevation.de>
  */
 
+import { HTTP } from "@/lib/http";
+import { displayError, displayInfo } from "@/lib/errors";
 import { version } from "../../package.json";
 
 // initial state
@@ -140,85 +142,32 @@
     }
   },
   actions: {
-    loadConfig({ commit, state }) {
-      if (!Object.keys(state.config).length) {
-        setTimeout(() => {
-          commit("config", {
-            ecdis_url: "https://service.d4d-portal.info/wms/",
-            bn_revtime_multiplier: 1.5,
-            gm_min_values_14d: 1124,
-            gm_latest_hours: 24,
-            gm_forecast_offset_24h: 15,
-            gm_forecast_offset_72h: 15,
-            gm_forecast_vs_reality_nsc_24h: -12.5,
-            gm_forecast_vs_reality_nsc_72h: -12.5,
-            morphology_classbreaks: [
-              1,
-              1.5,
-              1.7,
-              1.9,
-              2.1,
-              2.3,
-              2.5,
-              2.7,
-              2.9,
-              3.1,
-              3.3,
-              3.5,
-              4.0,
-              4.5,
-              5,
-              5.5,
-              6,
-              6.5,
-              7
-            ],
-            morphology_classbreaks_compare: [
-              -2,
-              -1.9,
-              -1.8,
-              -1.7,
-              -1.6,
-              -1.5,
-              -1.4,
-              -1.3,
-              -1.2,
-              -1.1,
-              -1,
-              -0.9,
-              -0.8,
-              -0.7,
-              -0.6,
-              -0.5,
-              -0.4,
-              -0.3,
-              -0.2,
-              -0.1,
-              0,
-              0.1,
-              0.2,
-              0.3,
-              0.4,
-              0.5,
-              0.6,
-              0.7,
-              0.8,
-              0.9,
-              1,
-              1.1,
-              1.2,
-              1.3,
-              1.4,
-              1.5,
-              1.6,
-              1.7,
-              1.8,
-              1.9,
-              2
-            ]
+    loadConfig({ commit }) {
+      HTTP.get("/system/settings", {
+        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+      }).then(response => {
+        commit("config", response.data);
+      });
+    },
+    saveConfig(context, config) {
+      HTTP.put("/system/settings", config, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-type": "application/json"
+        }
+      })
+        .then(() => {
+          displayInfo({
+            message: "Configuration saved!"
           });
-        }, 1000);
-      }
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
+        });
     }
   }
 };
--- a/pkg/controllers/routes.go	Wed Jun 12 18:26:26 2019 +0200
+++ b/pkg/controllers/routes.go	Thu Jun 13 11:20:17 2019 +0200
@@ -79,6 +79,15 @@
 		NoConn: true,
 	})).Methods(http.MethodGet)
 
+	api.Handle("/system/settings", any(&JSONHandler{
+		Handle: getSystemSettings,
+	})).Methods(http.MethodGet)
+
+	api.Handle("/system/settings", any(&JSONHandler{
+		Input:  func(*http.Request) interface{} { return &map[string]string{} },
+		Handle: setSystemSettings,
+	})).Methods(http.MethodPut)
+
 	api.Handle("/system/style/{feature}/{attr}", any(&JSONHandler{
 		Handle: getFeatureStyle,
 	})).Methods(http.MethodGet)
--- a/pkg/controllers/system.go	Wed Jun 12 18:26:26 2019 +0200
+++ b/pkg/controllers/system.go	Thu Jun 13 11:20:17 2019 +0200
@@ -21,9 +21,10 @@
 	"net/http"
 	"strings"
 
+	"github.com/gorilla/mux"
+
 	"gemma.intevation.de/gemma/pkg/config"
 	"gemma.intevation.de/gemma/pkg/models"
-	"github.com/gorilla/mux"
 )
 
 const (
@@ -33,6 +34,15 @@
 	setFeatureColourSQL = `UPDATE systemconf.feature_colours
 SET (r, g, b, a) = ($3, $4, $5, $6)
 WHERE feature_name = $1 AND style_attr = $2`
+
+	getSettingsSQL = `
+SELECT config_key, config_val
+FROM sys_admin.system_config`
+
+	updateSettingSQL = `
+INSERT INTO sys_admin.system_config (config_key, config_val)
+VALUES ($1, $2)
+ON CONFLICT (config_key) DO UPDATE SET config_val = $2`
 )
 
 // System status end points
@@ -100,6 +110,75 @@
 	return
 }
 
+func getSystemSettings(
+	_ interface{},
+	req *http.Request,
+	conn *sql.Conn,
+) (jr JSONResult, err error) {
+
+	var rows *sql.Rows
+	if rows, err = conn.QueryContext(req.Context(), getSettingsSQL); err != nil {
+		return
+	}
+	defer rows.Close()
+
+	settings := map[string]string{}
+
+	for rows.Next() {
+		var key, val string
+		if err = rows.Scan(&key, &val); err != nil {
+			return
+		}
+		settings[key] = val
+	}
+	if err = rows.Err(); err != nil {
+		return
+	}
+
+	jr = JSONResult{Result: settings}
+	return
+}
+
+func setSystemSettings(
+	input interface{},
+	req *http.Request,
+	conn *sql.Conn,
+) (jr JSONResult, err error) {
+
+	settings := input.(*map[string]string)
+
+	ctx := req.Context()
+	var tx *sql.Tx
+	if tx, err = conn.BeginTx(ctx, nil); err != nil {
+		return
+	}
+	defer tx.Rollback()
+
+	var setStmt *sql.Stmt
+	if setStmt, err = tx.PrepareContext(ctx, updateSettingSQL); err != nil {
+		return
+	}
+	defer setStmt.Close()
+
+	for key, value := range *settings {
+		if _, err = setStmt.ExecContext(ctx, key, value); err != nil {
+			return
+		}
+	}
+
+	if err = tx.Commit(); err != nil {
+		return
+	}
+
+	jr = JSONResult{
+		Code: http.StatusCreated,
+		Result: struct {
+			Result string `json:"result"`
+		}{"success"},
+	}
+	return
+}
+
 // Map/Feature style end points
 
 func getFeatureStyle(
--- a/schema/auth.sql	Wed Jun 12 18:26:26 2019 +0200
+++ b/schema/auth.sql	Thu Jun 13 11:20:17 2019 +0200
@@ -25,10 +25,11 @@
 --
 -- Privileges for waterway_user
 --
-GRANT USAGE ON SCHEMA public, users, waterway, systemconf, caching TO waterway_user;
+GRANT USAGE ON SCHEMA public, users, waterway, systemconf, sys_admin, caching TO waterway_user;
 GRANT SELECT ON ALL TABLES IN SCHEMA public, users, waterway TO waterway_user;
 GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA caching TO waterway_user;
 GRANT SELECT ON systemconf.feature_colours TO waterway_user;
+GRANT SELECT ON sys_admin.system_config TO waterway_user;
 GRANT UPDATE (pw, map_extent, email_address) ON users.list_users
     TO waterway_user;
 
@@ -65,7 +66,7 @@
     ON users.list_users, users.responsibility_areas TO sys_admin;
 GRANT USAGE ON SCHEMA sys_admin TO sys_admin;
 GRANT SELECT ON ALL TABLES IN SCHEMA sys_admin TO sys_admin;
-GRANT UPDATE ON sys_admin.system_config TO sys_admin;
+GRANT INSERT, UPDATE ON sys_admin.system_config TO sys_admin;
 GRANT UPDATE ON systemconf.feature_colours TO sys_admin;
 GRANT UPDATE ON sys_admin.published_services TO sys_admin;
 GRANT INSERT, DELETE ON sys_admin.password_reset_requests TO sys_admin;
--- a/schema/default_sysconfig.sql	Wed Jun 12 18:26:26 2019 +0200
+++ b/schema/default_sysconfig.sql	Thu Jun 13 11:20:17 2019 +0200
@@ -23,4 +23,19 @@
 INSERT INTO systemconf.feature_colours VALUES ('Bottlenecks', 'stroke', 250, 40, 255, 1);
 INSERT INTO systemconf.feature_colours VALUES ('Bottlenecks', 'fill', 255, 37, 196, 0.14);
 
+--
+-- Settings
+--
+INSERT INTO sys_admin.system_config VALUES ('ecdis_wms_url', 'https://service.d4d-portal.info/wms/');
+INSERT INTO sys_admin.system_config VALUES ('ecdis_wms_params', '{"LAYERS": "d4d", "VERSION": "1.1.1", "TILED": true}');
+INSERT INTO sys_admin.system_config VALUES ('bn_revtime_multiplier', 1.5);
+INSERT INTO sys_admin.system_config VALUES ('gm_min_values_14d', 1224);
+INSERT INTO sys_admin.system_config VALUES ('gm_latest_hours', 24);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_offset_24h', 15);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_offset_72h', 15);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_vs_reality_nsc_24h', -12.5);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_vs_reality_nsc_72h', -12.5);
+INSERT INTO sys_admin.system_config VALUES ('morphology_classbreaks', '1,1.5,1.7,1.9,2.1,2.3,2.5,2.7,2.9,3.1,3.3,3.5,4.0,4.5,5,5.5,6,6.5,7');
+INSERT INTO sys_admin.system_config VALUES ('morphology_classbreaks_compare', '-2,-1.9,-1.8,-1.7,-1.6,-1.5,-1.4,-1.3,-1.2,-1.1,-1,-0.9,-0.8,-0.7,-0.6,-0.5,-0.4,-0.3,-0.2,-0.1,0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2');
+
 COMMIT;