Skip to content
bitzorcas
中EN

Recipe

Practical Guide: Add a Dashboard Management Page (React 19)

Master building enterprise management dashboard pages in BitzOrcas.Modern React 19 + TanStack Query: route registration, OpenAPI SDK generation, metadata table hydration, and RBAC permission gates.

Last updated

In traditional single-page architectures, creating a data management dashboard page frequently introduces severe maintenance overhead: frontend developers manually transcribe Swagger definitions into hand-crafted TypeScript interfaces and ad-hoc fetch functions. When backend models change, the mismatch often escapes undetected until a runtime white screen occurs in production. Worse, developers often stash roles and permissions in localStorage and perform client-only checks—violating the cardinal rule that the backend is the sole source of truth and opening security vulnerabilities.

BitzOrcas.Modern frontend architecture (located at frontend/apps/app) eliminates these pitfalls through strict contract-driven design and governed systems:

  1. Zero-Handwritten OpenAPI SDK (@bitz/platform-sdk): Backend Minimal APIs compile down to OpenAPI 3.0 artifacts, from which a typed client and TypeScript DTOs are synchronized via a single script;
  2. Tamper-Proof Credential Lifecycle: Web refresh tokens are managed via backend HttpOnly cookies (WebRefreshCookieService), while access tokens reside purely in JavaScript memory—eliminating XSS theft and client privilege escalation;
  3. Automated Metadata Hydration: Tables leverage __meta projections attached to API responses to resolve localized dictionary labels and related entity titles with zero additional N+1 queries;
  4. Frozen Design System v1: All pages strictly consume @bitz/components and @bitz/widgets, prohibiting arbitrary CSS variables or undocumented UI patterns.

This guide demonstrates building a production-ready Civil Litigation Matter Intake Dashboard (MatterIntakeDashboardPage) from route declaration and SDK synchronization to metadata hydration and fine-grained RBAC action gates.

Frontend Page Development Sequence

".NET 10 API Host (/api/legal/matters)""@bitz/widgets (ServerDataTable)""MatterIntakeDashboardPage.tsx""routes.tsx (AppRouteConfig)""@bitz/platform-sdk""sync-openapi-sdk.sh"".NET 10 API Host (/api/legal/matters)""@bitz/widgets (ServerDataTable)""MatterIntakeDashboardPage.tsx""routes.tsx (AppRouteConfig)""@bitz/platform-sdk""sync-openapi-sdk.sh""Frontend Developer"1. Run scripts/build/sync-openapi-sdk.sh12. Export OpenAPI 3.0 specification artifact23. Generate typed api.listLegalMatters & LegalMatterSummaryDto34. Register route node, RBAC action code & workspace parent45. Compose usePlatform with @tanstack/react-query56. Dispatch paginated request (in-memory JWT & tenant context)67. Return typed payload accompanied by __meta projections78. Render governed dashboard with permission gates and hydration8"Frontend Developer"

Step 1: Declare Route Metadata and Permission Boundaries

Register the new page in frontend/apps/app/src/routes.tsx. BitzOrcas uses declarative route configuration where business pages reside within the authenticated workspace shell (authenticated-workspace-shell), explicitly declaring navigation ownership and RBAC permissions.

