- Introduced custodian_hub_v3.mmd, custodian_hub_v4_final.mmd, and custodian_hub_v5_FINAL.mmd for Mermaid representation. - Created custodian_hub_FINAL.puml and custodian_hub_v3.puml for PlantUML representation. - Defined entities such as CustodianReconstruction, Identifier, TimeSpan, Agent, CustodianName, CustodianObservation, ReconstructionActivity, Appellation, ConfidenceMeasure, Custodian, LanguageCode, and SourceDocument. - Established relationships and associations between entities, including temporal extents, observations, and reconstruction activities. - Incorporated enumerations for various types, statuses, and classifications relevant to custodians and their activities.
16 KiB
16 KiB
Frontend Architecture Design
System Overview
The Heritage Custodian Frontend follows a layered architecture pattern with clear separation of concerns, building upon proven patterns from the example_ld project while extending them for heritage institution data visualization needs.
Architecture Layers
1. Presentation Layer
┌─────────────────────────────────────────────┐
│ React Components │
├─────────────────────────────────────────────┤
│ Pages │ Layouts │ Features │ Common │
├─────────────────────────────────────────────┤
│ Component Library (UI Kit) │
└─────────────────────────────────────────────┘
Components Structure:
src/
├── components/
│ ├── common/
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ ├── Navigation.tsx
│ │ └── LoadingSpinner.tsx
│ ├── rdf/
│ │ ├── RDFViewer.tsx
│ │ ├── TripleTable.tsx
│ │ ├── GraphExplorer.tsx
│ │ └── FormatSelector.tsx
│ ├── visualization/
│ │ ├── NetworkGraph.tsx
│ │ ├── Timeline.tsx
│ │ ├── GeoMap.tsx
│ │ └── UMLDiagram.tsx
│ ├── query/
│ │ ├── SPARQLEditor.tsx
│ │ ├── QueryBuilder.tsx
│ │ ├── ResultsView.tsx
│ │ └── SavedQueries.tsx
│ └── custodian/
│ ├── CustodianCard.tsx
│ ├── CustodianDetail.tsx
│ ├── CustodianList.tsx
│ └── CustodianSearch.tsx
2. State Management Layer
┌─────────────────────────────────────────────┐
│ Zustand Stores │
├─────────────────────────────────────────────┤
│ RDF Store │ Query Store │ UI Store │
├─────────────────────────────────────────────┤
│ React Query (Data Fetching) │
└─────────────────────────────────────────────┘
Store Architecture:
// stores/rdfStore.ts
interface RDFStore {
triples: Triple[];
graphs: Graph[];
currentFormat: RDFFormat;
loadRDF: (url: string, format: RDFFormat) => Promise<void>;
parseRDF: (content: string, format: RDFFormat) => void;
clearStore: () => void;
}
// stores/queryStore.ts
interface QueryStore {
queries: SavedQuery[];
currentQuery: string;
results: QueryResult[];
isExecuting: boolean;
executeQuery: (sparql: string) => Promise<void>;
saveQuery: (query: SavedQuery) => void;
}
// stores/uiStore.ts
interface UIStore {
theme: 'light' | 'dark';
sidebarOpen: boolean;
activeView: ViewType;
notifications: Notification[];
toggleSidebar: () => void;
setActiveView: (view: ViewType) => void;
}
3. Data Processing Layer
┌─────────────────────────────────────────────┐
│ Data Processors │
├─────────────────────────────────────────────┤
│ RDF Parser │ SPARQL Engine │ Transformer │
├─────────────────────────────────────────────┤
│ Data Validation & Sanitization │
└─────────────────────────────────────────────┘
Processing Modules:
// lib/rdf/parser.ts
export class RDFParser {
async parse(content: string, format: RDFFormat): Promise<Triple[]>
async serialize(triples: Triple[], format: RDFFormat): Promise<string>
validateRDF(content: string): ValidationResult
}
// lib/sparql/engine.ts
export class SPARQLEngine {
constructor(store: N3.Store)
execute(query: string): Promise<QueryResult>
prepare(query: string): PreparedQuery
explain(query: string): QueryPlan
}
// lib/transform/custodian.ts
export class CustodianTransformer {
fromRDF(triples: Triple[]): CustodianObservation[]
toRDF(custodian: CustodianObservation): Triple[]
toGeoJSON(custodians: CustodianObservation[]): FeatureCollection
toCSV(custodians: CustodianObservation[]): string
}
4. Visualization Layer
┌─────────────────────────────────────────────┐
│ D3.js Visualizations │
├─────────────────────────────────────────────┤
│ Force Graph │ Tree │ Timeline │ Geo Map │
├─────────────────────────────────────────────┤
│ Visualization Utilities │
└─────────────────────────────────────────────┘
Visualization Services:
// lib/viz/d3/uml.ts
export class UMLVisualizer {
renderClassDiagram(schema: LinkMLSchema): D3Selection
renderRelationships(relations: Relationship[]): D3Selection
animateTransition(from: Diagram, to: Diagram): void
}
// lib/viz/d3/network.ts
export class NetworkVisualizer {
renderForceGraph(nodes: Node[], edges: Edge[]): D3Selection
applyLayout(layout: LayoutType): void
enableInteraction(callbacks: InteractionCallbacks): void
}
// lib/viz/maps/geo.ts
export class GeoVisualizer {
renderMap(center: [number, number], zoom: number): LeafletMap
addCustodianMarkers(custodians: CustodianObservation[]): void
createChoropleth(data: GeoStats): void
}
5. API Communication Layer
┌─────────────────────────────────────────────┐
│ API Clients │
├─────────────────────────────────────────────┤
│ GraphQL │ REST │ WebSocket │ SPARQL │
├─────────────────────────────────────────────┤
│ Request/Response Handling │
└─────────────────────────────────────────────┘
API Services:
// lib/api/graphql.ts
export class GraphQLClient {
async query<T>(query: string, variables?: Variables): Promise<T>
async mutation<T>(mutation: string, variables?: Variables): Promise<T>
subscribe<T>(subscription: string): Observable<T>
}
// lib/api/sparql.ts
export class SPARQLClient {
constructor(endpoint: string)
async query(sparql: string): Promise<ResultSet>
async update(sparql: string): Promise<void>
async construct(sparql: string): Promise<Triple[]>
}
// lib/api/websocket.ts
export class WebSocketClient {
connect(url: string): void
on(event: string, handler: EventHandler): void
emit(event: string, data: any): void
disconnect(): void
}
Data Flow Architecture
1. RDF Loading Flow
User selects RDF file
↓
FileReader API reads content
↓
RDFParser validates and parses
↓
Triples stored in N3.Store
↓
UI components re-render
↓
Visualizations update
2. SPARQL Query Flow
User writes SPARQL query
↓
Query syntax validation
↓
SPARQLEngine execution
↓
Results transformation
↓
Visualization selection
↓
D3.js rendering
3. UML Diagram Rendering Flow
Load Mermaid/PlantUML file
↓
Parse diagram syntax
↓
Extract nodes and relationships
↓
Transform to D3.js data structure
↓
Apply force-directed layout
↓
Render interactive SVG
↓
Enable pan/zoom/click interactions
Component Communication
Event-Driven Architecture
// Event bus for decoupled communication
class EventBus {
private events: Map<string, EventHandler[]> = new Map();
on(event: string, handler: EventHandler): void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(handler);
}
emit(event: string, data?: any): void {
const handlers = this.events.get(event);
handlers?.forEach(handler => handler(data));
}
off(event: string, handler: EventHandler): void {
const handlers = this.events.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
}
// Usage example
eventBus.on('custodian:selected', (custodian) => {
// Update detail view
// Highlight on map
// Show in timeline
});
eventBus.emit('custodian:selected', selectedCustodian);
Props Drilling Prevention
Using React Context for cross-cutting concerns:
// contexts/RDFContext.tsx
export const RDFContext = createContext<RDFContextValue>({
store: new N3.Store(),
format: 'turtle',
loading: false,
});
// contexts/ThemeContext.tsx
export const ThemeContext = createContext<ThemeContextValue>({
theme: 'light',
toggleTheme: () => {},
});
// contexts/AuthContext.tsx
export const AuthContext = createContext<AuthContextValue>({
user: null,
login: async () => {},
logout: () => {},
});
Performance Optimization
1. Code Splitting
// Lazy load heavy components
const NetworkGraph = lazy(() => import('./components/visualization/NetworkGraph'));
const SPARQLEditor = lazy(() => import('./components/query/SPARQLEditor'));
const UMLDiagram = lazy(() => import('./components/visualization/UMLDiagram'));
// Route-based splitting
const routes = [
{
path: '/explore',
element: <Suspense fallback={<Loading />}><ExplorePage /></Suspense>
},
{
path: '/query',
element: <Suspense fallback={<Loading />}><QueryPage /></Suspense>
}
];
2. Virtual Scrolling
// For large lists of custodians
import { useVirtualizer } from '@tanstack/react-virtual';
function CustodianList({ custodians }: { custodians: CustodianObservation[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: custodians.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 120, // Estimated item height
overscan: 5,
});
return (
<div ref={parentRef} className="h-screen overflow-auto">
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<CustodianCard custodian={custodians[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
3. Web Workers
// workers/rdfParser.worker.ts
import { parse } from 'n3';
self.addEventListener('message', async (event) => {
const { content, format } = event.data;
try {
const triples = await parseRDF(content, format);
self.postMessage({ type: 'success', triples });
} catch (error) {
self.postMessage({ type: 'error', error: error.message });
}
});
// Usage in component
const parseInWorker = (content: string, format: string) => {
return new Promise((resolve, reject) => {
const worker = new Worker('/workers/rdfParser.worker.js');
worker.onmessage = (event) => {
if (event.data.type === 'success') {
resolve(event.data.triples);
} else {
reject(new Error(event.data.error));
}
worker.terminate();
};
worker.postMessage({ content, format });
});
};
Security Considerations
1. Content Security Policy
// helmet configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // For D3.js
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://query.wikidata.org"],
},
},
}));
2. Input Sanitization
// Sanitize SPARQL queries
function sanitizeSPARQL(query: string): string {
// Remove comments
query = query.replace(/#.*$/gm, '');
// Check for injection patterns
const dangerous = [
/DROP\s+GRAPH/i,
/CLEAR\s+GRAPH/i,
/DELETE\s+WHERE/i,
];
for (const pattern of dangerous) {
if (pattern.test(query)) {
throw new Error('Potentially dangerous SPARQL query detected');
}
}
return query;
}
Deployment Architecture
Docker Configuration
# Multi-stage build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Environment Configuration
// config/env.ts
export const config = {
api: {
baseUrl: process.env.VITE_API_URL || 'http://localhost:3000',
timeout: parseInt(process.env.VITE_API_TIMEOUT || '30000'),
},
sparql: {
endpoint: process.env.VITE_SPARQL_ENDPOINT || 'http://localhost:3030/dataset',
},
maps: {
tileUrl: process.env.VITE_MAP_TILES || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
},
features: {
enableWebSockets: process.env.VITE_ENABLE_WS === 'true',
enableOffline: process.env.VITE_ENABLE_OFFLINE === 'true',
},
};
Testing Architecture
1. Unit Testing
// Component testing with React Testing Library
describe('CustodianCard', () => {
it('renders custodian information correctly', () => {
const custodian = mockCustodianObservation();
render(<CustodianCard custodian={custodian} />);
expect(screen.getByText(custodian.name)).toBeInTheDocument();
expect(screen.getByText(custodian.institution_type)).toBeInTheDocument();
});
});
2. Integration Testing
// API integration tests
describe('SPARQL API', () => {
it('executes queries and returns results', async () => {
const query = 'SELECT * WHERE { ?s ?p ?o } LIMIT 10';
const results = await sparqlClient.query(query);
expect(results).toHaveLength(10);
expect(results[0]).toHaveProperty('s');
expect(results[0]).toHaveProperty('p');
expect(results[0]).toHaveProperty('o');
});
});
3. E2E Testing
// Playwright E2E tests
test('complete user journey', async ({ page }) => {
await page.goto('/');
// Load RDF file
await page.click('[data-testid="load-rdf"]');
await page.setInputFiles('input[type="file"]', 'fixtures/custodian.ttl');
// Navigate to query page
await page.click('a[href="/query"]');
// Execute SPARQL query
await page.fill('[data-testid="sparql-editor"]', 'SELECT * WHERE { ?s a :CustodianObservation }');
await page.click('[data-testid="execute-query"]');
// Verify results
await expect(page.locator('[data-testid="results-table"]')).toBeVisible();
await expect(page.locator('tr')).toHaveCount(11); // Header + 10 results
});
Related Documents
- 02-design-patterns.md - Frontend design patterns
- 03-tdd-strategy.md - Test-driven development approach
- 04-example-ld-mapping.md - Reusable modules from example_ld
- 05-d3-visualization.md - D3.js visualization strategy