Browse Source

remove chart, statistics from server

let user decide what he wants to display
pull/1397/head
Bernd Storath 9 months ago
parent
commit
07a810582e
  1. 1
      src/app/app.vue
  2. 2
      src/app/components/ClientCard/Address.vue
  3. 18
      src/app/components/ClientCard/Charts.vue
  4. 9
      src/app/components/ClientCard/ClientCard.vue
  5. 17
      src/app/components/ClientCard/Config.vue
  6. 1
      src/app/components/ClientCard/ExpireDate.vue
  7. 29
      src/app/components/ClientCard/InlineTransfer.vue
  8. 5
      src/app/components/ClientCard/LastSeen.vue
  9. 7
      src/app/components/ClientCard/OneTimeLink.vue
  10. 17
      src/app/components/ClientCard/OneTimeLinkBtn.vue
  11. 10
      src/app/components/ClientCard/QRCode.vue
  12. 53
      src/app/components/ClientCard/Switch.vue
  13. 0
      src/app/components/Clients/List.vue
  14. 8
      src/app/components/Clients/Sort.vue
  15. 2
      src/app/components/base/Switch.vue
  16. 2
      src/app/components/form/ArrayField.vue
  17. 1
      src/app/layouts/default.vue
  18. 1
      src/app/pages/admin.vue
  19. 3
      src/app/pages/admin/metrics.vue
  20. 115
      src/app/pages/admin/statistics.vue
  21. 4
      src/app/pages/index.vue
  22. 23
      src/app/stores/global.ts
  23. 38
      src/app/utils/api.ts
  24. 6
      src/app/utils/chart.ts
  25. 1
      src/app/utils/localStorage.ts
  26. 8
      src/server/api/admin/statistics.post.ts
  27. 4
      src/server/api/statistics.get.ts
  28. 1
      src/server/utils/WireGuard.ts
  29. 20
      src/services/database/lowdb.ts
  30. 1
      src/services/database/migrations/1.ts
  31. 8
      src/services/database/repositories/system.ts
  32. 14
      src/tailwind.config.ts

1
src/app/app.vue