frontend/apps/app/src/routes.tsx (Excerpt)
import { Navigate } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';
import App from './App';
import { RouteError } from './components/route-error';
import { RouteGuard } from './components/route-guard';
import { RouteLoading } from './components/route-loading';
export type AppPermission = string;
export type AppSurface = 'tenant' | 'host' | 'shared';
export type AppRouteMeta = {
/** Parent menu code for workspace shell highlight and context persistence */
navigationCode?: string;
/** Stable guidance key for interactive product tours */
guidanceKey?: string;
/** Granular RBAC action codes required to access this route */
permission?: AppPermission[];
/** Permission evaluation mode: 'all' requires every action, 'any' requires at least one */
permissionMode?: 'all' | 'any';
/** Whether the route requires an authenticated session */
requiresAuth?: boolean;
/** Layout profile: 'identity' for standalone auth screens, 'workspace' for operational app */
layout?: 'identity' | 'workspace';
/** Operating surface: 'tenant' client, 'host' SaaS administration, or 'shared' */
surface?: AppSurface;
/** Human-readable page title for browser tabs and breadcrumbs */
title?: string;
};
export type AppRouteConfig = Omit<RouteObject, 'children' | 'lazy'> & {
lazy?: RouteObject['lazy'] | string;
meta?: AppRouteMeta;
hide?: boolean;
children?: AppRouteConfig[];
};
export const routes: AppRouteConfig[] = [
{
path: '/',
element: <App />,
errorElement: <RouteError />,
HydrateFallback: RouteLoading,
children: [
{
element: <RouteGuard />,
meta: { requiresAuth: true },
children: [
{
// AppShell layout: renders sidebar, header tenant switcher, and active user state
lazy: './components/authenticated-workspace-shell',
children: [
// --- Legal Practice Management Sub-tree ---
{
path: 'legal/matters',
lazy: async () => {
const { MatterIntakeDashboardPage } = await import(
'./pages/legal/matters/matter-intake-dashboard-page'
);
return { Component: MatterIntakeDashboardPage };
},
meta: {
title: 'Matter Intake Dashboard',
requiresAuth: true,
surface: 'tenant',
navigationCode: 'workspace.legal-matters',
// Backend Authorization remains the sole source of truth
permission: ['legal.matters.view'],
guidanceKey: 'legal-matters-intake-dashboard',
},
},
],
},
],
},
],
},
];

Step 2: Synchronize the OpenAPI SDK Contract

Whenever the backend introduces a new Minimal API endpoint (e.g. api/legal/matters), run the synchronization script:

Terminal window
# Export OpenAPI specification and regenerate @bitz/platform-sdk typed client
./scripts/build/sync-openapi-sdk.sh

This script reads artifacts/openapi/openapi-v1.json and updates frontend/packages/platform-sdk with full TypeScript definitions:

  • api.listLegalMatters(params)
  • interface LegalMatterSummaryDto
  • interface LegalMatterFilterRequest

[!CAUTION] Strict Architecture Red Lines:

  1. Never manually edit generated code inside packages/platform-sdk;
  2. Never manually declare backend DTO interface shapes in page code;
  3. Never execute raw fetch() or axios calls directly from React components. All network requests must flow through usePlatform().api.

Step 3: Implement the Production-Grade Dashboard Page

Create frontend/apps/app/src/pages/legal/matters/matter-intake-dashboard-page.tsx. This implementation strictly follows Frozen Design System v1 and @bitz/widgets compound patterns:

