@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.
Files changed (196) hide show
  1. package/README.md +40 -2
  2. package/dist/components/ConfirmDialog.d.ts +13 -0
  3. package/dist/components/ConfirmDialog.js +29 -0
  4. package/dist/components/CronInput.d.ts +15 -0
  5. package/dist/components/CronInput.js +112 -0
  6. package/dist/components/DataTable.d.ts +22 -0
  7. package/dist/components/DataTable.js +13 -0
  8. package/dist/components/ErrorBoundary.d.ts +16 -0
  9. package/dist/components/ErrorBoundary.js +24 -0
  10. package/dist/components/FormField.d.ts +10 -0
  11. package/dist/components/FormField.js +22 -0
  12. package/dist/components/ImpersonationBanner.d.ts +3 -0
  13. package/dist/components/ImpersonationBanner.js +10 -0
  14. package/dist/components/NotificationBell.d.ts +3 -0
  15. package/dist/components/NotificationBell.js +47 -0
  16. package/dist/components/OnboardingChecklist.d.ts +12 -0
  17. package/dist/components/OnboardingChecklist.js +43 -0
  18. package/dist/components/Page.d.ts +17 -0
  19. package/dist/components/Page.js +16 -0
  20. package/dist/components/SecretModal.d.ts +16 -0
  21. package/dist/components/SecretModal.js +45 -0
  22. package/dist/components/SettingsCard.d.ts +10 -0
  23. package/dist/components/SettingsCard.js +6 -0
  24. package/dist/components/SiteBanner.d.ts +6 -0
  25. package/dist/components/SiteBanner.js +11 -0
  26. package/dist/components/SiteBannerLayer.d.ts +4 -0
  27. package/dist/components/SiteBannerLayer.js +9 -0
  28. package/dist/components/UserMenu.d.ts +13 -0
  29. package/dist/components/UserMenu.js +30 -0
  30. package/dist/components/WorkspaceSwitcher.d.ts +1 -0
  31. package/dist/components/WorkspaceSwitcher.js +53 -0
  32. package/dist/components/toast.d.ts +7 -0
  33. package/dist/components/toast.js +11 -0
  34. package/dist/components/ui/Badge.d.ts +13 -0
  35. package/dist/components/ui/Badge.js +21 -0
  36. package/dist/components/ui/Button.d.ts +10 -0
  37. package/dist/components/ui/Button.js +33 -0
  38. package/dist/components/ui/Input.d.ts +3 -0
  39. package/dist/components/ui/Input.js +10 -0
  40. package/dist/components/ui/alert-dialog.d.ts +18 -0
  41. package/dist/components/ui/alert-dialog.js +41 -0
  42. package/dist/components/ui/avatar.d.ts +11 -0
  43. package/dist/components/ui/avatar.js +22 -0
  44. package/dist/components/ui/breadcrumb.d.ts +10 -0
  45. package/dist/components/ui/breadcrumb.js +36 -0
  46. package/dist/components/ui/card.d.ts +13 -0
  47. package/dist/components/ui/card.js +24 -0
  48. package/dist/components/ui/checkbox.d.ts +3 -0
  49. package/dist/components/ui/checkbox.js +8 -0
  50. package/dist/components/ui/dialog.d.ts +15 -0
  51. package/dist/components/ui/dialog.js +26 -0
  52. package/dist/components/ui/empty.d.ts +13 -0
  53. package/dist/components/ui/empty.js +36 -0
  54. package/dist/components/ui/field.d.ts +24 -0
  55. package/dist/components/ui/field.js +67 -0
  56. package/dist/components/ui/label.d.ts +3 -0
  57. package/dist/components/ui/label.js +6 -0
  58. package/dist/components/ui/menu.d.ts +29 -0
  59. package/dist/components/ui/menu.js +50 -0
  60. package/dist/components/ui/popover.d.ts +9 -0
  61. package/dist/components/ui/popover.js +22 -0
  62. package/dist/components/ui/progress.d.ts +7 -0
  63. package/dist/components/ui/progress.js +19 -0
  64. package/dist/components/ui/select.d.ts +15 -0
  65. package/dist/components/ui/select.js +33 -0
  66. package/dist/components/ui/separator.d.ts +3 -0
  67. package/dist/components/ui/separator.js +7 -0
  68. package/dist/components/ui/sheet.d.ts +14 -0
  69. package/dist/components/ui/sheet.js +36 -0
  70. package/dist/components/ui/sidebar.d.ts +63 -0
  71. package/dist/components/ui/sidebar.js +265 -0
  72. package/dist/components/ui/skeleton.d.ts +3 -0
  73. package/dist/components/ui/skeleton.js +6 -0
  74. package/dist/components/ui/switch.d.ts +5 -0
  75. package/dist/components/ui/switch.js +7 -0
  76. package/dist/components/ui/tabs.d.ts +10 -0
  77. package/dist/components/ui/tabs.js +28 -0
  78. package/dist/components/ui/tooltip.d.ts +6 -0
  79. package/dist/components/ui/tooltip.js +16 -0
  80. package/dist/hooks/use-mobile.d.ts +1 -0
  81. package/dist/hooks/use-mobile.js +15 -0
  82. package/dist/hooks/useApi.d.ts +10 -0
  83. package/dist/hooks/useApi.js +11 -0
  84. package/dist/hooks/useAudit.d.ts +39 -0
  85. package/dist/hooks/useAudit.js +55 -0
  86. package/dist/hooks/useAuth.d.ts +1 -0
  87. package/dist/hooks/useAuth.js +4 -0
  88. package/dist/hooks/useBilling.d.ts +44 -0
  89. package/dist/hooks/useBilling.js +64 -0
  90. package/dist/hooks/useLogin.d.ts +10 -0
  91. package/dist/hooks/useLogin.js +39 -0
  92. package/dist/hooks/useMembers.d.ts +125 -0
  93. package/dist/hooks/useMembers.js +127 -0
  94. package/dist/hooks/useNotificationPrefs.d.ts +20 -0
  95. package/dist/hooks/useNotificationPrefs.js +42 -0
  96. package/dist/hooks/usePasswordReset.d.ts +5 -0
  97. package/dist/hooks/usePasswordReset.js +12 -0
  98. package/dist/hooks/useSEO.d.ts +6 -0
  99. package/dist/hooks/useSEO.js +14 -0
  100. package/dist/hooks/useSessions.d.ts +20 -0
  101. package/dist/hooks/useSessions.js +38 -0
  102. package/dist/hooks/useSiteConfig.d.ts +12 -0
  103. package/dist/hooks/useSiteConfig.js +11 -0
  104. package/dist/hooks/useTokens.d.ts +36 -0
  105. package/dist/hooks/useTokens.js +35 -0
  106. package/dist/hooks/useUsage.d.ts +20 -0
  107. package/dist/hooks/useUsage.js +21 -0
  108. package/dist/hooks/useWorkspace.d.ts +26 -0
  109. package/dist/hooks/useWorkspace.js +32 -0
  110. package/dist/hooks/useZodForm.d.ts +6 -0
  111. package/dist/hooks/useZodForm.js +6 -0
  112. package/dist/hooks/ws.d.ts +4 -0
  113. package/dist/hooks/ws.js +21 -0
  114. package/dist/index.d.ts +124 -0
  115. package/dist/index.js +94 -0
  116. package/dist/layouts/AppShell.d.ts +20 -0
  117. package/dist/layouts/AppShell.js +21 -0
  118. package/dist/layouts/AuthShell.d.ts +7 -0
  119. package/dist/layouts/AuthShell.js +4 -0
  120. package/dist/layouts/SettingsShell.d.ts +19 -0
  121. package/dist/layouts/SettingsShell.js +21 -0
  122. package/dist/lib/api-error.d.ts +1 -0
  123. package/dist/lib/api-error.js +74 -0
  124. package/dist/lib/perm.d.ts +2 -0
  125. package/dist/lib/perm.js +7 -0
  126. package/dist/lib/utils.d.ts +12 -0
  127. package/dist/lib/utils.js +84 -0
  128. package/dist/lib/ws-query-bridge.d.ts +7 -0
  129. package/dist/lib/ws-query-bridge.js +35 -0
  130. package/dist/pages/LandingPage.d.ts +22 -0
  131. package/dist/pages/LandingPage.js +5 -0
  132. package/dist/pages/LoginPage.d.ts +15 -0
  133. package/dist/pages/LoginPage.js +87 -0
  134. package/dist/pages/account/NotificationPreferencesPage.d.ts +13 -0
  135. package/dist/pages/account/NotificationPreferencesPage.js +41 -0
  136. package/dist/pages/account/ProfileSettingsPage.d.ts +4 -0
  137. package/dist/pages/account/ProfileSettingsPage.js +96 -0
  138. package/dist/pages/account/SecuritySettingsPage.d.ts +1 -0
  139. package/dist/pages/account/SecuritySettingsPage.js +134 -0
  140. package/dist/pages/account/TokensPage.d.ts +1 -0
  141. package/dist/pages/account/TokensPage.js +117 -0
  142. package/dist/pages/admin/AdminSettingsPage.d.ts +1 -0
  143. package/dist/pages/admin/AdminSettingsPage.js +210 -0
  144. package/dist/pages/admin/AdminShell.d.ts +7 -0
  145. package/dist/pages/admin/AdminShell.js +79 -0
  146. package/dist/pages/admin/AuditPage.d.ts +1 -0
  147. package/dist/pages/admin/AuditPage.js +164 -0
  148. package/dist/pages/admin/OrdersPage.d.ts +1 -0
  149. package/dist/pages/admin/OrdersPage.js +236 -0
  150. package/dist/pages/admin/OverviewPage.d.ts +1 -0
  151. package/dist/pages/admin/OverviewPage.js +200 -0
  152. package/dist/pages/admin/SSOPage.d.ts +1 -0
  153. package/dist/pages/admin/SSOPage.js +53 -0
  154. package/dist/pages/admin/UserDetailPage.d.ts +1 -0
  155. package/dist/pages/admin/UserDetailPage.js +266 -0
  156. package/dist/pages/admin/UsersPage.d.ts +4 -0
  157. package/dist/pages/admin/UsersPage.js +201 -0
  158. package/dist/pages/admin/WebhookDeliveriesPage.d.ts +1 -0
  159. package/dist/pages/admin/WebhookDeliveriesPage.js +128 -0
  160. package/dist/pages/admin/WorkspaceDetailPage.d.ts +1 -0
  161. package/dist/pages/admin/WorkspaceDetailPage.js +202 -0
  162. package/dist/pages/admin/WorkspacesPage.d.ts +1 -0
  163. package/dist/pages/admin/WorkspacesPage.js +133 -0
  164. package/dist/pages/admin/shared.d.ts +8 -0
  165. package/dist/pages/admin/shared.js +23 -0
  166. package/dist/pages/auth/ForgotPasswordPage.d.ts +6 -0
  167. package/dist/pages/auth/ForgotPasswordPage.js +32 -0
  168. package/dist/pages/auth/InviteAcceptPage.d.ts +5 -0
  169. package/dist/pages/auth/InviteAcceptPage.js +40 -0
  170. package/dist/pages/auth/RegisterPage.d.ts +15 -0
  171. package/dist/pages/auth/RegisterPage.js +80 -0
  172. package/dist/pages/auth/ResetPasswordPage.d.ts +4 -0
  173. package/dist/pages/auth/ResetPasswordPage.js +71 -0
  174. package/dist/pages/auth/VerifyEmailPage.d.ts +1 -0
  175. package/dist/pages/auth/VerifyEmailPage.js +45 -0
  176. package/dist/pages/workspace/BillingPage.d.ts +4 -0
  177. package/dist/pages/workspace/BillingPage.js +183 -0
  178. package/dist/pages/workspace/BillingSuccessPage.d.ts +4 -0
  179. package/dist/pages/workspace/BillingSuccessPage.js +72 -0
  180. package/dist/pages/workspace/MembersPage.d.ts +3 -0
  181. package/dist/pages/workspace/MembersPage.js +262 -0
  182. package/dist/pages/workspace/SchedulesPage.d.ts +4 -0
  183. package/dist/pages/workspace/SchedulesPage.js +177 -0
  184. package/dist/pages/workspace/UsagePage.d.ts +15 -0
  185. package/dist/pages/workspace/UsagePage.js +74 -0
  186. package/dist/pages/workspace/WebhooksPage.d.ts +3 -0
  187. package/dist/pages/workspace/WebhooksPage.js +242 -0
  188. package/dist/pages/workspace/WorkspaceAuditPage.d.ts +3 -0
  189. package/dist/pages/workspace/WorkspaceAuditPage.js +109 -0
  190. package/dist/pages/workspace/WorkspaceGeneralPage.d.ts +4 -0
  191. package/dist/pages/workspace/WorkspaceGeneralPage.js +101 -0
  192. package/dist/provider/PloykitProvider.d.ts +50 -0
  193. package/dist/provider/PloykitProvider.js +240 -0
  194. package/dist/realtime.d.ts +3 -0
  195. package/dist/realtime.js +11 -0
  196. 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,3 @@
1
+ export interface WorkspaceAuditPageProps {
2
+ }
3
+ export declare function WorkspaceAuditPage(_props?: WorkspaceAuditPageProps): import("react").JSX.Element;
@@ -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,4 @@
1
+ export interface WorkspaceGeneralPageProps {
2
+ exitTo?: string;
3
+ }
4
+ export declare function WorkspaceGeneralPage({ exitTo }?: WorkspaceGeneralPageProps): import("react").JSX.Element;
@@ -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;