Frontend: Added forms to delete nodes.
This commit is contained in:
parent
a0d186da3a
commit
3a2f0799eb
13 changed files with 531 additions and 13 deletions
frontend/src/components/nodes
99
frontend/src/components/nodes/NodeDeleteConfirmationForm.vue
Normal file
99
frontend/src/components/nodes/NodeDeleteConfirmationForm.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<script setup lang="ts">
|
||||
import ValidationForm from "@/components/form/ValidationForm.vue";
|
||||
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 ErrorCard from "@/components/ErrorCard.vue";
|
||||
import {useConfigStore} from "@/stores/config";
|
||||
|
||||
interface Props {
|
||||
node: StoredNode;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const nodeStore = useNodeStore();
|
||||
const configStore = useConfigStore();
|
||||
const email = computed(() => configStore.getConfig?.community.contactEmail);
|
||||
|
||||
const errorDeletingNode = ref<boolean>(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete", hostname: Hostname): void
|
||||
}>();
|
||||
|
||||
async function onSubmit() {
|
||||
errorDeletingNode.value = false;
|
||||
// Make sure to re-render error message to trigger scrolling into view.
|
||||
await nextTick();
|
||||
|
||||
const token = props.node.token;
|
||||
|
||||
try {
|
||||
await nodeStore.deleteByToken(token);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
// If the node has been deleted in the meantime, we move on as if no error had occured.
|
||||
if (!error.isNotFoundError()) {
|
||||
errorDeletingNode.value = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
emit("delete", props.node.hostname);
|
||||
}
|
||||
|
||||
async function onAbort() {
|
||||
await router.push(route(RouteName.HOME));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ValidationForm @submit="onSubmit">
|
||||
<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!
|
||||
</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>.
|
||||
</ErrorCard>
|
||||
|
||||
<NodePreviewCard class="preview" :node="props.node"/>
|
||||
|
||||
<ButtonGroup :align="ComponentAlignment.CENTER" :button-size="ButtonSize.SMALL">
|
||||
<ActionButton
|
||||
type="submit"
|
||||
icon="trash"
|
||||
:variant="ComponentVariant.WARNING"
|
||||
:size="ButtonSize.MEDIUM">
|
||||
Knoten löschen
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
type="reset"
|
||||
icon="times"
|
||||
:variant="ComponentVariant.SECONDARY"
|
||||
:size="ButtonSize.MEDIUM"
|
||||
@click="onAbort">
|
||||
Abbrechen
|
||||
</ActionButton>
|
||||
</ButtonGroup>
|
||||
</ValidationForm>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
122
frontend/src/components/nodes/NodeDeleteForm.vue
Normal file
122
frontend/src/components/nodes/NodeDeleteForm.vue
Normal file
|
@ -0,0 +1,122 @@
|
|||
<script setup lang="ts">
|
||||
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 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 RouteButton from "@/components/form/RouteButton.vue";
|
||||
import {ApiError} from "@/utils/Api";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const email = computed(() => configStore.getConfig?.community.contactEmail);
|
||||
|
||||
const nodeStore = useNodeStore();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit", node: StoredNode): void;
|
||||
}>();
|
||||
|
||||
const token = ref("");
|
||||
const nodeNotFound = ref<boolean>(false);
|
||||
const generalError = ref<boolean>(false);
|
||||
|
||||
async function onSubmit() {
|
||||
nodeNotFound.value = false;
|
||||
generalError.value = false;
|
||||
// Make sure to re-render error message to trigger scrolling into view.
|
||||
await nextTick();
|
||||
|
||||
try {
|
||||
const node = await nodeStore.fetchByToken(token.value as Token);
|
||||
emit("submit", node);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
if (error.isNotFoundError()) {
|
||||
nodeNotFound.value = true;
|
||||
} else {
|
||||
console.error(error);
|
||||
generalError.value = true;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ValidationForm novalidate="" ref="form" @submit="onSubmit">
|
||||
<h2>Knoten löschen</h2>
|
||||
|
||||
<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.
|
||||
</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>.
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<ErrorCard v-if="nodeNotFound">
|
||||
Zum Token wurde kein passender Knoten gefunden.
|
||||
</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>.
|
||||
</ErrorCard>
|
||||
|
||||
<fieldset>
|
||||
<ValidationFormInput
|
||||
v-model="token"
|
||||
label="Token"
|
||||
placeholder="Dein 16-stelliger Token"
|
||||
: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">
|
||||
<ActionButton
|
||||
type="submit"
|
||||
icon="trash"
|
||||
:variant="ComponentVariant.WARNING"
|
||||
:size="ButtonSize.SMALL">
|
||||
Knoten löschen
|
||||
</ActionButton>
|
||||
<RouteButton
|
||||
type="reset"
|
||||
icon="times"
|
||||
:variant="ComponentVariant.SECONDARY"
|
||||
:size="ButtonSize.SMALL"
|
||||
:route="route(RouteName.HOME)">
|
||||
Abbrechen
|
||||
</RouteButton>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
|
||||
<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
|
||||
(Konfigurationsoberfläche des Routers) hinterlegten.
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
</ValidationForm>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
80
frontend/src/components/nodes/NodeDeletedPanel.vue
Normal file
80
frontend/src/components/nodes/NodeDeletedPanel.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<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 RouteButton from "@/components/form/RouteButton.vue";
|
||||
import {route, RouteName} from "@/router";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const email = computed(() => configStore.getConfig?.community.contactEmail);
|
||||
const mapUrl = computed(() => configStore.getConfig?.map.mapUrl);
|
||||
|
||||
interface Props {
|
||||
hostname: Hostname;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<div class="summary">
|
||||
<div class="deleted-node">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"/> <span class="node">{{ 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.
|
||||
</em>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Bei Fragen wende Dich gerne an
|
||||
<a :href="`mailto:${ email }`">{{ email }}</a>.
|
||||
</p>
|
||||
|
||||
<ButtonGroup :align="ComponentAlignment.CENTER" :button-size="ButtonSize.MEDIUM">
|
||||
<RouteButton
|
||||
icon="reply"
|
||||
:variant="ComponentVariant.SECONDARY"
|
||||
:size="ButtonSize.MEDIUM"
|
||||
:route="route(RouteName.HOME)">
|
||||
Zurück zum Anfang
|
||||
</RouteButton>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../scss/variables";
|
||||
|
||||
.summary {
|
||||
text-align: center;
|
||||
|
||||
.deleted-node {
|
||||
display: inline-block;
|
||||
|
||||
padding: $node-deleted-summary-padding;
|
||||
|
||||
border: $node-deleted-summary-border;
|
||||
border-radius: $node-deleted-summary-border-radius;
|
||||
|
||||
font-size: $node-deleted-summary-font-size;
|
||||
}
|
||||
}
|
||||
</style>
|
82
frontend/src/components/nodes/NodePreviewCard.vue
Normal file
82
frontend/src/components/nodes/NodePreviewCard.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<script setup lang="ts">
|
||||
import type {StoredNode} from "@/types";
|
||||
import {MonitoringState} from "@/types";
|
||||
|
||||
interface Props {
|
||||
node: StoredNode;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-preview-card">
|
||||
<h3>
|
||||
<i class="fa fa-dot-circle-o" aria-hidden="true" title="Knotenname:" />
|
||||
{{ node.hostname }}
|
||||
</h3>
|
||||
|
||||
<div class="field">
|
||||
<strong>Token:</strong> <code>{{ node.token }}</code>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<i class="fa fa-map-marker" aria-hidden="true" title="Standort:" />
|
||||
{{ node.coords || "nicht angegeben" }}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>MAC-Adresse:</strong> <code>{{ node.mac }}</code>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>VPN-Schlüssel:</strong> <code>{{ node.key || "nicht angegeben" }}</code>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>Monitoring:</strong>
|
||||
<span v-if="node.monitoringState === MonitoringState.PENDING">
|
||||
Bestätigung ausstehend
|
||||
</span>
|
||||
<span v-if="node.monitoringState === MonitoringState.ACTIVE">
|
||||
aktiv
|
||||
</span>
|
||||
<span v-if="node.monitoringState === MonitoringState.DISABLED">
|
||||
nicht aktiv
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../scss/variables";
|
||||
|
||||
.node-preview-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding: $node-preview-card-padding;
|
||||
|
||||
border: $node-preview-card-border;
|
||||
border-radius: $node-preview-card-border-radius;
|
||||
|
||||
h3 {
|
||||
font-size: $node-preview-card-headline-font-size;
|
||||
margin: $node-preview-card-headline-margin;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin: $node-preview-card-field-margin;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: $node-preview-card-field-code-font-size;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue