Skip to content

Commit f6377ed

Browse files
committed
New API key design
1 parent d63a823 commit f6377ed

File tree

7 files changed

+958
-290
lines changed

7 files changed

+958
-290
lines changed

apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/tags/page-client.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import useTags from "@/lib/swr/use-tags";
44
import useTagsCount from "@/lib/swr/use-tags-count";
5-
import useWorkspace from "@/lib/swr/use-workspace";
65
import { TAGS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/tags";
76
import { useAddEditTagModal } from "@/ui/modals/add-edit-tag-modal";
87
import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state";
@@ -28,7 +27,6 @@ export const TagsListContext = createContext<{
2827

2928
export default function WorkspaceTagsClient() {
3029
const { searchParams, queryParams } = useRouterStuff();
31-
const { id: workspaceId } = useWorkspace();
3230

3331
const { AddEditTagModal, AddTagButton } = useAddEditTagModal();
3432

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"use client";
2+
3+
import { scopesToName } from "@/lib/api/tokens/scopes";
4+
import useWorkspace from "@/lib/swr/use-workspace";
5+
import { TokenProps } from "@/lib/types";
6+
import { useAddEditTokenModal } from "@/ui/modals/add-edit-token-modal";
7+
import { useDeleteTokenModal } from "@/ui/modals/delete-token-modal";
8+
import { useTokenCreatedModal } from "@/ui/modals/token-created-modal";
9+
import EmptyState from "@/ui/shared/empty-state";
10+
import { Delete } from "@/ui/shared/icons";
11+
import {
12+
Avatar,
13+
Badge,
14+
Button,
15+
LoadingSpinner,
16+
Popover,
17+
TokenAvatar,
18+
Tooltip,
19+
} from "@dub/ui";
20+
import { Key } from "@dub/ui/icons";
21+
import { fetcher, timeAgo } from "@dub/utils";
22+
import { Edit3, MoreVertical } from "lucide-react";
23+
import { useState } from "react";
24+
import useSWR from "swr";
25+
26+
export default function TokensPageClient() {
27+
const { id: workspaceId } = useWorkspace();
28+
const { data: tokens, isLoading } = useSWR<TokenProps[]>(
29+
`/api/tokens?workspaceId=${workspaceId}`,
30+
fetcher,
31+
);
32+
33+
const [createdToken, setCreatedToken] = useState<string | null>(null);
34+
const { TokenCreatedModal, setShowTokenCreatedModal } = useTokenCreatedModal({
35+
token: createdToken || "",
36+
});
37+
38+
const onTokenCreated = (token: string) => {
39+
setCreatedToken(token);
40+
setShowTokenCreatedModal(true);
41+
};
42+
43+
const { AddEditTokenModal, AddTokenButton } = useAddEditTokenModal({
44+
onTokenCreated,
45+
});
46+
47+
return (
48+
<>
49+
<TokenCreatedModal />
50+
<AddEditTokenModal />
51+
<div className="rounded-lg border border-gray-200 bg-white">
52+
<div className="flex flex-col items-center justify-between gap-4 space-y-3 border-b border-gray-200 p-5 sm:flex-row sm:space-y-0 sm:p-10">
53+
<div className="flex max-w-screen-sm flex-col space-y-3">
54+
<h2 className="text-xl font-medium">Secret keys</h2>
55+
<p className="text-sm text-gray-500">
56+
These API keys allow other apps to access your workspace. Use it
57+
with caution – do not share your API key with others, or expose it
58+
in the browser or other client-side code.{" "}
59+
<a
60+
href="https://dub.co/docs/api-reference/tokens"
61+
target="_blank"
62+
className="font-medium underline underline-offset-4 hover:text-black"
63+
>
64+
Learn more
65+
</a>
66+
</p>
67+
</div>
68+
<AddTokenButton />
69+
</div>
70+
{isLoading || !tokens ? (
71+
<div className="flex flex-col items-center justify-center space-y-4 py-20">
72+
<LoadingSpinner className="h-6 w-6 text-gray-500" />
73+
<p className="text-sm text-gray-500">Fetching API keys...</p>
74+
</div>
75+
) : tokens.length > 0 ? (
76+
<div>
77+
<div className="hidden grid-cols-5 border-b border-gray-200 px-5 py-2 text-sm font-medium text-gray-500 sm:grid sm:px-10">
78+
<div className="col-span-3">Name</div>
79+
<div>Key</div>
80+
<div className="text-center">Last used</div>
81+
</div>
82+
<div className="divide-y divide-gray-200">
83+
{tokens.map((token) => (
84+
<TokenRow key={token.id} {...token} />
85+
))}
86+
</div>
87+
</div>
88+
) : (
89+
<div className="flex flex-col items-center justify-center gap-y-4 py-20">
90+
<EmptyState
91+
icon={Key}
92+
title="No API keys found for this workspace"
93+
/>
94+
<AddTokenButton />
95+
</div>
96+
)}
97+
</div>
98+
</>
99+
);
100+
}
101+
102+
const TokenRow = (token: TokenProps) => {
103+
const [openPopover, setOpenPopover] = useState(false);
104+
105+
const { setShowAddEditTokenModal, AddEditTokenModal } = useAddEditTokenModal({
106+
token: {
107+
id: token.id,
108+
name: token.name,
109+
isMachine: token.user.isMachine,
110+
scopes: mapScopesToResource(token.scopes),
111+
},
112+
});
113+
114+
const { DeleteTokenModal, setShowDeleteTokenModal } = useDeleteTokenModal({
115+
token,
116+
});
117+
118+
return (
119+
<>
120+
<AddEditTokenModal />
121+
<DeleteTokenModal />
122+
<div className="relative flex flex-col gap-2 px-5 py-3 sm:grid sm:grid-cols-5 sm:items-center sm:px-10">
123+
<div className="col-span-3 flex flex-col gap-3 sm:flex-row sm:items-center">
124+
<TokenAvatar id={token.id} />
125+
<div className="flex flex-col gap-y-px">
126+
<div className="flex items-center gap-x-2">
127+
<p className="font-semibold text-gray-700">{token.name}</p>
128+
<Badge variant="neutral">{scopesToName(token.scopes).name}</Badge>
129+
</div>
130+
<div className="flex items-center gap-x-1">
131+
<Tooltip
132+
content={
133+
<div className="w-full max-w-xs p-4">
134+
<Avatar user={token.user} className="h-10 w-10" />
135+
<div className="mt-2 flex items-center gap-x-1.5">
136+
<p className="text-sm font-semibold text-gray-700">
137+
{token.user.name || "Anonymous User"}
138+
</p>
139+
</div>
140+
<p className="mt-1 text-xs text-gray-500">
141+
Created{" "}
142+
{new Date(token.createdAt).toLocaleDateString("en-us", {
143+
month: "short",
144+
day: "numeric",
145+
year: "numeric",
146+
})}
147+
</p>
148+
</div>
149+
}
150+
>
151+
<div>
152+
<Avatar user={token.user} className="h-4 w-4" />
153+
</div>
154+
</Tooltip>
155+
<p></p>
156+
<p className="text-sm text-gray-500" suppressHydrationWarning>
157+
Created {timeAgo(token.createdAt)}
158+
</p>
159+
</div>
160+
</div>
161+
</div>
162+
<div className="font-mono text-sm">{token.partialKey}</div>
163+
<div
164+
className="text-sm text-gray-500 sm:text-center"
165+
suppressHydrationWarning
166+
>
167+
{timeAgo(token.lastUsed)}
168+
</div>
169+
<Popover
170+
content={
171+
<div className="w-full sm:w-48">
172+
<div className="grid gap-px p-2">
173+
<Button
174+
text="Edit API Key"
175+
variant="outline"
176+
icon={<Edit3 className="h-4 w-4" />}
177+
className="h-9 justify-start px-2 font-medium"
178+
onClick={() => {
179+
setOpenPopover(false);
180+
setShowAddEditTokenModal(true);
181+
}}
182+
/>
183+
<Button
184+
text="Delete API Key"
185+
variant="danger-outline"
186+
icon={<Delete className="h-4 w-4" />}
187+
className="h-9 justify-start px-2 font-medium"
188+
onClick={() => {
189+
setOpenPopover(false);
190+
setShowDeleteTokenModal(true);
191+
}}
192+
/>
193+
</div>
194+
</div>
195+
}
196+
align="end"
197+
openPopover={openPopover}
198+
setOpenPopover={setOpenPopover}
199+
>
200+
<button
201+
onClick={() => {
202+
setOpenPopover(!openPopover);
203+
}}
204+
className="absolute right-4 rounded-md px-1 py-2 transition-all duration-75 hover:bg-gray-100 active:bg-gray-200"
205+
>
206+
<MoreVertical className="h-5 w-5 text-gray-500" />
207+
</button>
208+
</Popover>
209+
</div>
210+
</>
211+
);
212+
};
213+
214+
const mapScopesToResource = (scopes: string[]) => {
215+
const result = scopes.map((scope) => {
216+
const [resource] = scope.split(".");
217+
218+
return {
219+
[resource]: scope,
220+
};
221+
});
222+
223+
return Object.assign({}, ...result);
224+
};

0 commit comments

Comments
 (0)