Display config values in header and footer.

This commit is contained in:
baldo 2022-05-19 13:33:48 +02:00
parent 59f7897d8e
commit fde340ead0
13 changed files with 344 additions and 80 deletions

View file

@ -11,6 +11,7 @@
},
"dependencies": {
"pinia": "^2.0.14",
"sparkson": "^1.3.6",
"vue": "^3.2.34",
"vue-router": "^4.0.15"
},

View file

@ -14,4 +14,15 @@ import PageFooter from "@/components/PageFooter.vue";
<PageFooter />
</template>
<style lang="scss" scoped></style>
<style lang="scss">
@import "scss/variables";
body {
background-color: $page-background-color;
color: $page-text-color;
}
a {
color: $link-color;
}
</style>

View file

@ -1,9 +1,12 @@
<script setup lang="ts">
import { useConfigStore } from "@/stores/config";
import { useVersionStore } from "@/stores/version";
const config = useConfigStore();
const version = useVersionStore();
function refresh(): void {
config.refresh();
version.refresh();
}
@ -11,9 +14,29 @@ refresh();
</script>
<template>
<footer>
{{ version.getVersion }}
<footer v-if="config.getConfig">
ffffng ({{ version.getVersion }})
<a href="https://github.com/freifunkhamburg/ffffng" target="_blank">Source Code</a>
<a href="https://github.com/freifunkhamburg/ffffng/issues" target="_blank">Fehler melden</a>
<a
v-if="config.getConfig.legal.privacyUrl"
:href="config.getConfig.legal.privacyUrl"
target="_blank">Datenschutz</a>
<a
v-if="config.getConfig.legal.imprintUrl"
:href="config.getConfig.legal.imprintUrl"
target="_blank">Impressum</a>
</footer>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
@import "../scss/variables";
footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
}
</style>

View file

@ -1,15 +1,39 @@
<script setup lang="ts">
import { useConfigStore } from "@/stores/config";
const config = useConfigStore();
function refresh(): void {
config.refresh();
}
refresh();
</script>
<template>
<header>
<div class="wrapper">
<nav>
<RouterLink to="/">Home</RouterLink> |
<RouterLink to="/admin">Admin</RouterLink>
</nav>
</div>
<header v-if="config.getConfig">
<nav>
<RouterLink to="/">
<img src="icon.svg" alt="Symbol: Gelbes Zahnrad in zwei konzentrischen Kreisen (hellgrau und magenta)"/>
{{ config.getConfig.community.name }} Knotenverwaltung
</RouterLink>
<RouterLink to="/admin">Admin-Panel</RouterLink>
</nav>
</header>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
@import "../scss/variables";
header {
position: absolute;
top: 0;
left: 0;
right: 0;
img {
width: 2em;
height: 2em;
}
}
</style>

View file

@ -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;

View file

@ -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<void> {
this.config = await api.get<ClientConfig>(
"config",
isClientConfig
);
},
},
});

View file

@ -2,7 +2,11 @@
</script>
<template>
Home
<div>
<h1>Willkommen!</h1>
<p>Du hast einen neuen Freifunk Hamburg Router (Knoten), den Du in Betrieb nehmen möchtest? Du hast schon einen Knoten in Betrieb und möchtest seine Daten ändern? Oder Du möchtest einen Knoten, der nicht mehr in Betrieb ist löschen? Dann bist Du hier richtig!</p>
</div>
</template>
<style lang="scss" scoped></style>

View file

@ -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": [

View file

@ -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"

View file

@ -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
);
}

View file

@ -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);

View file

@ -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,
) {}
}

View file

@ -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<T>(arg: unknown, isT: (arg: unknown) => arg is T): arg is Array<T> {
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"
);
}