Browse Source

add tooltip info, extract strings

pull/1666/head
Bernd Storath 6 months ago
parent
commit
e0f50c0375
  1. 2
      src/app/components/admin/CidrDialog.vue
  2. 3
      src/app/components/base/Dialog.vue
  3. 24
      src/app/components/base/Tooltip.vue
  4. 2
      src/app/components/form/ArrayField.vue
  5. 9
      src/app/components/form/Heading.vue
  6. 7
      src/app/components/form/NullTextField.vue
  7. 7
      src/app/components/form/NumberField.vue
  8. 7
      src/app/components/form/SwitchField.vue
  9. 7
      src/app/components/form/TextField.vue
  10. 15
      src/app/components/icons/Info.vue
  11. 11
      src/app/components/ui/UserMenu.vue
  12. 42
      src/app/composables/useSubmit.ts
  13. 11
      src/app/pages/admin.vue
  14. 71
      src/app/pages/admin/config.vue
  15. 6
      src/app/pages/admin/hooks.vue
  16. 46
      src/app/pages/admin/index.vue
  17. 94
      src/app/pages/admin/interface.vue
  18. 1
      src/app/pages/login.vue
  19. 35
      src/app/stores/toast.ts
  20. 55
      src/i18n/locales/en.json

2
src/app/components/admin/CidrDialog.vue

@ -1,5 +1,5 @@
<template> <template>
<BaseDialog> <BaseDialog :trigger-class="triggerClass">
<template #trigger><slot /></template> <template #trigger><slot /></template>
<template #title>Change CIDR</template> <template #title>Change CIDR</template>
<template #description> <template #description>

3
src/app/components/base/Dialog.vue

@ -6,7 +6,7 @@
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50" class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/> />
<DialogContent <DialogContent
class="data-[state=open]:animate-contentShow fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700" class="fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
> >
<DialogTitle <DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200" class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"
@ -27,5 +27,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// TODO: improve
defineProps<{ triggerClass?: string }>(); defineProps<{ triggerClass?: string }>();
</script> </script>

24
src/app/components/base/Tooltip.vue

@ -0,0 +1,24 @@
<template>
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-gray-400 outline-none focus:shadow-sm focus:shadow-black"
>
<slot />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent
class="select-none rounded bg-gray-600 px-3 py-2 text-sm leading-none text-white shadow-lg will-change-[transform,opacity]"
:side-offset="5"
>
{{ text }}
<TooltipArrow class="fill-gray-600" :width="8" />
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
</TooltipProvider>
</template>
<script lang="ts" setup>
defineProps<{ text: string }>();
</script>

2
src/app/components/form/ArrayField.vue

@ -18,6 +18,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// TODO: style
const data = defineModel<string[]>(); const data = defineModel<string[]>();
defineProps<{ emptyText?: string[]; name: string }>(); defineProps<{ emptyText?: string[]; name: string }>();

9
src/app/components/form/Heading.vue

@ -1,5 +1,12 @@
<template> <template>
<h4 class="col-span-full py-6 text-2xl"> <h4 class="col-span-full flex items-center py-6 text-2xl">
<slot /> <slot />
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</h4> </h4>
</template> </template>
<script lang="ts" setup>
defineProps<{ description?: string }>();
</script>

7
src/app/components/form/NullTextField.vue

