# HG changeset patch # User Thomas Junk # Date 1536311636 -7200 # Node ID ef307bd6b5d87fb3f6c5159200500c9a47a06b67 # Parent 8b66a10aaf8abd3a39e96d2e4b3b16f6cd3ee354 refac: restructured client application To make the application more accessible for developers, the structure was reorganized. Instead of sticking to technical terminology, the application terminology is according to the domain: I.e. "map" contains everything regarding map (including store). diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/App.vue --- a/client/src/App.vue Fri Sep 07 09:13:03 2018 +0200 +++ b/client/src/App.vue Fri Sep 07 11:13:56 2018 +0200 @@ -42,8 +42,8 @@ + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/Topbar.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/Topbar.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,73 @@ + + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/User.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/User.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,38 @@ + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/assets/application.scss --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/assets/application.scss Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,24 @@ +$offset: 20px; +$small-offset: 10px; +$large-offset: 30px; +$x-large-offset: 50px; +$iconsize: 3em; +$iconLineHeight: 0.25em; +$iconwidth: 20px; +$basic-shadow: 1px 3px 8px 2px rgba(220, 220, 220, 0.75); +$basic-shadow-light: 1px 1px 12px 1px rgba(235, 235, 235, 0.75); +$transition: 0.5s; +$transition-fast: 0.1s; +$transition-slow: 3s; +$topbarheight: 5vh; + +%fully-centered { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.ui-element { + pointer-events: auto; +} diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/assets/logo.png Binary file client/src/application/assets/logo.png has changed diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/assets/tooltip.scss --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/assets/tooltip.scss Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,109 @@ +.tooltip { + display: block !important; + z-index: 10000; + + .tooltip-inner { + background: black; + color: white; + border-radius: 16px; + padding: 5px 10px 4px; + } + + .tooltip-arrow { + width: 0; + height: 0; + border-style: solid; + position: absolute; + margin: 5px; + border-color: black; + z-index: 1; + } + + &[x-placement^="top"] { + margin-bottom: 5px; + + .tooltip-arrow { + border-width: 5px 5px 0 5px; + border-left-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + bottom: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; + } + } + + &[x-placement^="bottom"] { + margin-top: 5px; + + .tooltip-arrow { + border-width: 0 5px 5px 5px; + border-left-color: transparent !important; + border-right-color: transparent !important; + border-top-color: transparent !important; + top: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; + } + } + + &[x-placement^="right"] { + margin-left: 5px; + + .tooltip-arrow { + border-width: 5px 5px 5px 0; + border-left-color: transparent !important; + border-top-color: transparent !important; + border-bottom-color: transparent !important; + left: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; + } + } + + &[x-placement^="left"] { + margin-right: 5px; + + .tooltip-arrow { + border-width: 5px 0 5px 5px; + border-top-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + right: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; + } + } + + &.popover { + $color: #f9f9f9; + + .popover-inner { + background: $color; + color: black; + padding: 24px; + border-radius: 5px; + box-shadow: 0 5px 30px rgba(black, 0.1); + } + + .popover-arrow { + border-color: $color; + } + } + + &[aria-hidden="true"] { + visibility: hidden; + opacity: 0; + transition: opacity 0.15s, visibility 0.15s; + } + + &[aria-hidden="false"] { + visibility: visible; + opacity: 1; + transition: opacity 0.15s; + } +} diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/assets/user.png Binary file client/src/application/assets/user.png has changed diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/lib/errors.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/lib/errors.js Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,11 @@ +/** facade to wrap calls to vue2-toastr */ +import app from "../../main"; + +const displayError = ({ title, message }) => { + app.$toast.error({ + title: title, + message: message + }); +}; + +export { displayError }; diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/lib/http.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/lib/http.js Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,8 @@ +import axios from "axios"; + +export const HTTP = axios.create({ + baseURL: process.env.VUE_APP_API_URL || "/api" + /* headers: { + Authorization: 'Bearer {token}' + }*/ +}); diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/lib/session.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/lib/session.js Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,23 @@ +/** + * Compares whether session is current + * based on the expiry information and the + * current date + * + * @param {number} expiresFromPastSession + */ +function sessionStillActive(expiresFromPastSession) { + if (!expiresFromPastSession) return false; + const now = Date.now(); + const stillActive = now < expiresFromPastSession; + return stillActive; +} +/** + * Converts a given unix time to Milliseconds + * + * @param {string} timestring + */ +function toMillisFromString(timestring) { + return timestring * 1000; +} + +export { sessionStillActive, toMillisFromString }; diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/stores/application.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/stores/application.js Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,41 @@ +const defaultCollapseState = true; + +const Application = { + namespaced: true, + state: { + appTitle: process.env.VUE_APP_TITLE, + secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL, + sidebar: { + iscollapsed: defaultCollapseState + }, + countries: ["AT", "SK", "HU", "HR", "RS", "BiH", "BG", "RO", "UA"] + }, + getters: { + countries: state => { + return state.countries; + }, + sidebarCollapsed: state => { + return state.sidebar.iscollapsed; + }, + appTitle: state => { + return state.appTitle; + }, + secondaryLogo: state => { + return state.secondaryLogo; + } + }, + mutations: { + toggleSidebar: state => { + state.sidebar.iscollapsed = !state.sidebar.iscollapsed; + }, + resetSidebar: state => { + state.sidebar.iscollapsed = defaultCollapseState; + }, + collapseSidebar: state => { + state.sidebar.iscollapsed = true; + } + }, + actions: {} +}; + +export default Application; diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/application/stores/user.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/application/stores/user.js Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,81 @@ +import { HTTP } from "../lib/http"; + +const User = { + namespaced: true, + state: { + authenticated: false, + expires: null, + roles: [], + user: "" + }, + getters: { + isAuthenticated: state => { + return state.authenticated; + }, + userinfo: state => { + return state.user; + }, + roles: state => { + return state.roles; + }, + expires: state => { + return state.expires; + }, + isWaterwayAdmin: state => { + return state.roles.includes("waterway_admin"); + }, + isSysAdmin: state => { + return state.roles.includes("sys_admin"); + } + }, + mutations: { + auth_success: (state, data) => { + const { token, user, expires, roles } = data; + localStorage.setItem("expires", expires); + localStorage.setItem("roles", roles); + localStorage.setItem("token", token); + localStorage.setItem("user", user); + state.expires = expires; + state.roles = roles; + state.user = user; + state.authenticated = true; + }, + clear_auth: state => { + state.authenticated = false; + state.expires = null; + state.roles = []; + state.user = ""; + localStorage.clear(); + }, + set_user: (state, name) => { + state.user = name; + }, + set_roles: (state, roles) => { + state.roles = roles; + }, + set_expires: (state, expires) => { + state.expires = expires; + }, + set_authenticate: state => { + state.authenticated = true; + } + }, + actions: { + login({ commit }, user) { + // using POST is a bit more secure than GET + return new Promise((resolve, reject) => { + HTTP.post("/login", user) + .then(response => { + commit("auth_success", response.data); + resolve(response); + }) + .catch(error => { + commit("clear_auth"); + reject(error); + }); + }); + } + } +}; + +export default User; diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/fairway/Fairwayprofile.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/fairway/Fairwayprofile.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,163 @@ + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/layers/Layers.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/layers/Layers.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,41 @@ + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/layers/Layerselect.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/layers/Layerselect.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,23 @@ + + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/login/Login.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/login/Login.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,201 @@ +() + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/map/Maplayer.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/map/Maplayer.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,107 @@ + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/map/Mapview.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/map/Mapview.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,24 @@ + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/map/store.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/map/store.js Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,58 @@ +//import { HTTP } from "../lib/http"; + +import TileWMS from "ol/source/TileWMS.js"; +import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer.js"; +import OSM from "ol/source/OSM"; +import { Stroke, Style } from "ol/style.js"; +import VectorSource from "ol/source/Vector.js"; + +const MapStore = { + namespaced: true, + state: { + layers: [ + { + name: "Open Streetmap", + data: new TileLayer({ + source: new OSM() + }), + isVisible: true + }, + { + name: "D4D", + data: new TileLayer({ + source: new TileWMS({ + url: "https://demo.d4d-portal.info/wms", + params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true } + }) + }), + isVisible: true + }, + { + name: "Fairways Dimensions", + data: new VectorLayer({ + source: new VectorSource(), + style: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, 1.0)", + width: 2 + }) + }) + }), + isVisible: true + } + ] + }, + getters: { + layers: state => { + return state.layers; + } + }, + mutations: { + toggleVisibility: (state, layer) => { + state.layers[layer].isVisible = !state.layers[layer].isVisible; + state.layers[layer].data.setVisible(state.layers[layer].isVisible); + } + } +}; + +export default MapStore; diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/router.js --- a/client/src/router.js Fri Sep 07 09:13:03 2018 +0200 +++ b/client/src/router.js Fri Sep 07 11:13:56 2018 +0200 @@ -1,12 +1,15 @@ import Vue from "vue"; import Router from "vue-router"; import store from "./store"; -import { sessionStillActive, toMillisFromString } from "./lib/session"; +import { + sessionStillActive, + toMillisFromString +} from "./application/lib/session"; /* facilitate codesplitting */ -const Login = () => import("./views/Login.vue"); -const Main = () => import("./views/Main.vue"); -const Usermanagement = () => import("./views/Usermanagement.vue"); +const Login = () => import("./login/Login.vue"); +const Main = () => import("./map/Mapview.vue"); +const Usermanagement = () => import("./usermanagement/Usermanagement.vue"); Vue.use(Router); diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/store.js --- a/client/src/store.js Fri Sep 07 09:13:03 2018 +0200 +++ b/client/src/store.js Fri Sep 07 11:13:56 2018 +0200 @@ -1,9 +1,9 @@ import Vue from "vue"; import Vuex from "vuex"; -import Application from "./stores/application"; -import user from "./stores/user"; -import usermanagement from "./stores/usermanagement"; -import mapstore from "./stores/mapstore"; +import Application from "./application/stores/application"; +import user from "./application/stores/user"; +import usermanagement from "./usermanagement/store"; +import mapstore from "./map/store"; Vue.use(Vuex); diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/usermanagement/Passwordfield.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/usermanagement/Passwordfield.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,43 @@ + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/usermanagement/Userdetail.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/usermanagement/Userdetail.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,262 @@ + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/usermanagement/Usermanagement.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/usermanagement/Usermanagement.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,284 @@ + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/usermanagement/Users.vue --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/usermanagement/Users.vue Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,283 @@ + + + + + diff -r 8b66a10aaf8a -r ef307bd6b5d8 client/src/usermanagement/store.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/usermanagement/store.js Fri Sep 07 11:13:56 2018 +0200 @@ -0,0 +1,134 @@ +import { HTTP } from "../application/lib/http"; + +const newUser = () => { + return { + user: "", + email: "", + country: null, + role: null, + isNew: true, + password: "", + roleLabel: "" + }; +}; + +const UserManagement = { + namespaced: true, + state: { + users: null, + currentUser: null, + userDetailsVisible: false + }, + getters: { + isUserDetailsVisible: state => { + return state.userDetailsVisible; + }, + currentUser: state => { + return state.currentUser; + }, + users: state => { + return state.users; + }, + getUserByName: state => name => { + return state.users.find(user => { + return user.user === name; + }); + } + }, + mutations: { + setUserDetailsInvisible: state => { + state.userDetailsVisible = false; + }, + setUserDetailsVisible: state => { + state.userDetailsVisible = true; + }, + usersLoaded: (state, data) => { + const resolveLabel = x => { + const labels = { + waterway_user: "Waterway User", + waterway_admin: "Waterway Administrator", + sys_admin: "System Admininistrator" + }; + return labels[x]; + }; + let users = data.users.map(u => { + u["roleLabel"] = resolveLabel(u["role"]); + return u; + }); + state.users = users; + }, + setCurrentUser: (state, data) => { + state.currentUser = data; + state.userDetailsVisible = true; + }, + clearCurrentUser: state => { + state.currentUser = newUser(); + } + }, + actions: { + deleteUser({ commit }, data) { + const { name } = data; + return new Promise((resolve, reject) => { + HTTP.delete("/users/" + name, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + commit("clearCurrentUser"); + commit("setUserDetailsInvisible"); + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, + saveCurrentUser({ commit }, data) { + const { path, user } = data; + if (user.isNew) { + return new Promise((resolve, reject) => { + HTTP.post("/users", user, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + commit("setUserDetailsInvisible"); + commit("clearCurrentUser"); + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + } else { + return new Promise((resolve, reject) => { + HTTP.put("/users/" + path, user, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + commit("setUserDetailsInvisible"); + commit("clearCurrentUser"); + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + } + }, + loadUsers({ commit }) { + return new Promise((resolve, reject) => { + HTTP.get("/users", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + commit("usersLoaded", response.data); + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + } + } +}; + +export default UserManagement;