@ -13,7 +13,6 @@
const globalStore = useGlobalStore();
globalStore.fetchRelease();
globalStore.setLanguage();
globalStore.fetchStatistics();
useHead({
bodyAttrs: {
class: 'bg-gray-50 dark:bg-neutral-800',

2
src/app/components/ClientCard/Address4.vue → src/app/components/ClientCard/Address.vue

@ -1,6 +1,6 @@
<template>
<span class="inline-block">
{{ client.address4 }}
{{ client.address4 }}, {{ client.address6 }}
</span>
</template>

18
src/app/components/ClientCard/Charts.vue

@ -1,13 +1,11 @@
<template>
<div
v-if="globalStore.statistics.chartType"
:class="`absolute bottom-0 left-0 right-0 z-0 h-6 ${globalStore.statistics.chartType === 1 && 'line-chart'}`"
:class="`absolute bottom-0 left-0 right-0 z-0 h-6 ${globalStore.uiChartType === 'line' && 'line-chart'}`"
>
<UiChart :options="chartOptionsTX" :series="client.transferTxSeries" />
</div>
<div
v-if="globalStore.statistics.chartType"
:class="`absolute left-0 right-0 top-0 z-0 h-6 ${globalStore.statistics.chartType === 1 && 'line-chart'}`"
:class="`absolute left-0 right-0 top-0 z-0 h-6 ${globalStore.uiChartType === 'line' && 'line-chart'}`"
>
<UiChart
:options="chartOptionsRX"
@ -32,10 +30,8 @@ const chartOptionsTX = computed(() => {
...chartOptions,
colors: [CHART_COLORS.tx[theme.value]],
};
opts.chart.type =
UI_CHART_TYPES[globalStore.statistics.chartType]?.type || undefined;
opts.stroke.width =
UI_CHART_TYPES[globalStore.statistics.chartType]?.strokeWidth ?? 0;
opts.chart.type = globalStore.uiChartType;
opts.stroke.width = UI_CHART_PROPS[globalStore.uiChartType].strokeWidth;
return opts;
});
@ -44,10 +40,8 @@ const chartOptionsRX = computed(() => {
...chartOptions,
colors: [CHART_COLORS.rx[theme.value]],
};
opts.chart.type =
UI_CHART_TYPES[globalStore.statistics.chartType]?.type || undefined;
opts.stroke.width =
UI_CHART_TYPES[globalStore.statistics.chartType]?.strokeWidth ?? 0;
opts.chart.type = globalStore.uiChartType;
opts.stroke.width = UI_CHART_PROPS[globalStore.uiChartType].strokeWidth;
return opts;
});

9
src/app/components/ClientCard/ClientCard.vue

@ -12,11 +12,7 @@
<div
class="block pb-1 text-xs text-gray-500 md:inline-block md:pb-0 dark:text-neutral-400"
>
<ClientCardAddress4 :client="client" />
<ClientCardInlineTransfer
v-if="!globalStore.statistics.enabled"
:client="client"
/>
<ClientCardAddress :client="client" />
<ClientCardLastSeen :client="client" />
</div>
<ClientCardOneTimeLink :client="client" />
@ -25,7 +21,6 @@
<!-- Info -->
<div
v-if="globalStore.statistics.enabled"
class="mt-px flex shrink-0 items-center justify-end gap-2 text-xs text-gray-400 dark:text-neutral-400"
>
<ClientCardTransfer :client="client" />
@ -53,6 +48,4 @@
defineProps<{
client: LocalClient;
}>();
const globalStore = useGlobalStore();
</script>

17
src/app/components/ClientCard/Config.vue

@ -1,20 +1,9 @@
<template>
<a
:disabled="!client.downloadableConfig"
:href="'./api/client/' + client.id + '/configuration'"
:download="client.downloadableConfig ? 'configuration' : null"
class="inline-block rounded bg-gray-100 p-2 align-middle transition dark:bg-neutral-600 dark:text-neutral-300"
:class="{
'hover:bg-red-800 hover:text-white dark:hover:bg-red-800 dark:hover:text-white':
client.downloadableConfig,
'is-disabled': !client.downloadableConfig,
}"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('downloadConfig')"
@click="
if (!client.downloadableConfig) {
$event.preventDefault();
}
"
download
class="inline-block rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('downloadConfig')"
>
<IconsDownload class="w-5" />
</a>

1
src/app/components/ClientCard/ExpireDate.vue

