@ploykit/ui 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -2
- package/dist/components/ConfirmDialog.d.ts +13 -0
- package/dist/components/ConfirmDialog.js +29 -0
- package/dist/components/CronInput.d.ts +15 -0
- package/dist/components/CronInput.js +112 -0
- package/dist/components/DataTable.d.ts +22 -0
- package/dist/components/DataTable.js +13 -0
- package/dist/components/ErrorBoundary.d.ts +16 -0
- package/dist/components/ErrorBoundary.js +24 -0
- package/dist/components/FormField.d.ts +10 -0
- package/dist/components/FormField.js +22 -0
- package/dist/components/ImpersonationBanner.d.ts +3 -0
- package/dist/components/ImpersonationBanner.js +10 -0
- package/dist/components/NotificationBell.d.ts +3 -0
- package/dist/components/NotificationBell.js +47 -0
- package/dist/components/OnboardingChecklist.d.ts +12 -0
- package/dist/components/OnboardingChecklist.js +43 -0
- package/dist/components/Page.d.ts +17 -0
- package/dist/components/Page.js +16 -0
- package/dist/components/SecretModal.d.ts +16 -0
- package/dist/components/SecretModal.js +45 -0
- package/dist/components/SettingsCard.d.ts +10 -0
- package/dist/components/SettingsCard.js +6 -0
- package/dist/components/SiteBanner.d.ts +6 -0
- package/dist/components/SiteBanner.js +11 -0
- package/dist/components/SiteBannerLayer.d.ts +4 -0
- package/dist/components/SiteBannerLayer.js +9 -0
- package/dist/components/UserMenu.d.ts +13 -0
- package/dist/components/UserMenu.js +30 -0
- package/dist/components/WorkspaceSwitcher.d.ts +1 -0
- package/dist/components/WorkspaceSwitcher.js +53 -0
- package/dist/components/toast.d.ts +7 -0
- package/dist/components/toast.js +11 -0
- package/dist/components/ui/Badge.d.ts +13 -0
- package/dist/components/ui/Badge.js +21 -0
- package/dist/components/ui/Button.d.ts +10 -0
- package/dist/components/ui/Button.js +33 -0
- package/dist/components/ui/Input.d.ts +3 -0
- package/dist/components/ui/Input.js +10 -0
- package/dist/components/ui/alert-dialog.d.ts +18 -0
- package/dist/components/ui/alert-dialog.js +41 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +22 -0
- package/dist/components/ui/breadcrumb.d.ts +10 -0
- package/dist/components/ui/breadcrumb.js +36 -0
- package/dist/components/ui/card.d.ts +13 -0
- package/dist/components/ui/card.js +24 -0
- package/dist/components/ui/checkbox.d.ts +3 -0
- package/dist/components/ui/checkbox.js +8 -0
- package/dist/components/ui/dialog.d.ts +15 -0
- package/dist/components/ui/dialog.js +26 -0
- package/dist/components/ui/empty.d.ts +13 -0
- package/dist/components/ui/empty.js +36 -0
- package/dist/components/ui/field.d.ts +24 -0
- package/dist/components/ui/field.js +67 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/label.js +6 -0
- package/dist/components/ui/menu.d.ts +29 -0
- package/dist/components/ui/menu.js +50 -0
- package/dist/components/ui/popover.d.ts +9 -0
- package/dist/components/ui/popover.js +22 -0
- package/dist/components/ui/progress.d.ts +7 -0
- package/dist/components/ui/progress.js +19 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +33 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/separator.js +7 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +36 -0
- package/dist/components/ui/sidebar.d.ts +63 -0
- package/dist/components/ui/sidebar.js +265 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.js +6 -0
- package/dist/components/ui/switch.d.ts +5 -0
- package/dist/components/ui/switch.js +7 -0
- package/dist/components/ui/tabs.d.ts +10 -0
- package/dist/components/ui/tabs.js +28 -0
- package/dist/components/ui/tooltip.d.ts +6 -0
- package/dist/components/ui/tooltip.js +16 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/hooks/useApi.d.ts +10 -0
- package/dist/hooks/useApi.js +11 -0
- package/dist/hooks/useAudit.d.ts +39 -0
- package/dist/hooks/useAudit.js +55 -0
- package/dist/hooks/useAuth.d.ts +1 -0
- package/dist/hooks/useAuth.js +4 -0
- package/dist/hooks/useBilling.d.ts +44 -0
- package/dist/hooks/useBilling.js +64 -0
- package/dist/hooks/useLogin.d.ts +10 -0
- package/dist/hooks/useLogin.js +39 -0
- package/dist/hooks/useMembers.d.ts +125 -0
- package/dist/hooks/useMembers.js +127 -0
- package/dist/hooks/useNotificationPrefs.d.ts +20 -0
- package/dist/hooks/useNotificationPrefs.js +42 -0
- package/dist/hooks/usePasswordReset.d.ts +5 -0
- package/dist/hooks/usePasswordReset.js +12 -0
- package/dist/hooks/useSEO.d.ts +6 -0
- package/dist/hooks/useSEO.js +14 -0
- package/dist/hooks/useSessions.d.ts +20 -0
- package/dist/hooks/useSessions.js +38 -0
- package/dist/hooks/useSiteConfig.d.ts +12 -0
- package/dist/hooks/useSiteConfig.js +11 -0
- package/dist/hooks/useTokens.d.ts +36 -0
- package/dist/hooks/useTokens.js +35 -0
- package/dist/hooks/useUsage.d.ts +20 -0
- package/dist/hooks/useUsage.js +21 -0
- package/dist/hooks/useWorkspace.d.ts +26 -0
- package/dist/hooks/useWorkspace.js +32 -0
- package/dist/hooks/useZodForm.d.ts +6 -0
- package/dist/hooks/useZodForm.js +6 -0
- package/dist/hooks/ws.d.ts +4 -0
- package/dist/hooks/ws.js +21 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +94 -0
- package/dist/layouts/AppShell.d.ts +20 -0
- package/dist/layouts/AppShell.js +21 -0
- package/dist/layouts/AuthShell.d.ts +7 -0
- package/dist/layouts/AuthShell.js +4 -0
- package/dist/layouts/SettingsShell.d.ts +19 -0
- package/dist/layouts/SettingsShell.js +21 -0
- package/dist/lib/api-error.d.ts +1 -0
- package/dist/lib/api-error.js +74 -0
- package/dist/lib/perm.d.ts +2 -0
- package/dist/lib/perm.js +7 -0
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.js +84 -0
- package/dist/lib/ws-query-bridge.d.ts +7 -0
- package/dist/lib/ws-query-bridge.js +35 -0
- package/dist/pages/LandingPage.d.ts +22 -0
- package/dist/pages/LandingPage.js +5 -0
- package/dist/pages/LoginPage.d.ts +15 -0
- package/dist/pages/LoginPage.js +87 -0
- package/dist/pages/account/NotificationPreferencesPage.d.ts +13 -0
- package/dist/pages/account/NotificationPreferencesPage.js +41 -0
- package/dist/pages/account/ProfileSettingsPage.d.ts +4 -0
- package/dist/pages/account/ProfileSettingsPage.js +96 -0
- package/dist/pages/account/SecuritySettingsPage.d.ts +1 -0
- package/dist/pages/account/SecuritySettingsPage.js +134 -0
- package/dist/pages/account/TokensPage.d.ts +1 -0
- package/dist/pages/account/TokensPage.js +117 -0
- package/dist/pages/admin/AdminSettingsPage.d.ts +1 -0
- package/dist/pages/admin/AdminSettingsPage.js +210 -0
- package/dist/pages/admin/AdminShell.d.ts +7 -0
- package/dist/pages/admin/AdminShell.js +79 -0
- package/dist/pages/admin/AuditPage.d.ts +1 -0
- package/dist/pages/admin/AuditPage.js +164 -0
- package/dist/pages/admin/OrdersPage.d.ts +1 -0
- package/dist/pages/admin/OrdersPage.js +236 -0
- package/dist/pages/admin/OverviewPage.d.ts +1 -0
- package/dist/pages/admin/OverviewPage.js +200 -0
- package/dist/pages/admin/SSOPage.d.ts +1 -0
- package/dist/pages/admin/SSOPage.js +53 -0
- package/dist/pages/admin/UserDetailPage.d.ts +1 -0
- package/dist/pages/admin/UserDetailPage.js +266 -0
- package/dist/pages/admin/UsersPage.d.ts +4 -0
- package/dist/pages/admin/UsersPage.js +201 -0
- package/dist/pages/admin/WebhookDeliveriesPage.d.ts +1 -0
- package/dist/pages/admin/WebhookDeliveriesPage.js +128 -0
- package/dist/pages/admin/WorkspaceDetailPage.d.ts +1 -0
- package/dist/pages/admin/WorkspaceDetailPage.js +202 -0
- package/dist/pages/admin/WorkspacesPage.d.ts +1 -0
- package/dist/pages/admin/WorkspacesPage.js +133 -0
- package/dist/pages/admin/shared.d.ts +8 -0
- package/dist/pages/admin/shared.js +23 -0
- package/dist/pages/auth/ForgotPasswordPage.d.ts +6 -0
- package/dist/pages/auth/ForgotPasswordPage.js +32 -0
- package/dist/pages/auth/InviteAcceptPage.d.ts +5 -0
- package/dist/pages/auth/InviteAcceptPage.js +40 -0
- package/dist/pages/auth/RegisterPage.d.ts +15 -0
- package/dist/pages/auth/RegisterPage.js +80 -0
- package/dist/pages/auth/ResetPasswordPage.d.ts +4 -0
- package/dist/pages/auth/ResetPasswordPage.js +71 -0
- package/dist/pages/auth/VerifyEmailPage.d.ts +1 -0
- package/dist/pages/auth/VerifyEmailPage.js +45 -0
- package/dist/pages/workspace/BillingPage.d.ts +4 -0
- package/dist/pages/workspace/BillingPage.js +183 -0
- package/dist/pages/workspace/BillingSuccessPage.d.ts +4 -0
- package/dist/pages/workspace/BillingSuccessPage.js +72 -0
- package/dist/pages/workspace/MembersPage.d.ts +3 -0
- package/dist/pages/workspace/MembersPage.js +262 -0
- package/dist/pages/workspace/SchedulesPage.d.ts +4 -0
- package/dist/pages/workspace/SchedulesPage.js +177 -0
- package/dist/pages/workspace/UsagePage.d.ts +15 -0
- package/dist/pages/workspace/UsagePage.js +74 -0
- package/dist/pages/workspace/WebhooksPage.d.ts +3 -0
- package/dist/pages/workspace/WebhooksPage.js +242 -0
- package/dist/pages/workspace/WorkspaceAuditPage.d.ts +3 -0
- package/dist/pages/workspace/WorkspaceAuditPage.js +109 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.d.ts +4 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.js +101 -0
- package/dist/provider/PloykitProvider.d.ts +50 -0
- package/dist/provider/PloykitProvider.js +240 -0
- package/dist/realtime.d.ts +3 -0
- package/dist/realtime.js +11 -0
- package/package.json +61 -4
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { MoreHorizontal, Plus } from 'lucide-react';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { PageHeader, PageLoading, PageError, PageEmpty, } from '../../components/Page';
|
|
7
|
+
import { DataTable } from '../../components/DataTable';
|
|
8
|
+
import { FormField } from '../../components/FormField';
|
|
9
|
+
import { SecretModal } from '../../components/SecretModal';
|
|
10
|
+
import { useConfirm } from '../../components/ConfirmDialog';
|
|
11
|
+
import { toast } from '../../components/toast';
|
|
12
|
+
import { Button } from '../../components/ui/Button';
|
|
13
|
+
import { Input } from '../../components/ui/Input';
|
|
14
|
+
import { StatusBadge } from '../../components/ui/Badge';
|
|
15
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
|
|
16
|
+
import { Switch } from '../../components/ui/switch';
|
|
17
|
+
import { Checkbox } from '../../components/ui/checkbox';
|
|
18
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '../../components/ui/dialog';
|
|
19
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../../components/ui/menu';
|
|
20
|
+
import { useApi } from '../../hooks/useApi';
|
|
21
|
+
import { useWorkspace } from '../../hooks/useWorkspace';
|
|
22
|
+
import { useZodForm } from '../../hooks/useZodForm';
|
|
23
|
+
import { queryKeys } from '../../provider/PloykitProvider';
|
|
24
|
+
import { formatDateTime } from '../../lib/utils';
|
|
25
|
+
import { apiErrorMessage } from '../../lib/api-error';
|
|
26
|
+
import { roleSatisfies } from '../../lib/perm';
|
|
27
|
+
const createSchema = z.object({
|
|
28
|
+
url: z
|
|
29
|
+
.string()
|
|
30
|
+
.url('请输入合法的回调 URL(如 https://example.com/webhook)')
|
|
31
|
+
.refine((v) => v.startsWith('https://'), '回调地址必须使用 https(明文 http 会被服务端拒绝)'),
|
|
32
|
+
description: z.string(),
|
|
33
|
+
events: z.array(z.string()).min(1, '至少订阅一个事件类型'),
|
|
34
|
+
});
|
|
35
|
+
export function WebhooksPage(_props = {}) {
|
|
36
|
+
const { current } = useWorkspace();
|
|
37
|
+
const api = useApi();
|
|
38
|
+
const confirm = useConfirm();
|
|
39
|
+
const wsId = current?.id ?? '';
|
|
40
|
+
const base = `/api/workspaces/${wsId}/webhooks`;
|
|
41
|
+
const canManage = roleSatisfies(current?.role, 'admin');
|
|
42
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
43
|
+
const [secret, setSecret] = useState(null);
|
|
44
|
+
const [secretTitle, setSecretTitle] = useState(undefined);
|
|
45
|
+
const subsQ = useQuery({
|
|
46
|
+
queryKey: [...queryKeys.webhooks(wsId), 'subscriptions'],
|
|
47
|
+
queryFn: () => api.get(`${base}/subscriptions`),
|
|
48
|
+
enabled: !!wsId,
|
|
49
|
+
});
|
|
50
|
+
const deliveriesQ = useQuery({
|
|
51
|
+
queryKey: [...queryKeys.webhooks(wsId), 'deliveries'],
|
|
52
|
+
queryFn: () => api.get(`${base}/deliveries?limit=50`),
|
|
53
|
+
enabled: !!wsId,
|
|
54
|
+
refetchInterval: 15_000,
|
|
55
|
+
});
|
|
56
|
+
const eventsQ = useQuery({
|
|
57
|
+
queryKey: [...queryKeys.webhooks(wsId), 'events'],
|
|
58
|
+
queryFn: () => api.get(`${base}/events`),
|
|
59
|
+
enabled: !!wsId,
|
|
60
|
+
});
|
|
61
|
+
const qc = useQueryClient();
|
|
62
|
+
const invalidate = () => qc.invalidateQueries({ queryKey: queryKeys.webhooks(wsId) });
|
|
63
|
+
const onError = (e, fallback) => toast.error(apiErrorMessage(e, fallback));
|
|
64
|
+
const toggle = useMutation({
|
|
65
|
+
mutationFn: (s) => api.patch(`${base}/subscriptions/${s.id}`, { is_active: !s.is_active }),
|
|
66
|
+
onSuccess: async () => { toast.success('状态已更新'); await invalidate(); },
|
|
67
|
+
onError: (e) => onError(e, '操作失败'),
|
|
68
|
+
});
|
|
69
|
+
const ping = useMutation({
|
|
70
|
+
mutationFn: (id) => api.post(`${base}/subscriptions/${id}/ping`),
|
|
71
|
+
onSuccess: async () => { toast.success('Ping 已入队,结果见投递记录'); await invalidate(); },
|
|
72
|
+
onError: (e) => onError(e, 'Ping 失败'),
|
|
73
|
+
});
|
|
74
|
+
const remove = useMutation({
|
|
75
|
+
mutationFn: (id) => api.delete(`${base}/subscriptions/${id}`),
|
|
76
|
+
onSuccess: async () => { toast.success('订阅已删除'); await invalidate(); },
|
|
77
|
+
onError: (e) => onError(e, '删除失败'),
|
|
78
|
+
});
|
|
79
|
+
const redeliver = useMutation({
|
|
80
|
+
mutationFn: (id) => api.post(`${base}/deliveries/${id}/redeliver`),
|
|
81
|
+
onSuccess: async () => { toast.success('已重新入队投递'); await invalidate(); },
|
|
82
|
+
onError: (e) => onError(e, '重新投递失败'),
|
|
83
|
+
});
|
|
84
|
+
const create = useMutation({
|
|
85
|
+
mutationFn: (values) => api.post(`${base}/subscriptions`, values),
|
|
86
|
+
onSuccess: async (data) => {
|
|
87
|
+
toast.success('订阅已创建');
|
|
88
|
+
setSecretTitle(undefined);
|
|
89
|
+
setSecret(data.secret);
|
|
90
|
+
await invalidate();
|
|
91
|
+
},
|
|
92
|
+
onError: (e) => onError(e, '创建失败'),
|
|
93
|
+
});
|
|
94
|
+
const rotate = useMutation({
|
|
95
|
+
mutationFn: (id) => api.post(`${base}/subscriptions/${id}/rotate-secret`),
|
|
96
|
+
onSuccess: async (data) => {
|
|
97
|
+
toast.success('密钥已轮换');
|
|
98
|
+
setSecretTitle('Webhook 签名密钥轮换成功');
|
|
99
|
+
setSecret(data.secret);
|
|
100
|
+
await invalidate();
|
|
101
|
+
},
|
|
102
|
+
onError: (e) => onError(e, '轮换密钥失败'),
|
|
103
|
+
});
|
|
104
|
+
const latestBySub = useMemo(() => {
|
|
105
|
+
const map = new Map();
|
|
106
|
+
for (const d of deliveriesQ.data?.items ?? []) {
|
|
107
|
+
if (!map.has(d.subscription_id))
|
|
108
|
+
map.set(d.subscription_id, d);
|
|
109
|
+
}
|
|
110
|
+
return map;
|
|
111
|
+
}, [deliveriesQ.data]);
|
|
112
|
+
const subById = useMemo(() => {
|
|
113
|
+
const map = new Map();
|
|
114
|
+
for (const s of subsQ.data?.items ?? [])
|
|
115
|
+
map.set(s.id, s);
|
|
116
|
+
return map;
|
|
117
|
+
}, [subsQ.data]);
|
|
118
|
+
const form = useZodForm({
|
|
119
|
+
schema: createSchema,
|
|
120
|
+
defaultValues: { url: '', description: '', events: [] },
|
|
121
|
+
});
|
|
122
|
+
const selectedEvents = form.watch('events') ?? [];
|
|
123
|
+
const toggleEvent = (type, checked) => {
|
|
124
|
+
form.setValue('events', checked ? [...selectedEvents, type] : selectedEvents.filter((t) => t !== type), { shouldValidate: true });
|
|
125
|
+
};
|
|
126
|
+
const submitCreate = form.handleSubmit((values) => {
|
|
127
|
+
create.mutate(values, {
|
|
128
|
+
onSuccess: () => {
|
|
129
|
+
form.reset();
|
|
130
|
+
setCreateOpen(false);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
const closeSecret = () => {
|
|
135
|
+
setSecret(null);
|
|
136
|
+
setSecretTitle(undefined);
|
|
137
|
+
};
|
|
138
|
+
const onRedeliverLatest = async (s) => {
|
|
139
|
+
const latest = latestBySub.get(s.id);
|
|
140
|
+
if (!latest) {
|
|
141
|
+
toast.info('该订阅暂无投递记录,可先发送 Ping');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await redeliver.mutateAsync(latest.id).catch(() => { });
|
|
145
|
+
};
|
|
146
|
+
const onRemove = async (s) => {
|
|
147
|
+
if (!(await confirm({
|
|
148
|
+
title: '删除订阅',
|
|
149
|
+
description: `确定删除 ${s.url} 吗?该操作不可恢复。`,
|
|
150
|
+
confirmText: '删除', danger: true,
|
|
151
|
+
})))
|
|
152
|
+
return;
|
|
153
|
+
remove.mutate(s.id);
|
|
154
|
+
};
|
|
155
|
+
const onRotate = async (s) => {
|
|
156
|
+
if (!(await confirm({
|
|
157
|
+
title: '轮换签名密钥',
|
|
158
|
+
description: `确定轮换 ${s.url} 的签名密钥吗?新密钥仅显示一次;旧密钥在 24h 宽限期内继续有效(期间投递同时携带新/旧双签名),之后仅新密钥可用。`,
|
|
159
|
+
confirmText: '轮换',
|
|
160
|
+
})))
|
|
161
|
+
return;
|
|
162
|
+
rotate.mutate(s.id);
|
|
163
|
+
};
|
|
164
|
+
const subColumns = [
|
|
165
|
+
{
|
|
166
|
+
key: 'url', header: '回调地址',
|
|
167
|
+
render: (s) => (_jsxs("div", { className: "min-w-0 max-w-xs", children: [_jsx("p", { className: "truncate font-medium", children: s.url }), s.description && _jsx("p", { className: "truncate text-xs text-muted-foreground", children: s.description })] })),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: 'events', header: '事件类型',
|
|
171
|
+
render: (s) => (_jsx("div", { className: "flex max-w-xs flex-wrap gap-1", children: s.event_types.map((t) => (_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: t }, t))) })),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: 'is_active', header: '启用',
|
|
175
|
+
render: (s) => canManage ? (_jsx(Switch, { checked: s.is_active, disabled: toggle.isPending, onCheckedChange: () => toggle.mutate(s), "aria-label": `${s.is_active ? '暂停' : '恢复'}订阅 ${s.url}` })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: s.is_active ? '已启用' : '已暂停' })),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
key: 'latest', header: '最近投递',
|
|
179
|
+
render: (s) => {
|
|
180
|
+
const d = latestBySub.get(s.id);
|
|
181
|
+
return d ? (_jsxs("div", { className: "min-w-0", children: [_jsx(StatusBadge, { status: d.status }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: formatDateTime(d.created_at) })] })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: 'attempts', header: '尝试',
|
|
186
|
+
render: (s) => {
|
|
187
|
+
const d = latestBySub.get(s.id);
|
|
188
|
+
return _jsx("span", { className: "tabular-nums", children: d ? d.attempts : 0 });
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'actions', header: '', className: 'w-10',
|
|
193
|
+
render: (s) => canManage ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: "inline-flex size-8 items-center justify-center rounded-md hover:bg-accent", "aria-label": `订阅 ${s.url} 的操作`, children: [_jsx(MoreHorizontal, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "\u6253\u5F00\u83DC\u5355" })] }), _jsxs(DropdownMenuContent, { align: "end", className: "w-40", children: [_jsx(DropdownMenuItem, { onClick: () => ping.mutate(s.id), children: "\u53D1\u9001 Ping" }), _jsx(DropdownMenuItem, { onClick: () => onRedeliverLatest(s), children: "\u91CD\u6295\u6700\u8FD1\u4E00\u6761" }), _jsx(DropdownMenuItem, { onClick: () => onRotate(s), children: "\u8F6E\u6362\u5BC6\u94A5" }), _jsx(DropdownMenuItem, { variant: "destructive", onClick: () => onRemove(s), children: "\u5220\u9664" })] })] })) : null,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
const deliveryColumns = [
|
|
197
|
+
{
|
|
198
|
+
key: 'event', header: '事件',
|
|
199
|
+
render: (d) => (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: d.event_type }), _jsx("p", { className: "truncate font-mono text-xs text-muted-foreground", children: d.event_id })] })),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: 'subscription', header: '归属订阅',
|
|
203
|
+
render: (d) => {
|
|
204
|
+
const sub = subById.get(d.subscription_id);
|
|
205
|
+
if (sub) {
|
|
206
|
+
return (_jsx("span", { className: "block max-w-xs truncate font-mono text-xs", title: sub.url, children: sub.url }));
|
|
207
|
+
}
|
|
208
|
+
return subsQ.isSuccess ? (_jsx("span", { className: "inline-flex items-center whitespace-nowrap rounded-md border px-1.5 py-0.5 text-xs text-muted-foreground", children: "\u5DF2\u5220\u9664\u8BA2\u9605" })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{ key: 'status', header: '状态', render: (d) => _jsx(StatusBadge, { status: d.status }) },
|
|
212
|
+
{
|
|
213
|
+
key: 'attempts', header: '尝试',
|
|
214
|
+
render: (d) => (_jsxs("span", { className: "tabular-nums", children: [d.attempts, d.last_status_code ? _jsxs("span", { className: "ml-1 text-xs text-muted-foreground", children: ["HTTP ", d.last_status_code] }) : null] })),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: 'last_error', header: '最后错误',
|
|
218
|
+
render: (d) => d.last_error ? (_jsx("span", { className: "block max-w-xs truncate text-xs text-destructive", title: d.last_error, children: d.last_error })) : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
key: 'created_at', header: '时间',
|
|
222
|
+
render: (d) => _jsx("span", { className: "whitespace-nowrap text-muted-foreground", children: formatDateTime(d.created_at) }),
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
key: 'actions', header: '操作', className: 'text-right',
|
|
226
|
+
render: (d) => canManage ? (_jsx(Button, { size: "sm", variant: "outline", disabled: redeliver.isPending, onClick: () => redeliver.mutate(d.id), children: "\u91CD\u65B0\u6295\u9012" })) : null,
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
if (!wsId) {
|
|
230
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "Webhooks", description: "\u51FA\u7AD9\u4E8B\u4EF6\u8BA2\u9605\u4E0E\u6295\u9012\u7BA1\u7406" }), _jsx(PageEmpty, { label: "\u8BF7\u5148\u521B\u5EFA\u5E76\u9009\u62E9\u4E00\u4E2A\u5DE5\u4F5C\u533A" })] }));
|
|
231
|
+
}
|
|
232
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "Webhooks", description: `出站事件订阅与投递管理(工作区:${current?.name ?? wsId})`, actions: canManage ? _jsxs(Button, { onClick: () => setCreateOpen(true), children: [_jsx(Plus, {}), " \u521B\u5EFA\u8BA2\u9605"] }) : undefined }), _jsxs(Tabs, { defaultValue: "subscriptions", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "subscriptions", children: "\u8BA2\u9605" }), _jsx(TabsTrigger, { value: "deliveries", children: "\u6295\u9012\u8BB0\u5F55" })] }), _jsx(TabsContent, { value: "subscriptions", className: "mt-4 space-y-4", children: subsQ.isError ? (_jsx(PageError, { message: apiErrorMessage(subsQ.error, '订阅列表加载失败'), onRetry: () => subsQ.refetch() })) : subsQ.isPending ? (_jsx(PageLoading, { label: "\u52A0\u8F7D\u8BA2\u9605\u2026" })) : (_jsx(DataTable, { columns: subColumns, rows: subsQ.data?.items ?? [], rowKey: (s) => s.id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u8BA2\u9605", hint: "\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u521B\u5EFA\u8BA2\u9605\u201D\u63A5\u6536\u7B2C\u4E00\u4E2A\u4E8B\u4EF6" }) })) }), _jsx(TabsContent, { value: "deliveries", className: "mt-4", children: deliveriesQ.isError ? (_jsx(PageError, { message: apiErrorMessage(deliveriesQ.error, '投递记录加载失败'), onRetry: () => deliveriesQ.refetch() })) : deliveriesQ.isPending ? (_jsx(PageLoading, { label: "\u52A0\u8F7D\u6295\u9012\u8BB0\u5F55\u2026" })) : (_jsx(DataTable, { columns: deliveryColumns, rows: deliveriesQ.data?.items ?? [], rowKey: (d) => d.id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u6295\u9012", hint: "\u89E6\u53D1\u4E8B\u4EF6\u6216\u53D1\u9001 Ping \u540E\uFF0C\u6295\u9012\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC" }) })) })] }), _jsx(Dialog, { open: createOpen, onOpenChange: setCreateOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "\u521B\u5EFA\u8BA2\u9605" }), _jsx(DialogDescription, { children: "\u4E8B\u4EF6\u5C06\u4EE5 HMAC \u7B7E\u540D\u6295\u9012\u5230\u56DE\u8C03\u5730\u5740\uFF1B\u7B7E\u540D\u5BC6\u94A5\u5728\u521B\u5EFA\u540E\u4EC5\u5C55\u793A\u4E00\u6B21\u3002" })] }), _jsxs("form", { className: "space-y-4", onSubmit: submitCreate, children: [_jsx(FormField, { label: "\u56DE\u8C03 URL", error: form.formState.errors.url?.message, htmlFor: "whk-url", children: _jsx(Input, { id: "whk-url", placeholder: "https://example.com/webhook", ...form.register('url') }) }), _jsx(FormField, { label: "\u5907\u6CE8\uFF08\u53EF\u9009\uFF09", error: form.formState.errors.description?.message, htmlFor: "whk-desc", children: _jsx(Input, { id: "whk-desc", placeholder: "\u7528\u9014\u8BF4\u660E\uFF0C\u4FBF\u4E8E\u8BC6\u522B", ...form.register('description') }) }), _jsx(FormField, { label: "\u4E8B\u4EF6\u7C7B\u578B", error: form.formState.errors.events?.message, children: _jsxs("div", { className: "max-h-44 space-y-2 overflow-y-auto rounded-md border p-3", children: [(eventsQ.data?.items ?? []).map((ev) => {
|
|
233
|
+
const cbId = `whk-evt-${ev.type.replace(/[^a-zA-Z0-9-]/g, '-')}`;
|
|
234
|
+
const checked = selectedEvents.includes(ev.type);
|
|
235
|
+
return (_jsxs("label", { className: "flex items-start gap-2 text-sm", htmlFor: cbId, onClick: (e) => {
|
|
236
|
+
if (e.target.closest('[data-slot="checkbox"]'))
|
|
237
|
+
return;
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
toggleEvent(ev.type, !checked);
|
|
240
|
+
}, children: [_jsx(Checkbox, { id: cbId, checked: checked, onCheckedChange: (c) => toggleEvent(ev.type, c === true) }), _jsxs("span", { className: "min-w-0", children: [_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: ev.type }), ev.description && _jsx("span", { className: "ml-2 text-xs text-muted-foreground", children: ev.description })] })] }, ev.type));
|
|
241
|
+
}), eventsQ.isPending && _jsx("p", { className: "text-xs text-muted-foreground", children: "\u52A0\u8F7D\u4E8B\u4EF6\u76EE\u5F55\u2026" })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setCreateOpen(false), children: "\u53D6\u6D88" }), _jsx(Button, { type: "submit", disabled: create.isPending, children: create.isPending ? '创建中…' : '创建订阅' })] })] })] }) }), _jsx(SecretModal, { open: secret !== null, onOpenChange: (o) => !o && closeSecret(), secretName: "Webhook \u7B7E\u540D\u5BC6\u94A5", secretValue: secret ?? '', title: secretTitle })] }));
|
|
242
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useSearchParams } from 'react-router';
|
|
4
|
+
import { Download, Search } from 'lucide-react';
|
|
5
|
+
import { PageHeader, PageLoading, PageError, PageEmpty } from '../../components/Page';
|
|
6
|
+
import { DataTable } from '../../components/DataTable';
|
|
7
|
+
import { Button } from '../../components/ui/Button';
|
|
8
|
+
import { Input } from '../../components/ui/Input';
|
|
9
|
+
import { toast } from '../../components/toast';
|
|
10
|
+
import { useAudit, exportAuditCsv, } from '../../hooks/useAudit';
|
|
11
|
+
import { formatDateTime } from '../../lib/utils';
|
|
12
|
+
import { apiErrorMessage } from '../../lib/api-error';
|
|
13
|
+
const PAGE_SIZE = 20;
|
|
14
|
+
function draftFromParams(p) {
|
|
15
|
+
return {
|
|
16
|
+
action: p.get('action') ?? '',
|
|
17
|
+
actorId: p.get('actor_id') ?? '',
|
|
18
|
+
resourceType: p.get('resource_type') ?? '',
|
|
19
|
+
from: p.get('from') ?? '',
|
|
20
|
+
to: p.get('to') ?? '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function paramsFromDraft(d, offset) {
|
|
24
|
+
const p = new URLSearchParams();
|
|
25
|
+
if (d.action.trim())
|
|
26
|
+
p.set('action', d.action.trim());
|
|
27
|
+
if (d.actorId.trim())
|
|
28
|
+
p.set('actor_id', d.actorId.trim());
|
|
29
|
+
if (d.resourceType.trim())
|
|
30
|
+
p.set('resource_type', d.resourceType.trim());
|
|
31
|
+
if (d.from)
|
|
32
|
+
p.set('from', d.from);
|
|
33
|
+
if (d.to)
|
|
34
|
+
p.set('to', d.to);
|
|
35
|
+
if (offset > 0)
|
|
36
|
+
p.set('offset', String(offset));
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
39
|
+
function toFilters(d) {
|
|
40
|
+
return {
|
|
41
|
+
action: d.action.trim(),
|
|
42
|
+
actorId: d.actorId.trim(),
|
|
43
|
+
resourceType: d.resourceType.trim(),
|
|
44
|
+
from: d.from ? `${d.from}T00:00:00Z` : '',
|
|
45
|
+
to: d.to ? `${d.to}T23:59:59Z` : '',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function WorkspaceAuditPage(_props = {}) {
|
|
49
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
50
|
+
const [draft, setDraft] = useState(() => draftFromParams(searchParams));
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setDraft(draftFromParams(searchParams));
|
|
53
|
+
}, [searchParams]);
|
|
54
|
+
const [rangeError, setRangeError] = useState(null);
|
|
55
|
+
const appliedDraft = useMemo(() => draftFromParams(searchParams), [searchParams]);
|
|
56
|
+
const applied = useMemo(() => toFilters(appliedDraft), [appliedDraft]);
|
|
57
|
+
const offset = Math.max(0, Number.parseInt(searchParams.get('offset') ?? '0', 10) || 0);
|
|
58
|
+
const { wsId, items, total, loading, error, refetch } = useAudit(applied, PAGE_SIZE, offset);
|
|
59
|
+
const apply = () => {
|
|
60
|
+
if (draft.from && draft.to && draft.from > draft.to) {
|
|
61
|
+
setRangeError('起始日期不能晚于结束日期');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setRangeError(null);
|
|
65
|
+
setSearchParams(paramsFromDraft(draft, 0));
|
|
66
|
+
};
|
|
67
|
+
const onPageChange = (p) => {
|
|
68
|
+
setSearchParams(paramsFromDraft(appliedDraft, Math.max(0, (p - 1) * PAGE_SIZE)));
|
|
69
|
+
};
|
|
70
|
+
const exportCsv = async () => {
|
|
71
|
+
try {
|
|
72
|
+
await exportAuditCsv(applied);
|
|
73
|
+
toast.success('CSV 导出已开始下载');
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
toast.error(apiErrorMessage(e, '导出失败'));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const columns = [
|
|
80
|
+
{
|
|
81
|
+
key: 'created_at', header: '时间', className: 'w-[16%]',
|
|
82
|
+
render: (e) => _jsx("span", { className: "whitespace-nowrap text-muted-foreground", children: formatDateTime(e.created_at) }),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'actor', header: '操作者', className: 'w-[20%]',
|
|
86
|
+
render: (e) => {
|
|
87
|
+
const email = typeof e.actor_snapshot?.email === 'string' ? e.actor_snapshot.email : '';
|
|
88
|
+
return (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate", children: email || e.actor_id || '—' }), _jsx("p", { className: "text-xs text-muted-foreground", children: e.actor_type })] }));
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'action', header: '动作', className: 'w-[22%]',
|
|
93
|
+
render: (e) => _jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: e.action }),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'resource', header: '资源', className: 'w-[42%]',
|
|
97
|
+
render: (e) => (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate", children: e.resource_type }), e.resource_id && _jsx("p", { className: "truncate font-mono text-xs text-muted-foreground", children: e.resource_id })] })),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
if (!wsId) {
|
|
101
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u5BA1\u8BA1\u65E5\u5FD7", description: "\u5DE5\u4F5C\u533A\u5185\u7684\u5173\u952E\u64CD\u4F5C\u8BB0\u5F55" }), _jsx(PageEmpty, { label: "\u8BF7\u5148\u521B\u5EFA\u5E76\u9009\u62E9\u4E00\u4E2A\u5DE5\u4F5C\u533A" })] }));
|
|
102
|
+
}
|
|
103
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u5BA1\u8BA1\u65E5\u5FD7", description: "\u5DE5\u4F5C\u533A\u5185\u7684\u5173\u952E\u64CD\u4F5C\u8BB0\u5F55\uFF08\u4FDD\u7559\u6700\u8FD1\u4E8B\u4EF6\uFF0C\u6309\u65F6\u95F4\u5012\u5E8F\uFF09" }), _jsxs("div", { className: "mb-4 flex flex-wrap items-end gap-2", children: [_jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u52A8\u4F5C" }), _jsx(Input, { className: "w-44", placeholder: "\u5982 workspace.rename", value: draft.action, onChange: (e) => setDraft((d) => ({ ...d, action: e.target.value })) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u64CD\u4F5C\u8005 ID" }), _jsx(Input, { className: "w-52", placeholder: "actor uuid", value: draft.actorId, onChange: (e) => setDraft((d) => ({ ...d, actorId: e.target.value })) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u8D44\u6E90\u7C7B\u578B" }), _jsx(Input, { className: "w-40", placeholder: "\u5982 workspace", value: draft.resourceType, onChange: (e) => setDraft((d) => ({ ...d, resourceType: e.target.value })) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u5F00\u59CB\u65E5\u671F" }), _jsx(Input, { className: "w-40", type: "date", value: draft.from, onChange: (e) => setDraft((d) => ({ ...d, from: e.target.value })) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7ED3\u675F\u65E5\u671F" }), _jsx(Input, { className: "w-40", type: "date", value: draft.to, onChange: (e) => setDraft((d) => ({ ...d, to: e.target.value })) })] }), _jsxs(Button, { onClick: apply, children: [_jsx(Search, {}), " \u67E5\u8BE2"] }), _jsxs(Button, { variant: "outline", onClick: exportCsv, children: [_jsx(Download, {}), " \u5BFC\u51FA CSV"] })] }), rangeError && (_jsx("p", { role: "alert", className: "mb-4 text-sm text-destructive", children: rangeError })), loading ? (_jsx(PageLoading, { label: "\u52A0\u8F7D\u5BA1\u8BA1\u65E5\u5FD7\u2026" })) : error ? (_jsx(PageError, { message: apiErrorMessage(error, '审计日志加载失败'), onRetry: () => refetch() })) : (_jsx(_Fragment, { children: _jsx(DataTable, { columns: columns, rows: items, rowKey: (e) => e.id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u5BA1\u8BA1\u8BB0\u5F55", hint: "\u8C03\u6574\u8FC7\u6EE4\u6761\u4EF6\u6216\u89E6\u53D1\u4E00\u4E9B\u64CD\u4F5C\u540E\u518D\u6765" }), pagination: {
|
|
104
|
+
page: Math.floor(offset / PAGE_SIZE) + 1,
|
|
105
|
+
pageSize: PAGE_SIZE,
|
|
106
|
+
total,
|
|
107
|
+
onPageChange,
|
|
108
|
+
} }) }))] }));
|
|
109
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useNavigate } from 'react-router';
|
|
4
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { PageHeader, PageError } from '../../components/Page';
|
|
7
|
+
import { SettingsCard } from '../../components/SettingsCard';
|
|
8
|
+
import { FormField } from '../../components/FormField';
|
|
9
|
+
import { ImpactConfirmation } from '../../components/SecretModal';
|
|
10
|
+
import { useConfirm } from '../../components/ConfirmDialog';
|
|
11
|
+
import { toast } from '../../components/toast';
|
|
12
|
+
import { Button } from '../../components/ui/Button';
|
|
13
|
+
import { Input } from '../../components/ui/Input';
|
|
14
|
+
import { useApi } from '../../hooks/useApi';
|
|
15
|
+
import { useWorkspace } from '../../hooks/useWorkspace';
|
|
16
|
+
import { useZodForm } from '../../hooks/useZodForm';
|
|
17
|
+
import { queryKeys } from '../../provider/PloykitProvider';
|
|
18
|
+
import { apiErrorMessage } from '../../lib/api-error';
|
|
19
|
+
import { roleSatisfies } from '../../lib/perm';
|
|
20
|
+
const renameSchema = z.object({
|
|
21
|
+
name: z.string().trim().min(1, '请输入工作区名称').max(64, '名称最多 64 字符'),
|
|
22
|
+
});
|
|
23
|
+
export function WorkspaceGeneralPage({ exitTo = '/app' } = {}) {
|
|
24
|
+
const { current, clear } = useWorkspace();
|
|
25
|
+
const api = useApi();
|
|
26
|
+
const confirm = useConfirm();
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
const qc = useQueryClient();
|
|
29
|
+
const [saving, setSaving] = useState(false);
|
|
30
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
31
|
+
const ws = current;
|
|
32
|
+
const form = useZodForm({
|
|
33
|
+
schema: renameSchema,
|
|
34
|
+
defaultValues: { name: ws?.name ?? '' },
|
|
35
|
+
values: ws ? { name: ws.name } : undefined, // 工作区数据到达后同步进表单
|
|
36
|
+
});
|
|
37
|
+
if (!ws) {
|
|
38
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u5DE5\u4F5C\u533A\u6982\u51B5" }), _jsx(PageError, { message: "\u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728\u6216\u5C1A\u672A\u9009\u62E9", onRetry: () => qc.invalidateQueries({ queryKey: queryKeys.workspaceList }) })] }));
|
|
39
|
+
}
|
|
40
|
+
const canRename = roleSatisfies(ws.role, 'admin');
|
|
41
|
+
const canDelete = roleSatisfies(ws.role, 'owner');
|
|
42
|
+
const refreshList = async () => {
|
|
43
|
+
await qc.invalidateQueries({ queryKey: queryKeys.workspace });
|
|
44
|
+
};
|
|
45
|
+
const handleRename = form.handleSubmit(async (values) => {
|
|
46
|
+
setSaving(true);
|
|
47
|
+
try {
|
|
48
|
+
await api.patch(`/api/workspaces/${ws.id}`, { name: values.name });
|
|
49
|
+
await refreshList();
|
|
50
|
+
toast.success('工作区名称已更新');
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
toast.error(apiErrorMessage(e, '重命名失败'));
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
setSaving(false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const afterExit = async () => {
|
|
60
|
+
clear();
|
|
61
|
+
await refreshList();
|
|
62
|
+
navigate(exitTo);
|
|
63
|
+
};
|
|
64
|
+
const handleLeave = async () => {
|
|
65
|
+
if (!(await confirm({
|
|
66
|
+
title: '退出工作区',
|
|
67
|
+
description: `退出后将失去「${ws.name}」的访问权限;你创建的数据会保留在工作区内。`,
|
|
68
|
+
confirmText: '退出', danger: true,
|
|
69
|
+
})))
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
await api.post(`/api/workspaces/${ws.id}/leave`);
|
|
73
|
+
toast.success('已退出工作区');
|
|
74
|
+
await afterExit();
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
toast.error(is409(e)
|
|
78
|
+
? '最后一个所有者不能离开工作区,请先转移所有权或删除工作区'
|
|
79
|
+
: apiErrorMessage(e, '退出失败'));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const handleDelete = async () => {
|
|
83
|
+
try {
|
|
84
|
+
await api.delete(`/api/workspaces/${ws.id}`);
|
|
85
|
+
toast.success('工作区已删除');
|
|
86
|
+
await afterExit();
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
toast.error(apiErrorMessage(e, '删除失败'));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u5DE5\u4F5C\u533A\u6982\u51B5", description: `工作区 ${ws.name}(${ws.slug})的基础设置` }), _jsx(SettingsCard, { title: "\u57FA\u7840\u4FE1\u606F", description: "\u5DE5\u4F5C\u533A\u540D\u79F0\u5BF9\u6210\u5458\u53EF\u89C1\uFF1Bslug \u662F URL \u6807\u8BC6\uFF0C\u521B\u5EFA\u540E\u4E0D\u53EF\u4FEE\u6539\u3002", children: _jsxs("form", { className: "max-w-md space-y-4", onSubmit: handleRename, children: [_jsx(FormField, { label: "\u5DE5\u4F5C\u533A\u540D\u79F0", error: form.formState.errors.name?.message, htmlFor: "ws-name", children: _jsx(Input, { id: "ws-name", disabled: !canRename, ...form.register('name') }) }), _jsx(FormField, { label: "Slug\uFF08\u53EA\u8BFB\uFF09", htmlFor: "ws-slug", children: _jsx(Input, { id: "ws-slug", value: ws.slug, readOnly: true, disabled: true }) }), canRename && (_jsx(Button, { type: "submit", disabled: saving, children: saving ? '保存中…' : '保存修改' }))] }) }), _jsx(SettingsCard, { danger: true, title: "\u5371\u9669\u533A", description: "\u4EE5\u4E0B\u64CD\u4F5C\u5F71\u54CD\u5168\u90E8\u6210\u5458\uFF0C\u8BF7\u8C28\u614E\u6267\u884C\u3002", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-row items-center justify-between gap-4 rounded-lg border p-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: "\u9000\u51FA\u5DE5\u4F5C\u533A" }), _jsxs("p", { className: "mt-0.5 text-sm text-muted-foreground", children: ["\u9000\u51FA\u540E\u4F60\u5C06\u4E0D\u518D\u662F\u300C", ws.name, "\u300D\u7684\u6210\u5458\u3002\u6700\u540E\u4E00\u4F4D owner \u65E0\u6CD5\u9000\u51FA\u3002"] })] }), _jsx(Button, { variant: "outline", onClick: handleLeave, children: "\u9000\u51FA\u5DE5\u4F5C\u533A" })] }), canDelete && (_jsxs("div", { className: "flex flex-row items-center justify-between gap-4 rounded-lg border border-destructive/40 p-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-destructive", children: "\u5220\u9664\u5DE5\u4F5C\u533A" }), _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: "\u786C\u5220\u9664\u5DE5\u4F5C\u533A\u53CA\u5176\u5168\u90E8\u6570\u636E\uFF0C\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002" })] }), _jsx(Button, { variant: "destructive", onClick: () => setDeleteOpen(true), children: "\u5220\u9664\u5DE5\u4F5C\u533A" })] }))] }) }), _jsx(ImpactConfirmation, { open: deleteOpen, onOpenChange: setDeleteOpen, onConfirm: handleDelete, title: `删除工作区「${ws.name}」`, impacts: [
|
|
93
|
+
'工作区全部数据将被永久删除,且不可恢复',
|
|
94
|
+
'所有成员立即失去访问权限',
|
|
95
|
+
'未撤销的邀请与分享链接同时失效',
|
|
96
|
+
'历史审计记录将保留(合规要求),但不再关联可访问的工作区',
|
|
97
|
+
], confirmText: "\u6C38\u4E45\u5220\u9664", danger: true })] }));
|
|
98
|
+
}
|
|
99
|
+
function is409(e) {
|
|
100
|
+
return typeof e === 'object' && e !== null && 'status' in e && e.status === 409;
|
|
101
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
import type { APIUser, APIWorkspace, APISubscription, APIOrder, APIPlan } from '@ploykit/client';
|
|
4
|
+
export declare const queryKeys: {
|
|
5
|
+
auth: readonly ["auth"];
|
|
6
|
+
me: readonly ["auth", "me"];
|
|
7
|
+
sessions: readonly ["auth", "sessions"];
|
|
8
|
+
tokens: readonly ["auth", "tokens"];
|
|
9
|
+
workspace: readonly ["workspace"];
|
|
10
|
+
workspaceList: readonly ["workspace", "list"];
|
|
11
|
+
members: (wsId: string) => readonly ["workspace", string, "members"];
|
|
12
|
+
invitations: (wsId: string) => readonly ["workspace", string, "invitations"];
|
|
13
|
+
shareLinks: (wsId: string) => readonly ["workspace", string, "share-links"];
|
|
14
|
+
myInvitations: readonly ["workspace", "my-invitations"];
|
|
15
|
+
audit: (wsId: string, q: object) => readonly ["audit", string, object];
|
|
16
|
+
usage: (wsId: string) => readonly ["usage", string];
|
|
17
|
+
billing: readonly ["billing"];
|
|
18
|
+
billingSubscription: (workspaceId: string) => readonly ["billing", "subscription", string];
|
|
19
|
+
billingPlans: readonly ["billing", "plans"];
|
|
20
|
+
notifications: readonly ["notifications"];
|
|
21
|
+
notificationPrefs: readonly ["notifications", "preferences"];
|
|
22
|
+
webhooks: (wsId: string) => readonly ["webhooks", string];
|
|
23
|
+
admin: readonly ["admin"];
|
|
24
|
+
};
|
|
25
|
+
export type PKUser = APIUser;
|
|
26
|
+
export type PKWorkspace = APIWorkspace;
|
|
27
|
+
export type PKSubscription = APISubscription;
|
|
28
|
+
export type PKOrder = APIOrder;
|
|
29
|
+
export type PKPlan = APIPlan;
|
|
30
|
+
export interface AuthContextValue {
|
|
31
|
+
user: PKUser | null;
|
|
32
|
+
workspaces: PKWorkspace[];
|
|
33
|
+
loading: boolean;
|
|
34
|
+
error: unknown;
|
|
35
|
+
refresh(): Promise<void>;
|
|
36
|
+
logout(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export declare const AuthContext: import("react").Context<AuthContextValue | null>;
|
|
39
|
+
export interface WorkspaceContextValue {
|
|
40
|
+
current: PKWorkspace | null;
|
|
41
|
+
switchTo(id: string): void;
|
|
42
|
+
clear(): void;
|
|
43
|
+
}
|
|
44
|
+
export declare const WorkspaceContext: import("react").Context<WorkspaceContextValue | null>;
|
|
45
|
+
export declare function useAuthCtx(): AuthContextValue;
|
|
46
|
+
export declare function useWorkspaceCtx(): WorkspaceContextValue;
|
|
47
|
+
export declare function PloykitProvider({ children, queryClient }: {
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
queryClient?: QueryClient;
|
|
50
|
+
}): import("react").JSX.Element;
|