mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-24 23:22:31 +08:00
feat(admin): add audit logs placeholder
This commit is contained in:
@@ -128,6 +128,7 @@ export const FEATURE_REGISTRY = [
|
||||
category: 'pro',
|
||||
community: false,
|
||||
pro: true,
|
||||
gateKey: 'audit_log',
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ const FEATURE_LABELS: Record<ProFeature, string> = {
|
||||
open_registration: 'open registration',
|
||||
teams_unlimited: 'unlimited teams',
|
||||
storages_unlimited: 'unlimited storages',
|
||||
audit_log: 'audit logs',
|
||||
}
|
||||
|
||||
interface UpgradeHintProps {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { ArrowLeft, BadgeCheck, Database, KeyRound, LayoutDashboard, Mail, Settings, Users } from 'lucide-react'
|
||||
import {
|
||||
ArrowLeft,
|
||||
BadgeCheck,
|
||||
Database,
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Mail,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -19,9 +29,10 @@ const adminNavItems = [
|
||||
{ titleKey: 'admin.nav.overview', url: '/admin', icon: LayoutDashboard },
|
||||
{ titleKey: 'admin.nav.users', url: '/admin/users', icon: Users },
|
||||
{ titleKey: 'admin.nav.storages', url: '/admin/storages', icon: Database },
|
||||
{ titleKey: 'admin.nav.settings', url: '/admin/settings', icon: Settings },
|
||||
{ titleKey: 'admin.nav.auth', url: '/admin/settings/oauth', icon: KeyRound },
|
||||
{ titleKey: 'admin.nav.email', url: '/admin/settings/email', icon: Mail },
|
||||
{ titleKey: 'admin.nav.settings', url: '/admin/settings', icon: Settings },
|
||||
{ titleKey: 'admin.nav.audit', url: '/admin/audit', icon: ShieldCheck },
|
||||
{ titleKey: 'admin.nav.licensing', url: '/admin/licensing', icon: BadgeCheck },
|
||||
]
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ const FEATURE_LABELS: Record<ProFeature, string> = {
|
||||
open_registration: 'Open registration',
|
||||
teams_unlimited: 'Unlimited teams',
|
||||
storages_unlimited: 'Unlimited storages',
|
||||
audit_log: 'Audit logs',
|
||||
}
|
||||
|
||||
function formatTimestamp(ts: number | undefined, fallback: string): string {
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"admin.nav.management": "Management",
|
||||
"admin.nav.overview": "Overview",
|
||||
"admin.nav.storages": "Storages",
|
||||
"admin.nav.audit": "Audit Logs",
|
||||
"admin.nav.users": "Users",
|
||||
"admin.nav.settings": "Settings",
|
||||
"admin.nav.email": "Email",
|
||||
@@ -158,6 +159,11 @@
|
||||
"admin.users.inviteColExpiresAt": "Expires At",
|
||||
"admin.users.copyInviteLink": "Copy Invite Link",
|
||||
"admin.users.inviteLinkCopied": "Invite link copied to clipboard",
|
||||
"admin.audit.title": "Audit Logs",
|
||||
"admin.audit.description": "Track administrative and user activity across the instance.",
|
||||
"admin.audit.proTooltip": "Audit Logs are available with ZPan Pro.",
|
||||
"admin.audit.placeholderTitle": "Audit Logs Coming Soon",
|
||||
"admin.audit.placeholderDescription": "This page is reserved for the future audit log experience.",
|
||||
"admin.users.resendInvite": "Resend Invite",
|
||||
"admin.users.revokeInvite": "Revoke Invite",
|
||||
"admin.users.inviteStatus.pending": "Pending",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"admin.nav.management": "管理",
|
||||
"admin.nav.overview": "概览",
|
||||
"admin.nav.storages": "存储",
|
||||
"admin.nav.audit": "审计日志",
|
||||
"admin.nav.users": "用户",
|
||||
"admin.nav.settings": "设置",
|
||||
"admin.nav.email": "邮件",
|
||||
@@ -158,6 +159,11 @@
|
||||
"admin.users.inviteColExpiresAt": "过期时间",
|
||||
"admin.users.copyInviteLink": "复制邀请链接",
|
||||
"admin.users.inviteLinkCopied": "邀请链接已复制到剪贴板",
|
||||
"admin.audit.title": "审计日志",
|
||||
"admin.audit.description": "跟踪实例中的管理和用户操作。",
|
||||
"admin.audit.proTooltip": "审计日志仅在 ZPan Pro 中可用。",
|
||||
"admin.audit.placeholderTitle": "审计日志即将推出",
|
||||
"admin.audit.placeholderDescription": "此页面已预留给未来的审计日志体验。",
|
||||
"admin.users.resendInvite": "重新发送邀请",
|
||||
"admin.users.revokeInvite": "撤销邀请",
|
||||
"admin.users.inviteStatus.pending": "待使用",
|
||||
|
||||
@@ -33,6 +33,7 @@ import { Route as AuthenticatedSettingsPasswordRouteImport } from './routes/_aut
|
||||
import { Route as AuthenticatedSettingsIhostRouteImport } from './routes/_authenticated/settings/ihost'
|
||||
import { Route as AuthenticatedSettingsAppearanceRouteImport } from './routes/_authenticated/settings/appearance'
|
||||
import { Route as AuthenticatedAdminLicensingRouteImport } from './routes/_authenticated/admin/licensing'
|
||||
import { Route as AuthenticatedAdminAuditRouteImport } from './routes/_authenticated/admin/audit'
|
||||
import { Route as AuthenticatedTeamsTeamIdRouteRouteImport } from './routes/_authenticated/teams/$teamId/route'
|
||||
import { Route as AuthenticatedTeamsTeamIdIndexRouteImport } from './routes/_authenticated/teams/$teamId/index'
|
||||
import { Route as AuthenticatedAdminUsersIndexRouteImport } from './routes/_authenticated/admin/users/index'
|
||||
@@ -174,6 +175,11 @@ const AuthenticatedAdminLicensingRoute =
|
||||
path: '/licensing',
|
||||
getParentRoute: () => AuthenticatedAdminRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedAdminAuditRoute = AuthenticatedAdminAuditRouteImport.update({
|
||||
id: '/audit',
|
||||
path: '/audit',
|
||||
getParentRoute: () => AuthenticatedAdminRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedTeamsTeamIdRouteRoute =
|
||||
AuthenticatedTeamsTeamIdRouteRouteImport.update({
|
||||
id: '/teams/$teamId',
|
||||
@@ -245,6 +251,7 @@ export interface FileRoutesByFullPath {
|
||||
'/s/$token': typeof STokenRoute
|
||||
'/u/$username': typeof UUsernameRoute
|
||||
'/teams/$teamId': typeof AuthenticatedTeamsTeamIdRouteRouteWithChildren
|
||||
'/admin/audit': typeof AuthenticatedAdminAuditRoute
|
||||
'/admin/licensing': typeof AuthenticatedAdminLicensingRoute
|
||||
'/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
|
||||
'/settings/ihost': typeof AuthenticatedSettingsIhostRoute
|
||||
@@ -277,6 +284,7 @@ export interface FileRoutesByTo {
|
||||
'/s/$token': typeof STokenRoute
|
||||
'/u/$username': typeof UUsernameRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/admin/audit': typeof AuthenticatedAdminAuditRoute
|
||||
'/admin/licensing': typeof AuthenticatedAdminLicensingRoute
|
||||
'/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
|
||||
'/settings/ihost': typeof AuthenticatedSettingsIhostRoute
|
||||
@@ -314,6 +322,7 @@ export interface FileRoutesById {
|
||||
'/u/$username': typeof UUsernameRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/teams/$teamId': typeof AuthenticatedTeamsTeamIdRouteRouteWithChildren
|
||||
'/_authenticated/admin/audit': typeof AuthenticatedAdminAuditRoute
|
||||
'/_authenticated/admin/licensing': typeof AuthenticatedAdminLicensingRoute
|
||||
'/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
|
||||
'/_authenticated/settings/ihost': typeof AuthenticatedSettingsIhostRoute
|
||||
@@ -351,6 +360,7 @@ export interface FileRouteTypes {
|
||||
| '/s/$token'
|
||||
| '/u/$username'
|
||||
| '/teams/$teamId'
|
||||
| '/admin/audit'
|
||||
| '/admin/licensing'
|
||||
| '/settings/appearance'
|
||||
| '/settings/ihost'
|
||||
@@ -383,6 +393,7 @@ export interface FileRouteTypes {
|
||||
| '/s/$token'
|
||||
| '/u/$username'
|
||||
| '/'
|
||||
| '/admin/audit'
|
||||
| '/admin/licensing'
|
||||
| '/settings/appearance'
|
||||
| '/settings/ihost'
|
||||
@@ -419,6 +430,7 @@ export interface FileRouteTypes {
|
||||
| '/u/$username'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/teams/$teamId'
|
||||
| '/_authenticated/admin/audit'
|
||||
| '/_authenticated/admin/licensing'
|
||||
| '/_authenticated/settings/appearance'
|
||||
| '/_authenticated/settings/ihost'
|
||||
@@ -623,6 +635,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedAdminLicensingRouteImport
|
||||
parentRoute: typeof AuthenticatedAdminRouteRoute
|
||||
}
|
||||
'/_authenticated/admin/audit': {
|
||||
id: '/_authenticated/admin/audit'
|
||||
path: '/audit'
|
||||
fullPath: '/admin/audit'
|
||||
preLoaderRoute: typeof AuthenticatedAdminAuditRouteImport
|
||||
parentRoute: typeof AuthenticatedAdminRouteRoute
|
||||
}
|
||||
'/_authenticated/teams/$teamId': {
|
||||
id: '/_authenticated/teams/$teamId'
|
||||
path: '/teams/$teamId'
|
||||
@@ -697,6 +716,7 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
|
||||
interface AuthenticatedAdminRouteRouteChildren {
|
||||
AuthenticatedAdminAuditRoute: typeof AuthenticatedAdminAuditRoute
|
||||
AuthenticatedAdminLicensingRoute: typeof AuthenticatedAdminLicensingRoute
|
||||
AuthenticatedAdminIndexRoute: typeof AuthenticatedAdminIndexRoute
|
||||
AuthenticatedAdminSettingsEmailRoute: typeof AuthenticatedAdminSettingsEmailRoute
|
||||
@@ -708,6 +728,7 @@ interface AuthenticatedAdminRouteRouteChildren {
|
||||
|
||||
const AuthenticatedAdminRouteRouteChildren: AuthenticatedAdminRouteRouteChildren =
|
||||
{
|
||||
AuthenticatedAdminAuditRoute: AuthenticatedAdminAuditRoute,
|
||||
AuthenticatedAdminLicensingRoute: AuthenticatedAdminLicensingRoute,
|
||||
AuthenticatedAdminIndexRoute: AuthenticatedAdminIndexRoute,
|
||||
AuthenticatedAdminSettingsEmailRoute: AuthenticatedAdminSettingsEmailRoute,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { ShieldCheck } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ProBadge } from '@/components/ProBadge'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/admin/audit')({
|
||||
component: AuditLogsPage,
|
||||
})
|
||||
|
||||
function AuditLogsPage() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-semibold">{t('admin.audit.title')}</h2>
|
||||
<ProBadge tooltip={t('admin.audit.proTooltip')} />
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">{t('admin.audit.description')}</p>
|
||||
|
||||
<Card className="border-dashed">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-2xl border border-border/60 bg-primary/10 p-2 text-primary">
|
||||
<ShieldCheck className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{t('admin.audit.placeholderTitle')}</CardTitle>
|
||||
<CardDescription>{t('admin.audit.placeholderDescription')}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Audit Logs UI placeholder only. No backend events are wired yet.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -65,18 +65,25 @@ describe('resolveBillingView', () => {
|
||||
// Pro features — label mapping in BoundStatusCard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type ProFeature = 'white_label' | 'open_registration' | 'teams_unlimited' | 'storages_unlimited'
|
||||
type ProFeature = 'white_label' | 'open_registration' | 'teams_unlimited' | 'storages_unlimited' | 'audit_log'
|
||||
|
||||
const FEATURE_LABELS: Record<ProFeature, string> = {
|
||||
white_label: 'White-label branding',
|
||||
open_registration: 'Open registration',
|
||||
teams_unlimited: 'Unlimited teams',
|
||||
storages_unlimited: 'Unlimited storages',
|
||||
audit_log: 'Audit logs',
|
||||
}
|
||||
|
||||
describe('FEATURE_LABELS', () => {
|
||||
it('maps all known ProFeature keys', () => {
|
||||
const features: ProFeature[] = ['white_label', 'open_registration', 'teams_unlimited', 'storages_unlimited']
|
||||
const features: ProFeature[] = [
|
||||
'white_label',
|
||||
'open_registration',
|
||||
'teams_unlimited',
|
||||
'storages_unlimited',
|
||||
'audit_log',
|
||||
]
|
||||
for (const f of features) {
|
||||
expect(FEATURE_LABELS[f]).toBeTruthy()
|
||||
}
|
||||
@@ -89,6 +96,10 @@ describe('FEATURE_LABELS', () => {
|
||||
it('storages_unlimited maps to readable label', () => {
|
||||
expect(FEATURE_LABELS.storages_unlimited).toBe('Unlimited storages')
|
||||
})
|
||||
|
||||
it('audit_log maps to readable label', () => {
|
||||
expect(FEATURE_LABELS.audit_log).toBe('Audit logs')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -142,6 +153,7 @@ const COMPARISON_ROWS: FeatureRow[] = [
|
||||
{ label: 'Unlimited teams', community: false, pro: true },
|
||||
{ label: 'Open registration', community: false, pro: true },
|
||||
{ label: 'Unlimited storages', community: false, pro: true },
|
||||
{ label: 'Audit logs', community: false, pro: true },
|
||||
{ label: 'White-label branding', community: false, pro: true },
|
||||
]
|
||||
|
||||
@@ -161,11 +173,12 @@ describe('ComparisonTable rows', () => {
|
||||
expect(communityOnlyRows).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('all 4 Pro-exclusive features are represented', () => {
|
||||
it('all 5 Pro-exclusive features are represented', () => {
|
||||
const proOnlyLabels = COMPARISON_ROWS.filter((r) => !r.community).map((r) => r.label)
|
||||
expect(proOnlyLabels).toContain('Unlimited teams')
|
||||
expect(proOnlyLabels).toContain('Open registration')
|
||||
expect(proOnlyLabels).toContain('Unlimited storages')
|
||||
expect(proOnlyLabels).toContain('Audit logs')
|
||||
expect(proOnlyLabels).toContain('White-label branding')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user