✨ refactor(web/default): adopt drill-in sidebar pattern for System Settings
Replace the ad-hoc "workspace" abstraction with a focused, URL-driven
"sidebar view" registry that implements the modern Vercel / Cloudflare
drill-in pattern: clicking a top-level entry (e.g. System Settings)
swaps the sidebar to a contextual workspace, with a `← Back to
Dashboard` affordance, instead of stacking sub-navigation in the root.
Architecture
------------
- types.ts
+ SidebarView — declarative nested view config
(id, pathPattern, parent, getNavGroups)
+ SidebarViewParent — back-navigation descriptor
+ ResolvedSidebarView — { key, view, navGroups } returned by hook
+ SidebarData — slimmed to { navGroups } only
- Workspace — removed (logo/plan never rendered)
- lib/sidebar-view-registry.ts (new, replaces workspace-registry.ts)
+ SIDEBAR_VIEWS array — single source of truth for nested views
+ resolveSidebarView(pathname)
+ getNavGroupsForPath(pathname, t) — back-compat helper for the
command palette
- config/system-settings.config.ts
Refactored to export a single SYSTEM_SETTINGS_VIEW (SidebarView)
with parent `/dashboard/overview` + label `Back to Dashboard`.
- components/sidebar-view-header.tsx (new)
Renders only the back affordance (chevron + label). Uses the
default SidebarMenuButton size so its typography matches the
nav items below; collapses gracefully into icon mode via the
existing tooltip behavior. The redundant "title + icon" row was
removed — workspace context is already carried by the nav groups.
- hooks/use-sidebar-view.ts (new)
Encapsulates view resolution and root-nav filtering:
· matched view → returns its nav groups verbatim (route-level
beforeLoad guards already enforce access);
· no match → returns root nav groups, narrowed by user
role (admin gate) and useSidebarConfig
(admin × user sidebar_modules overlay).
- components/app-sidebar.tsx
Now a thin presentation layer: reads { key, view, navGroups }
from useSidebarView() and orchestrates the view transition via
AnimatePresence + MOTION_VARIANTS.sidebarSlide (respects
prefers-reduced-motion). No logic, no role checks, no path
matching — those live in the hook.
- components/command-menu.tsx
Switched to the new getNavGroupsForPath() API; behavior preserved.
Cleanup
-------
- Deleted layout/context/workspace-context.tsx (zero consumers).
- Deleted layout/lib/workspace-registry.ts and its
workspace-registry.example.ts companion (over-abstracted: name/id
metadata, isInWorkspace / getAllWorkspaces / WORKSPACE_IDS were
registered but never read).
- Removed `workspaces` field from useSidebarData (never consumed
after the top-switcher was dropped).
- Dropped WorkspaceProvider from authenticated-layout.tsx.
- Trimmed dead `Manage and configure` translation key from all six
locale files and from static-keys.ts.
i18n
----
Added the `Back to Dashboard` key to en, zh, fr, ja, ru, vi, and
registered it in static-keys.ts under "Sidebar views".
Verification
------------
- bun run typecheck: passes
- Lint: no new warnings/errors on the touched files
- Adding a new drill-in workspace now only requires registering a
SidebarView in SIDEBAR_VIEWS — no changes to AppSidebar required.
This commit is contained in:
+2
-8
@@ -441,9 +441,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
{sensitiveVisible ? log.username : '••••'}
|
||||
</TooltipTrigger>
|
||||
{sensitiveVisible && log.username.length > 12 && (
|
||||
<TooltipContent side='top'>
|
||||
{log.username}
|
||||
</TooltipContent>
|
||||
<TooltipContent side='top'>{log.username}</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -484,11 +482,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
<div className='flex max-w-[200px] flex-col gap-0.5'>
|
||||
<TooltipProvider delay={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div className='max-w-full' />
|
||||
}
|
||||
>
|
||||
<TooltipTrigger render={<div className='max-w-full' />}>
|
||||
<StatusBadge
|
||||
label={displayName}
|
||||
icon={KeyRound}
|
||||
|
||||
@@ -297,9 +297,7 @@ export function CommonLogsFilterBar<TData>(
|
||||
<Input
|
||||
placeholder={t('Upstream Request ID')}
|
||||
value={filters.upstreamRequestId || ''}
|
||||
onChange={(e) =>
|
||||
handleChange('upstreamRequestId', e.target.value)
|
||||
}
|
||||
onChange={(e) => handleChange('upstreamRequestId', e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={inputClass}
|
||||
/>
|
||||
|
||||
+27
-29
@@ -986,35 +986,33 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
||||
)}
|
||||
|
||||
{/* Param override */}
|
||||
{other?.po &&
|
||||
Array.isArray(other.po) &&
|
||||
other.po.length > 0 && (
|
||||
<DetailSection
|
||||
icon={<Settings2 className='size-3.5' aria-hidden='true' />}
|
||||
label={`${t('Param Override')} (${other.po.length})`}
|
||||
>
|
||||
{other.po.filter(Boolean).map((line, idx) => {
|
||||
const parsed = parseAuditLine(line)
|
||||
if (!parsed) return null
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className='bg-background/60 flex min-w-0 flex-col gap-1.5 rounded border p-2 sm:flex-row sm:items-start sm:gap-2'
|
||||
>
|
||||
<StatusBadge
|
||||
variant='neutral'
|
||||
label={getParamOverrideActionLabel(parsed.action, t)}
|
||||
className='shrink-0 font-medium'
|
||||
copyable={false}
|
||||
/>
|
||||
<span className='min-w-0 font-mono text-[11px] leading-relaxed break-all sm:break-words'>
|
||||
{parsed.content}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DetailSection>
|
||||
)}
|
||||
{other?.po && Array.isArray(other.po) && other.po.length > 0 && (
|
||||
<DetailSection
|
||||
icon={<Settings2 className='size-3.5' aria-hidden='true' />}
|
||||
label={`${t('Param Override')} (${other.po.length})`}
|
||||
>
|
||||
{other.po.filter(Boolean).map((line, idx) => {
|
||||
const parsed = parseAuditLine(line)
|
||||
if (!parsed) return null
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className='bg-background/60 flex min-w-0 flex-col gap-1.5 rounded border p-2 sm:flex-row sm:items-start sm:gap-2'
|
||||
>
|
||||
<StatusBadge
|
||||
variant='neutral'
|
||||
label={getParamOverrideActionLabel(parsed.action, t)}
|
||||
className='shrink-0 font-medium'
|
||||
copyable={false}
|
||||
/>
|
||||
<span className='min-w-0 font-mono text-[11px] leading-relaxed break-all sm:break-words'>
|
||||
{parsed.content}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DetailSection>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{details && (
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ function UsageLogsContent() {
|
||||
<div className='space-y-4'>
|
||||
{showTaskSwitcher && (
|
||||
<Tabs value={activeCategory} onValueChange={handleSectionChange}>
|
||||
<TabsList className='group-data-horizontal/tabs:h-auto max-w-full flex-wrap justify-start'>
|
||||
<TabsList className='max-w-full flex-wrap justify-start group-data-horizontal/tabs:h-auto'>
|
||||
{visibleSections.map((section) => (
|
||||
<TabsTrigger key={section} value={section}>
|
||||
{t(SECTION_META[section].titleKey)}
|
||||
|
||||
Reference in New Issue
Block a user