frontend/apps/app/src/pages/legal/matters/matter-intake-dashboard-page.tsx
import { useEffect, useMemo, useState, type JSX } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { usePlatform, type LegalMatterSummaryDto } from '@bitz/platform-sdk';
import { useT } from '@bitz/i18n';
import {
DataTableObjectCell,
InlineNotice,
OperationField,
PermissionGate,
QueryToolbar,
ServerDataTable,
StatusBadge,
WorkspacePage,
type DataTableColumn,
type StatusTone,
} from '@bitz/widgets';
import { Button, Input, NativeSelect, NativeSelectOption } from '@bitz/components';
import {
MetadataListSearchPanel,
readMetadataString,
useMetadataListSearchPanel,
} from '../../../components/metadata-list-search-panel';
import { WorkspaceContextHeader } from '../../../components/workspace-context-header';
import { useListQueryState } from '../../../utils/list-query-state';
/** Query key constant for TanStack Query caching and localized cache invalidation */
const MATTERS_QUERY_KEY = ['legal', 'matters', 'dashboard'] as const;
/** Allowed matter status filter values */
const MATTER_STATUS_OPTIONS = ['Draft', 'PendingReview', 'Active', 'Closed'] as const;
/** Parameter mapping configuration for metadata-driven search panel */
const MATTER_SEARCH_PANEL_OPTIONS = {
paramNames: {
SearchText: 'q',
Status: 'status',
},
defaultPageSize: 20,
} as const;
/**
* Civil Litigation Matter Intake Dashboard Component
*/
export function MatterIntakeDashboardPage(): JSX.Element {
const { api, currentUser } = usePlatform();
const t = useT();
const queryClient = useQueryClient();
// 1. Pagination and URL query state synchronization
const { state: listState, update: updateListState } = useListQueryState({
allowedStatuses: MATTER_STATUS_OPTIONS,
});
// 2. Metadata-driven search panel hook
const metadataSearch = useMetadataListSearchPanel(
'Legal.MatterSummaryList',
MATTER_SEARCH_PANEL_OPTIONS,
);
const activeKeyword = metadataSearch.panelEnabled
? readMetadataString(metadataSearch.values, 'SearchText')
: listState.keyword;
const activeStatus = metadataSearch.panelEnabled
? readMetadataString(metadataSearch.values, 'Status')
: listState.status;
const pageIndex = metadataSearch.panelEnabled ? metadataSearch.pageIndex : listState.pageIndex;
const pageSize = metadataSearch.panelEnabled ? metadataSearch.pageSize : listState.pageSize;
const [keywordDraft, setKeywordDraft] = useState(activeKeyword);
const [statusDraft, setStatusDraft] = useState(activeStatus);
const [selectedMatterId, setSelectedMatterId] = useState<string | null>(null);
// 3. Current user permissions evaluation (read-only client reflection of backend claims)
const permissions = useMemo(() => new Set(currentUser?.permissions ?? []), [currentUser]);
const canCreate = permissions.has('legal.matters.create');
const canAdvanceStage = permissions.has('legal.matters.advance-stage');
useEffect(() => {
setKeywordDraft(activeKeyword);
setStatusDraft(activeStatus);
}, [activeKeyword, activeStatus]);
// 4. Fetch paginated data using TanStack Query
const mattersQuery = useQuery({
queryKey: [...MATTERS_QUERY_KEY, pageIndex, pageSize, activeKeyword, activeStatus],
queryFn: () =>
api.listLegalMatters({
pageIndex,
pageSize,
searchText: activeKeyword.trim() || undefined,
status: activeStatus || undefined,
}),
staleTime: 30_000, // 30-second stale time
});
// 5. Map status string to visual status tone
const resolveStatusTone = (status: string): StatusTone => {
switch (status) {
case 'Active':
return 'success';
case 'PendingReview':
return 'warning';
case 'Closed':
return 'neutral';
default:
return 'neutral';
}
};
// 6. Strongly-typed column definitions with metadata hydration
const columns: DataTableColumn<LegalMatterSummaryDto>[] = [
{
id: 'matterCode',
header: 'Tracking Code',
cell: (row) => (
<span className="font-mono text-sm font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
{row.matterCode}
</span>
),
width: '180px',
},
{
id: 'title',
header: 'Matter Title',
cell: (row) => (
<div className="flex flex-col gap-0.5">
<span className="font-medium text-neutral-900 dark:text-neutral-100">{row.title}</span>
<span className="text-xs text-neutral-500">{row.caseNumber || 'Unassigned Official Docket'}</span>
</div>
),
},
{
id: 'client',
header: 'Client / Retainer',
cell: (row) => (
// Metadata Hydration: zero-latency lookup via __meta projection without N+1 queries
<DataTableObjectCell
primaryText={row.__meta?.clientName ?? row.clientId}
secondaryText={row.__meta?.clientUnifiedSocialCreditCode}
/>
),
width: '220px',
},
{
id: 'claimAmount',
header: 'Claim Amount',
cell: (row) => (
<span className="font-mono text-sm text-right tabular-nums">
¥ {row.claimAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })}
</span>
),
align: 'right',
width: '150px',
},
{
id: 'status',
header: 'Status',
cell: (row) => (
<StatusBadge
tone={resolveStatusTone(row.status)}
label={row.__meta?.statusDisplayText ?? row.status}
/>
),
width: '130px',
},
{
id: 'actions',
header: 'Actions',
cell: (row) => (
<OperationField>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedMatterId(row.id)}
>
View Dossier
</Button>
{/* Fine-grained RBAC action gate */}
<PermissionGate permission="legal.matters.advance-stage">
<Button
variant="outline"
size="sm"
disabled={row.status !== 'Active'}
onClick={() => {
// Open stage advancement modal dialog
}}
>
Advance Stage
</Button>
</PermissionGate>
</OperationField>
),
width: '160px',
},
];
return (
<WorkspacePage>
{/* Workspace Context Header */}
<WorkspaceContextHeader
title="Matter Intake Dashboard"
description="Search litigation matters across legal entities and practice groups, conduct conflict checks, and track court milestones."
actions={
<div className="flex items-center gap-3">
<Button
variant="outline"
onClick={() => queryClient.invalidateQueries({ queryKey: MATTERS_QUERY_KEY })}
>
Refresh
</Button>
{/* Declarative Create Button RBAC Gate */}
<PermissionGate permission="legal.matters.create">
<Button
variant="primary"
onClick={() => {
// Open create matter intake sheet
}}
>
New Matter Intake
</Button>
</PermissionGate>
</div>
}
/>
{/* Query and Filter Toolbar */}
<QueryToolbar
search={
<Input
placeholder="Search by title, docket code, or client..."
value={keywordDraft}
onChange={(e) => setKeywordDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
updateListState({ keyword: keywordDraft, pageIndex: 1 });
}
}}
/>
}
filters={
<NativeSelect
value={statusDraft}
onChange={(e) => {
const val = e.target.value;
setStatusDraft(val);
updateListState({ status: val, pageIndex: 1 });
}}
>
<NativeSelectOption value="">All Statuses</NativeSelectOption>
<NativeSelectOption value="Draft">Draft</NativeSelectOption>
<NativeSelectOption value="PendingReview">Pending Review</NativeSelectOption>
<NativeSelectOption value="Active">Active Litigation</NativeSelectOption>
<NativeSelectOption value="Closed">Closed & Archived</NativeSelectOption>
</NativeSelect>
}
/>
{/* Inline Error Notice */}
{mattersQuery.isError && (
<InlineNotice
tone="critical"
title="Data Loading Failed"
message={mattersQuery.error instanceof Error ? mattersQuery.error.message : 'Network error occurred. Please retry.'}
/>
)}
{/* Server-Side Paginated Table */}
<ServerDataTable
rowKey="id"
columns={columns}
data={mattersQuery.data?.items ?? []}
isLoading={mattersQuery.isLoading}
pagination={{
pageIndex,
pageSize,
totalCount: mattersQuery.data?.totalCount ?? 0,
onPageChange: (newPageIndex) => updateListState({ pageIndex: newPageIndex }),
onPageSizeChange: (newPageSize) => updateListState({ pageSize: newPageSize, pageIndex: 1 }),
}}
/>
</WorkspacePage>
);
}

Step 4: Automated CI and Quality Gate Assertions

Before committing changes, execute the full suite of automated assertions:

Terminal window
# 1. Enforce strict lockfile immutability
yarn install --immutable
# 2. Check peer dependency integrity
yarn peer:check
# 3. Validate Prettier baseline formatting
yarn format:check
# 4. Run workspace-wide ESLint static analysis
yarn lint
# 5. Execute TypeScript strict type checking (0 errors)
yarn typecheck
# 6. Build Web application and verify bundle budget gates
yarn workspace @bitz/app build

Summary

Following the BitzOrcas.Modern frontend engineering standard yields enterprise-level quality and reliability:

  • Compile-Time Contract Integrity: Synchronized via sync-openapi-sdk.sh, backend model changes immediately flag compile errors in tsc, preventing runtime failures;
  • Defense in Depth: Eliminates insecure localStorage permission patterns; client-side PermissionGate enhances user experience while backend pipelines strictly enforce authorization;
  • Automated Metadata Hydration: Consuming __meta projections via ServerDataTable resolves multi-lingual dictionary texts and foreign keys without extra API calls;
  • Design Uniformity: Composing standard @bitz/widgets and @bitz/components guarantees visual coherence and prevents rogue styles.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%