From fde340ead098eea32249a03a985b1331e1ff6634 Mon Sep 17 00:00:00 2001 From: baldo Date: Thu, 19 May 2022 13:33:48 +0200 Subject: [PATCH] Display config values in header and footer. --- frontend/package.json | 1 + frontend/src/App.vue | 13 +- frontend/src/components/PageFooter.vue | 29 +++- frontend/src/components/PageHeader.vue | 40 ++++-- frontend/src/scss/_variables.scss | 20 +++ frontend/src/stores/config.ts | 29 ++++ frontend/src/views/HomeView.vue | 6 +- frontend/tsconfig.json | 6 +- frontend/yarn.lock | 24 ++++ server/resources/configResource.ts | 10 ++ server/router.ts | 2 + server/types/config.ts | 67 +--------- server/types/shared.ts | 177 +++++++++++++++++++++++++ 13 files changed, 344 insertions(+), 80 deletions(-) create mode 100644 frontend/src/scss/_variables.scss create mode 100644 frontend/src/stores/config.ts create mode 100644 server/resources/configResource.ts diff --git a/frontend/package.json b/frontend/package.json index 3c7b4d3..0ec3881 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "pinia": "^2.0.14", + "sparkson": "^1.3.6", "vue": "^3.2.34", "vue-router": "^4.0.15" }, diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 0d53028..c657b4a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,4 +14,15 @@ import PageFooter from "@/components/PageFooter.vue"; - + diff --git a/frontend/src/components/PageFooter.vue b/frontend/src/components/PageFooter.vue index d0d5efe..dc594ba 100644 --- a/frontend/src/components/PageFooter.vue +++ b/frontend/src/components/PageFooter.vue @@ -1,9 +1,12 @@ - + diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue index a1a5eeb..4b3e6fe 100644 --- a/frontend/src/components/PageHeader.vue +++ b/frontend/src/components/PageHeader.vue @@ -1,15 +1,39 @@ - + diff --git a/frontend/src/scss/_variables.scss b/frontend/src/scss/_variables.scss new file mode 100644 index 0000000..44ac9a0 --- /dev/null +++ b/frontend/src/scss/_variables.scss @@ -0,0 +1,20 @@ +// Grays +$gray-darker: #333333; +$gray-dark: #444444; +$gray: #666666; +$gray-light: #d6d6d6; +$gray-lighter: #ededed; + +// Colors +$color-primary: #e5287a; +$color-success: #449d44; +$color-warning: #fdbc41; +$color-danger: #c9302c; +$color-info: #009ee0; + +// Page +$page-background-color: $gray-darker; +$page-text-color: $gray-lighter; + +// Links +$link-color: $color-primary; diff --git a/frontend/src/stores/config.ts b/frontend/src/stores/config.ts new file mode 100644 index 0000000..7cd0e75 --- /dev/null +++ b/frontend/src/stores/config.ts @@ -0,0 +1,29 @@ +import {defineStore} from "pinia"; +import {type ClientConfig, isClientConfig} from "@/types"; +import {api} from "@/utils/Api"; + +interface ConfigStoreState { + config: ClientConfig | null; +} + +export const useConfigStore = defineStore({ + id: "config", + state(): ConfigStoreState { + return { + config: null, + }; + }, + getters: { + getConfig(state: ConfigStoreState): ClientConfig | null { + return state.config; + }, + }, + actions: { + async refresh(): Promise { + this.config = await api.get( + "config", + isClientConfig + ); + }, + }, +}); diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index cd9b7b1..e2970be 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -2,7 +2,11 @@ diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 2551bf2..0c3ee8c 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -5,8 +5,10 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@/*": ["./src/*"] - } + "@/*": ["./src/*"], + }, + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ }, "references": [ diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 34238c6..7c2f9b8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1269,6 +1269,16 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.times@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.times/-/lodash.times-4.3.2.tgz#3e1f2565c431754d54ab57f2ed1741939285ca1d" + integrity sha1-Ph8lZcQxdU1Uq1fy7RdBk5KFyh0= + +lodash.zip@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020" + integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA= + lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -1506,6 +1516,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +reflect-metadata@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2" + integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A== + regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -1611,6 +1626,15 @@ sourcemap-codec@^1.4.8: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +sparkson@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/sparkson/-/sparkson-1.3.6.tgz#c165748898053344e4a36d4b6c4ba51abe65e079" + integrity sha512-hn75TVt4lHgVIe367YZqrprYrsVDb9DHzBbFLeDH3U0BqOH47jL6X0OzkqtcQ16G0gJn8D69Ev4Vnj/rvxNV4g== + dependencies: + lodash.times "4.3.2" + lodash.zip "4.2.0" + reflect-metadata "0.1.12" + strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" diff --git a/server/resources/configResource.ts b/server/resources/configResource.ts new file mode 100644 index 0000000..17c2928 --- /dev/null +++ b/server/resources/configResource.ts @@ -0,0 +1,10 @@ +import {success} from "../utils/resources"; +import {config} from "../config"; +import {Request, Response} from "express"; + +export function get (req: Request, res: Response): void { + success( + res, + config.client + ); +} diff --git a/server/router.ts b/server/router.ts index 49e25ef..b1f0c4d 100644 --- a/server/router.ts +++ b/server/router.ts @@ -3,6 +3,7 @@ import express from "express" import {app} from "./app" import {config} from "./config" +import * as ConfigResource from "./resources/configResource" import * as VersionResource from "./resources/versionResource" import * as StatisticsResource from "./resources/statisticsResource" import * as FrontendResource from "./resources/frontendResource" @@ -16,6 +17,7 @@ export function init (): void { router.post('/', FrontendResource.render); + router.get('/api/config', ConfigResource.get); router.get('/api/version', VersionResource.get); router.post('/api/node', NodeResource.create); diff --git a/server/types/config.ts b/server/types/config.ts index f369ccb..d550516 100644 --- a/server/types/config.ts +++ b/server/types/config.ts @@ -1,4 +1,5 @@ import {ArrayField, Field, RawJsonField} from "sparkson" +import {ClientConfig} from "./shared"; // TODO: Replace string types by more specific types like URL, Password, etc. @@ -50,73 +51,9 @@ export class ServerConfig { ) {} } -export class CommunityConfig { - constructor( - @Field("name") public name: string, - @Field("domain") public domain: string, - @Field("contactEmail") public contactEmail: string, - @ArrayField("sites", String) public sites: string[], - @ArrayField("domains", String) public domains: string[], - ) {} -} - -export class LegalConfig { - constructor( - @Field("privacyUrl", true) public privacyUrl?: string, - @Field("imprintUrl", true) public imprintUrl?: string, - ) {} -} - -export class ClientMapConfig { - constructor( - @Field("mapUrl") public mapUrl: string, - ) {} -} -export class MonitoringConfig { - constructor( - @Field("enabled") public enabled: boolean, - ) {} -} - -export class Coords { - constructor( - @Field("lat") public lat: number, - @Field("lng") public lng: number, - ) {} -} - -export class CoordsSelectorConfig { - constructor( - @Field("lat") public lat: number, - @Field("lng") public lng: number, - @Field("defaultZoom") public defaultZoom: number, - @RawJsonField("layers") public layers: any, // TODO: Better types! - ) {} -} - -export class OtherCommunityInfoConfig { - constructor( - @Field("showInfo") public showInfo: boolean, - @Field("showBorderForDebugging") public showBorderForDebugging: boolean, - @ArrayField("localCommunityPolygon", Coords) public localCommunityPolygon: Coords[], - ) {} -} - -export class ClientConfig { - constructor( - @Field("community") public community: CommunityConfig, - @Field("legal") public legal: LegalConfig, - @Field("map") public map: ClientMapConfig, - @Field("monitoring") public monitoring: MonitoringConfig, - @Field("coordsSelector") public coordsSelector: CoordsSelectorConfig, - @Field("otherCommunityInfo") public otherCommunityInfo: OtherCommunityInfoConfig, - @Field("rootPath", true, undefined, "/") public rootPath: string, - ) {} -} - export class Config { constructor( @Field("server") public server: ServerConfig, - @Field("client") public client: ClientConfig + @Field("client") public client: ClientConfig, ) {} } diff --git a/server/types/shared.ts b/server/types/shared.ts index 4d143bd..2078f1b 100644 --- a/server/types/shared.ts +++ b/server/types/shared.ts @@ -1,9 +1,27 @@ +import {ArrayField, Field, RawJsonField} from "sparkson"; + // Types shared with the client. export function isObject(arg: unknown): arg is object { return arg !== null && typeof arg === "object"; } +export function isArray(arg: unknown, isT: (arg: unknown) => arg is T): arg is Array { + if (!Array.isArray(arg)) { + return false; + } + for (const element of arg) { + if (!isT(element)) { + return false + } + } + return true; +} + +export function isString(arg: unknown): arg is string { + return typeof arg === "string" +} + export type Version = string; export function isVersion(arg: unknown): arg is Version { @@ -43,3 +61,162 @@ export interface Statistics { export function isStatistics(arg: unknown): arg is Statistics { return isObject(arg) && isNodeStatistics((arg as Statistics).nodes); } + +export class CommunityConfig { + constructor( + @Field("name") public name: string, + @Field("domain") public domain: string, + @Field("contactEmail") public contactEmail: string, + @ArrayField("sites", String) public sites: string[], + @ArrayField("domains", String) public domains: string[], + ) {} +} + +export function isCommunityConfig(arg: unknown): arg is CommunityConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as CommunityConfig; + return ( + typeof cfg.name === "string" && + typeof cfg.domain === "string" && + typeof cfg.contactEmail === "string" && + isArray(cfg.sites, isString) && + isArray(cfg.domains, isString) + ); +} + +export class LegalConfig { + constructor( + @Field("privacyUrl", true) public privacyUrl?: string, + @Field("imprintUrl", true) public imprintUrl?: string, + ) {} +} + +export function isLegalConfig(arg: unknown): arg is LegalConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as LegalConfig; + return ( + (cfg.privacyUrl === undefined || typeof cfg.privacyUrl === "string") && + (cfg.imprintUrl === undefined || typeof cfg.imprintUrl === "string") + ); +} + +export class ClientMapConfig { + constructor( + @Field("mapUrl") public mapUrl: string, + ) {} +} + +export function isClientMapConfig(arg: unknown): arg is ClientMapConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as ClientMapConfig; + return typeof cfg.mapUrl === "string"; +} + +export class MonitoringConfig { + constructor( + @Field("enabled") public enabled: boolean, + ) {} +} + +export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as MonitoringConfig; + return typeof cfg.enabled === "boolean"; +} + +export class Coords { + constructor( + @Field("lat") public lat: number, + @Field("lng") public lng: number, + ) {} +} + +export function isCoords(arg: unknown): arg is Coords { + if (!isObject(arg)) { + return false; + } + const coords = arg as Coords; + return ( + typeof coords.lat === "number" && + typeof coords.lng === "number" + ); +} + +export class CoordsSelectorConfig { + constructor( + @Field("lat") public lat: number, + @Field("lng") public lng: number, + @Field("defaultZoom") public defaultZoom: number, + @RawJsonField("layers") public layers: any, // TODO: Better types! + ) {} +} + +export function isCoordsSelectorConfig(arg: unknown): arg is CoordsSelectorConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as CoordsSelectorConfig; + return ( + typeof cfg.lat === "number" && + typeof cfg.lng === "number" && + typeof cfg.defaultZoom === "number" && + isObject(cfg.layers) // TODO: Better types! + ); +} + +export class OtherCommunityInfoConfig { + constructor( + @Field("showInfo") public showInfo: boolean, + @Field("showBorderForDebugging") public showBorderForDebugging: boolean, + @ArrayField("localCommunityPolygon", Coords) public localCommunityPolygon: Coords[], + ) {} +} + +export function isOtherCommunityInfoConfig(arg: unknown): arg is OtherCommunityInfoConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as OtherCommunityInfoConfig; + return ( + typeof cfg.showInfo === "boolean" && + typeof cfg.showBorderForDebugging === "boolean" && + isArray(cfg.localCommunityPolygon, isCoords) + ); +} + +export class ClientConfig { + constructor( + @Field("community") public community: CommunityConfig, + @Field("legal") public legal: LegalConfig, + @Field("map") public map: ClientMapConfig, + @Field("monitoring") public monitoring: MonitoringConfig, + @Field("coordsSelector") public coordsSelector: CoordsSelectorConfig, + @Field("otherCommunityInfo") public otherCommunityInfo: OtherCommunityInfoConfig, + @Field("rootPath", true, undefined, "/") public rootPath: string, + ) { + } +} + +export function isClientConfig(arg: unknown): arg is ClientConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as ClientConfig; + return ( + isCommunityConfig(cfg.community) && + isLegalConfig(cfg.legal) && + isClientMapConfig(cfg.map) && + isMonitoringConfig(cfg.monitoring) && + isCoordsSelectorConfig(cfg.coordsSelector) && + isOtherCommunityInfoConfig(cfg.otherCommunityInfo) && + typeof cfg.rootPath === "string" + ); +}