@ -9,7 +9,6 @@
<script setup lang="ts">
defineProps<{ client: LocalClient }>();
const globalStore = useGlobalStore();
const { t, locale } = useI18n();
function expiredDateFormat(value: string | null) {

29
src/app/components/ClientCard/InlineTransfer.vue

@ -1,29 +0,0 @@
<template>
<!-- Inline Transfer TX -->
<span
v-if="client.transferTx"
class="whitespace-nowrap"
:title="$t('totalDownload') + bytes(client.transferTx)"
>
·
<IconsArrowDown class="inline h-3 align-middle" />
{{ bytes(client.transferTxCurrent) }}/s
</span>
<!-- Inline Transfer RX -->
<span
v-if="client.transferRx"
class="whitespace-nowrap"
:title="$t('totalUpload') + bytes(client.transferRx)"
>
·
<IconsArrowUp class="inline h-3 align-middle" />
{{ bytes(client.transferRxCurrent) }}/s
</span>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>

5
src/app/components/ClientCard/LastSeen.vue

@ -4,8 +4,7 @@
class="whitespace-nowrap text-gray-400 dark:text-neutral-500"
:title="$t('lastSeen') + dateTime(new Date(client.latestHandshakeAt))"
>
{{ !globalStore.statistics.enabled ? ' · ' : ''
}}{{ timeago(new Date(client.latestHandshakeAt)) }}
· {{ timeago(new Date(client.latestHandshakeAt)) }}
</span>
</template>
@ -15,6 +14,4 @@ import { format as timeago } from 'timeago.js';
defineProps<{
client: LocalClient;
}>();
const globalStore = useGlobalStore();
</script>

7
src/app/components/ClientCard/OneTimeLink.vue

@ -4,7 +4,7 @@
:ref="'client-' + client.id + '-link'"
class="text-xs text-gray-400"
>
<a :href="'./cnf/' + client.oneTimeLink + ''">{{ path }}</a>
<a :href="'./cnf/' + client.oneTimeLink.oneTimeLink">{{ path }}</a>
</div>
</template>
@ -13,8 +13,9 @@ const props = defineProps<{ client: LocalClient }>();
const path = computed(() => {
if (import.meta.client) {
return `${document.location.protocol}//${document.location.host}/cnf/${props.client.oneTimeLink}`;
// TODO: show how long its still valid
return `${document.location.protocol}//${document.location.host}/cnf/${props.client.oneTimeLink?.oneTimeLink}`;
}
return '';
return 'Loading...';
});
</script>

17
src/app/components/ClientCard/OneTimeLinkBtn.vue

@ -1,18 +1,8 @@
<template>
<button
:disabled="!client.downloadableConfig"
class="inline-block rounded bg-gray-100 p-2 align-middle transition dark:bg-neutral-600 dark:text-neutral-300"
:class="{
'hover:bg-red-800 hover:text-white dark:hover:bg-red-800 dark:hover:text-white':
client.downloadableConfig,
'is-disabled': !client.downloadableConfig,
}"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('OneTimeLink')"
@click="
if (client.downloadableConfig) {
showOneTimeLink(client);
}
"
class="inline-block rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('OneTimeLink')"
@click="showOneTimeLink(client)"
>
<svg
class="w-5"
@ -35,7 +25,6 @@
defineProps<{ client: LocalClient }>();
const clientsStore = useClientsStore();
const globalStore = useGlobalStore();
function showOneTimeLink(client: LocalClient) {
api

10
src/app/components/ClientCard/QRCode.vue

@ -1,13 +1,7 @@
<template>
<button
:disabled="!client.downloadableConfig"
class="rounded bg-gray-100 p-2 align-middle transition dark:bg-neutral-600 dark:text-neutral-300"
:class="{
'hover:bg-red-800 hover:text-white dark:hover:bg-red-800 dark:hover:text-white':
client.downloadableConfig,
'is-disabled': !client.downloadableConfig,
}"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('showQR')"
class="rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('showQR')"
@click="modalStore.qrcode = `./api/client/${client.id}/qrcode.svg`"
>
<IconsQRCode class="w-5" />

53
src/app/components/ClientCard/Switch.vue

@ -1,40 +1,35 @@
<template>
<div
v-if="client.enabled === true"
:title="$t('disableClient')"
class="mr-1 inline-block h-6 w-10 cursor-pointer rounded-full bg-red-800 align-middle transition-all hover:bg-red-700"
@click="disableClient(client)"
>
<div class="m-1 ml-5 h-4 w-4 rounded-full bg-white" />
</div>
<div
v-if="client.enabled === false"
:title="$t('enableClient')"
class="mr-1 inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-200 align-middle transition-all hover:bg-gray-300 dark:bg-neutral-400 dark:hover:bg-neutral-500"
@click="enableClient(client)"
>
<div class="m-1 h-4 w-4 rounded-full bg-white" />
</div>
<BaseSwitch
v-model="enabled"
:title="client.enabled ? $t('disableClient') : $t('enableClient')"
@click="toggleClient"
/>
</template>
<script setup lang="ts">
defineProps<{
const props = defineProps<{
client: LocalClient;
}>();
const enabled = ref(props.client.enabled);
const clientsStore = useClientsStore();
function enableClient(client: WGClient) {
api
.enableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => clientsStore.refresh().catch(console.error));
}
function disableClient(client: WGClient) {
api
.disableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => clientsStore.refresh().catch(console.error));
async function toggleClient() {
try {
if (props.client.enabled) {
await $fetch(`/api/client/${props.client.id}/disable`, {
method: 'post',
});
} else {
await $fetch(`/api/client/${props.client.id}/enable`, {
method: 'post',
});
}
} catch (err) {
alert(err);
} finally {
clientsStore.refresh().catch(console.error);
}
}
</script>