@ -1,7 +1,12 @@
<template> <template>
<div class="flex items-center">
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> <Label :for="id" class="font-semibold md:align-middle md:leading-10">
{{ label }} {{ label }}
</Label> </Label>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<input <input
:id="id" :id="id"
v-model.trim="data" v-model.trim="data"
@ -12,7 +17,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string }>(); defineProps<{ id: string; label: string; description?: string }>();
const data = defineModel<string | null>({ const data = defineModel<string | null>({
set(value) { set(value) {

7
src/app/components/form/NumberField.vue

@ -1,7 +1,12 @@
<template> <template>
<div class="flex items-center">
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> <Label :for="id" class="font-semibold md:align-middle md:leading-10">
{{ label }} {{ label }}
</Label> </Label>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<input <input
:id="id" :id="id"
v-model.number="data" v-model.number="data"
@ -12,7 +17,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string }>(); defineProps<{ id: string; label: string; description?: string }>();
const data = defineModel<number>(); const data = defineModel<number>();
</script> </script>

7
src/app/components/form/SwitchField.vue

@ -1,11 +1,16 @@
<template> <template>
<div class="flex items-center">
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> <Label :for="id" class="font-semibold md:align-middle md:leading-10">
{{ label }} {{ label }}
</Label> </Label>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseSwitch :id="id" v-model="data" /> <BaseSwitch :id="id" v-model="data" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string }>(); defineProps<{ id: string; label: string; description?: string }>();
const data = defineModel<boolean>(); const data = defineModel<boolean>();
</script> </script>

7
src/app/components/form/TextField.vue

@ -1,7 +1,12 @@
<template> <template>
<div class="flex items-center">
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> <Label :for="id" class="font-semibold md:align-middle md:leading-10">
{{ label }} {{ label }}
</Label> </Label>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<input <input
:id="id" :id="id"
v-model.trim="data" v-model.trim="data"
@ -12,7 +17,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string }>(); defineProps<{ id: string; label: string; description?: string }>();
const data = defineModel<string>(); const data = defineModel<string>();
</script> </script>

15
src/app/components/icons/Info.vue

@ -0,0 +1,15 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</template>

11
src/app/components/ui/UserMenu.vue

@ -1,15 +1,14 @@
<template> <template>
<DropdownMenuRoot v-model:open="toggleState"> <DropdownMenuRoot v-model:open="toggleState">
<DropdownMenuTrigger> <DropdownMenuTrigger>
<button <span
class="flex items-center rounded-full pe-1 text-sm font-medium text-gray-400 hover:text-red-800 focus:ring-4 focus:ring-gray-100 md:me-0 dark:text-neutral-400 dark:hover:text-red-800 dark:focus:ring-gray-700" class="flex items-center rounded-full pe-1 text-sm font-medium text-gray-400 hover:text-red-800 focus:ring-4 focus:ring-gray-100 md:me-0 dark:text-neutral-400 dark:hover:text-red-800 dark:focus:ring-gray-700"
type="button"
> >
<BaseAvatar class="h-8 w-8"> <BaseAvatar class="h-8 w-8">
{{ fallbackName }} {{ fallbackName }}
</BaseAvatar> </BaseAvatar>
{{ authStore.userData?.name }} {{ authStore.userData?.name }}
</button> </span>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
@ -26,7 +25,7 @@
to="/" to="/"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
> >
Clients {{ $t('pages.clients') }}
</NuxtLink> </NuxtLink>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem>
@ -34,7 +33,7 @@
to="/me" to="/me"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
> >
Account {{ $t('pages.me') }}
</NuxtLink> </NuxtLink>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
@ -47,7 +46,7 @@
to="/admin" to="/admin"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
> >
Admin Panel {{ $t('pages.admin.panel') }}
</NuxtLink> </NuxtLink>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem>

42
src/app/composables/useSubmit.ts

@ -0,0 +1,42 @@
import type { NitroFetchRequest, NitroFetchOptions } from 'nitropack/types';
import { FetchError } from 'ofetch';
type RevertFn = () => Promise<void>;
export function useSubmit<
R extends NitroFetchRequest,
O extends NitroFetchOptions<R>,
>(url: R, options: O, revert: RevertFn, success?: string, error?: string) {
const toast = useToast();
const { t: $t } = useI18n();
return async () => {
try {
const res = await $fetch(url, options);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(res as any).success) {
throw new Error(error || $t('toast.errored'));
}
toast.showToast({
type: 'success',
message: success,
});
await revert();
} catch (e) {
if (e instanceof FetchError) {
toast.showToast({
type: 'error',
message: e.data.message,
});
} else if (e instanceof Error) {
toast.showToast({
type: 'error',
message: e.message,
});
} else {
console.error(e);
}
await revert();
}
};
}

11
src/app/pages/admin.vue

@ -5,7 +5,7 @@
<div class="mr-4 w-64 rounded-lg bg-white p-4 dark:bg-neutral-700"> <div class="mr-4 w-64 rounded-lg bg-white p-4 dark:bg-neutral-700">
<NuxtLink to="/admin"> <NuxtLink to="/admin">
<h2 class="mb-4 text-xl font-bold dark:text-neutral-200"> <h2 class="mb-4 text-xl font-bold dark:text-neutral-200">
Admin Panel {{ t('pages.admin.panel') }}
</h2> </h2>
</NuxtLink> </NuxtLink>
<div class="flex flex-col space-y-2"> <div class="flex flex-col space-y-2">
@ -37,14 +37,15 @@
<script setup lang="ts"> <script setup lang="ts">
const authStore = useAuthStore(); const authStore = useAuthStore();
authStore.update(); authStore.update();
const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const menuItems = [ const menuItems = [
{ id: '', name: 'General' }, { id: '', name: t('pages.admin.general') },
{ id: 'config', name: 'Config' }, { id: 'config', name: t('pages.admin.config') },
{ id: 'interface', name: 'Interface' }, { id: 'interface', name: t('pages.admin.interface') },
{ id: 'hooks', name: 'Hooks' }, { id: 'hooks', name: t('pages.admin.hooks') },
]; ];
const activeMenuItem = computed(() => { const activeMenuItem = computed(() => {

71
src/app/pages/admin/config.vue

@ -2,79 +2,74 @@
<main v-if="data"> <main v-if="data">
<FormElement @submit.prevent="submit"> <FormElement @submit.prevent="submit">
<FormGroup> <FormGroup>
<FormHeading>Connection</FormHeading> <FormHeading>{{ $t('admin.config.connection') }}</FormHeading>
<FormTextField id="host" v-model="data.host" label="Host" /> <FormTextField
<FormNumberField id="port" v-model="data.port" label="Port" /> id="host"
v-model="data.host"
:label="$t('admin.config.host')"
:description="$t('admin.config.hostDesc')"
/>
<FormNumberField
id="port"
v-model="data.port"
:label="$t('admin.generic.port')"
:description="$t('admin.config.portDesc')"
/>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>Allowed IPs</FormHeading> <FormHeading :description="$t('admin.config.allowedIpsDesc')">{{
$t('admin.config.allowedIps')
}}</FormHeading>
<FormArrayField <FormArrayField
v-model="data.defaultAllowedIps" v-model="data.defaultAllowedIps"
name="defaultAllowedIps" name="defaultAllowedIps"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>DNS</FormHeading> <FormHeading :description="$t('admin.config.dnsDesc')">{{
$t('admin.config.dns')
}}</FormHeading>
<FormArrayField v-model="data.defaultDns" name="defaultDns" /> <FormArrayField v-model="data.defaultDns" name="defaultDns" />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>Advanced</FormHeading> <FormHeading>{{ $t('admin.config.advanced') }}</FormHeading>
<FormNumberField <FormNumberField
id="defaultMtu" id="defaultMtu"
v-model="data.defaultMtu" v-model="data.defaultMtu"
label="MTU" :label="$t('admin.generic.mtu')"
:description="$t('admin.config.mtuDesc')"
/> />
<FormNumberField <FormNumberField
id="defaultPersistentKeepalive" id="defaultPersistentKeepalive"
v-model="data.defaultPersistentKeepalive" v-model="data.defaultPersistentKeepalive"
label="Persistent Keepalive" :label="$t('admin.config.persistentKeepalive')"
:description="$t('admin.config.persistentKeepaliveDesc')"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>Actions</FormHeading> <FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" label="Save" /> <FormActionField type="submit" :label="$t('form.save')" />
<FormActionField label="Revert" @click="revert" /> <FormActionField :label="$t('form.revert')" @click="revert" />
</FormGroup> </FormGroup>
</FormElement> </FormElement>
</main> </main>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const toast = useToast();
const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, { const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, {
method: 'get', method: 'get',
}); });
const data = toRef(_data.value); const data = toRef(_data.value);
async function submit() { const submit = useSubmit(
try { `/api/admin/userconfig`,
const res = await $fetch(`/api/admin/userconfig`, { {
method: 'post', method: 'post',
body: data.value, body: data.value,
}); },
toast.showToast({ revert
type: 'success', );
title: 'Success',
message: 'Saved',
});
if (!res.success) {
throw new Error('Failed to save');
}
// TODO: avoid refreshNuxtData
await revert();
} catch (e) {
if (e instanceof Error) {
toast.showToast({
type: 'error',
title: 'Error',
message: e.message,
});
}
await revert();
}
}
async function revert() { async function revert() {
await refresh(); await refresh();

6
src/app/pages/admin/hooks.vue

@ -8,9 +8,9 @@
<FormTextField id="PostDown" v-model="data.postDown" label="PostDown" /> <FormTextField id="PostDown" v-model="data.postDown" label="PostDown" />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>Actions</FormHeading> <FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" label="Save" /> <FormActionField type="submit" :label="$t('form.save')" />
<FormActionField label="Revert" @click="revert" /> <FormActionField :label="$t('form.revert')" @click="revert" />
</FormGroup> </FormGroup>
</FormElement> </FormElement>
</main> </main>

46
src/app/pages/admin/index.vue

@ -5,25 +5,29 @@
<FormNumberField <FormNumberField
id="session" id="session"
v-model="data.sessionTimeout" v-model="data.sessionTimeout"
:label="$t('general.sessionTimeout')" :label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>{{ $t('general.metrics') }}</FormHeading> <FormHeading>{{ $t('admin.general.metrics') }}</FormHeading>
<FormNullTextField <FormNullTextField
id="password" id="password"
v-model="data.metricsPassword" v-model="data.metricsPassword"
:label="$t('passsword')" :label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')"
/> />
<FormSwitchField <FormSwitchField
id="prometheus" id="prometheus"
v-model="data.metricsPrometheus" v-model="data.metricsPrometheus"
:label="$t('general.prometheus')" :label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')"
/> />
<FormSwitchField <FormSwitchField
id="json" id="json"
v-model="data.metricsJson" v-model="data.metricsJson"
:label="$t('general.json')" :label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@ -36,39 +40,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const toast = useToast();
const { data: _data, refresh } = await useFetch(`/api/admin/general`, { const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get', method: 'get',
}); });
const data = toRef(_data.value); const data = toRef(_data.value);
async function submit() { const submit = useSubmit(
try { `/api/admin/general`,
const res = await $fetch(`/api/admin/general`, { {
method: 'post', method: 'post',
body: data.value, body: data.value,
}); },
toast.showToast({ revert
type: 'success', );
title: 'Success',
message: 'Saved',
});
if (!res.success) {
throw new Error('Failed to save');
}
await refreshNuxtData();
} catch (e) {
if (e instanceof Error) {
toast.showToast({
type: 'error',
title: 'Error',
message: e.message,
});
}
}
}
async function revert() { async function revert() {
await refresh(); await refresh();

94
src/app/pages/admin/interface.vue

@ -2,22 +2,39 @@
<main v-if="data"> <main v-if="data">
<FormElement @submit.prevent="submit"> <FormElement @submit.prevent="submit">
<FormGroup> <FormGroup>
<FormHeading>Interface Settings</FormHeading> <FormNumberField
<FormNumberField id="mtu" v-model="data.mtu" label="MTU" /> id="mtu"
<FormNumberField id="port" v-model="data.port" label="Port" /> v-model="data.mtu"
<FormTextField id="device" v-model="data.device" label="Device" /> :label="$t('admin.generic.mtu')"
:description="$t('admin.interface.mtuDesc')"
/>
<FormNumberField
id="port"
v-model="data.port"
:label="$t('admin.generic.port')"
:description="$t('admin.interface.portDesc')"
/>
<FormTextField
id="device"
v-model="data.device"
:label="$t('admin.interface.device')"
:description="$t('admin.interface.deviceDesc')"
/>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>Actions</FormHeading> <FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" label="Save" /> <FormActionField type="submit" :label="$t('form.save')" />
<FormActionField label="Revert" @click="revert" /> <FormActionField :label="$t('form.revert')" @click="revert" />
<AdminCidrDialog <AdminCidrDialog
trigger-class="col-span-2" trigger-class="col-span-2"
:ipv4-cidr="data.ipv4Cidr" :ipv4-cidr="data.ipv4Cidr"
:ipv6-cidr="data.ipv6Cidr" :ipv6-cidr="data.ipv6Cidr"
@change="changeCidr" @change="changeCidr"
> >
<FormActionField label="Change CIDR" class="w-full" /> <FormActionField
:label="$t('admin.interface.changeCidr')"
class="w-full"
/>
</AdminCidrDialog> </AdminCidrDialog>
</FormGroup> </FormGroup>
</FormElement> </FormElement>
@ -25,7 +42,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const toast = useToast(); const { t } = useI18n();
const { data: _data, refresh } = await useFetch(`/api/admin/interface`, { const { data: _data, refresh } = await useFetch(`/api/admin/interface`, {
method: 'get', method: 'get',
@ -33,31 +50,14 @@ const { data: _data, refresh } = await useFetch(`/api/admin/interface`, {
const data = toRef(_data.value); const data = toRef(_data.value);
async function submit() { const submit = useSubmit(
try { `/api/admin/interface`,
const res = await $fetch(`/api/admin/interface`, { {
method: 'post', method: 'post',
body: data.value, body: data.value,
}); },
toast.showToast({ revert
type: 'success', );
title: 'Success',
message: 'Saved',
});
if (!res.success) {
throw new Error('Failed to save');
}
await refreshNuxtData();
} catch (e) {
if (e instanceof Error) {
toast.showToast({
type: 'error',
title: 'Error',
message: e.message,
});
}
}
}
async function revert() { async function revert() {
await refresh(); await refresh();
@ -65,28 +65,16 @@ async function revert() {
} }
async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) { async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) {
try { const _changeCidr = useSubmit(
const res = await $fetch(`/api/admin/interface/cidr`, { `/api/admin/interface/cidr`,
{
method: 'post', method: 'post',
body: { ipv4Cidr, ipv6Cidr }, body: { ipv4Cidr, ipv6Cidr },
}); },
toast.showToast({ revert,
type: 'success', t('admin.interface.cidrSuccess'),
title: 'Success', t('admin.interface.cidrError')
message: 'Changed CIDR', );
}); await _changeCidr();
if (!res.success) {
throw new Error('Failed to change CIDR');
}
await refreshNuxtData();
} catch (e) {
if (e instanceof Error) {
toast.showToast({
type: 'error',
title: 'Error',
message: e.message,
});
}
}
} }
</script> </script>

1
src/app/pages/login.vue

@ -78,6 +78,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO: improve
import { FetchError } from 'ofetch'; import { FetchError } from 'ofetch';
const { t } = useI18n(); const { t } = useI18n();

35
src/app/stores/toast.ts

@ -1,4 +1,6 @@
export const useToast = defineStore('Toast', () => { export const useToast = defineStore('Toast', () => {
const { t: $t } = useI18n();
type ToastInterface = { type ToastInterface = {
publish: (e: { title: string; message: string }) => void; publish: (e: { title: string; message: string }) => void;
}; };
@ -11,15 +13,32 @@ export const useToast = defineStore('Toast', () => {
toast.value = toastInstance; toast.value = toastInstance;
} }
function showToast({ type ShowToast =
title, | {
message, type: 'success';
}: { title?: string;
type: 'success' | 'error'; message?: string;
title: string; }
| {
type: 'error';
title?: string;
message: string; message: string;
}) { };
toast.value?.value?.publish({ title, message });
function showToast({ type, title, message }: ShowToast) {
if (type === 'success') {
if (!title) {
title = $t('toast.success');
}
if (!message) {
message = $t('toast.saved');
}
} else if (type === 'error') {
if (!title) {
title = $t('toast.error');
}
}
toast.value?.value?.publish({ title: title ?? '', message: message ?? '' });
} }
return { setToast, showToast }; return { setToast, showToast };

55
src/i18n/locales/en.json

@ -1,7 +1,14 @@
{ {
"pages": { "pages": {
"me": "Account", "me": "Account",
"clients": "Clients" "clients": "Clients",
"admin": {
"panel": "Admin Panel",
"general": "General",
"config": "Config",
"interface": "Interface",
"hooks": "Hooks"
}
}, },
"me": { "me": {
"sectionGeneral": "General", "sectionGeneral": "General",
@ -80,12 +87,58 @@
"clear": "Clear", "clear": "Clear",
"login": "Log in error" "login": "Log in error"
}, },
"toast": {
"success": "Success",
"saved": "Saved",
"error": "Error",
"errored": "Failed to save"
},
"form": { "form": {
"actions": "Actions", "actions": "Actions",
"save": "Save", "save": "Save",
"revert": "Revert" "revert": "Revert"
}, },
"password": "Password", "password": "Password",
"admin": {
"general": {
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Session duration for Remember Me (seconds)",
"metrics": "Metrics",
"metricsPassword": "Password",
"metricsPasswordDesc": "Bearer Password for the metrics endpoint (argon2 hash)",
"json": "JSON",
"jsonDesc": "Route for metrics in JSON format",
"prometheus": "Prometheus",
"prometheusDesc": "Route for Prometheus metrics"
},
"config": {
"connection": "Connection",
"host": "Host",
"hostDesc": "Public hostname clients will connect to (invalidates config)",
"portDesc": "Public UDP port clients will connect to (invalidates config)",
"allowedIps": "Allowed IPs",
"allowedIpsDesc": "Allowed IPs clients will use (invalidates config)",
"dns": "DNS",
"dnsDesc": "DNS server clients will use (invalidates config)",
"advanced": "Advanced",
"mtuDesc": "MTU clients will use (invalidates config)",
"persistentKeepalive": "Persistent Keepalive",
"persistentKeepaliveDesc": "Interval in seconds to send keepalives to the server. 0 = disabled (invalidates config)"
},
"interface": {
"cidrSuccess": "Changed CIDR",
"cidrError": "Failed to change CIDR",
"device": "Device",
"deviceDesc": "Ethernet device the wireguard traffic should be forwarded through",
"mtuDesc": "MTU WireGuard will use",
"portDesc": "UDP Port WireGuard will listen on (could invalidate config)",
"changeCidr": "Change CIDR"
},
"generic": {
"mtu": "MTU",
"port": "Port"
}
},
"zod": { "zod": {
"generic": { "generic": {
"required": "{0} is required", "required": "{0} is required",

Loading…
Cancel
Save