- 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.
584 lines
16 KiB
Markdown
584 lines
16 KiB
Markdown
# 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:**
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```dockerfile
|
|
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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](02-design-patterns.md) - Frontend design patterns
|
|
- [03-tdd-strategy.md](03-tdd-strategy.md) - Test-driven development approach
|
|
- [04-example-ld-mapping.md](04-example-ld-mapping.md) - Reusable modules from example_ld
|
|
- [05-d3-visualization.md](05-d3-visualization.md) - D3.js visualization strategy
|