Browse Source

configure quota

pull/2677/head
Bernd Storath 3 weeks ago
parent
commit
80a985b770
  1. 45
      src/app/components/Form/QuotaField.vue
  2. 20
      src/app/pages/clients/[id].vue
  3. 14
      src/app/utils/math.ts
  4. 5
      src/i18n/locales/en.json
  5. 4
      src/test/tsconfig.json
  6. 32
      src/test/unit/math.app.spec.ts
  7. 1
      src/vitest.config.ts

45
src/app/components/Form/QuotaField.vue

@ -0,0 +1,45 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<div
class="flex overflow-hidden rounded-lg border-2 border-gray-100 dark:border-neutral-800"
>
<BaseInput
:id="id"
v-model.number="quotaGiB"
:max="maximumGiB"
:min="minimumGiB"
:name="id"
step="any"
type="number"
class="w-full !border-0"
/>
<div
class="flex h-full items-center bg-neutral-200 px-4 dark:bg-neutral-800"
>
<span>GiB</span>
</div>
</div>
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
const quotaBytes = defineModel<number | null>({ required: true });
const minimumGiB = quotaBytesToGiB(1)!;
const maximumGiB = quotaBytesToGiB(Number.MAX_SAFE_INTEGER)!;
const quotaGiB = computed<number | null>({
get: () => quotaBytesToGiB(quotaBytes.value),
set: (value) => {
const normalized = (value as number | string | null) === '' ? null : value;
quotaBytes.value = quotaGiBToBytes(normalized);
},
});
</script>

20
src/app/pages/clients/[id].vue

@ -29,6 +29,26 @@
:label="$t('client.expireDate')"
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('client.trafficQuotasDesc')">
{{ $t('client.trafficQuotas') }}
</FormHeading>
<FormQuotaField
id="dailyQuota"
v-model="data.dailyQuota"
:label="$t('client.dailyQuota')"
/>
<FormQuotaField
id="weeklyQuota"
v-model="data.weeklyQuota"
:label="$t('client.weeklyQuota')"
/>
<FormQuotaField
id="monthlyQuota"
v-model="data.monthlyQuota"
:label="$t('client.monthlyQuota')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('client.address') }}</FormHeading>
<FormTextField

14
src/app/utils/math.ts

@ -1,3 +1,17 @@
export const GIBIBYTE_IN_BYTES = 1024 ** 3;
export function quotaBytesToGiB(bytes: number | null): number | null {
return bytes === null ? null : bytes / GIBIBYTE_IN_BYTES;
}
export function quotaGiBToBytes(gibibytes: number | null): number | null {
if (gibibytes === null || gibibytes === 0) {
return null;
}
return Math.round(gibibytes * GIBIBYTE_IN_BYTES);
}
export function bytes(
bytes: number,
decimals = 2,

5
src/i18n/locales/en.json

@ -102,6 +102,11 @@
"deleteDialog1": "Are you sure you want to delete",
"deleteDialog2": "This action cannot be undone.",
"enabled": "Enabled",
"trafficQuotas": "Traffic Quotas",
"trafficQuotasDesc": "Limits include combined upload and download. Blank for unlimited",
"dailyQuota": "Daily Quota",
"weeklyQuota": "Weekly Quota",
"monthlyQuota": "Monthly Quota",
"address": "Address",
"serverAllowedIps": "Server Allowed IPs",
"otlDesc": "Generate short one time link",

4
src/test/tsconfig.json

@ -1,7 +1,9 @@
{
"extends": "../.nuxt/tsconfig.server.json",
"compilerOptions": {
"types": ["vitest/globals"]
"paths": {
"#app/*": ["../app/*"]
}
},
"include": ["./**/*.ts"]
}

32
src/test/unit/math.app.spec.ts

@ -0,0 +1,32 @@
import { describe, expect, test } from 'vitest';
import {
GIBIBYTE_IN_BYTES,
quotaBytesToGiB,
quotaGiBToBytes,
} from '#app/utils/math';
describe('quota conversions', () => {
test('keeps disabled quotas null', () => {
expect(quotaBytesToGiB(null)).toBeNull();
expect(quotaGiBToBytes(null)).toBeNull();
expect(quotaGiBToBytes(0)).toBeNull();
});
test('converts whole and fractional GiB to bytes', () => {
expect(quotaGiBToBytes(1)).toBe(GIBIBYTE_IN_BYTES);
expect(quotaGiBToBytes(0.5)).toBe(GIBIBYTE_IN_BYTES / 2);
expect(quotaGiBToBytes(1 / GIBIBYTE_IN_BYTES)).toBe(1);
});
test('rounds edited GiB values to the nearest byte', () => {
expect(quotaGiBToBytes(1.5 / GIBIBYTE_IN_BYTES)).toBe(2);
});
test.each([1, 150, GIBIBYTE_IN_BYTES, Number.MAX_SAFE_INTEGER])(
'round-trips the exact byte value %s',
(bytes) => {
expect(quotaGiBToBytes(quotaBytesToGiB(bytes))).toBe(bytes);
}
);
});

1
src/vitest.config.ts

@ -10,6 +10,7 @@ export default defineConfig({
alias: {
'#server': fileURLToPath(new URL('./server', import.meta.url)),
'#shared': fileURLToPath(new URL('./shared', import.meta.url)),
'#app': fileURLToPath(new URL('./app', import.meta.url)),
},
},
test: {

Loading…
Cancel
Save