ESLint: Auto format and fix some warnings / errors.
This commit is contained in:
parent
90ac67efbe
commit
867be21f68
32 changed files with 737 additions and 489 deletions
|
@ -1,17 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import {RouterView} from "vue-router";
|
||||
import { RouterView } from "vue-router";
|
||||
import PageHeader from "@/components/page/PageHeader.vue";
|
||||
import PageFooter from "@/components/page/PageFooter.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader/>
|
||||
<PageHeader />
|
||||
|
||||
<main>
|
||||
<RouterView/>
|
||||
<RouterView />
|
||||
</main>
|
||||
|
||||
<PageFooter/>
|
||||
<PageFooter />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="spinner-container">
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import {computed, defineProps} from "vue";
|
||||
import { computed, defineProps } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
page: {
|
||||
|
@ -17,10 +17,17 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const firstItem = computed(() => {
|
||||
return Math.min(props.totalItems, (props.page - 1) * props.itemsPerPage + 1)
|
||||
return Math.min(
|
||||
props.totalItems,
|
||||
(props.page - 1) * props.itemsPerPage + 1
|
||||
);
|
||||
});
|
||||
const lastItem = computed(() => Math.min(props.totalItems, firstItem.value + props.itemsPerPage - 1));
|
||||
const lastPage = computed(() => Math.ceil(props.totalItems / props.itemsPerPage));
|
||||
const lastItem = computed(() =>
|
||||
Math.min(props.totalItems, firstItem.value + props.itemsPerPage - 1)
|
||||
);
|
||||
const lastPage = computed(() =>
|
||||
Math.ceil(props.totalItems / props.itemsPerPage)
|
||||
);
|
||||
const pages = computed(() => {
|
||||
const pages: number[] = [];
|
||||
if (lastPage.value <= 2) {
|
||||
|
@ -48,11 +55,13 @@ const showFirstEllipsis = computed(
|
|||
() => pages.value.length > 0 && pages.value[0] > 2
|
||||
);
|
||||
const showLastEllipsis = computed(
|
||||
() => pages.value.length > 0 && pages.value[pages.value.length - 1] < lastPage.value - 1
|
||||
() =>
|
||||
pages.value.length > 0 &&
|
||||
pages.value[pages.value.length - 1] < lastPage.value - 1
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "changePage", page: number): void,
|
||||
(e: "changePage", page: number): void;
|
||||
}>();
|
||||
|
||||
function toPage(page: number): void {
|
||||
|
@ -60,7 +69,9 @@ function toPage(page: number): void {
|
|||
}
|
||||
|
||||
// noinspection JSIncompatibleTypesComparison
|
||||
const classes = computed(() => (p: number) => p === props.page ? ["current-page"] : []);
|
||||
const classes = computed(
|
||||
() => (p: number) => p === props.page ? ["current-page"] : []
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -77,9 +88,22 @@ const classes = computed(() => (p: number) => p === props.page ? ["current-page"
|
|||
<li v-if="page > 1" @click="toPage(page - 1)">‹</li>
|
||||
<li :class="classes(1)" @click="toPage(1)">1</li>
|
||||
<li v-if="showFirstEllipsis" class="ellipsis">…</li>
|
||||
<li v-for="page in pages" :class="classes(page)" @click="toPage(page)">{{ page }}</li>
|
||||
<li
|
||||
v-for="page in pages"
|
||||
v-bind:key="page"
|
||||
:class="classes(page)"
|
||||
@click="toPage(page)"
|
||||
>
|
||||
{{ page }}
|
||||
</li>
|
||||
<li v-if="showLastEllipsis" class="ellipsis">…</li>
|
||||
<li v-if="lastPage > 1" :class="classes(lastPage)" @click="toPage(lastPage)">{{ lastPage }}</li>
|
||||
<li
|
||||
v-if="lastPage > 1"
|
||||
:class="classes(lastPage)"
|
||||
@click="toPage(lastPage)"
|
||||
>
|
||||
{{ lastPage }}
|
||||
</li>
|
||||
<li v-if="page < lastPage" @click="toPage(page + 1)">›</li>
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,18 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import ActivityIndicator from "@/components/ActivityIndicator.vue";
|
||||
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'loading-container': true, loading: loading }">
|
||||
<Spinner class="spinner" v-if="loading" />
|
||||
<div class="content" v-if="!loading">
|
||||
<div :class="{ 'loading-container': true, loading: props.loading }">
|
||||
<ActivityIndicator v-if="props.loading" />
|
||||
<div class="content" v-if="!props.loading">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,5 +31,4 @@ const props = defineProps({
|
|||
height: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import {computed, defineProps} from "vue";
|
||||
import type {ComponentVariant, NodesFilter} from "@/types";
|
||||
import type {RouteName} from "@/router";
|
||||
import router, {route} from "@/router";
|
||||
import { computed, defineProps } from "vue";
|
||||
import type { ComponentVariant, NodesFilter } from "@/types";
|
||||
import type { RouteName } from "@/router";
|
||||
import { route } from "@/router";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
@ -24,8 +24,11 @@ const linkTarget = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink :to="linkTarget" :class="['statistics-card', 'statistics-card-' + variant]">
|
||||
<i :class="['fa', 'fa-' + icon]" aria-hidden="true"/>
|
||||
<RouterLink
|
||||
:to="linkTarget"
|
||||
:class="['statistics-card', 'statistics-card-' + variant]"
|
||||
>
|
||||
<i :class="['fa', 'fa-' + icon]" aria-hidden="true" />
|
||||
<dl>
|
||||
<dt>{{ title }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type {ButtonSize, ComponentVariant} from "@/types";
|
||||
import type { ButtonSize, ComponentVariant } from "@/types";
|
||||
|
||||
interface Props {
|
||||
variant: ComponentVariant;
|
||||
|
@ -7,10 +7,10 @@ interface Props {
|
|||
icon: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void,
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
|
||||
function onClick() {
|
||||
|
@ -19,8 +19,8 @@ function onClick() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<button :class="[size, variant]" @click="onClick">
|
||||
<i :class="['fa', `fa-${icon}`]" aria-hidden="true"/>
|
||||
<button :class="[props.size, props.variant]" @click="onClick">
|
||||
<i :class="['fa', `fa-${props.icon}`]" aria-hidden="true" />
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
@ -44,7 +44,8 @@ button {
|
|||
border-color: $color;
|
||||
color: map-get($variant-text-colors, $variant);
|
||||
|
||||
&:hover, &:active {
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: $page-background-color;
|
||||
border-color: $color;
|
||||
color: $color;
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import type {ButtonSize, ComponentAlignment} from "@/types";
|
||||
import type { ButtonSize, ComponentAlignment } from "@/types";
|
||||
|
||||
interface Props {
|
||||
buttonSize: ButtonSize;
|
||||
align: ComponentAlignment;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['button-group', buttonSize, align]">
|
||||
<div :class="['button-group', props.buttonSize, props.align]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type {ButtonSize, ComponentVariant} from "@/types";
|
||||
import type {Route} from "@/router";
|
||||
import type { ButtonSize, ComponentVariant } from "@/types";
|
||||
import type { Route } from "@/router";
|
||||
import router from "@/router";
|
||||
import ActionButton from "@/components/form/ActionButton.vue";
|
||||
|
||||
|
@ -19,15 +19,9 @@ async function onClick() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ActionButton
|
||||
:variant="variant"
|
||||
:size="size"
|
||||
:icon="icon"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot/>
|
||||
<ActionButton :variant="variant" :size="size" :icon="icon" @click="onClick">
|
||||
<slot />
|
||||
</ActionButton>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import {type ComponentInternalInstance, ref, type Slot} from "vue";
|
||||
import { type ComponentInternalInstance, ref } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit"): void
|
||||
(e: "submit"): void;
|
||||
}>();
|
||||
|
||||
const validationComponents = ref<ComponentInternalInstance[]>([]);
|
||||
|
@ -10,14 +10,16 @@ const validationComponents = ref<ComponentInternalInstance[]>([]);
|
|||
defineExpose({
|
||||
registerValidationComponent(component: ComponentInternalInstance): void {
|
||||
validationComponents.value.push(component);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function validate(): boolean {
|
||||
let valid = true;
|
||||
for (const component of validationComponents.value) {
|
||||
if (!(component.exposed?.validate)) {
|
||||
throw new Error(`Component has no exposed validate() method: ${component.type.__name}`);
|
||||
if (!component.exposed?.validate) {
|
||||
throw new Error(
|
||||
`Component has no exposed validate() method: ${component.type.__name}`
|
||||
);
|
||||
}
|
||||
valid = component.exposed.validate() && valid;
|
||||
}
|
||||
|
@ -32,14 +34,12 @@ function onSubmit() {
|
|||
}
|
||||
// TODO: Else scroll to first error and focus input.
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<slot/>
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
<style lang="scss"></style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import {getCurrentInstance, onMounted, ref} from "vue";
|
||||
import {type Constraint, forConstraint} from "@/shared/validation/validator";
|
||||
import { getCurrentInstance, onMounted, ref } from "vue";
|
||||
import { type Constraint, forConstraint } from "@/shared/validation/validator";
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
|
@ -30,7 +30,9 @@ function registerValidationComponent() {
|
|||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
throw new Error("Could not find matching ValidationForm for ValidationFormInpunt.");
|
||||
throw new Error(
|
||||
"Could not find matching ValidationForm for ValidationFormInpunt."
|
||||
);
|
||||
}
|
||||
|
||||
function onInput() {
|
||||
|
@ -39,7 +41,7 @@ function onInput() {
|
|||
}
|
||||
const element = input.value;
|
||||
if (!element) {
|
||||
console.warn("Could not get referenced input element.")
|
||||
console.warn("Could not get referenced input element.");
|
||||
return;
|
||||
}
|
||||
emit("update:modelValue", element.value);
|
||||
|
@ -48,7 +50,7 @@ function onInput() {
|
|||
function validate(): boolean {
|
||||
const element = input.value;
|
||||
if (!element) {
|
||||
console.warn("Could not get referenced input element.")
|
||||
console.warn("Could not get referenced input element.");
|
||||
return false;
|
||||
}
|
||||
valid.value = forConstraint(props.constraint, false)(element.value);
|
||||
|
@ -57,7 +59,7 @@ function validate(): boolean {
|
|||
}
|
||||
|
||||
defineExpose({
|
||||
validate
|
||||
validate,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -68,7 +70,7 @@ onMounted(() => {
|
|||
<template>
|
||||
<div>
|
||||
<label>
|
||||
{{label}}:
|
||||
{{ label }}:
|
||||
<input
|
||||
ref="input"
|
||||
:value="modelValue"
|
||||
|
@ -78,7 +80,7 @@ onMounted(() => {
|
|||
/>
|
||||
</label>
|
||||
<div class="validation-error" v-if="!valid">
|
||||
{{validationError}}
|
||||
{{ validationError }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import ValidationForm from "@/components/form/ValidationForm.vue";
|
||||
import {useNodeStore} from "@/stores/node";
|
||||
import { useNodeStore } from "@/stores/node";
|
||||
import ButtonGroup from "@/components/form/ButtonGroup.vue";
|
||||
import ActionButton from "@/components/form/ActionButton.vue";
|
||||
import NodePreviewCard from "@/components/nodes/NodePreviewCard.vue";
|
||||
import type {Hostname, StoredNode} from "@/types";
|
||||
import {ButtonSize, ComponentAlignment, ComponentVariant} from "@/types";
|
||||
import router, {route, RouteName} from "@/router";
|
||||
import {computed, nextTick, ref} from "vue";
|
||||
import {ApiError} from "@/utils/Api";
|
||||
import type { Hostname, StoredNode } from "@/types";
|
||||
import { ButtonSize, ComponentAlignment, ComponentVariant } from "@/types";
|
||||
import router, { route, RouteName } from "@/router";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { ApiError } from "@/utils/Api";
|
||||
import ErrorCard from "@/components/ErrorCard.vue";
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
|
||||
interface Props {
|
||||
node: StoredNode;
|
||||
|
@ -24,7 +24,7 @@ const email = computed(() => configStore.getConfig?.community.contactEmail);
|
|||
const errorDeletingNode = ref<boolean>(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete", hostname: Hostname): void
|
||||
(e: "delete", hostname: Hostname): void;
|
||||
}>();
|
||||
|
||||
async function onSubmit() {
|
||||
|
@ -61,23 +61,31 @@ async function onAbort() {
|
|||
<h2>Soll der Knoten wirklich gelöscht werden?</h2>
|
||||
|
||||
<p>
|
||||
Soll der Knoten <strong>{{ props.node.hostname }}</strong> wirklich endgültig gelöscht werden?
|
||||
Du kannst ihn selbstverständlich später jederzeit erneut anmelden!
|
||||
Soll der Knoten <strong>{{ props.node.hostname }}</strong> wirklich
|
||||
endgültig gelöscht werden? Du kannst ihn selbstverständlich später
|
||||
jederzeit erneut anmelden!
|
||||
</p>
|
||||
|
||||
<ErrorCard v-if="errorDeletingNode">
|
||||
Beim Löschen des Knotens ist ein Fehler aufgetreten. Bitte probiere es später nochmal. Sollte dieses Problem
|
||||
weiter bestehen, so wende dich bitte per E-Mail an <a v-if="email" :href="`mailto:${email}`">{{ email }}</a>.
|
||||
Beim Löschen des Knotens ist ein Fehler aufgetreten. Bitte probiere
|
||||
es später nochmal. Sollte dieses Problem weiter bestehen, so wende
|
||||
dich bitte per E-Mail an
|
||||
<a v-if="email" :href="`mailto:${email}`">{{ email }}</a
|
||||
>.
|
||||
</ErrorCard>
|
||||
|
||||
<NodePreviewCard class="preview" :node="props.node"/>
|
||||
<NodePreviewCard class="preview" :node="props.node" />
|
||||
|
||||
<ButtonGroup :align="ComponentAlignment.CENTER" :button-size="ButtonSize.SMALL">
|
||||
<ButtonGroup
|
||||
:align="ComponentAlignment.CENTER"
|
||||
:button-size="ButtonSize.SMALL"
|
||||
>
|
||||
<ActionButton
|
||||
type="submit"
|
||||
icon="trash"
|
||||
:variant="ComponentVariant.WARNING"
|
||||
:size="ButtonSize.MEDIUM">
|
||||
:size="ButtonSize.MEDIUM"
|
||||
>
|
||||
Knoten löschen
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
|
@ -85,7 +93,8 @@ async function onAbort() {
|
|||
icon="times"
|
||||
:variant="ComponentVariant.SECONDARY"
|
||||
:size="ButtonSize.MEDIUM"
|
||||
@click="onAbort">
|
||||
@click="onAbort"
|
||||
>
|
||||
Abbrechen
|
||||
</ActionButton>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
import {useNodeStore} from "@/stores/node";
|
||||
import {computed, nextTick, ref} from "vue";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
import { useNodeStore } from "@/stores/node";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import CONSTRAINTS from "@/shared/validation/constraints";
|
||||
import ActionButton from "@/components/form/ActionButton.vue";
|
||||
import type {StoredNode, Token} from "@/types";
|
||||
import {ButtonSize, ComponentAlignment, ComponentVariant} from "@/types";
|
||||
import type { StoredNode, Token } from "@/types";
|
||||
import { ButtonSize, ComponentAlignment, ComponentVariant } from "@/types";
|
||||
import ErrorCard from "@/components/ErrorCard.vue";
|
||||
import ButtonGroup from "@/components/form/ButtonGroup.vue";
|
||||
import ValidationForm from "@/components/form/ValidationForm.vue";
|
||||
import ValidationFormInput from "@/components/form/ValidationFormInput.vue";
|
||||
import {route, RouteName} from "@/router";
|
||||
import { route, RouteName } from "@/router";
|
||||
import RouteButton from "@/components/form/RouteButton.vue";
|
||||
import {ApiError} from "@/utils/Api";
|
||||
import { ApiError } from "@/utils/Api";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const email = computed(() => configStore.getConfig?.community.contactEmail);
|
||||
|
@ -57,15 +57,18 @@ async function onSubmit() {
|
|||
|
||||
<div>
|
||||
<p>
|
||||
Um die Daten Deines Knotens zu löschen, benötigen wir den passenden Token (eine 16-stellige Folge aus
|
||||
Ziffern und Buchstaben). Diesen hast Du beim ersten Anmelden Deines Knotens erhalten. Sinn des Tokens
|
||||
ist,
|
||||
Dich davor zu schützen, dass Dritte unbefugt Deine Daten einsehen oder ändern können.
|
||||
Um die Daten Deines Knotens zu löschen, benötigen wir den
|
||||
passenden Token (eine 16-stellige Folge aus Ziffern und
|
||||
Buchstaben). Diesen hast Du beim ersten Anmelden Deines Knotens
|
||||
erhalten. Sinn des Tokens ist, Dich davor zu schützen, dass
|
||||
Dritte unbefugt Deine Daten einsehen oder ändern können.
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
Solltest Du den Token nicht mehr haben, wende Dich einfach per E-Mail an
|
||||
<a v-if="email" :href="`mailto:${email}`">{{ email }}</a>.
|
||||
Solltest Du den Token nicht mehr haben, wende Dich einfach
|
||||
per E-Mail an
|
||||
<a v-if="email" :href="`mailto:${email}`">{{ email }}</a
|
||||
>.
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
|
@ -74,9 +77,11 @@ async function onSubmit() {
|
|||
</ErrorCard>
|
||||
|
||||
<ErrorCard v-if="generalError">
|
||||
Beim Abrufen des Knotens ist ein Fehler aufgetreten. Bitte probiere es später nochmal. Sollte dieses
|
||||
Problem weiter bestehen, so wende dich bitte per E-Mail an
|
||||
<a v-if="email" :href="`mailto:${email}`">{{ email }}</a>.
|
||||
Beim Abrufen des Knotens ist ein Fehler aufgetreten. Bitte
|
||||
probiere es später nochmal. Sollte dieses Problem weiter
|
||||
bestehen, so wende dich bitte per E-Mail an
|
||||
<a v-if="email" :href="`mailto:${email}`">{{ email }}</a
|
||||
>.
|
||||
</ErrorCard>
|
||||
|
||||
<fieldset>
|
||||
|
@ -87,12 +92,16 @@ async function onSubmit() {
|
|||
:constraint="CONSTRAINTS.token"
|
||||
validation-error="Das Token ist ein 16-stelliger Wert bestehend aus 0-9 und a-f."
|
||||
/>
|
||||
<ButtonGroup :align="ComponentAlignment.RIGHT" :button-size="ButtonSize.SMALL">
|
||||
<ButtonGroup
|
||||
:align="ComponentAlignment.RIGHT"
|
||||
:button-size="ButtonSize.SMALL"
|
||||
>
|
||||
<ActionButton
|
||||
type="submit"
|
||||
icon="trash"
|
||||
:variant="ComponentVariant.WARNING"
|
||||
:size="ButtonSize.SMALL">
|
||||
:size="ButtonSize.SMALL"
|
||||
>
|
||||
Knoten löschen
|
||||
</ActionButton>
|
||||
<RouteButton
|
||||
|
@ -100,7 +109,8 @@ async function onSubmit() {
|
|||
icon="times"
|
||||
:variant="ComponentVariant.SECONDARY"
|
||||
:size="ButtonSize.SMALL"
|
||||
:route="route(RouteName.HOME)">
|
||||
:route="route(RouteName.HOME)"
|
||||
>
|
||||
Abbrechen
|
||||
</RouteButton>
|
||||
</ButtonGroup>
|
||||
|
@ -108,9 +118,11 @@ async function onSubmit() {
|
|||
|
||||
<p>
|
||||
<em>
|
||||
Hinweis: Nach dem Löschen kann der Knoten ggf. weiterhin in der Knotenkarte angezeigt werden. Dies
|
||||
ist dann der Fall, wenn der Knoten eingeschaltet ist und in Reichweite eines anderen aktiven Knotens
|
||||
steht. Die angezeigten Daten sind dann die während der Einrichtung des Knotens im Config-Mode
|
||||
Hinweis: Nach dem Löschen kann der Knoten ggf. weiterhin in
|
||||
der Knotenkarte angezeigt werden. Dies ist dann der Fall,
|
||||
wenn der Knoten eingeschaltet ist und in Reichweite eines
|
||||
anderen aktiven Knotens steht. Die angezeigten Daten sind
|
||||
dann die während der Einrichtung des Knotens im Config-Mode
|
||||
(Konfigurationsoberfläche des Routers) hinterlegten.
|
||||
</em>
|
||||
</p>
|
||||
|
@ -118,5 +130,4 @@ async function onSubmit() {
|
|||
</ValidationForm>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import ButtonGroup from "@/components/form/ButtonGroup.vue";
|
||||
import type {Hostname} from "@/types";
|
||||
import {ButtonSize, ComponentAlignment, ComponentVariant} from "@/types";
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
import {computed} from "vue";
|
||||
import type { Hostname } from "@/types";
|
||||
import { ButtonSize, ComponentAlignment, ComponentVariant } from "@/types";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
import { computed } from "vue";
|
||||
import RouteButton from "@/components/form/RouteButton.vue";
|
||||
import {route, RouteName} from "@/router";
|
||||
import { route, RouteName } from "@/router";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const email = computed(() => configStore.getConfig?.community.contactEmail);
|
||||
|
@ -22,38 +22,48 @@ const props = defineProps<Props>();
|
|||
<div>
|
||||
<h1>Erledigt!</h1>
|
||||
<p>
|
||||
Die Daten Deines Freifunk-Knotens sind gelöscht worden. Es kann jetzt noch bis zu 20 Minuten dauern,
|
||||
bis die Änderungen überall wirksam werden und sich in der
|
||||
<a :href="configStore.getConfig?.map.mapUrl" target="_blank">Knotenkarte</a> auswirken.
|
||||
Die Daten Deines Freifunk-Knotens sind gelöscht worden. Es kann
|
||||
jetzt noch bis zu 20 Minuten dauern, bis die Änderungen überall
|
||||
wirksam werden und sich in der
|
||||
<a :href="mapUrl" target="_blank">Knotenkarte</a> auswirken.
|
||||
</p>
|
||||
|
||||
<div class="summary">
|
||||
<div class="deleted-node">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"/> <span class="node">{{ hostname }}</span>
|
||||
<i class="fa fa-trash-o" aria-hidden="true" />
|
||||
<span class="node">{{ props.hostname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<em>
|
||||
Hinweis: Nach dem Löschen kann der Knoten ggf. weiterhin als online in der Knotenkarte angezeigt werden.
|
||||
Dies ist dann der Fall, wenn der Knoten eingeschaltet ist und in Reichweite eines anderen aktiven
|
||||
Knotens steht. Die angezeigten Daten sind dann die während der Einrichtung des Knotens im Config-Mode
|
||||
(Konfigurationsoberfläche des Routers) hinterlegten. Außerdem kann es nach dem Löschen noch etwa einen
|
||||
Monat dauern, bis der Knoten in der Karte auch nicht mehr als offline erscheint.
|
||||
Hinweis: Nach dem Löschen kann der Knoten ggf. weiterhin als
|
||||
online in der Knotenkarte angezeigt werden. Dies ist dann der
|
||||
Fall, wenn der Knoten eingeschaltet ist und in Reichweite eines
|
||||
anderen aktiven Knotens steht. Die angezeigten Daten sind dann
|
||||
die während der Einrichtung des Knotens im Config-Mode
|
||||
(Konfigurationsoberfläche des Routers) hinterlegten. Außerdem
|
||||
kann es nach dem Löschen noch etwa einen Monat dauern, bis der
|
||||
Knoten in der Karte auch nicht mehr als offline erscheint.
|
||||
</em>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Bei Fragen wende Dich gerne an
|
||||
<a :href="`mailto:${ email }`">{{ email }}</a>.
|
||||
<a :href="`mailto:${email}`">{{ email }}</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<ButtonGroup :align="ComponentAlignment.CENTER" :button-size="ButtonSize.MEDIUM">
|
||||
<ButtonGroup
|
||||
:align="ComponentAlignment.CENTER"
|
||||
:button-size="ButtonSize.MEDIUM"
|
||||
>
|
||||
<RouteButton
|
||||
icon="reply"
|
||||
:variant="ComponentVariant.SECONDARY"
|
||||
:size="ButtonSize.MEDIUM"
|
||||
:route="route(RouteName.HOME)">
|
||||
:route="route(RouteName.HOME)"
|
||||
>
|
||||
Zurück zum Anfang
|
||||
</RouteButton>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type {StoredNode} from "@/types";
|
||||
import {MonitoringState} from "@/types";
|
||||
import type { StoredNode } from "@/types";
|
||||
import { MonitoringState } from "@/types";
|
||||
|
||||
interface Props {
|
||||
node: StoredNode;
|
||||
|
@ -12,36 +12,43 @@ const props = defineProps<Props>();
|
|||
<template>
|
||||
<div class="node-preview-card">
|
||||
<h3>
|
||||
<i class="fa fa-dot-circle-o" aria-hidden="true" title="Knotenname:" />
|
||||
{{ node.hostname }}
|
||||
<i
|
||||
class="fa fa-dot-circle-o"
|
||||
aria-hidden="true"
|
||||
title="Knotenname:"
|
||||
/>
|
||||
{{ props.node.hostname }}
|
||||
</h3>
|
||||
|
||||
<div class="field">
|
||||
<strong>Token:</strong> <code>{{ node.token }}</code>
|
||||
<strong>Token:</strong> <code>{{ props.node.token }}</code>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<i class="fa fa-map-marker" aria-hidden="true" title="Standort:" />
|
||||
{{ node.coords || "nicht angegeben" }}
|
||||
{{ props.node.coords || "nicht angegeben" }}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>MAC-Adresse:</strong> <code>{{ node.mac }}</code>
|
||||
<strong>MAC-Adresse:</strong> <code>{{ props.node.mac }}</code>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>VPN-Schlüssel:</strong> <code>{{ node.key || "nicht angegeben" }}</code>
|
||||
<strong>VPN-Schlüssel:</strong>
|
||||
<code>{{ props.node.key || "nicht angegeben" }}</code>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>Monitoring:</strong>
|
||||
<span v-if="node.monitoringState === MonitoringState.PENDING">
|
||||
<span v-if="props.node.monitoringState === MonitoringState.PENDING">
|
||||
Bestätigung ausstehend
|
||||
</span>
|
||||
<span v-if="node.monitoringState === MonitoringState.ACTIVE">
|
||||
<span v-if="props.node.monitoringState === MonitoringState.ACTIVE">
|
||||
aktiv
|
||||
</span>
|
||||
<span v-if="node.monitoringState === MonitoringState.DISABLED">
|
||||
<span
|
||||
v-if="props.node.monitoringState === MonitoringState.DISABLED"
|
||||
>
|
||||
nicht aktiv
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -10,8 +10,8 @@ import {
|
|||
type SearchTerm,
|
||||
type UnixTimestampMilliseconds,
|
||||
} from "@/types";
|
||||
import {computed, nextTick, onMounted, ref, watch} from "vue";
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
|
||||
interface Props {
|
||||
searchTerm: SearchTerm;
|
||||
|
@ -20,7 +20,7 @@ interface Props {
|
|||
|
||||
const SEARCH_THROTTLE_DELAY_MS = 500;
|
||||
|
||||
const FILTER_LABELS: Record<string, string | Map<any, any>> = {
|
||||
const FILTER_LABELS: Record<string, string | Map<string | boolean, any>> = {
|
||||
hasKey: new Map([
|
||||
[true, "Mit VPN-Schlüssel"],
|
||||
[false, "Ohne VPN-Schlüssel"],
|
||||
|
@ -40,10 +40,10 @@ const FILTER_LABELS: Record<string, string | Map<any, any>> = {
|
|||
[OnlineState.ONLINE, "online"],
|
||||
[OnlineState.OFFLINE, "offline"],
|
||||
]),
|
||||
}
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "updateFilter", filter: NodesFilter, searchTerm: SearchTerm): void,
|
||||
(e: "updateFilter", filter: NodesFilter, searchTerm: SearchTerm): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
@ -53,15 +53,18 @@ const suggestedFiltersExpanded = ref(false);
|
|||
const configStore = useConfigStore();
|
||||
|
||||
type Filter = {
|
||||
field: string,
|
||||
value: any,
|
||||
field: string;
|
||||
value: any;
|
||||
};
|
||||
const selectedFilters = ref<Filter[]>([]);
|
||||
|
||||
function selectedFilterIndex(filter: Filter): number {
|
||||
for (let i = 0; i < selectedFilters.value.length; i += 1) {
|
||||
const selectedFilter = selectedFilters.value[i];
|
||||
if (selectedFilter.field === filter.field && selectedFilter.value === filter.value) {
|
||||
if (
|
||||
selectedFilter.field === filter.field &&
|
||||
selectedFilter.value === filter.value
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +83,12 @@ function selectedFilterIndexForField(field: string): number {
|
|||
return -1;
|
||||
}
|
||||
|
||||
function pushFilter(filterGroup: Filter[], filter: Filter): void {
|
||||
if (selectedFilterIndex(filter) < 0) {
|
||||
filterGroup.push(filter);
|
||||
}
|
||||
}
|
||||
|
||||
const suggestedFilters = computed<Filter[][]>(() => {
|
||||
const cfg = configStore.getConfig;
|
||||
const sites = cfg?.community.sites || [];
|
||||
|
@ -88,18 +97,12 @@ const suggestedFilters = computed<Filter[][]>(() => {
|
|||
const filterGroups: Filter[][] = [];
|
||||
|
||||
for (const field of Object.keys(NODES_FILTER_FIELDS)) {
|
||||
const filterGroup: Filter[] = []
|
||||
|
||||
function pushFilter(filter: Filter): void {
|
||||
if (selectedFilterIndex(filter) < 0) {
|
||||
filterGroup.push(filter);
|
||||
}
|
||||
}
|
||||
const filterGroup: Filter[] = [];
|
||||
|
||||
switch (field) {
|
||||
case "site":
|
||||
for (const site of sites) {
|
||||
pushFilter({
|
||||
pushFilter(filterGroup, {
|
||||
field: "site",
|
||||
value: site,
|
||||
});
|
||||
|
@ -108,25 +111,26 @@ const suggestedFilters = computed<Filter[][]>(() => {
|
|||
|
||||
case "domain":
|
||||
for (const domain of domains) {
|
||||
pushFilter({
|
||||
pushFilter(filterGroup, {
|
||||
field: "domain",
|
||||
value: domain,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
default: {
|
||||
const labels = FILTER_LABELS[field];
|
||||
if (!isMap(labels)) {
|
||||
throw new Error(`Missing case for field ${field}.`);
|
||||
}
|
||||
|
||||
for (const value of labels.keys()) {
|
||||
pushFilter({
|
||||
pushFilter(filterGroup, {
|
||||
field,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterGroups.push(filterGroup);
|
||||
|
@ -152,7 +156,7 @@ function updateSelectedFilters() {
|
|||
const filter = props.filter as Record<string, any>;
|
||||
selectedFilters.value = [];
|
||||
for (const field of Object.keys(NODES_FILTER_FIELDS)) {
|
||||
if (filter.hasOwnProperty(field)) {
|
||||
if (Object.prototype.hasOwnProperty.call(filter, field)) {
|
||||
addSelectedFilter({
|
||||
field,
|
||||
value: filter[field],
|
||||
|
@ -165,7 +169,7 @@ watch(props, updateSelectedFilters);
|
|||
onMounted(updateSelectedFilters);
|
||||
|
||||
function renderFilter(filter: Filter): string {
|
||||
if (!FILTER_LABELS.hasOwnProperty(filter.field)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(FILTER_LABELS, filter.field)) {
|
||||
throw new Error(`Filter has no translation: ${filter.field}`);
|
||||
}
|
||||
|
||||
|
@ -175,7 +179,11 @@ function renderFilter(filter: Filter): string {
|
|||
}
|
||||
|
||||
if (!label.has(filter.value)) {
|
||||
throw new Error(`Filter ${filter.field} has no translation for value: ${filter.value}(${typeof filter.value})`);
|
||||
throw new Error(
|
||||
`Filter ${filter.field} has no translation for value: ${
|
||||
filter.value
|
||||
}(${typeof filter.value})`
|
||||
);
|
||||
}
|
||||
|
||||
return label.get(filter.value);
|
||||
|
@ -204,7 +212,8 @@ function buildNodesFilter(): NodesFilter {
|
|||
return nodesFilter;
|
||||
}
|
||||
|
||||
let lastSearchTimestamp: UnixTimestampMilliseconds = 0 as UnixTimestampMilliseconds;
|
||||
let lastSearchTimestamp: UnixTimestampMilliseconds =
|
||||
0 as UnixTimestampMilliseconds;
|
||||
let searchTimeout: NodeJS.Timeout | undefined = undefined;
|
||||
let lastSearchTerm: SearchTerm = "" as SearchTerm;
|
||||
|
||||
|
@ -217,17 +226,18 @@ function doSearch(): void {
|
|||
|
||||
function doThrottledSearch(): void {
|
||||
if (lastSearchTerm === input.value.value) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Share utils.
|
||||
const now: UnixTimestampMilliseconds = Date.now() as UnixTimestampMilliseconds;
|
||||
const now: UnixTimestampMilliseconds =
|
||||
Date.now() as UnixTimestampMilliseconds;
|
||||
if (now - SEARCH_THROTTLE_DELAY_MS >= lastSearchTimestamp) {
|
||||
lastSearchTimestamp = now;
|
||||
doSearch();
|
||||
} else if (!searchTimeout) {
|
||||
searchTimeout = setTimeout(() => {
|
||||
searchTimeout = undefined
|
||||
searchTimeout = undefined;
|
||||
doThrottledSearch();
|
||||
}, SEARCH_THROTTLE_DELAY_MS);
|
||||
}
|
||||
|
@ -235,19 +245,25 @@ function doThrottledSearch(): void {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'nodes-filter-panel': true, 'focus': hasFocus}" @click="focusInput">
|
||||
<div
|
||||
:class="{ 'nodes-filter-panel': true, focus: hasFocus }"
|
||||
@click="focusInput"
|
||||
>
|
||||
<div class="nodes-filter-input">
|
||||
<div class="selected-filters">
|
||||
<span
|
||||
v-for="filter in selectedFilters"
|
||||
v-bind:key="`${filter.field}:${filter.value}`"
|
||||
class="selected-filter"
|
||||
@click="removeSelectedFilter(filter)"
|
||||
:title="renderFilter(filter)">
|
||||
:title="renderFilter(filter)"
|
||||
>
|
||||
{{ renderFilter(filter) }}
|
||||
<i
|
||||
class="fa fa-times remove-filter"
|
||||
aria-hidden="true"
|
||||
title="Filter entfernen"/>
|
||||
title="Filter entfernen"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
|
@ -258,21 +274,28 @@ function doThrottledSearch(): void {
|
|||
maxlength="64"
|
||||
type="search"
|
||||
:value="searchTerm"
|
||||
placeholder="Knoten durchsuchen..."/>
|
||||
<i class="fa fa-search search" @click="doSearch()"/>
|
||||
placeholder="Knoten durchsuchen..."
|
||||
/>
|
||||
<i class="fa fa-search search" @click="doSearch()" />
|
||||
</div>
|
||||
<div class="suggested-filters" v-if="suggestedFiltersExpanded">
|
||||
<div class="suggested-filter-group" v-for="filterGroup in suggestedFilters">
|
||||
<div
|
||||
class="suggested-filter-group"
|
||||
v-for="filterGroup in suggestedFilters"
|
||||
>
|
||||
<span
|
||||
class="suggested-filter"
|
||||
v-for="filter in filterGroup"
|
||||
v-bind:key="filter.field"
|
||||
@click="addSelectedFilter(filter)"
|
||||
:title="renderFilter(filter)">
|
||||
:title="renderFilter(filter)"
|
||||
>
|
||||
{{ renderFilter(filter) }}
|
||||
<i
|
||||
class="fa fa-plus add-filter"
|
||||
aria-hidden="true"
|
||||
title="Filter hinzufügen"/>
|
||||
title="Filter hinzufügen"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -281,14 +304,16 @@ function doThrottledSearch(): void {
|
|||
v-if="suggestedFiltersExpanded"
|
||||
class="toggle-suggested-filters"
|
||||
href="javascript:"
|
||||
@click="showSuggestedFilters(false)">
|
||||
@click="showSuggestedFilters(false)"
|
||||
>
|
||||
Erweiterte Suche ausblenden
|
||||
</a>
|
||||
<a
|
||||
v-if="!suggestedFiltersExpanded"
|
||||
class="toggle-suggested-filters"
|
||||
href="javascript:"
|
||||
@click="showSuggestedFilters(true)">
|
||||
@click="showSuggestedFilters(true)"
|
||||
>
|
||||
Erweiterte Suche einblenden
|
||||
</a>
|
||||
</template>
|
||||
|
@ -329,12 +354,14 @@ function doThrottledSearch(): void {
|
|||
user-select: none;
|
||||
white-space: nowrap;
|
||||
|
||||
.add-filter, .remove-filter {
|
||||
.add-filter,
|
||||
.remove-filter {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-filters, .suggested-filter-group {
|
||||
.selected-filters,
|
||||
.suggested-filter-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
import {useVersionStore} from "@/stores/version";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
import { useVersionStore } from "@/stores/version";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const versionStore = useVersionStore();
|
||||
|
@ -12,17 +12,24 @@ const versionStore = useVersionStore();
|
|||
<a href="https://github.com/freifunkhamburg/ffffng" target="_blank">
|
||||
<i class="fa fa-code" aria-hidden="true" /> Source Code
|
||||
</a>
|
||||
<a href="https://github.com/freifunkhamburg/ffffng/issues" target="_blank">
|
||||
<a
|
||||
href="https://github.com/freifunkhamburg/ffffng/issues"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa fa-bug" aria-hidden="true" /> Fehler melden
|
||||
</a>
|
||||
<a
|
||||
v-if="configStore.getConfig.legal.privacyUrl"
|
||||
:href="configStore.getConfig.legal.privacyUrl"
|
||||
target="_blank">Datenschutz</a>
|
||||
target="_blank"
|
||||
>Datenschutz</a
|
||||
>
|
||||
<a
|
||||
v-if="configStore.getConfig.legal.imprintUrl"
|
||||
:href="configStore.getConfig.legal.imprintUrl"
|
||||
target="_blank">Impressum</a>
|
||||
target="_blank"
|
||||
>Impressum</a
|
||||
>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
import {route, RouteName} from "@/router";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
import { route, RouteName } from "@/router";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
</script>
|
||||
|
@ -18,12 +18,13 @@ const configStore = useConfigStore();
|
|||
|
||||
<h1>
|
||||
<RouterLink :to="route(RouteName.HOME)">
|
||||
{{ configStore.getConfig.community.name }} – Knotenverwaltung
|
||||
{{ configStore.getConfig.community.name }} –
|
||||
Knotenverwaltung
|
||||
</RouterLink>
|
||||
</h1>
|
||||
|
||||
<RouterLink class="admin-link" :to="route(RouteName.ADMIN)">
|
||||
<i class="fa fa-wrench" aria-hidden="true"/> Admin-Panel
|
||||
<i class="fa fa-wrench" aria-hidden="true" /> Admin-Panel
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import {type Component, defineComponent, type PropType} from "vue";
|
||||
import {type EnumValue, SortDirection} from "@/types";
|
||||
import { type Component, defineComponent, type PropType } from "vue";
|
||||
import { type EnumValue, SortDirection } from "@/types";
|
||||
|
||||
type Props<SortField> = {
|
||||
field: PropType<EnumValue<SortField>>,
|
||||
currentField: PropType<EnumValue<SortField>>,
|
||||
currentDirection: PropType<SortDirection>,
|
||||
field: PropType<EnumValue<SortField>>;
|
||||
currentField: PropType<EnumValue<SortField>>;
|
||||
currentDirection: PropType<SortDirection>;
|
||||
};
|
||||
|
||||
type SortTH<SortField> = Component<Props<SortField>>;
|
||||
|
@ -21,7 +21,9 @@ function defineGenericComponent<SortField>(): SortTH<SortField> {
|
|||
props,
|
||||
computed: {
|
||||
sortDirection: function () {
|
||||
return this.field === this.currentField ? this.currentDirection : undefined;
|
||||
return this.field === this.currentField
|
||||
? this.currentDirection
|
||||
: undefined;
|
||||
},
|
||||
isAscending: function () {
|
||||
return this.sortDirection === SortDirection.ASCENDING;
|
||||
|
@ -33,11 +35,13 @@ function defineGenericComponent<SortField>(): SortTH<SortField> {
|
|||
methods: {
|
||||
onClick(): void {
|
||||
this.$emit(
|
||||
'sort',
|
||||
"sort",
|
||||
this.field,
|
||||
this.isAscending ? SortDirection.DESCENDING : SortDirection.ASCENDING
|
||||
this.isAscending
|
||||
? SortDirection.DESCENDING
|
||||
: SortDirection.ASCENDING
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -55,7 +59,7 @@ export default component;
|
|||
<template>
|
||||
<th>
|
||||
<a href="javascript:" title="Sortieren" @click="onClick">
|
||||
<slot/>
|
||||
<slot />
|
||||
<i v-if="sortDirection && isAscending" class="fa fa-chevron-down" />
|
||||
<i v-if="sortDirection && !isAscending" class="fa fa-chevron-up" />
|
||||
</a>
|
||||
|
|
|
@ -3,15 +3,19 @@ import { createPinia } from "pinia";
|
|||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
import {useVersionStore} from "@/stores/version";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
import { useVersionStore } from "@/stores/version";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
useConfigStore().refresh().catch(err => console.error(err));
|
||||
useVersionStore().refresh().catch(err => console.error(err));
|
||||
useConfigStore()
|
||||
.refresh()
|
||||
.catch((err) => console.error(err));
|
||||
useVersionStore()
|
||||
.refresh()
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
app.mount("#app");
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import {createRouter, createWebHistory, type LocationQueryRaw} from "vue-router";
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
type LocationQueryRaw,
|
||||
} from "vue-router";
|
||||
import AdminDashboardView from "@/views/AdminDashboardView.vue";
|
||||
import AdminNodesView from "@/views/AdminNodesView.vue";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import NodeDeleteView from "@/views/NodeDeleteView.vue";
|
||||
import {isNodesFilter, isNodeSortField, isSortDirection, type SearchTerm} from "@/types";
|
||||
import {
|
||||
isNodesFilter,
|
||||
isNodeSortField,
|
||||
isSortDirection,
|
||||
type SearchTerm,
|
||||
} from "@/types";
|
||||
|
||||
export interface Route {
|
||||
name: RouteName;
|
||||
|
@ -46,9 +55,11 @@ const router = createRouter({
|
|||
path: "/admin/nodes",
|
||||
name: RouteName.ADMIN_NODES,
|
||||
component: AdminNodesView,
|
||||
props: route => {
|
||||
let filter: any;
|
||||
if (route.query.hasOwnProperty("filter")) {
|
||||
props: (route) => {
|
||||
let filter: unknown;
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(route.query, "filter")
|
||||
) {
|
||||
try {
|
||||
filter = JSON.parse(route.query.filter as string);
|
||||
} catch (e) {
|
||||
|
@ -59,14 +70,20 @@ const router = createRouter({
|
|||
filter = {};
|
||||
}
|
||||
|
||||
const searchTerm = route.query.q ? route.query.q as SearchTerm : undefined;
|
||||
const searchTerm = route.query.q
|
||||
? (route.query.q as SearchTerm)
|
||||
: undefined;
|
||||
return {
|
||||
filter: isNodesFilter(filter) ? filter : {},
|
||||
searchTerm,
|
||||
sortDirection: isSortDirection(route.query.sortDir) ? route.query.sortDir : undefined,
|
||||
sortField: isNodeSortField(route.query.sortField) ? route.query.sortField : undefined,
|
||||
}
|
||||
}
|
||||
sortDirection: isSortDirection(route.query.sortDir)
|
||||
? route.query.sortDir
|
||||
: undefined,
|
||||
sortField: isNodeSortField(route.query.sortField)
|
||||
? route.query.sortField
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {defineStore} from "pinia";
|
||||
import {type ClientConfig, isClientConfig} from "@/types";
|
||||
import {api} from "@/utils/Api";
|
||||
import { defineStore } from "pinia";
|
||||
import { type ClientConfig, isClientConfig } from "@/types";
|
||||
import { api } from "@/utils/Api";
|
||||
|
||||
interface ConfigStoreState {
|
||||
config: ClientConfig | null;
|
||||
|
@ -20,10 +20,7 @@ export const useConfigStore = defineStore({
|
|||
},
|
||||
actions: {
|
||||
async refresh(): Promise<void> {
|
||||
this.config = await api.get<ClientConfig>(
|
||||
"config",
|
||||
isClientConfig
|
||||
);
|
||||
this.config = await api.get<ClientConfig>("config", isClientConfig);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {defineStore} from "pinia";
|
||||
import {isStoredNode, type StoredNode, type Token} from "@/types";
|
||||
import {api} from "@/utils/Api";
|
||||
import { defineStore } from "pinia";
|
||||
import { isStoredNode, type StoredNode, type Token } from "@/types";
|
||||
import { api } from "@/utils/Api";
|
||||
|
||||
interface NodeStoreState {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface NodeStoreState {}
|
||||
|
||||
export const useNodeStore = defineStore({
|
||||
id: "node",
|
||||
|
@ -18,6 +18,6 @@ export const useNodeStore = defineStore({
|
|||
|
||||
async deleteByToken(token: Token): Promise<void> {
|
||||
await api.delete(`node/${token}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import {defineStore} from "pinia";
|
||||
import {type DomainSpecificNodeResponse, isDomainSpecificNodeResponse, type NodesFilter, NodeSortField, SortDirection} from "@/types";
|
||||
import {internalApi} from "@/utils/Api";
|
||||
import { defineStore } from "pinia";
|
||||
import {
|
||||
type DomainSpecificNodeResponse,
|
||||
isDomainSpecificNodeResponse,
|
||||
type NodesFilter,
|
||||
NodeSortField,
|
||||
SortDirection,
|
||||
} from "@/types";
|
||||
import { internalApi } from "@/utils/Api";
|
||||
|
||||
interface NodesStoreState {
|
||||
nodes: DomainSpecificNodeResponse[];
|
||||
|
@ -33,11 +39,11 @@ export const useNodesStore = defineStore({
|
|||
},
|
||||
|
||||
getNodesPerPage(state: NodesStoreState): number {
|
||||
return state.nodesPerPage
|
||||
return state.nodesPerPage;
|
||||
},
|
||||
|
||||
getPage(state: NodesStoreState): number {
|
||||
return state.page
|
||||
return state.page;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
|
@ -49,20 +55,23 @@ export const useNodesStore = defineStore({
|
|||
filter: NodesFilter,
|
||||
searchTerm?: string
|
||||
): Promise<void> {
|
||||
const query: Record<string, any> = {
|
||||
const query: Record<string, unknown> = {
|
||||
...filter,
|
||||
};
|
||||
if (searchTerm) {
|
||||
query.q = searchTerm;
|
||||
}
|
||||
const result = await internalApi.getPagedList<DomainSpecificNodeResponse, NodeSortField>(
|
||||
const result = await internalApi.getPagedList<
|
||||
DomainSpecificNodeResponse,
|
||||
NodeSortField
|
||||
>(
|
||||
"nodes",
|
||||
isDomainSpecificNodeResponse,
|
||||
page,
|
||||
nodesPerPage,
|
||||
sortDirection,
|
||||
sortField,
|
||||
query,
|
||||
query
|
||||
);
|
||||
this.nodes = result.entries;
|
||||
this.totalNodes = result.total;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {defineStore} from "pinia";
|
||||
import {isObject, isVersion, type Version} from "@/types";
|
||||
import {api} from "@/utils/Api";
|
||||
import { defineStore } from "pinia";
|
||||
import { isObject, isVersion, type Version } from "@/types";
|
||||
import { api } from "@/utils/Api";
|
||||
|
||||
interface VersionResponse {
|
||||
version: Version;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {SortDirection, toIsArray, type TypeGuard} from "@/types";
|
||||
import type {Headers} from "request";
|
||||
import {parseInteger} from "@/utils/Numbers";
|
||||
import { SortDirection, toIsArray, type TypeGuard } from "@/types";
|
||||
import type { Headers } from "request";
|
||||
import { parseInteger } from "@/utils/Numbers";
|
||||
|
||||
type Method = "GET" | "PUT" | "POST" | "DELETE";
|
||||
|
||||
|
@ -17,7 +17,7 @@ export class ApiError extends Error {
|
|||
private constructor(
|
||||
message: string,
|
||||
private status: number,
|
||||
private errorType: ApiErrorType,
|
||||
private errorType: ApiErrorType
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
@ -25,13 +25,13 @@ export class ApiError extends Error {
|
|||
static async requestFailed(
|
||||
method: Method,
|
||||
path: string,
|
||||
response: Response,
|
||||
response: Response
|
||||
): Promise<ApiError> {
|
||||
const body = await response.text();
|
||||
return new ApiError(
|
||||
`API ${method} request failed: ${path} => ${response.status} - ${body}`,
|
||||
response.status,
|
||||
ApiErrorType.REQUEST_FAILED,
|
||||
ApiErrorType.REQUEST_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -39,17 +39,20 @@ export class ApiError extends Error {
|
|||
method: Method,
|
||||
path: string,
|
||||
response: Response,
|
||||
json: any,
|
||||
json: unknown
|
||||
): Promise<ApiError> {
|
||||
return new ApiError(
|
||||
`API ${method} request result has unexpected type. ${path} => ${json}`,
|
||||
response.status,
|
||||
ApiErrorType.UNEXPECTED_RESULT_TYPE,
|
||||
ApiErrorType.UNEXPECTED_RESULT_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
isNotFoundError(): boolean {
|
||||
return this.errorType === ApiErrorType.REQUEST_FAILED && this.status === HttpStatusCode.NOT_FOUND;
|
||||
return (
|
||||
this.errorType === ApiErrorType.REQUEST_FAILED &&
|
||||
this.status === HttpStatusCode.NOT_FOUND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,7 +81,9 @@ class Api {
|
|||
if (queryParams) {
|
||||
const queryStrings: string[] = [];
|
||||
for (const [key, value] of Object.entries(queryParams)) {
|
||||
queryStrings.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||
queryStrings.push(
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
||||
);
|
||||
}
|
||||
if (queryStrings.length > 0) {
|
||||
queryString = `?${queryStrings.join("&")}`;
|
||||
|
@ -87,12 +92,26 @@ class Api {
|
|||
return this.baseURL + this.apiPrefix + path + queryString;
|
||||
}
|
||||
|
||||
private async sendRequest(method: Method, path: string, queryParams?: object): Promise<ApiResponse<undefined>>;
|
||||
private async sendRequest<T>(method: Method, path: string, isT: TypeGuard<T>, queryParams?: object): Promise<ApiResponse<T>>;
|
||||
private async sendRequest<T>(method: Method, path: string, isT?: TypeGuard<T>, queryParams?: object): Promise<ApiResponse<T>> {
|
||||
private async sendRequest(
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: object
|
||||
): Promise<ApiResponse<undefined>>;
|
||||
private async sendRequest<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
isT: TypeGuard<T>,
|
||||
queryParams?: object
|
||||
): Promise<ApiResponse<T>>;
|
||||
private async sendRequest<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
isT?: TypeGuard<T>,
|
||||
queryParams?: object
|
||||
): Promise<ApiResponse<T>> {
|
||||
const url = this.toURL(path, queryParams);
|
||||
const response = await fetch(url, {
|
||||
method
|
||||
method,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
@ -103,7 +122,12 @@ class Api {
|
|||
const json = await response.json();
|
||||
if (isT && !isT(json)) {
|
||||
console.log(json);
|
||||
throw await ApiError.unexpectedResultType(method, path, response, json);
|
||||
throw await ApiError.unexpectedResultType(
|
||||
method,
|
||||
path,
|
||||
response,
|
||||
json
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -112,13 +136,17 @@ class Api {
|
|||
};
|
||||
} else {
|
||||
return {
|
||||
result: undefined as any as T,
|
||||
result: undefined as unknown as T,
|
||||
headers: response.headers,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async doGet<T>(path: string, isT: TypeGuard<T>, queryParams?: object): Promise<ApiResponse<T>> {
|
||||
private async doGet<T>(
|
||||
path: string,
|
||||
isT: TypeGuard<T>,
|
||||
queryParams?: object
|
||||
): Promise<ApiResponse<T>> {
|
||||
return await this.sendRequest<T>("GET", path, isT, queryParams);
|
||||
}
|
||||
|
||||
|
@ -134,7 +162,7 @@ class Api {
|
|||
itemsPerPage: number,
|
||||
sortDirection?: SortDirection,
|
||||
sortField?: SortField,
|
||||
filter?: object,
|
||||
filter?: object
|
||||
): Promise<PagedListResult<Element>> {
|
||||
const response = await this.doGet(path, toIsArray(isElement), {
|
||||
_page: page,
|
||||
|
@ -149,7 +177,7 @@ class Api {
|
|||
return {
|
||||
entries: response.result,
|
||||
total,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async delete(path: string): Promise<void> {
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
export function isInteger(arg: unknown): arg is number {
|
||||
return typeof arg === "number" && Number.isInteger(arg);
|
||||
}
|
||||
|
||||
// TODO: Write tests!
|
||||
export function parseInteger(arg: any, radix: number): number {
|
||||
if (Number.isInteger(arg)) {
|
||||
export function parseInteger(arg: unknown, radix: number): number {
|
||||
if (isInteger(arg)) {
|
||||
return arg;
|
||||
}
|
||||
switch (typeof arg) {
|
||||
case "number":
|
||||
throw new Error(`Not an integer: ${arg}`);
|
||||
case "string":
|
||||
case "string": {
|
||||
if (radix < 2 || radix > 36 || isNaN(radix)) {
|
||||
throw new Error(`Radix out of range: ${radix}`);
|
||||
}
|
||||
|
@ -16,9 +20,12 @@ export function parseInteger(arg: any, radix: number): number {
|
|||
throw new Error(`Not a valid number (radix: ${radix}): ${str}`);
|
||||
}
|
||||
if (num.toString(radix).toLowerCase() !== str.toLowerCase()) {
|
||||
throw new Error(`Parsed integer does not match given string (radix: {radix}): ${str}`);
|
||||
throw new Error(
|
||||
`Parsed integer does not match given string (radix: {radix}): ${str}`
|
||||
);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Cannot parse number (radix: ${radix}): ${arg}`);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import StatisticsCard from "@/components/admin/StatisticsCard.vue";
|
||||
import {useStatisticsStore} from "@/stores/statistics";
|
||||
import {ComponentVariant, MonitoringState} from "@/types";
|
||||
import {RouteName} from "@/router";
|
||||
import { useStatisticsStore } from "@/stores/statistics";
|
||||
import { ComponentVariant, MonitoringState } from "@/types";
|
||||
import { RouteName } from "@/router";
|
||||
import PageContainer from "@/components/page/PageContainer.vue";
|
||||
|
||||
const statisticsStore = useStatisticsStore();
|
||||
|
@ -32,7 +32,7 @@ refresh();
|
|||
:variant="ComponentVariant.WARNING"
|
||||
:value="statisticsStore.getStatistics.nodes.withVPN"
|
||||
:route="RouteName.ADMIN_NODES"
|
||||
:filter="{hasKey: true}"
|
||||
:filter="{ hasKey: true }"
|
||||
/>
|
||||
<StatisticsCard
|
||||
title="Mit Koordinaten"
|
||||
|
@ -40,7 +40,7 @@ refresh();
|
|||
:variant="ComponentVariant.SUCCESS"
|
||||
:value="statisticsStore.getStatistics.nodes.withCoords"
|
||||
:route="RouteName.ADMIN_NODES"
|
||||
:filter="{hasCoords: true}"
|
||||
:filter="{ hasCoords: true }"
|
||||
/>
|
||||
<StatisticsCard
|
||||
title="Monitoring aktiv"
|
||||
|
@ -48,7 +48,7 @@ refresh();
|
|||
:variant="ComponentVariant.SUCCESS"
|
||||
:value="statisticsStore.getStatistics.nodes.monitoring.active"
|
||||
:route="RouteName.ADMIN_NODES"
|
||||
:filter="{monitoringState: MonitoringState.ACTIVE}"
|
||||
:filter="{ monitoringState: MonitoringState.ACTIVE }"
|
||||
/>
|
||||
<StatisticsCard
|
||||
title="Monitoring noch nicht bestätigt"
|
||||
|
@ -56,7 +56,7 @@ refresh();
|
|||
:variant="ComponentVariant.DANGER"
|
||||
:value="statisticsStore.getStatistics.nodes.monitoring.pending"
|
||||
:route="RouteName.ADMIN_NODES"
|
||||
:filter="{monitoringState: MonitoringState.PENDING}"
|
||||
:filter="{ monitoringState: MonitoringState.PENDING }"
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
|
@ -78,5 +78,4 @@ refresh();
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import {useNodesStore} from "@/stores/nodes";
|
||||
import {onMounted, type PropType, ref, watch} from "vue";
|
||||
import type {DomainSpecificNodeResponse, MAC, NodesFilter, SearchTerm} from "@/types";
|
||||
import {ButtonSize, ComponentVariant, NodeSortField, SortDirection} from "@/types";
|
||||
import Pager from "@/components/Pager.vue";
|
||||
import { useNodesStore } from "@/stores/nodes";
|
||||
import { onMounted, type PropType, ref, watch } from "vue";
|
||||
import type {
|
||||
DomainSpecificNodeResponse,
|
||||
MAC,
|
||||
NodesFilter,
|
||||
SearchTerm,
|
||||
} from "@/types";
|
||||
import {
|
||||
ButtonSize,
|
||||
ComponentVariant,
|
||||
NodeSortField,
|
||||
SortDirection,
|
||||
} from "@/types";
|
||||
import ListPager from "@/components/ListPager.vue";
|
||||
import ActionButton from "@/components/form/ActionButton.vue";
|
||||
import LoadingContainer from "@/components/LoadingContainer.vue";
|
||||
import NodesFilterPanel from "@/components/nodes/NodesFilterPanel.vue";
|
||||
import {SortTH} from "@/components/table/SortTH.vue";
|
||||
import router, {route, RouteName} from "@/router";
|
||||
import { SortTH } from "@/components/table/SortTH.vue";
|
||||
import router, { route, RouteName } from "@/router";
|
||||
|
||||
const NODE_PER_PAGE = 50;
|
||||
|
||||
|
@ -41,7 +51,7 @@ type NodeRedactFieldsMap = Partial<Record<NodeRedactField, boolean>>;
|
|||
type NodesRedactFieldsMap = Partial<Record<MAC, NodeRedactFieldsMap>>;
|
||||
const nodesStore = useNodesStore();
|
||||
const redactFieldsByDefault = ref(true);
|
||||
const nodesRedactFieldsMap = ref({} as NodesRedactFieldsMap)
|
||||
const nodesRedactFieldsMap = ref({} as NodesRedactFieldsMap);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
|
@ -55,7 +65,7 @@ async function refresh(page: number): Promise<void> {
|
|||
props.sortDirection,
|
||||
props.sortField,
|
||||
props.filter,
|
||||
props.searchTerm,
|
||||
props.searchTerm
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
@ -67,16 +77,25 @@ function redactAllFields(shallRedactFields: boolean): void {
|
|||
nodesRedactFieldsMap.value = {};
|
||||
}
|
||||
|
||||
function shallRedactField(node: DomainSpecificNodeResponse, field: NodeRedactField): boolean {
|
||||
function shallRedactField(
|
||||
node: DomainSpecificNodeResponse,
|
||||
field: NodeRedactField
|
||||
): boolean {
|
||||
const redactFieldsMap = nodesRedactFieldsMap.value[node.mac];
|
||||
if (!redactFieldsMap) {
|
||||
return redactFieldsByDefault.value;
|
||||
}
|
||||
const redactField = redactFieldsMap[field];
|
||||
return redactField === undefined ? redactFieldsByDefault.value : redactField;
|
||||
return redactField === undefined
|
||||
? redactFieldsByDefault.value
|
||||
: redactField;
|
||||
}
|
||||
|
||||
function setRedactField(node: DomainSpecificNodeResponse, field: NodeRedactField, value: boolean): void {
|
||||
function setRedactField(
|
||||
node: DomainSpecificNodeResponse,
|
||||
field: NodeRedactField,
|
||||
value: boolean
|
||||
): void {
|
||||
let redactFieldsMap = nodesRedactFieldsMap.value[node.mac];
|
||||
if (!redactFieldsMap) {
|
||||
redactFieldsMap = {};
|
||||
|
@ -89,28 +108,42 @@ async function updateRouterState(
|
|||
filter: NodesFilter,
|
||||
searchTerm: SearchTerm,
|
||||
sortDirection: SortDirection,
|
||||
sortField: NodeSortField,
|
||||
sortField: NodeSortField
|
||||
): Promise<void> {
|
||||
const filterStr = Object.keys(filter).length > 0 ? JSON.stringify(filter) : undefined;
|
||||
const filterStr =
|
||||
Object.keys(filter).length > 0 ? JSON.stringify(filter) : undefined;
|
||||
await router.replace(
|
||||
route(
|
||||
RouteName.ADMIN_NODES,
|
||||
{
|
||||
q: searchTerm || undefined,
|
||||
filter: filterStr,
|
||||
sortDir: sortDirection,
|
||||
sortField: sortField,
|
||||
}
|
||||
)
|
||||
route(RouteName.ADMIN_NODES, {
|
||||
q: searchTerm || undefined,
|
||||
filter: filterStr,
|
||||
sortDir: sortDirection,
|
||||
sortField: sortField,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function updateFilter(filter: NodesFilter, searchTerm: SearchTerm): Promise<void> {
|
||||
await updateRouterState(filter, searchTerm, props.sortDirection, props.sortField);
|
||||
async function updateFilter(
|
||||
filter: NodesFilter,
|
||||
searchTerm: SearchTerm
|
||||
): Promise<void> {
|
||||
await updateRouterState(
|
||||
filter,
|
||||
searchTerm,
|
||||
props.sortDirection,
|
||||
props.sortField
|
||||
);
|
||||
}
|
||||
|
||||
async function updateSortOrder(sortField: NodeSortField, sortDirection: SortDirection): Promise<void> {
|
||||
await updateRouterState(props.filter, props.searchTerm, sortDirection, sortField);
|
||||
async function updateSortOrder(
|
||||
sortField: NodeSortField,
|
||||
sortDirection: SortDirection
|
||||
): Promise<void> {
|
||||
await updateRouterState(
|
||||
props.filter,
|
||||
props.searchTerm,
|
||||
sortDirection,
|
||||
sortField
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -124,13 +157,18 @@ watch(props, async () => {
|
|||
<template>
|
||||
<h2>Knoten</h2>
|
||||
|
||||
<NodesFilterPanel :search-term="searchTerm" :filter="filter" @update-filter="updateFilter"/>
|
||||
<NodesFilterPanel
|
||||
:search-term="searchTerm"
|
||||
:filter="filter"
|
||||
@update-filter="updateFilter"
|
||||
/>
|
||||
|
||||
<Pager
|
||||
<ListPager
|
||||
:page="nodesStore.getPage"
|
||||
:itemsPerPage="nodesStore.getNodesPerPage"
|
||||
:totalItems="nodesStore.getTotalNodes"
|
||||
@changePage="refresh"/>
|
||||
@changePage="refresh"
|
||||
/>
|
||||
|
||||
<div class="actions">
|
||||
<ActionButton
|
||||
|
@ -138,7 +176,8 @@ watch(props, async () => {
|
|||
:variant="ComponentVariant.WARNING"
|
||||
:size="ButtonSize.SMALL"
|
||||
icon="eye"
|
||||
@click="redactAllFields(false)">
|
||||
@click="redactAllFields(false)"
|
||||
>
|
||||
Sensible Daten einblenden
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
|
@ -146,7 +185,8 @@ watch(props, async () => {
|
|||
:variant="ComponentVariant.SUCCESS"
|
||||
:size="ButtonSize.SMALL"
|
||||
icon="eye-slash"
|
||||
@click="redactAllFields(true)">
|
||||
@click="redactAllFields(true)"
|
||||
>
|
||||
Sensible Daten ausblenden
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
@ -154,190 +194,223 @@ watch(props, async () => {
|
|||
<LoadingContainer :loading="loading">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<sth
|
||||
:field="NodeSortField.HOSTNAME"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
Name
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.NICKNAME"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
Besitzer*in
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.EMAIL"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
E-Mail
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.TOKEN"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
Token
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.MAC"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
MAC
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.KEY"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
VPN
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.SITE"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
Site
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.DOMAIN"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
Domäne
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.COORDS"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
GPS
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.ONLINE_STATE"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
Status
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.MONITORING_STATE"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder">
|
||||
Monitoring
|
||||
</sth>
|
||||
</tr>
|
||||
<tr>
|
||||
<sth
|
||||
:field="NodeSortField.HOSTNAME"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
Name
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.NICKNAME"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
Besitzer*in
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.EMAIL"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
E-Mail
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.TOKEN"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
Token
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.MAC"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
MAC
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.KEY"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
VPN
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.SITE"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
Site
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.DOMAIN"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
Domäne
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.COORDS"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
GPS
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.ONLINE_STATE"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
Status
|
||||
</sth>
|
||||
<sth
|
||||
:field="NodeSortField.MONITORING_STATE"
|
||||
:currentField="sortField"
|
||||
:currentDirection="sortDirection"
|
||||
@sort="updateSortOrder"
|
||||
>
|
||||
Monitoring
|
||||
</sth>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="node in nodesStore.getNodes"
|
||||
:class="[node.onlineState ? node.onlineState.toLowerCase() : 'online-state-unknown']">
|
||||
<td>{{ node.hostname }}</td>
|
||||
<td v-if="shallRedactField(node, 'nickname')">
|
||||
<span
|
||||
class="redacted"
|
||||
@click="setRedactField(node, 'nickname', false)">
|
||||
nickname
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="!shallRedactField(node, 'nickname')">
|
||||
<span
|
||||
class="redactable"
|
||||
@click="setRedactField(node, 'nickname', true)">
|
||||
{{ node.nickname }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="shallRedactField(node, 'email')">
|
||||
<span
|
||||
class="redacted"
|
||||
@click="setRedactField(node, 'email', false)">
|
||||
email@example.com
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="!shallRedactField(node, 'email')">
|
||||
<span
|
||||
class="redactable"
|
||||
@click="setRedactField(node, 'email', true)">
|
||||
{{ node.email }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="shallRedactField(node, 'token')">
|
||||
<span
|
||||
class="redacted"
|
||||
@click="setRedactField(node, 'token', false)">
|
||||
0123456789abcdef
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="!shallRedactField(node, 'token')">
|
||||
<span
|
||||
class="redactable"
|
||||
@click="setRedactField(node, 'token', true)">
|
||||
{{ node.token }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ node.mac }}</td>
|
||||
<td class="icon">
|
||||
<i
|
||||
v-if="node.key"
|
||||
class="fa fa-lock"
|
||||
aria-hidden="true"
|
||||
title="Hat VPN-Schlüssel"/>
|
||||
<i
|
||||
v-if="!node.key"
|
||||
class="fa fa-times not-available"
|
||||
aria-hidden="true"
|
||||
title="Hat keinen VPN-Schlüssel"/>
|
||||
</td>
|
||||
<td>{{ node.site }}</td>
|
||||
<td>{{ node.domain }}</td>
|
||||
<td class="icon">
|
||||
<i
|
||||
v-if="node.coords"
|
||||
class="fa fa-map-marker"
|
||||
aria-hidden="true"
|
||||
title="Hat Koordinaten"/>
|
||||
<i
|
||||
v-if="!node.coords"
|
||||
class="fa fa-times not-available"
|
||||
aria-hidden="true"
|
||||
title="Hat keinen Koordinaten"/>
|
||||
</td>
|
||||
<td v-if="node.onlineState !== undefined">{{ node.onlineState.toLowerCase() }}</td>
|
||||
<td v-if="node.onlineState === undefined">unbekannt</td>
|
||||
<td class="icon">
|
||||
<i
|
||||
v-if="node.monitoring && node.monitoringConfirmed"
|
||||
class="fa fa-heartbeat"
|
||||
aria-hidden="true"
|
||||
title="Monitoring aktiv"/>
|
||||
<i
|
||||
v-if="node.monitoring && !node.monitoringConfirmed"
|
||||
class="fa fa-envelope"
|
||||
aria-hidden="true"
|
||||
title="Monitoring nicht bestätigt"/>
|
||||
<i
|
||||
v-if="!node.monitoring"
|
||||
class="fa fa-times not-available"
|
||||
aria-hidden="true"
|
||||
title="Monitoring deaktiviert"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="node in nodesStore.getNodes"
|
||||
v-bind:key="node.mac"
|
||||
:class="[
|
||||
node.onlineState
|
||||
? node.onlineState.toLowerCase()
|
||||
: 'online-state-unknown',
|
||||
]"
|
||||
>
|
||||
<td>{{ node.hostname }}</td>
|
||||
<td v-if="shallRedactField(node, 'nickname')">
|
||||
<span
|
||||
class="redacted"
|
||||
@click="setRedactField(node, 'nickname', false)"
|
||||
>
|
||||
nickname
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="!shallRedactField(node, 'nickname')">
|
||||
<span
|
||||
class="redactable"
|
||||
@click="setRedactField(node, 'nickname', true)"
|
||||
>
|
||||
{{ node.nickname }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="shallRedactField(node, 'email')">
|
||||
<span
|
||||
class="redacted"
|
||||
@click="setRedactField(node, 'email', false)"
|
||||
>
|
||||
email@example.com
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="!shallRedactField(node, 'email')">
|
||||
<span
|
||||
class="redactable"
|
||||
@click="setRedactField(node, 'email', true)"
|
||||
>
|
||||
{{ node.email }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="shallRedactField(node, 'token')">
|
||||
<span
|
||||
class="redacted"
|
||||
@click="setRedactField(node, 'token', false)"
|
||||
>
|
||||
0123456789abcdef
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="!shallRedactField(node, 'token')">
|
||||
<span
|
||||
class="redactable"
|
||||
@click="setRedactField(node, 'token', true)"
|
||||
>
|
||||
{{ node.token }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ node.mac }}</td>
|
||||
<td class="icon">
|
||||
<i
|
||||
v-if="node.key"
|
||||
class="fa fa-lock"
|
||||
aria-hidden="true"
|
||||
title="Hat VPN-Schlüssel"
|
||||
/>
|
||||
<i
|
||||
v-if="!node.key"
|
||||
class="fa fa-times not-available"
|
||||
aria-hidden="true"
|
||||
title="Hat keinen VPN-Schlüssel"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ node.site }}</td>
|
||||
<td>{{ node.domain }}</td>
|
||||
<td class="icon">
|
||||
<i
|
||||
v-if="node.coords"
|
||||
class="fa fa-map-marker"
|
||||
aria-hidden="true"
|
||||
title="Hat Koordinaten"
|
||||
/>
|
||||
<i
|
||||
v-if="!node.coords"
|
||||
class="fa fa-times not-available"
|
||||
aria-hidden="true"
|
||||
title="Hat keinen Koordinaten"
|
||||
/>
|
||||
</td>
|
||||
<td v-if="node.onlineState !== undefined">
|
||||
{{ node.onlineState.toLowerCase() }}
|
||||
</td>
|
||||
<td v-if="node.onlineState === undefined">unbekannt</td>
|
||||
<td class="icon">
|
||||
<i
|
||||
v-if="node.monitoring && node.monitoringConfirmed"
|
||||
class="fa fa-heartbeat"
|
||||
aria-hidden="true"
|
||||
title="Monitoring aktiv"
|
||||
/>
|
||||
<i
|
||||
v-if="node.monitoring && !node.monitoringConfirmed"
|
||||
class="fa fa-envelope"
|
||||
aria-hidden="true"
|
||||
title="Monitoring nicht bestätigt"
|
||||
/>
|
||||
<i
|
||||
v-if="!node.monitoring"
|
||||
class="fa fa-times not-available"
|
||||
aria-hidden="true"
|
||||
title="Monitoring deaktiviert"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</LoadingContainer>
|
||||
|
||||
<Pager
|
||||
<ListPager
|
||||
:page="nodesStore.getPage"
|
||||
:itemsPerPage="nodesStore.getNodesPerPage"
|
||||
:totalItems="nodesStore.getTotalNodes"
|
||||
@changePage="refresh"/>
|
||||
@changePage="refresh"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -354,7 +427,8 @@ table {
|
|||
background-color: $gray-darker;
|
||||
}
|
||||
|
||||
th, td {
|
||||
th,
|
||||
td {
|
||||
padding: 0.5em 0.25em;
|
||||
}
|
||||
|
||||
|
@ -378,7 +452,8 @@ table {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.redacted, .redactable {
|
||||
.redacted,
|
||||
.redactable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -387,7 +462,7 @@ table {
|
|||
}
|
||||
|
||||
.not-available {
|
||||
color: $gray-dark
|
||||
color: $gray-dark;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,38 +2,47 @@
|
|||
import ActionButton from "@/components/form/ActionButton.vue";
|
||||
import ButtonGroup from "@/components/form/ButtonGroup.vue";
|
||||
import PageContainer from "@/components/page/PageContainer.vue";
|
||||
import {ButtonSize, ComponentAlignment, ComponentVariant} from "@/types";
|
||||
import {route, RouteName} from "@/router";
|
||||
import RouteButton from "@/components/form/RouteButton.vue";</script>
|
||||
import { ButtonSize, ComponentAlignment, ComponentVariant } from "@/types";
|
||||
import { route, RouteName } from "@/router";
|
||||
import RouteButton from "@/components/form/RouteButton.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<h2>Willkommen!</h2>
|
||||
|
||||
<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!
|
||||
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>
|
||||
|
||||
<ButtonGroup class="actions" :align="ComponentAlignment.CENTER" :button-size="ButtonSize.LARGE">
|
||||
<ButtonGroup
|
||||
class="actions"
|
||||
:align="ComponentAlignment.CENTER"
|
||||
:button-size="ButtonSize.LARGE"
|
||||
>
|
||||
<ActionButton
|
||||
:variant="ComponentVariant.INFO"
|
||||
:size="ButtonSize.LARGE"
|
||||
icon="dot-circle-o">
|
||||
icon="dot-circle-o"
|
||||
>
|
||||
Neuen Knoten anmelden
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
:variant="ComponentVariant.PRIMARY"
|
||||
:size="ButtonSize.LARGE"
|
||||
icon="pencil">
|
||||
icon="pencil"
|
||||
>
|
||||
Knotendaten ändern
|
||||
</ActionButton>
|
||||
<RouteButton
|
||||
:variant="ComponentVariant.WARNING"
|
||||
:size="ButtonSize.LARGE"
|
||||
icon="trash"
|
||||
:route="route(RouteName.NODE_DELETE)">
|
||||
:route="route(RouteName.NODE_DELETE)"
|
||||
>
|
||||
Knoten löschen
|
||||
</RouteButton>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -3,8 +3,8 @@ import PageContainer from "@/components/page/PageContainer.vue";
|
|||
import NodeDeleteForm from "@/components/nodes/NodeDeleteForm.vue";
|
||||
import NodeDeleteConfirmationForm from "@/components/nodes/NodeDeleteConfirmationForm.vue";
|
||||
import NodeDeletedPanel from "@/components/nodes/NodeDeletedPanel.vue";
|
||||
import {ref} from "vue";
|
||||
import type {Hostname, StoredNode} from "@/types";
|
||||
import { ref } from "vue";
|
||||
import type { Hostname, StoredNode } from "@/types";
|
||||
|
||||
const node = ref<StoredNode | undefined>(undefined);
|
||||
const deletedHostname = ref<Hostname | undefined>(undefined);
|
||||
|
@ -20,11 +20,17 @@ async function onDelete(hostname: Hostname) {
|
|||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<NodeDeleteForm v-if="!node && !deletedHostname" @submit="onSelectNode"/>
|
||||
<NodeDeleteConfirmationForm v-if="node && !deletedHostname" @delete="onDelete" :node="node"/>
|
||||
<NodeDeletedPanel v-if="deletedHostname" :hostname="deletedHostname"/>
|
||||
<NodeDeleteForm
|
||||
v-if="!node && !deletedHostname"
|
||||
@submit="onSelectNode"
|
||||
/>
|
||||
<NodeDeleteConfirmationForm
|
||||
v-if="node && !deletedHostname"
|
||||
@delete="onDelete"
|
||||
:node="node"
|
||||
/>
|
||||
<NodeDeletedPanel v-if="deletedHostname" :hostname="deletedHostname" />
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {fileURLToPath, URL} from "url";
|
||||
import { fileURLToPath, URL } from "url";
|
||||
|
||||
import {defineConfig} from "vite";
|
||||
import { defineConfig } from "vite";
|
||||
import basicSsl from "@vitejs/plugin-basic-ssl";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
|
|
Loading…
Reference in a new issue