Browse Source

basic statistics page

pull/1397/head
Bernd Storath 7 months ago
parent
commit
814624bc65
  1. 7
      src/app/pages/admin/features.vue
  2. 115
      src/app/pages/admin/statistics.vue
  3. 7
      src/app/utils/api.ts
  4. 8
      src/server/api/admin/statistics.post.ts
  5. 17
      src/server/utils/types.ts
  6. 15
      src/services/database/lowdb.ts
  7. 1
      src/services/database/repositories/system.ts

7
src/app/pages/admin/features.vue

@ -37,7 +37,7 @@ const globalStore = useGlobalStore();
const open = ref(false); const open = ref(false);
type ExtendedFeatures = { type ExtendedFeatures = {
[K in keyof Omit<(typeof globalStore)['features'], 'trafficStats'>]: { [K in keyof (typeof globalStore)['features']]: {
name: string; name: string;
description: string; description: string;
enabled: boolean; enabled: boolean;
@ -71,7 +71,10 @@ function toggleFeature(key: keyof ExtendedFeatures) {
} }
async function submit() { async function submit() {
const response = await api.updateFeatures(featuresData.value); const response = await $fetch('/api/admin/features', {
method: 'post',
body: { features: featuresData.value },
});
if (response.success) { if (response.success) {
open.value = true; open.value = true;
} }

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

@ -0,0 +1,115 @@
<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-offset-2 focus:ring-red-800"
: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="inline-flex min-w-[160px] items-center justify-between rounded px-[15px] text-[13px] leading-none h-[35px] gap-[5px] bg-white text-grass11 shadow-[0_2px_10px] shadow-black/10 hover:bg-mauve3 focus:shadow-[0_0_0_2px] focus:shadow-black data-[placeholder]:text-green9 outline-none"
aria-label="Customize options"
>
<SelectValue placeholder="Select a fruit..." />
<IconsArrowDown class="h-3.5 w-3.5" />
</SelectTrigger>
<SelectPortal>
<SelectContent
class="min-w-[160px] bg-white rounded 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] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-[100]"
:side-offset="5"
>
<SelectScrollUpButton
class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default"
>
<IconsArrowUp />
</SelectScrollUpButton>
<SelectViewport class="p-[5px]">
<SelectItem
v-for="(option, index) in options"
:key="index"
class="text-[13px] leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
:value="option"
>
<SelectItemText>
{{ option }}
</SelectItemText>
</SelectItem>
</SelectViewport>
<SelectScrollDownButton
class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default"
>
<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>

7
src/app/utils/api.ts

@ -127,13 +127,6 @@ class API {
body: { username, password }, body: { username, password },
}); });
} }
async updateFeatures(features: Record<string, { enabled: boolean }>) {
return $fetch('/api/admin/features', {
method: 'post',
body: { features },
});
}
} }
type WGClientReturn = Awaited< type WGClientReturn = Awaited<

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

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

17
src/server/utils/types.ts

@ -59,7 +59,7 @@ const oneTimeLink = z
.pipe(safeStringRefine); .pipe(safeStringRefine);
const features = z.record( const features = z.record(
z.string({ message: 'key must be a valid object' }), z.string({ message: 'key must be a valid string' }),
z.object( z.object(
{ {
enabled: z.boolean({ message: 'enabled must be a valid boolean' }), enabled: z.boolean({ message: 'enabled must be a valid boolean' }),
@ -69,6 +69,14 @@ const features = z.record(
{ message: 'features must be a valid record' } { message: 'features must be a valid record' }
); );
const statistics = z.object(
{
enabled: z.boolean({ message: 'enabled must be a valid boolean' }),
chartType: z.number({ message: 'chartType must be a valid number' }),
},
{ message: 'statistics must be a valid object' }
);
const objectMessage = 'Body must be a valid object'; const objectMessage = 'Body must be a valid object';
export const clientIdType = z.object( export const clientIdType = z.object(
@ -145,6 +153,13 @@ export const featuresType = z.object(
{ message: objectMessage } { message: objectMessage }
); );
export const statisticsType = z.object(
{
statistics: statistics,
},
{ message: objectMessage }
);
export function validateZod<T>(schema: ZodSchema<T>) { export function validateZod<T>(schema: ZodSchema<T>) {
return async (data: unknown) => { return async (data: unknown) => {
try { try {

15
src/services/database/lowdb.ts

@ -20,9 +20,11 @@ import {
} from './repositories/client'; } from './repositories/client';
import { import {
AvailableFeatures, AvailableFeatures,
ChartType,
SystemRepository, SystemRepository,
type Feature, type Feature,
type Features, type Features,
type Statistics,
} from './repositories/system'; } from './repositories/system';
const DEBUG = debug('LowDB'); const DEBUG = debug('LowDB');
@ -54,6 +56,19 @@ export class LowDBSystem extends SystemRepository {
} }
}); });
} }
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;
}
});
}
} }
export class LowDBUser extends UserRepository { export class LowDBUser extends UserRepository {

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

@ -105,4 +105,5 @@ export abstract class SystemRepository {
abstract get(): Promise<System>; abstract get(): Promise<System>;
abstract updateFeatures(features: Record<string, Feature>): Promise<void>; abstract updateFeatures(features: Record<string, Feature>): Promise<void>;
abstract updateStatistics(statistics: Statistics): Promise<void>;
} }

Loading…
Cancel
Save