0
src/app/components/Clients/Clients.vue → src/app/components/Clients/List.vue

8
src/app/components/Clients/Sort.vue

@ -1,7 +1,7 @@
<template>
<button
class="inline-flex items-center border-2 border-gray-100 px-4 py-2 text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white max-md:border-x-0 md:rounded dark:border-neutral-600 dark:text-neutral-200"
@click="globalStore.sortClient = !globalStore.sortClient"
@click="toggleSort"
>
<svg
v-if="globalStore.sortClient === true"
@ -47,4 +47,10 @@
<script setup lang="ts">
const globalStore = useGlobalStore();
const clientsStore = useClientsStore();
function toggleSort() {
globalStore.sortClient = !globalStore.sortClient;
clientsStore.refresh().catch(console.error);
}
</script>

2
src/app/components/base/Switch.vue

@ -11,6 +11,6 @@
</template>
<script lang="ts" setup>
defineProps<{ id: string }>();
defineProps<{ id?: string }>();
const data = defineModel<boolean>();
</script>

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

@ -14,7 +14,7 @@
</template>
<script lang="ts" setup>
const data = defineModel<string[]>();
const data = defineModel<string[] | null>();
function update(i: number) {
return (v: string) => {

1
src/app/layouts/default.vue

@ -39,7 +39,6 @@
</button>
<!-- Show / hide charts -->
<label
v-if="globalStore.statistics.chartType > 0"
class="group inline-flex h-8 w-8 cursor-pointer items-center justify-center whitespace-nowrap rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
:title="$t('toggleCharts')"
>

1
src/app/pages/admin.vue

@ -44,7 +44,6 @@ const menuItems = [
{ id: '', name: 'General' },
{ id: 'defaults', name: 'Defaults' },
{ id: 'interface', name: 'Interface' },
{ id: 'statistics', name: 'Statistics' },
{ id: 'metrics', name: 'Metrics' },
];

3
src/app/pages/admin/metrics.vue

@ -0,0 +1,3 @@
<template><div></div></template>
<script lang="ts" setup></script>

115
src/app/pages/admin/statistics.vue

@ -1,115 +0,0 @@
<template>
<div class="flex flex-col">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-neutral-200">
Traffic Stats
</h3>
<p class="text-sm text-gray-500 dark:text-neutral-300">
Show more concise Stats about Traffic Usage
</p>
</div>
<SwitchRoot
v-model:checked="enabled"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-red-800 focus:ring-offset-2"
:class="enabled ? 'bg-red-800' : 'bg-gray-200'"
>
<SwitchThumb
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="enabled ? 'translate-x-6' : 'translate-x-1'"
/>
</SwitchRoot>
</div>
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-neutral-200">
Chart Type
</h3>
<p class="text-sm text-gray-500 dark:text-neutral-300">
Select Type of Chart you want to show
</p>
</div>
<SelectRoot v-model="chartType">
<SelectTrigger
class="text-grass11 hover:bg-mauve3 data-[placeholder]:text-green9 inline-flex h-[35px] min-w-[160px] items-center justify-between gap-[5px] rounded bg-white px-[15px] text-[13px] leading-none shadow-[0_2px_10px] shadow-black/10 outline-none focus:shadow-[0_0_0_2px] focus:shadow-black"
aria-label="Customize options"
>
<SelectValue placeholder="Select a fruit..." />
<IconsArrowDown class="h-3.5 w-3.5" />
</SelectTrigger>
<SelectPortal>
<SelectContent
class="data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-[100] min-w-[160px] rounded bg-white shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform]"
:side-offset="5"
>
<SelectScrollUpButton
class="text-violet11 flex h-[25px] cursor-default items-center justify-center bg-white"
>
<IconsArrowUp />
</SelectScrollUpButton>
<SelectViewport class="p-[5px]">
<SelectItem
v-for="(option, index) in options"
:key="index"
class="text-grass11 data-[disabled]:text-mauve8 data-[highlighted]:bg-green9 data-[highlighted]:text-green1 relative flex h-[25px] select-none items-center rounded-[3px] pl-[25px] pr-[35px] text-[13px] leading-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none"
:value="option"
>
<SelectItemText>
{{ option }}
</SelectItemText>
</SelectItem>
</SelectViewport>
<SelectScrollDownButton
class="text-violet11 flex h-[25px] cursor-default items-center justify-center bg-white"
>
<IconsArrowDown />
</SelectScrollDownButton>
</SelectContent>
</SelectPortal>
</SelectRoot>
</div>
<BaseButton class="self-end" @click="submit">Save</BaseButton>
<UiToast
v-model:open="open"
title="Saved successfully"
content="Statistics saved successfully"
/>
</div>
</template>
<script setup lang="ts">
const globalStore = useGlobalStore();
const open = ref(false);
const enabled = ref(globalStore.statistics.enabled);
const options: Record<number, string> = {
0: 'None',
1: 'Line',
2: 'Area',
3: 'Bar',
};
const stringToIndex = Object.entries(options).reduce(
(obj, [k, v]) => {
obj[v] = Number.parseInt(k);
return obj;
},
{} as Record<string, number>
);
const chartType = ref(options[globalStore.statistics.chartType]);
async function submit() {
const response = await $fetch('/api/admin/statistics', {
method: 'post',
body: {
statistics: {
enabled: enabled.value,
chartType: stringToIndex[chartType.value!],
},
},
});
if (response.success) {
open.value = true;
}
globalStore.fetchStatistics();
}
</script>

4
src/app/pages/index.vue

@ -10,7 +10,7 @@
</PanelHead>
<div>
<Clients
<ClientsList
v-if="clientsStore.clients && clientsStore.clients.length > 0"
/>
</div>
@ -48,7 +48,7 @@ onMounted(() => {
intervalId.value = setInterval(() => {
clientsStore
.refresh({
updateCharts: globalStore.updateCharts,
updateCharts: globalStore.uiShowCharts,
})
.catch(console.error);
}, 1000);

23
src/app/stores/global.ts

@ -38,25 +38,8 @@ export const useGlobalStore = defineStore('Global', () => {
updateAvailable.value = release.value.updateAvailable;
}
const statistics = ref({
enabled: false,
chartType: 0,
});
async function fetchStatistics() {
const { data: apiStatistics } = await useFetch('/api/statistics', {
method: 'get',
});
if (apiStatistics.value) {
statistics.value = apiStatistics.value;
}
}
const uiShowCharts = ref(getItem('uiShowCharts') === '1');
const updateCharts = computed(() => {
return statistics.value.chartType > 0 && uiShowCharts.value;
});
const uiChartType = ref(getItem('uiChartType') ?? 'area');
/**
* @throws if unsuccessful
@ -76,10 +59,8 @@ export const useGlobalStore = defineStore('Global', () => {
latestRelease,
updateAvailable,
fetchRelease,
statistics,
fetchStatistics,
uiShowCharts,
updateCharts,
uiChartType,
updateLang,
};
});

38
src/app/utils/api.ts

@ -57,44 +57,6 @@ class API {
});
}
async enableClient({ clientId }: { clientId: string }) {
return $fetch(`/api/client/${clientId}/enable`, {
method: 'post',
});
}
async disableClient({ clientId }: { clientId: string }) {
return $fetch(`/api/client/${clientId}/disable`, {
method: 'post',
});
}
async updateClientName({
clientId,
name,
}: {
clientId: string;
name: string;
}) {
return $fetch(`/api/client/${clientId}/name`, {
method: 'put',
body: { name },
});
}
async updateClientAddress4({
clientId,
address4,
}: {
clientId: string;
address4: string;
}) {
return $fetch(`/api/client/${clientId}/address4`, {
method: 'put',
body: { address4 },
});
}
async updateClientExpireDate({
clientId,
expireDate,

6
src/app/utils/chart.ts

@ -5,6 +5,12 @@ export const UI_CHART_TYPES = [
{ type: 'bar', strokeWidth: 0 },
] as const;
export const UI_CHART_PROPS = {
line: { strokeWidth: 3 },
area: { strokeWidth: 0 },
bar: { strokeWidth: 0 },
} as const;
export const CHART_COLORS = {
rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' },
tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' },

1
src/app/utils/localStorage.ts

@ -1,6 +1,7 @@
export type LocalStorage = {
uiShowCharts: '1' | '0';
lang: string;
uiChartType: 'area' | 'bar' | 'line';
};
export function getItem<K extends keyof LocalStorage>(

8
src/server/api/admin/statistics.post.ts

@ -1,8 +0,0 @@
export default defineEventHandler(async (event) => {
const { statistics } = await readValidatedBody(
event,
validateZod(statisticsType)
);
await Database.system.updateStatistics(statistics);
return { success: true };
});

4
src/server/api/statistics.get.ts

@ -1,4 +0,0 @@
export default defineEventHandler(async () => {
const system = await Database.system.get();
return system.statistics;
});

1
src/server/utils/WireGuard.ts

@ -61,7 +61,6 @@ class WireGuard {
expiresAt: client.expiresAt,
allowedIPs: client.allowedIPs,
oneTimeLink: client.oneTimeLink,
downloadableConfig: 'privateKey' in client,
persistentKeepalive: null as string | null,
latestHandshakeAt: null as Date | null,
endpoint: null as string | null,

20
src/services/database/lowdb.ts

@ -18,12 +18,7 @@ import {
type NewClient,
type OneTimeLink,
} from './repositories/client';
import {
ChartType,
SystemRepository,
type Lang,
type Statistics,
} from './repositories/system';
import { SystemRepository, type Lang } from './repositories/system';
import { SetupRepository, type Steps } from './repositories/setup';
import type { DeepReadonly } from 'vue';
@ -78,19 +73,6 @@ class LowDBSystem extends SystemRepository {
return makeReadonly(system);
}
async updateStatistics(statistics: Statistics) {
DEBUG('Update Statistics');
this.#db.update((v) => {
v.system.statistics.enabled = statistics.enabled;
if (
statistics.chartType >= ChartType.None &&
statistics.chartType <= ChartType.Bar
) {
v.system.statistics.chartType = statistics.chartType;
}
});
}
async updateLang(lang: Lang): Promise<void> {
DEBUG('Update Language');
this.#db.update((v) => {

1
src/services/database/migrations/1.ts

@ -1,6 +1,5 @@
import type { Low } from 'lowdb';
import type { Database } from '../repositories/database';
import { ChartType } from '../repositories/system';
import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';

8
src/services/database/repositories/system.ts

@ -39,11 +39,6 @@ export enum ChartType {
Bar = 3,
}
export type Statistics = {
enabled: boolean;
chartType: ChartType;
};
export type Prometheus = {
enabled: boolean;
password: string | null;
@ -67,8 +62,6 @@ export type System = {
iptables: IpTables;
statistics: Statistics;
metrics: Metrics;
sessionConfig: SessionConfig;
@ -82,7 +75,6 @@ export type System = {
export abstract class SystemRepository {
abstract get(): Promise<DeepReadonly<System>>;
abstract updateStatistics(statistics: Statistics): Promise<void>;
abstract updateLang(lang: Lang): Promise<void>;
abstract updateClientsHostPort(host: string, port: number): Promise<void>;
}

14
src/tailwind.config.ts

@ -1,5 +1,4 @@
import type { Config } from 'tailwindcss';
import type { PluginAPI } from 'tailwindcss/types/config';
import tailwindForms from '@tailwindcss/forms';
export default {
@ -16,16 +15,5 @@ export default {
'2xl': '1536px',
},
},
plugins: [
function addDisabledClass({ addUtilities }: PluginAPI) {
const newUtilities = {
'.is-disabled': {
opacity: '0.25',
cursor: 'default',
},
};
addUtilities(newUtilities);
},
tailwindForms,
],
plugins: [tailwindForms],
} satisfies Config;

Loading…
Cancel
Save