feat(admin): add audit logs placeholder

This commit is contained in:
saltbo
2026-04-27 20:55:14 -04:00
parent d7f1ceb6bc
commit bf83be3634
9 changed files with 106 additions and 5 deletions
+1
View File
@@ -128,6 +128,7 @@ export const FEATURE_REGISTRY = [
category: 'pro',
community: false,
pro: true,
gateKey: 'audit_log',
comingSoon: true,
},
{
+1
View File
@@ -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 {
+13 -2
View File
@@ -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 {
+6
View File
@@ -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",
+6
View File
@@ -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": "待使用",
+21
View File
@@ -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,
+41
View File
@@ -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')
})
})