Skip to content

Commit c44f343

Browse files
authored
Merge pull request #46 from AmRo045/dev
Bug fixes & stats form & minor improvements
2 parents 7f3c36a + 8c262f1 commit c44f343

File tree

17 files changed

+272
-166
lines changed

17 files changed

+272
-166
lines changed

scripts/dak-job.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ const main = async () => {
4141
count: accessKeys.length
4242
});
4343

44+
const dataLimit = dak.dataLimit ? Number(dak.dataLimit) : 0;
4445
const bytesPerMB = 1024 * 1024;
45-
const dataLimitInBytes = dak.dataLimit ? Number(dak.dataLimit) * bytesPerMB : 0;
46+
const dataLimitInBytes = dataLimit * bytesPerMB;
4647
const dataUsage = accessKeys.reduce((acc, key) => acc + Number(key.dataUsage || 0), 0);
47-
const isDataUsageExceeded = dak.dataLimit && dataUsage >= dataLimitInBytes;
48+
const isDataUsageExceeded = dataLimit && dataUsage >= dataLimitInBytes;
4849

49-
if (isDataUsageExceeded) {
50+
if (isDataUsageExceeded && dataLimit > 0) {
5051
logger.warn("DAK exceeded data limit — removing access keys", {
5152
id: dak.id,
5253
name: dak.name,

src/app/dynamic-access-keys/[dynamicAccessKeyId]/edit/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Metadata } from "next";
2+
import { notFound } from "next/navigation";
23

34
import { createPageTitle } from "@/src/core/utils";
45
import DynamicAccessKeyForm from "@/src/components/dynamic-access-key-form";
@@ -18,6 +19,11 @@ interface Props {
1819

1920
export default async function DynamicAccessKeyEditPage({ params }: Props) {
2021
const dynamicAccessKey = await findDynamicAccessKeyById(parseInt(params.dynamicAccessKeyId));
22+
23+
if (!dynamicAccessKey) {
24+
notFound();
25+
}
26+
2127
const servers = await getServers({ status: true });
2228
const tags = await getTags();
2329

src/app/servers/[serverId]/access-keys/[accessKeyId]/edit/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Metadata } from "next";
2+
import { notFound } from "next/navigation";
23

34
import { createPageTitle } from "@/src/core/utils";
45
import AccessKeyForm from "@/src/components/access-key-form";
@@ -20,5 +21,9 @@ export default async function AccessKeyCreatePage({ params }: Props) {
2021

2122
const accessKey = await getAccessKeyById(serverId, parseInt(params.accessKeyId));
2223

24+
if (!accessKey) {
25+
notFound();
26+
}
27+
2328
return <AccessKeyForm accessKeyData={accessKey} serverId={serverId} />;
2429
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Metadata } from "next";
2+
3+
import { createPageTitle } from "@/src/core/utils";
4+
import DynamicAccessKeyStatsForm from "@/src/components/dynamic-access-key-stats-form";
5+
6+
export const metadata: Metadata = {
7+
title: createPageTitle("Dynamic Access Key Stats")
8+
};
9+
10+
export default async function DynamicAccessKeyStatsPage() {
11+
return <DynamicAccessKeyStatsForm />;
12+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Chip } from "@heroui/react";
2+
import React from "react";
3+
import { DynamicAccessKey } from "@prisma/client";
4+
5+
import { InfinityIcon } from "@/src/components/icons";
6+
import { convertDataLimitToUnit, formatBytes } from "@/src/core/utils";
7+
import { DataLimitUnit, DynamicAccessKeyStats } from "@/src/core/definitions";
8+
9+
interface Props {
10+
item: DynamicAccessKey | DynamicAccessKeyStats;
11+
}
12+
13+
export default function DynamicAccessKeyDataUsageChip({ item }: Props) {
14+
const bytesPerMB = 1024 * 1024;
15+
const dataLimitInBytes = Number(item.dataLimit) * bytesPerMB;
16+
const isExceeded = item.dataLimit && item.dataUsage >= dataLimitInBytes;
17+
18+
return (
19+
<Chip color={isExceeded ? "danger" : "default"} radius="sm" size="sm" variant="flat">
20+
<div className="flex gap-2 items-center">
21+
<span>{formatBytes(Number(item.dataUsage))}</span>
22+
23+
{item.isSelfManaged && (
24+
<>
25+
<span className="text-default-500">of</span>
26+
{item.dataLimit ? (
27+
<span>{formatBytes(convertDataLimitToUnit(Number(item.dataLimit), DataLimitUnit.MB))}</span>
28+
) : (
29+
<InfinityIcon size={20} />
30+
)}
31+
</>
32+
)}
33+
</div>
34+
</Chip>
35+
);
36+
}

src/components/dynamic-access-key-form.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default function DynamicAccessKeyForm({ dynamicAccessKey, tags, servers }
6565
? JSON.parse(dynamicAccessKey.serverPoolValue)
6666
: null,
6767
validityPeriod: dynamicAccessKey.validityPeriod ? dynamicAccessKey.validityPeriod : null,
68-
dataLimit: Number(dynamicAccessKey.dataLimit)
68+
dataLimit: dynamicAccessKey.dataLimit ? Number(dynamicAccessKey.dataLimit) : undefined
6969
}
7070
: {
7171
name: "",
@@ -112,13 +112,13 @@ export default function DynamicAccessKeyForm({ dynamicAccessKey, tags, servers }
112112
if (dynamicAccessKey) {
113113
const updateData = data as EditDynamicAccessKeyRequest;
114114

115-
updateData.id = dynamicAccessKey.id;
116-
await updateDynamicAccessKey(updateData);
117-
118-
if (dynamicAccessKey.isSelfManaged && !updateData.isSelfManaged) {
115+
if (dynamicAccessKey.isSelfManaged) {
119116
await removeSelfManagedDynamicAccessKeyAccessKeys(dynamicAccessKey.id);
120117
}
121118

119+
updateData.id = dynamicAccessKey.id;
120+
await updateDynamicAccessKey(updateData);
121+
122122
if (!dynamicAccessKey.isSelfManaged && updateData.isSelfManaged) {
123123
await syncDynamicAccessKeyAccessKeys(dynamicAccessKey.id, []);
124124
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"use client";
2+
3+
import { useForm } from "react-hook-form";
4+
import { Button, ButtonGroup, Card, CardBody, CardHeader, Input, useDisclosure } from "@heroui/react";
5+
import React, { useState } from "react";
6+
import slugify from "slugify";
7+
8+
import { StatsIcon } from "@/src/components/icons";
9+
import { DynamicAccessKeyStats } from "@/src/core/definitions";
10+
import DynamicAccessKeyValidityChip from "@/src/components/dynamic-access-key-validity-chip";
11+
import DynamicAccessKeyDataUsageChip from "@/src/components/dynamic-access-key-data-usage-chip";
12+
import NoResult from "@/src/components/no-result";
13+
import { getDynamicAccessKeyStatsByPath } from "@/src/core/actions/dynamic-access-key";
14+
import MessageModal from "@/src/components/modals/message-modal";
15+
16+
function extractPath(url: string): string | null {
17+
try {
18+
const parsedUrl = new URL(url);
19+
const path = parsedUrl.pathname.replace(/^\/+|\/+$/g, "");
20+
const pathSegments = path.split("/").filter((segment) => segment.length > 0);
21+
22+
const dakIndex = pathSegments.indexOf("dak");
23+
24+
if (dakIndex === -1 || dakIndex === pathSegments.length - 1) {
25+
return null;
26+
}
27+
28+
return slugify(pathSegments[dakIndex + 1]);
29+
} catch (error) {
30+
return null;
31+
}
32+
}
33+
34+
interface FormProps {
35+
accessKey: string;
36+
}
37+
38+
// TODO: add captcha
39+
export default function DynamicAccessKeyStatsForm() {
40+
const form = useForm<FormProps>();
41+
const [stats, setStats] = useState<DynamicAccessKeyStats | null>();
42+
const errorModalDisclosure = useDisclosure();
43+
const [error, setError] = useState<string>();
44+
45+
const actualSubmit = async (data: FormProps) => {
46+
try {
47+
const path = extractPath(data.accessKey);
48+
49+
if (path) {
50+
setStats(await getDynamicAccessKeyStatsByPath(path));
51+
} else {
52+
setStats(null);
53+
}
54+
} catch (error) {
55+
setError((error as object).toString());
56+
errorModalDisclosure.onOpen();
57+
}
58+
};
59+
60+
const handleReset = () => {
61+
form.reset();
62+
setStats(undefined);
63+
};
64+
65+
return (
66+
<div className="grid gap-8">
67+
<MessageModal
68+
body={
69+
<div className="grid gap-2">
70+
<span>Something went wrong.</span>
71+
<pre className="text-sm break-words whitespace-pre-wrap text-danger-500">{error}</pre>
72+
</div>
73+
}
74+
disclosure={errorModalDisclosure}
75+
title="Error!"
76+
/>
77+
78+
<form
79+
className="flex flex-col items-center justify-center gap-2"
80+
onSubmit={form.handleSubmit(actualSubmit)}
81+
>
82+
<div className="mb-8 text-foreground grid gap-2 place-items-center px-4">
83+
<StatsIcon size={86} />
84+
<div className="text-center">Enter your access key to view your usage statistics</div>
85+
</div>
86+
87+
<Input
88+
className="w-[320px]"
89+
color="primary"
90+
errorMessage={form.formState.errors.accessKey?.message}
91+
isInvalid={!!form.formState.errors.accessKey}
92+
label="Access Key"
93+
variant="underlined"
94+
{...form.register("accessKey", {
95+
required: true,
96+
maxLength: 512
97+
})}
98+
/>
99+
100+
<ButtonGroup className="w-[320px]" fullWidth={true}>
101+
<Button color="primary" isLoading={form.formState.isSubmitting} type="submit" variant="shadow">
102+
Check
103+
</Button>
104+
{stats !== undefined && (
105+
<Button variant="shadow" onPress={handleReset}>
106+
Reset
107+
</Button>
108+
)}
109+
</ButtonGroup>
110+
</form>
111+
112+
{stats !== undefined &&
113+
(stats ? (
114+
<Card className="w-[320px] mx-auto">
115+
<CardHeader>
116+
<div className="grid gap-1">
117+
<span className="max-w-[320px] truncate">{stats.name}</span>
118+
<span className="max-w-[320px] truncate text-foreground-400 text-sm">{stats.path}</span>
119+
</div>
120+
</CardHeader>
121+
<CardBody className="text-sm grid gap-2">
122+
<div className="flex gap-1 justify-between items-center">
123+
<span>Data usage</span>
124+
<DynamicAccessKeyDataUsageChip item={stats} />
125+
</div>
126+
127+
<div className="flex gap-1 justify-between items-center">
128+
<span>Validity</span>
129+
<DynamicAccessKeyValidityChip dak={stats} />
130+
</div>
131+
</CardBody>
132+
</Card>
133+
) : (
134+
<NoResult />
135+
))}
136+
</div>
137+
);
138+
}

src/components/dynamic-access-key-validity-chip.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { DynamicAccessKey } from "@prisma/client";
55
import { InfinityIcon } from "@/src/components/icons";
66
import { formatAsDuration, getDakExpiryDateBasedOnValidityPeriod } from "@/src/core/utils";
77
import AccessKeyValidityChip from "@/src/components/access-key-validity-chip";
8+
import { DynamicAccessKeyStats } from "@/src/core/definitions";
89

910
interface Props {
10-
dak: DynamicAccessKey;
11+
dak: DynamicAccessKey | DynamicAccessKeyStats;
1112
}
1213

1314
export default function DynamicAccessKeyValidityChip({ dak }: Props) {

src/components/dynamic-access-keys-list.tsx

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { DynamicAccessKey } from "@prisma/client";
1919
import { Link } from "@heroui/link";
2020

2121
import ConfirmModal from "@/src/components/modals/confirm-modal";
22-
import { InfinityIcon, InfoIcon, PlusIcon, SelfManagedKeyIcon } from "@/src/components/icons";
23-
import { DataLimitUnit, DynamicAccessKeyWithAccessKeysCount } from "@/src/core/definitions";
22+
import { InfoIcon, PlusIcon, SelfManagedKeyIcon } from "@/src/components/icons";
23+
import { DynamicAccessKeyWithAccessKeysCount } from "@/src/core/definitions";
2424
import {
2525
getDynamicAccessKeys,
2626
getDynamicAccessKeysCount,
@@ -31,7 +31,7 @@ import DynamicAccessKeyModal from "@/src/components/modals/dynamic-access-key-mo
3131
import { app, PAGE_SIZE } from "@/src/core/config";
3232
import DynamicAccessKeyValidityChip from "@/src/components/dynamic-access-key-validity-chip";
3333
import DynamicAccessKeysSslWarning from "@/src/components/dynamic-access-keys-ssl-warning";
34-
import { convertDataLimitToUnit, formatBytes } from "@/src/core/utils";
34+
import DynamicAccessKeyDataUsageChip from "@/src/components/dynamic-access-key-data-usage-chip";
3535

3636
interface SearchFormProps {
3737
term: string;
@@ -109,33 +109,6 @@ export default function DynamicAccessKeysList() {
109109
updateData();
110110
}, [page]);
111111

112-
const renderDataUsageChip = (item: DynamicAccessKey) => {
113-
const bytesPerMB = 1024 * 1024;
114-
const dataLimitInBytes = Number(item.dataLimit) * bytesPerMB;
115-
const isExceeded = item.dataLimit && item.dataUsage >= dataLimitInBytes;
116-
117-
return (
118-
<Chip color={isExceeded ? "danger" : "default"} radius="sm" size="sm" variant="flat">
119-
<div className="flex gap-2 items-center">
120-
<span>{formatBytes(Number(item.dataUsage))}</span>
121-
122-
{item.isSelfManaged && (
123-
<>
124-
<span className="text-default-500">of</span>
125-
{item.dataLimit ? (
126-
<span>
127-
{formatBytes(convertDataLimitToUnit(Number(item.dataLimit), DataLimitUnit.MB))}
128-
</span>
129-
) : (
130-
<InfinityIcon size={20} />
131-
)}
132-
</>
133-
)}
134-
</div>
135-
</Chip>
136-
);
137-
};
138-
139112
return (
140113
<>
141114
<DynamicAccessKeyModal disclosure={dynamicAccessKeyModalDisclosure} value={getCurrentAccessKeyUrl()} />
@@ -239,7 +212,7 @@ export default function DynamicAccessKeysList() {
239212

240213
<div className="flex gap-1 justify-between items-center">
241214
<span>Data usage</span>
242-
{renderDataUsageChip(item)}
215+
<DynamicAccessKeyDataUsageChip item={item} />
243216
</div>
244217

245218
<div className="flex gap-1 justify-between items-center">
@@ -287,7 +260,7 @@ export default function DynamicAccessKeysList() {
287260
dynamicAccessKeyModalDisclosure.onOpen();
288261
}}
289262
>
290-
QR Code
263+
Share
291264
</Button>
292265

293266
{item.isSelfManaged ? (

src/components/icons.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ export const SelfManagedKeyIcon: React.FC<IconSvgProps> = ({ size = 24, width, h
4242
);
4343
};
4444

45+
export const StatsIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
46+
return (
47+
<svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>
48+
<path
49+
d="M13.272 2.004a.75.75 0 0 0-.772.75v7.996c0 .414.336.75.75.75h7.996a.75.75 0 0 0 .75-.772a9 9 0 0 0-8.724-8.724m-2.516 2.207a.75.75 0 0 1 .244.554v5.985A2.25 2.25 0 0 0 13.25 13h5.965a.75.75 0 0 1 .747.819a9.001 9.001 0 1 1-9.78-9.801a.75.75 0 0 1 .574.193"
50+
fill="currentColor"
51+
/>
52+
</svg>
53+
);
54+
};
55+
4556
export const InfoIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
4657
return (
4758
<svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>

0 commit comments

Comments
 (0)