@ploykit/ui 0.5.0 → 0.6.2
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/dist/AppProviders.js +2 -2
- package/dist/components/ConfirmDialog.js +1 -1
- package/dist/components/CronInput.js +3 -3
- package/dist/components/DataTable.js +4 -4
- package/dist/components/FormField.js +1 -1
- package/dist/components/ImpersonationBanner.js +1 -1
- package/dist/components/NotificationBell.js +1 -1
- package/dist/components/OnboardingChecklist.js +3 -3
- package/dist/components/Page.js +2 -2
- package/dist/components/ReauthDialog.js +4 -4
- package/dist/components/SecretModal.js +2 -2
- package/dist/components/SettingsCard.js +2 -2
- package/dist/components/SiteBanner.js +1 -1
- package/dist/components/SiteBannerLayer.js +1 -1
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/UserMenu.js +3 -3
- package/dist/components/WorkspaceSwitcher.js +7 -7
- package/dist/components/ui/alert-dialog.js +2 -2
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.js +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/dialog.js +2 -2
- package/dist/components/ui/dropdown-menu.js +1 -1
- package/dist/components/ui/empty.js +1 -1
- package/dist/components/ui/field.js +3 -3
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/label.js +1 -1
- package/dist/components/ui/popover.js +1 -1
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/separator.js +1 -1
- package/dist/components/ui/sheet.js +2 -2
- package/dist/components/ui/sidebar.js +8 -8
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/index.js +78 -78
- package/dist/layouts/AppShell.js +4 -4
- package/dist/layouts/SettingsShell.js +4 -4
- package/dist/pages/LoginPage.js +5 -5
- package/dist/pages/account/NotificationPreferencesPage.js +3 -3
- package/dist/pages/account/ProfileSettingsPage.js +9 -9
- package/dist/pages/account/SecuritySettingsPage.js +13 -13
- package/dist/pages/account/TokensPage.js +12 -12
- package/dist/pages/admin/AdminSettingsPage.js +11 -11
- package/dist/pages/admin/AdminShell.js +3 -3
- package/dist/pages/admin/AuditPage.js +8 -8
- package/dist/pages/admin/OrdersPage.js +12 -12
- package/dist/pages/admin/OverviewPage.js +13 -13
- package/dist/pages/admin/SSOPage.js +6 -6
- package/dist/pages/admin/UserDetailPage.js +13 -13
- package/dist/pages/admin/UsersPage.js +13 -13
- package/dist/pages/admin/WebhookDeliveriesPage.js +9 -9
- package/dist/pages/admin/WorkspaceDetailPage.js +14 -14
- package/dist/pages/admin/WorkspacesPage.js +9 -9
- package/dist/pages/admin/shared.js +1 -1
- package/dist/pages/auth/ForgotPasswordPage.js +4 -4
- package/dist/pages/auth/InviteAcceptPage.js +6 -6
- package/dist/pages/auth/RegisterPage.js +7 -7
- package/dist/pages/auth/ResetPasswordPage.js +5 -5
- package/dist/pages/auth/VerifyEmailPage.js +3 -3
- package/dist/pages/workspace/BillingPage.js +11 -11
- package/dist/pages/workspace/BillingSuccessPage.js +2 -2
- package/dist/pages/workspace/MembersPage.js +15 -15
- package/dist/pages/workspace/RolePermissionsPage.js +9 -9
- package/dist/pages/workspace/SchedulesPage.js +12 -12
- package/dist/pages/workspace/UsagePage.js +6 -6
- package/dist/pages/workspace/WebhooksPage.js +16 -16
- package/dist/pages/workspace/WorkspaceAuditPage.js +7 -7
- package/dist/pages/workspace/WorkspaceGeneralPage.js +9 -9
- package/package.json +6 -5
- package/src/AppProviders.tsx +29 -0
- package/src/components/ConfirmDialog.tsx +77 -0
- package/src/components/CronInput.tsx +195 -0
- package/src/components/DataTable.tsx +152 -0
- package/src/components/ErrorBoundary.tsx +46 -0
- package/src/components/FormField.tsx +56 -0
- package/src/components/ImpersonationBanner.tsx +23 -0
- package/src/components/NotificationBell.tsx +135 -0
- package/src/components/OnboardingChecklist.tsx +114 -0
- package/src/components/Page.tsx +67 -0
- package/src/components/ReauthDialog.tsx +94 -0
- package/src/components/SecretModal.tsx +147 -0
- package/src/components/SettingsCard.tsx +39 -0
- package/src/components/SiteBanner.tsx +32 -0
- package/src/components/SiteBannerLayer.tsx +29 -0
- package/src/components/StatusBadge.tsx +12 -0
- package/src/components/UserMenu.tsx +103 -0
- package/src/components/WorkspaceSwitcher.tsx +159 -0
- package/src/components/__tests__/AppProviders.test.tsx +111 -0
- package/src/components/__tests__/CronInput.test.tsx +180 -0
- package/src/components/__tests__/DataTable.test.tsx +208 -0
- package/src/components/__tests__/ErrorBoundary.test.tsx +61 -0
- package/src/components/__tests__/ImpersonationBanner.test.tsx +59 -0
- package/src/components/__tests__/NotificationBell.test.tsx +207 -0
- package/src/components/__tests__/OnboardingChecklist.test.tsx +97 -0
- package/src/components/__tests__/SecretModal.test.tsx +112 -0
- package/src/components/__tests__/SiteBannerLayer.test.tsx +44 -0
- package/src/components/__tests__/UserMenu.test.tsx +98 -0
- package/src/components/__tests__/WorkspaceSwitcher.test.tsx +162 -0
- package/src/components/__tests__/form.test.tsx +133 -0
- package/src/components/__tests__/semantic-tokens.test.ts +41 -0
- package/src/components/__tests__/smoke.test.tsx +11 -0
- package/src/components/__tests__/toast.test.tsx +27 -0
- package/src/components/toast.tsx +23 -0
- package/src/components/ui/__tests__/alert-dialog.test.tsx +66 -0
- package/src/components/ui/__tests__/avatar.test.tsx +30 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +18 -0
- package/src/components/ui/__tests__/dropdown-menu.test.tsx +33 -0
- package/src/components/ui/__tests__/field.test.tsx +43 -0
- package/src/components/ui/__tests__/overlay-size.test.tsx +84 -0
- package/src/components/ui/__tests__/popover.test.tsx +32 -0
- package/src/components/ui/__tests__/progress.test.tsx +24 -0
- package/src/components/ui/__tests__/select-callback.test.tsx +72 -0
- package/src/components/ui/__tests__/select.test.tsx +48 -0
- package/src/components/ui/__tests__/sidebar.test.tsx +121 -0
- package/src/components/ui/__tests__/stock-mirror.test.ts +39 -0
- package/src/components/ui/__tests__/subpath.test.tsx +38 -0
- package/src/components/ui/__tests__/switch.test.tsx +24 -0
- package/src/components/ui/__tests__/tabs.test.tsx +24 -0
- package/src/components/ui/__tests__/textarea.test.tsx +41 -0
- package/src/components/ui/alert-dialog.tsx +187 -0
- package/src/components/ui/avatar.tsx +108 -0
- package/src/components/ui/badge.tsx +51 -0
- package/src/components/ui/breadcrumb.tsx +124 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/dialog.tsx +166 -0
- package/src/components/ui/dropdown-menu.tsx +265 -0
- package/src/components/ui/empty.tsx +103 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +17 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/select.tsx +235 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +146 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +26 -0
- package/src/components/ui/tooltip.tsx +63 -0
- package/src/dev.css +1 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +150 -0
- package/src/layouts/AppShell.tsx +163 -0
- package/src/layouts/AuthShell.tsx +24 -0
- package/src/layouts/SettingsShell.tsx +129 -0
- package/src/layouts/__tests__/AppShell.test.tsx +163 -0
- package/src/layouts/__tests__/SettingsShell.test.tsx +146 -0
- package/src/lib/__tests__/api-error.test.ts +163 -0
- package/src/lib/__tests__/utils.test.ts +21 -0
- package/src/lib/api-error.ts +98 -0
- package/src/lib/utils.ts +109 -0
- package/src/pages/LandingPage.tsx +88 -0
- package/src/pages/LoginPage.tsx +291 -0
- package/src/pages/__tests__/LandingPage.test.tsx +58 -0
- package/src/pages/__tests__/LoginPage.test.tsx +229 -0
- package/src/pages/__tests__/RegisterPage.test.tsx +138 -0
- package/src/pages/__tests__/ssr-smoke.test.tsx +99 -0
- package/src/pages/account/NotificationPreferencesPage.tsx +138 -0
- package/src/pages/account/ProfileSettingsPage.tsx +189 -0
- package/src/pages/account/SecuritySettingsPage.tsx +214 -0
- package/src/pages/account/TokensPage.tsx +211 -0
- package/src/pages/account/__tests__/NotificationPreferencesPage.test.tsx +150 -0
- package/src/pages/account/__tests__/ProfileSettingsPage.test.tsx +173 -0
- package/src/pages/account/__tests__/SecuritySettingsPage.test.tsx +212 -0
- package/src/pages/account/__tests__/TokensPage.test.tsx +180 -0
- package/src/pages/admin/AdminSettingsPage.tsx +564 -0
- package/src/pages/admin/AdminShell.tsx +199 -0
- package/src/pages/admin/AuditPage.tsx +338 -0
- package/src/pages/admin/OrdersPage.tsx +642 -0
- package/src/pages/admin/OverviewPage.tsx +559 -0
- package/src/pages/admin/SSOPage.tsx +166 -0
- package/src/pages/admin/UserDetailPage.tsx +641 -0
- package/src/pages/admin/UsersPage.tsx +462 -0
- package/src/pages/admin/WebhookDeliveriesPage.tsx +318 -0
- package/src/pages/admin/WorkspaceDetailPage.tsx +551 -0
- package/src/pages/admin/WorkspacesPage.tsx +299 -0
- package/src/pages/admin/__tests__/AdminSettingsPage.test.tsx +260 -0
- package/src/pages/admin/__tests__/OrdersPageMarkPaid.test.tsx +207 -0
- package/src/pages/admin/__tests__/OverviewPageActivity.test.tsx +213 -0
- package/src/pages/admin/__tests__/SSOPage.test.tsx +128 -0
- package/src/pages/admin/__tests__/UsersPageActions.test.tsx +188 -0
- package/src/pages/admin/__tests__/WorkspaceDetailPageTransfer.test.tsx +170 -0
- package/src/pages/admin/__tests__/WorkspacesPagePlans.test.tsx +138 -0
- package/src/pages/admin/shared.ts +42 -0
- package/src/pages/auth/ForgotPasswordPage.tsx +83 -0
- package/src/pages/auth/InviteAcceptPage.tsx +105 -0
- package/src/pages/auth/RegisterPage.tsx +204 -0
- package/src/pages/auth/ResetPasswordPage.tsx +150 -0
- package/src/pages/auth/VerifyEmailPage.tsx +76 -0
- package/src/pages/auth/__tests__/ForgotPasswordPage.test.tsx +67 -0
- package/src/pages/auth/__tests__/InviteAcceptPage.test.tsx +86 -0
- package/src/pages/auth/__tests__/ResetPasswordPage.test.tsx +115 -0
- package/src/pages/auth/__tests__/VerifyEmailPage.test.tsx +74 -0
- package/src/pages/workspace/BillingPage.tsx +361 -0
- package/src/pages/workspace/BillingSuccessPage.tsx +115 -0
- package/src/pages/workspace/MembersPage.tsx +568 -0
- package/src/pages/workspace/RolePermissionsPage.tsx +202 -0
- package/src/pages/workspace/SchedulesPage.tsx +387 -0
- package/src/pages/workspace/UsagePage.tsx +207 -0
- package/src/pages/workspace/WebhooksPage.tsx +506 -0
- package/src/pages/workspace/WorkspaceAuditPage.tsx +229 -0
- package/src/pages/workspace/WorkspaceGeneralPage.tsx +184 -0
- package/src/pages/workspace/__tests__/BillingPage.test.tsx +302 -0
- package/src/pages/workspace/__tests__/BillingSuccessPage.test.tsx +134 -0
- package/src/pages/workspace/__tests__/MembersPage.test.tsx +347 -0
- package/src/pages/workspace/__tests__/RolePermissionsPage.test.tsx +138 -0
- package/src/pages/workspace/__tests__/SchedulesPage.test.tsx +239 -0
- package/src/pages/workspace/__tests__/UsagePage.test.tsx +223 -0
- package/src/pages/workspace/__tests__/WebhooksPage.test.tsx +309 -0
- package/src/pages/workspace/__tests__/WorkspaceAuditPage.test.tsx +112 -0
- package/src/pages/workspace/__tests__/WorkspaceGeneralPage.test.tsx +164 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
|
|
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, type Column } 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 {
|
|
11
|
+
useAudit, exportAuditCsv,
|
|
12
|
+
type AuditFilters, type PKAuditEvent,
|
|
13
|
+
} from '@ploykit/hooks'
|
|
14
|
+
import { formatDateTime } from '../../lib/utils'
|
|
15
|
+
import { apiErrorMessage } from '../../lib/api-error'
|
|
16
|
+
|
|
17
|
+
const PAGE_SIZE = 20
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
interface FilterDraft {
|
|
21
|
+
action: string
|
|
22
|
+
actorId: string
|
|
23
|
+
resourceType: string
|
|
24
|
+
from: string
|
|
25
|
+
to: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function draftFromParams(p: URLSearchParams): FilterDraft {
|
|
30
|
+
return {
|
|
31
|
+
action: p.get('action') ?? '',
|
|
32
|
+
actorId: p.get('actor_id') ?? '',
|
|
33
|
+
resourceType: p.get('resource_type') ?? '',
|
|
34
|
+
from: p.get('from') ?? '',
|
|
35
|
+
to: p.get('to') ?? '',
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
function paramsFromDraft(d: FilterDraft, offset: number): URLSearchParams {
|
|
41
|
+
const p = new URLSearchParams()
|
|
42
|
+
if (d.action.trim()) p.set('action', d.action.trim())
|
|
43
|
+
if (d.actorId.trim()) p.set('actor_id', d.actorId.trim())
|
|
44
|
+
if (d.resourceType.trim()) p.set('resource_type', d.resourceType.trim())
|
|
45
|
+
if (d.from) p.set('from', d.from)
|
|
46
|
+
if (d.to) p.set('to', d.to)
|
|
47
|
+
if (offset > 0) p.set('offset', String(offset))
|
|
48
|
+
return p
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toFilters(d: FilterDraft): AuditFilters {
|
|
52
|
+
return {
|
|
53
|
+
action: d.action.trim(),
|
|
54
|
+
actorId: d.actorId.trim(),
|
|
55
|
+
resourceType: d.resourceType.trim(),
|
|
56
|
+
from: d.from ? `${d.from}T00:00:00Z` : '',
|
|
57
|
+
to: d.to ? `${d.to}T23:59:59Z` : '',
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WorkspaceAuditPageProps {}
|
|
62
|
+
|
|
63
|
+
export function WorkspaceAuditPage(_props: WorkspaceAuditPageProps = {}) {
|
|
64
|
+
const [searchParams, setSearchParams] = useSearchParams()
|
|
65
|
+
|
|
66
|
+
const [draft, setDraft] = useState<FilterDraft>(() => draftFromParams(searchParams))
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setDraft(draftFromParams(searchParams))
|
|
70
|
+
}, [searchParams])
|
|
71
|
+
const [rangeError, setRangeError] = useState<string | null>(null)
|
|
72
|
+
const appliedDraft = useMemo(() => draftFromParams(searchParams), [searchParams])
|
|
73
|
+
const applied = useMemo(() => toFilters(appliedDraft), [appliedDraft])
|
|
74
|
+
const offset = Math.max(0, Number.parseInt(searchParams.get('offset') ?? '0', 10) || 0)
|
|
75
|
+
|
|
76
|
+
const { wsId, items, total, loading, error, refetch } = useAudit(applied, PAGE_SIZE, offset)
|
|
77
|
+
|
|
78
|
+
const apply = () => {
|
|
79
|
+
|
|
80
|
+
if (draft.from && draft.to && draft.from > draft.to) {
|
|
81
|
+
setRangeError('起始日期不能晚于结束日期')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
setRangeError(null)
|
|
85
|
+
setSearchParams(paramsFromDraft(draft, 0))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const onPageChange = (p: number) => {
|
|
89
|
+
setSearchParams(paramsFromDraft(appliedDraft, Math.max(0, (p - 1) * PAGE_SIZE)))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const exportCsv = async () => {
|
|
93
|
+
try {
|
|
94
|
+
await exportAuditCsv(applied)
|
|
95
|
+
toast.success('CSV 导出已开始下载')
|
|
96
|
+
} catch (e) {
|
|
97
|
+
toast.error(apiErrorMessage(e, '导出失败'))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const columns: Column<PKAuditEvent>[] = [
|
|
102
|
+
{
|
|
103
|
+
key: 'created_at', header: '时间', className: 'w-[16%]',
|
|
104
|
+
render: (e) => <span className="whitespace-nowrap text-muted-foreground">{formatDateTime(e.created_at)}</span>,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: 'actor', header: '操作者', className: 'w-[20%]',
|
|
108
|
+
render: (e) => {
|
|
109
|
+
const email = typeof e.actor_snapshot?.email === 'string' ? e.actor_snapshot.email : ''
|
|
110
|
+
return (
|
|
111
|
+
<div className="min-w-0">
|
|
112
|
+
<p className="truncate">{email || e.actor_id || '—'}</p>
|
|
113
|
+
<p className="text-xs text-muted-foreground">{e.actor_type}</p>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: 'action', header: '动作', className: 'w-[22%]',
|
|
120
|
+
render: (e) => <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">{e.action}</code>,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: 'resource', header: '资源', className: 'w-[42%]',
|
|
124
|
+
render: (e) => (
|
|
125
|
+
<div className="min-w-0">
|
|
126
|
+
<p className="truncate">{e.resource_type}</p>
|
|
127
|
+
{e.resource_id && <p className="truncate font-mono text-xs text-muted-foreground">{e.resource_id}</p>}
|
|
128
|
+
</div>
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
if (!wsId) {
|
|
134
|
+
return (
|
|
135
|
+
<div>
|
|
136
|
+
<PageHeader title="审计日志" description="工作区内的关键操作记录" />
|
|
137
|
+
<PageEmpty label="请先创建并选择一个工作区" />
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div>
|
|
144
|
+
<PageHeader title="审计日志" description="工作区内的关键操作记录(保留最近事件,按时间倒序)" />
|
|
145
|
+
|
|
146
|
+
<div className="mb-4 flex flex-wrap items-end gap-2">
|
|
147
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
148
|
+
<span className="text-muted-foreground">动作</span>
|
|
149
|
+
<Input
|
|
150
|
+
className="w-44"
|
|
151
|
+
placeholder="如 workspace.rename"
|
|
152
|
+
value={draft.action}
|
|
153
|
+
onChange={(e) => setDraft((d) => ({ ...d, action: e.target.value }))}
|
|
154
|
+
/>
|
|
155
|
+
</label>
|
|
156
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
157
|
+
<span className="text-muted-foreground">操作者 ID</span>
|
|
158
|
+
<Input
|
|
159
|
+
className="w-52"
|
|
160
|
+
placeholder="actor uuid"
|
|
161
|
+
value={draft.actorId}
|
|
162
|
+
onChange={(e) => setDraft((d) => ({ ...d, actorId: e.target.value }))}
|
|
163
|
+
/>
|
|
164
|
+
</label>
|
|
165
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
166
|
+
<span className="text-muted-foreground">资源类型</span>
|
|
167
|
+
<Input
|
|
168
|
+
className="w-40"
|
|
169
|
+
placeholder="如 workspace"
|
|
170
|
+
value={draft.resourceType}
|
|
171
|
+
onChange={(e) => setDraft((d) => ({ ...d, resourceType: e.target.value }))}
|
|
172
|
+
/>
|
|
173
|
+
</label>
|
|
174
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
175
|
+
<span className="text-muted-foreground">开始日期</span>
|
|
176
|
+
<Input
|
|
177
|
+
className="w-40"
|
|
178
|
+
type="date"
|
|
179
|
+
value={draft.from}
|
|
180
|
+
onChange={(e) => setDraft((d) => ({ ...d, from: e.target.value }))}
|
|
181
|
+
/>
|
|
182
|
+
</label>
|
|
183
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
184
|
+
<span className="text-muted-foreground">结束日期</span>
|
|
185
|
+
<Input
|
|
186
|
+
className="w-40"
|
|
187
|
+
type="date"
|
|
188
|
+
value={draft.to}
|
|
189
|
+
onChange={(e) => setDraft((d) => ({ ...d, to: e.target.value }))}
|
|
190
|
+
/>
|
|
191
|
+
</label>
|
|
192
|
+
<Button onClick={apply}>
|
|
193
|
+
<Search /> 查询
|
|
194
|
+
</Button>
|
|
195
|
+
<Button variant="outline" onClick={exportCsv}>
|
|
196
|
+
<Download /> 导出 CSV
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* P3-40:日期区间错误就地可见(不静默发起恒空查询) */}
|
|
201
|
+
{rangeError && (
|
|
202
|
+
<p role="alert" className="mb-4 text-sm text-destructive">
|
|
203
|
+
{rangeError}
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{loading ? (
|
|
208
|
+
<PageLoading label="加载审计日志…" />
|
|
209
|
+
) : error ? (
|
|
210
|
+
<PageError message={apiErrorMessage(error, '审计日志加载失败')} onRetry={() => refetch()} />
|
|
211
|
+
) : (
|
|
212
|
+
<>
|
|
213
|
+
<DataTable
|
|
214
|
+
columns={columns}
|
|
215
|
+
rows={items}
|
|
216
|
+
rowKey={(e) => e.id}
|
|
217
|
+
empty={<PageEmpty label="暂无审计记录" hint="调整过滤条件或触发一些操作后再来" />}
|
|
218
|
+
pagination={{
|
|
219
|
+
page: Math.floor(offset / PAGE_SIZE) + 1,
|
|
220
|
+
pageSize: PAGE_SIZE,
|
|
221
|
+
total,
|
|
222
|
+
onPageChange,
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
|
|
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 '@ploykit/hooks'
|
|
15
|
+
import { useWorkspace } from '@ploykit/hooks'
|
|
16
|
+
import { useZodForm } from '@ploykit/hooks'
|
|
17
|
+
import { queryKeys, type PKWorkspace } from '@ploykit/hooks'
|
|
18
|
+
import { apiErrorMessage } from '../../lib/api-error'
|
|
19
|
+
import { roleSatisfies } from '@ploykit/hooks'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const renameSchema = z.object({
|
|
24
|
+
name: z.string().trim().min(1, '请输入工作区名称').max(64, '名称最多 64 字符'),
|
|
25
|
+
})
|
|
26
|
+
type RenameForm = z.infer<typeof renameSchema>
|
|
27
|
+
|
|
28
|
+
export interface WorkspaceGeneralPageProps {
|
|
29
|
+
|
|
30
|
+
exitTo?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function WorkspaceGeneralPage({ exitTo = '/app' }: WorkspaceGeneralPageProps = {}) {
|
|
34
|
+
const { current, clear } = useWorkspace()
|
|
35
|
+
const api = useApi()
|
|
36
|
+
const confirm = useConfirm()
|
|
37
|
+
const navigate = useNavigate()
|
|
38
|
+
const qc = useQueryClient()
|
|
39
|
+
const [saving, setSaving] = useState(false)
|
|
40
|
+
const [deleteOpen, setDeleteOpen] = useState(false)
|
|
41
|
+
|
|
42
|
+
const ws = current
|
|
43
|
+
const form = useZodForm<RenameForm>({
|
|
44
|
+
schema: renameSchema,
|
|
45
|
+
defaultValues: { name: ws?.name ?? '' },
|
|
46
|
+
values: ws ? { name: ws.name } : undefined, // 工作区数据到达后同步进表单
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
if (!ws) {
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<PageHeader title="工作区概况" />
|
|
53
|
+
<PageError message="工作区不存在或尚未选择" onRetry={() => qc.invalidateQueries({ queryKey: queryKeys.workspaceList })} />
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
const canRename = roleSatisfies(ws.role, 'admin')
|
|
61
|
+
const canDelete = roleSatisfies(ws.role, 'owner')
|
|
62
|
+
|
|
63
|
+
const refreshList = async () => {
|
|
64
|
+
await qc.invalidateQueries({ queryKey: queryKeys.workspace })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const handleRename = form.handleSubmit(async (values) => {
|
|
68
|
+
setSaving(true)
|
|
69
|
+
try {
|
|
70
|
+
await api.patch<PKWorkspace>(`/api/workspaces/${ws.id}`, { name: values.name })
|
|
71
|
+
await refreshList()
|
|
72
|
+
toast.success('工作区名称已更新')
|
|
73
|
+
} catch (e) {
|
|
74
|
+
toast.error(apiErrorMessage(e, '重命名失败'))
|
|
75
|
+
} finally {
|
|
76
|
+
setSaving(false)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const afterExit = async () => {
|
|
81
|
+
clear()
|
|
82
|
+
await refreshList()
|
|
83
|
+
navigate(exitTo)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleLeave = async () => {
|
|
87
|
+
if (!(await confirm({
|
|
88
|
+
title: '退出工作区',
|
|
89
|
+
description: `退出后将失去「${ws.name}」的访问权限;你创建的数据会保留在工作区内。`,
|
|
90
|
+
confirmText: '退出', danger: true,
|
|
91
|
+
}))) return
|
|
92
|
+
try {
|
|
93
|
+
await api.post(`/api/workspaces/${ws.id}/leave`)
|
|
94
|
+
toast.success('已退出工作区')
|
|
95
|
+
await afterExit()
|
|
96
|
+
} catch (e) {
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
toast.error(is409(e)
|
|
101
|
+
? '最后一个所有者不能离开工作区,请先转移所有权或删除工作区'
|
|
102
|
+
: apiErrorMessage(e, '退出失败'))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const handleDelete = async () => {
|
|
107
|
+
try {
|
|
108
|
+
await api.delete(`/api/workspaces/${ws.id}`)
|
|
109
|
+
toast.success('工作区已删除')
|
|
110
|
+
await afterExit()
|
|
111
|
+
} catch (e) {
|
|
112
|
+
|
|
113
|
+
toast.error(apiErrorMessage(e, '删除失败'))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
<PageHeader title="工作区概况" description={`工作区 ${ws.name}(${ws.slug})的基础设置`} />
|
|
120
|
+
|
|
121
|
+
<SettingsCard
|
|
122
|
+
title="基础信息"
|
|
123
|
+
description="工作区名称对成员可见;slug 是 URL 标识,创建后不可修改。"
|
|
124
|
+
>
|
|
125
|
+
<form className="max-w-md space-y-4" onSubmit={handleRename}>
|
|
126
|
+
<FormField label="工作区名称" error={form.formState.errors.name?.message} htmlFor="ws-name">
|
|
127
|
+
<Input id="ws-name" disabled={!canRename} {...form.register('name')} />
|
|
128
|
+
</FormField>
|
|
129
|
+
<FormField label="Slug(只读)" htmlFor="ws-slug">
|
|
130
|
+
<Input id="ws-slug" value={ws.slug} readOnly disabled />
|
|
131
|
+
</FormField>
|
|
132
|
+
{canRename && (
|
|
133
|
+
<Button type="submit" disabled={saving}>{saving ? '保存中…' : '保存修改'}</Button>
|
|
134
|
+
)}
|
|
135
|
+
</form>
|
|
136
|
+
</SettingsCard>
|
|
137
|
+
|
|
138
|
+
<SettingsCard danger title="危险区" description="以下操作影响全部成员,请谨慎执行。">
|
|
139
|
+
<div className="space-y-4">
|
|
140
|
+
<div className="flex flex-row items-center justify-between gap-4 rounded-lg border p-4">
|
|
141
|
+
<div>
|
|
142
|
+
<p className="text-sm font-medium">退出工作区</p>
|
|
143
|
+
<p className="mt-0.5 text-sm text-muted-foreground">
|
|
144
|
+
退出后你将不再是「{ws.name}」的成员。最后一位 owner 无法退出。
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
<Button variant="outline" onClick={handleLeave}>退出工作区</Button>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{canDelete && (
|
|
151
|
+
<div className="flex flex-row items-center justify-between gap-4 rounded-lg border border-destructive/40 p-4">
|
|
152
|
+
<div>
|
|
153
|
+
<p className="text-sm font-medium text-destructive">删除工作区</p>
|
|
154
|
+
<p className="mt-0.5 text-sm text-muted-foreground">
|
|
155
|
+
硬删除工作区及其全部数据,操作不可恢复。
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
<Button variant="destructive" onClick={() => setDeleteOpen(true)}>删除工作区</Button>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
</SettingsCard>
|
|
163
|
+
|
|
164
|
+
<ImpactConfirmation
|
|
165
|
+
open={deleteOpen}
|
|
166
|
+
onOpenChange={setDeleteOpen}
|
|
167
|
+
onConfirm={handleDelete}
|
|
168
|
+
title={`删除工作区「${ws.name}」`}
|
|
169
|
+
impacts={[
|
|
170
|
+
'工作区全部数据将被永久删除,且不可恢复',
|
|
171
|
+
'所有成员立即失去访问权限',
|
|
172
|
+
'未撤销的邀请与分享链接同时失效',
|
|
173
|
+
'历史审计记录将保留(合规要求),但不再关联可访问的工作区',
|
|
174
|
+
]}
|
|
175
|
+
confirmText="永久删除"
|
|
176
|
+
danger
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function is409(e: unknown): boolean {
|
|
183
|
+
return typeof e === 'object' && e !== null && 'status' in e && (e as { status?: number }).status === 409
|
|
184
|
+
}
|