FARM Stack GUI Guide¶
AMRnet’s next-generation interface is built with the FARM (FastAPI + React + MongoDB) stack, providing a modern, responsive, and highly interactive user experience for antimicrobial resistance data exploration and analysis.
Overview¶
The FARM stack GUI represents a significant evolution of the AMRnet platform, offering:
⚡ Performance: 3x faster load times with server-side rendering
🎨 Modern UI: Material Design 3.0 with dark/light theme support
📱 Responsive: Mobile-first design that works on all devices
🔄 Real-time: Live data updates and collaborative features
🌍 Accessibility: WCAG 2.1 AA compliant interface
🚀 Progressive: Offline capabilities with service workers
Technology Stack¶
Frontend Architecture¶
The React frontend leverages modern development practices and cutting-edge libraries:
Core Technologies:
// package.json dependencies
{
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.0",
"next": "^14.0.0",
"tailwindcss": "^3.3.0"
}
Key Libraries:
Next.js 14: Server-side rendering and app router
TypeScript: Type-safe development
Tailwind CSS: Utility-first styling
Framer Motion: Smooth animations and transitions
React Query: Server state management
Zustand: Client state management
React Hook Form: Form handling with validation
Recharts: Advanced data visualizations
Mapbox GL: Interactive geographic mapping
Component Architecture:
// src/components/organisms/DashboardLayout.tsx
import React from 'react';
import { Header } from '@/components/molecules/Header';
import { Sidebar } from '@/components/molecules/Sidebar';
import { MainContent } from '@/components/atoms/MainContent';
interface DashboardLayoutProps {
children: React.ReactNode;
organism: string;
}
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
children,
organism
}) => {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header organism={organism} />
<div className="flex">
<Sidebar organism={organism} />
<MainContent>{children}</MainContent>
</div>
</div>
);
};
Backend Infrastructure¶
The FastAPI backend provides high-performance APIs with automatic documentation:
FastAPI Application:
# src/backend/main.py
from fastapi import FastAPI, WebSocket, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
import uvicorn
app = FastAPI(
title="AMRnet FARM API",
description="High-performance API for AMR surveillance data",
version="2.1.0",
docs_url="/api/docs",
redoc_url="/api/redoc"
)
# Middleware configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["https://farm.amrnet.org"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(GZipMiddleware, minimum_size=1000)
# API routers
from .routers import organisms, analytics, auth, websockets
app.include_router(organisms.router, prefix="/api/v2")
app.include_router(analytics.router, prefix="/api/v2/analytics")
app.include_router(auth.router, prefix="/api/v2/auth")
app.include_router(websockets.router, prefix="/ws")
MongoDB Integration:
# src/backend/database.py
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Optional
import asyncio
class Database:
client: Optional[AsyncIOMotorClient] = None
db = Database()
async def get_database() -> AsyncIOMotorClient:
return db.client
async def connect_to_mongo():
"""Create database connection"""
db.client = AsyncIOMotorClient(
"mongodb+srv://cluster.mongodb.net",
maxPoolSize=10,
minPoolSize=1,
serverSelectionTimeoutMS=5000
)
async def close_mongo_connection():
"""Close database connection"""
db.client.close()
Key Features¶
Interactive Dashboard Components¶
Organism Selector:
// src/components/molecules/OrganismSelector.tsx
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { useOrganismData } from '@/hooks/useOrganismData';
interface Organism {
id: string;
name: string;
description: string;
sampleCount: number;
icon: string;
}
export const OrganismSelector: React.FC = () => {
const [selectedOrganism, setSelectedOrganism] = useState<string>('styphi');
const { data: organisms, isLoading } = useOrganismData();
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{organisms?.map((organism: Organism) => (
<motion.div
key={organism.id}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`
p-6 rounded-xl border-2 cursor-pointer transition-all
${selectedOrganism === organism.id
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 hover:border-gray-300 dark:border-gray-700'
}
`}
onClick={() => setSelectedOrganism(organism.id)}
>
<div className="text-4xl mb-4">{organism.icon}</div>
<h3 className="font-semibold text-lg mb-2">{organism.name}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{organism.description}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{organism.sampleCount.toLocaleString()} samples
</span>
<span className="text-xs text-blue-600 dark:text-blue-400">
Explore →
</span>
</div>
</motion.div>
))}
</div>
);
};
Real-time Resistance Chart:
// src/components/organisms/ResistanceChart.tsx
import React, { useEffect, useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { useWebSocket } from '@/hooks/useWebSocket';
interface ResistanceData {
year: number;
month: number;
resistance_rate: number;
sample_count: number;
}
export const ResistanceChart: React.FC<{
organism: string;
antibiotic: string;
country?: string;
}> = ({ organism, antibiotic, country }) => {
const [data, setData] = useState<ResistanceData[]>([]);
// WebSocket connection for real-time updates
const { lastMessage, connectionStatus } = useWebSocket(
`wss://farm-api.amrnet.org/ws/${organism}/resistance/${antibiotic}`,
{
shouldReconnect: () => true,
reconnectAttempts: 10,
reconnectInterval: 3000,
}
);
useEffect(() => {
if (lastMessage?.data) {
const newData = JSON.parse(lastMessage.data);
setData(prevData => [...prevData.slice(-50), newData]); // Keep last 50 points
}
}, [lastMessage]);
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold">
{antibiotic} Resistance Trends
</h3>
<div className="flex items-center space-x-2">
<div className={`w-2 h-2 rounded-full ${
connectionStatus === 'Open' ? 'bg-green-500' : 'bg-red-500'
}`} />
<span className="text-sm text-gray-500">
{connectionStatus === 'Open' ? 'Live' : 'Disconnected'}
</span>
</div>
</div>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" className="dark:stroke-gray-700" />
<XAxis
dataKey="year"
className="dark:text-gray-300"
/>
<YAxis
domain={[0, 1]}
tickFormatter={(value) => `${(value * 100).toFixed(0)}%`}
className="dark:text-gray-300"
/>
<Tooltip
labelFormatter={(year) => `Year: ${year}`}
formatter={(value: number, name) => [
`${(value * 100).toFixed(1)}%`,
'Resistance Rate'
]}
contentStyle={{
backgroundColor: 'rgba(255, 255, 255, 0.95)',
border: '1px solid #e5e7eb',
borderRadius: '8px',
}}
/>
<Line
type="monotone"
dataKey="resistance_rate"
stroke="#3b82f6"
strokeWidth={3}
dot={{ fill: '#3b82f6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#3b82f6', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
};
Interactive Geographic Map:
// src/components/organisms/GeographicMap.tsx
import React, { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import { useQuery } from '@tanstack/react-query';
import { getGeographicData } from '@/api/organisms';
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;
export const GeographicMap: React.FC<{
organism: string;
metric: 'resistance_rate' | 'sample_count';
}> = ({ organism, metric }) => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map | null>(null);
const [mapLoaded, setMapLoaded] = useState(false);
const { data: geoData } = useQuery({
queryKey: ['geographic', organism, metric],
queryFn: () => getGeographicData(organism, metric),
refetchInterval: 60000, // Refresh every minute
});
useEffect(() => {
if (map.current || !mapContainer.current) return;
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/light-v11',
center: [0, 20],
zoom: 2,
projection: 'naturalEarth'
});
map.current.on('load', () => {
setMapLoaded(true);
});
return () => map.current?.remove();
}, []);
useEffect(() => {
if (!mapLoaded || !geoData || !map.current) return;
// Add choropleth layer
if (map.current.getSource('countries')) {
map.current.removeLayer('countries-fill');
map.current.removeSource('countries');
}
map.current.addSource('countries', {
type: 'geojson',
data: geoData
});
map.current.addLayer({
id: 'countries-fill',
type: 'fill',
source: 'countries',
paint: {
'fill-color': [
'interpolate',
['linear'],
['get', metric],
0, '#eff6ff',
0.25, '#bfdbfe',
0.5, '#60a5fa',
0.75, '#2563eb',
1, '#1d4ed8'
],
'fill-opacity': 0.8
}
});
// Add hover effects
map.current.on('mouseenter', 'countries-fill', (e) => {
map.current!.getCanvas().style.cursor = 'pointer';
});
map.current.on('mouseleave', 'countries-fill', () => {
map.current!.getCanvas().style.cursor = '';
});
}, [mapLoaded, geoData, metric]);
return (
<div className="relative w-full h-96 bg-gray-100 dark:bg-gray-800 rounded-xl overflow-hidden">
<div ref={mapContainer} className="w-full h-full" />
{!mapLoaded && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
</div>
)}
</div>
);
};
Advanced Analytics Interface¶
Statistical Analysis Dashboard:
// src/components/pages/AnalyticsDashboard.tsx
import React, { useState } from 'react';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { RegressionAnalysis } from '@/components/organisms/RegressionAnalysis';
import { ClusterAnalysis } from '@/components/organisms/ClusterAnalysis';
import { MLPredictions } from '@/components/organisms/MLPredictions';
import { StatisticalTests } from '@/components/organisms/StatisticalTests';
export const AnalyticsDashboard: React.FC<{ organism: string }> = ({
organism
}) => {
const [selectedFilters, setSelectedFilters] = useState({
countries: [] as string[],
yearRange: [2010, 2023] as [number, number],
antibiotics: [] as string[],
});
return (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
<h1 className="text-2xl font-bold mb-6">
Advanced Analytics - {organism.toUpperCase()}
</h1>
<Tabs selectedTabClassName="bg-blue-600 text-white">
<TabList className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
<Tab className="w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-blue-700 cursor-pointer">
Regression Analysis
</Tab>
<Tab className="w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-blue-700 cursor-pointer">
Clustering
</Tab>
<Tab className="w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-blue-700 cursor-pointer">
ML Predictions
</Tab>
<Tab className="w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-blue-700 cursor-pointer">
Statistical Tests
</Tab>
</TabList>
<TabPanel className="mt-6">
<RegressionAnalysis
organism={organism}
filters={selectedFilters}
/>
</TabPanel>
<TabPanel className="mt-6">
<ClusterAnalysis
organism={organism}
filters={selectedFilters}
/>
</TabPanel>
<TabPanel className="mt-6">
<MLPredictions
organism={organism}
filters={selectedFilters}
/>
</TabPanel>
<TabPanel className="mt-6">
<StatisticalTests
organism={organism}
filters={selectedFilters}
/>
</TabPanel>
</Tabs>
</div>
</div>
);
};
API Explorer Interface:
// src/components/pages/APIExplorer.tsx
import React, { useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { useMutation } from '@tanstack/react-query';
export const APIExplorer: React.FC = () => {
const [endpoint, setEndpoint] = useState('/api/v2/styphi');
const [method, setMethod] = useState('GET');
const [headers, setHeaders] = useState('{"Content-Type": "application/json"}');
const [params, setParams] = useState('{"limit": 100}');
const [response, setResponse] = useState<any>(null);
const makeRequest = useMutation({
mutationFn: async () => {
const url = new URL(`https://farm-api.amrnet.org${endpoint}`);
if (method === 'GET' && params) {
const parsedParams = JSON.parse(params);
Object.entries(parsedParams).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}
const response = await fetch(url.toString(), {
method,
headers: JSON.parse(headers),
body: method !== 'GET' ? params : undefined,
});
return response.json();
},
onSuccess: (data) => setResponse(data),
});
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-screen">
{/* Request Configuration */}
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
<h2 className="text-xl font-semibold mb-6">API Request Builder</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Method</label>
<select
value={method}
onChange={(e) => setMethod(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">Endpoint</label>
<input
type="text"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700"
placeholder="/api/v2/styphi"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Headers</label>
<textarea
value={headers}
onChange={(e) => setHeaders(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 h-20"
placeholder='{"Content-Type": "application/json"}'
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{method === 'GET' ? 'Query Parameters' : 'Request Body'}
</label>
<textarea
value={params}
onChange={(e) => setParams(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 h-32"
placeholder={method === 'GET' ? '{"limit": 100}' : '{"country": "BGD"}'}
/>
</div>
<button
onClick={() => makeRequest.mutate()}
disabled={makeRequest.isPending}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{makeRequest.isPending ? 'Sending...' : 'Send Request'}
</button>
</div>
</div>
{/* Response Display */}
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
<h2 className="text-xl font-semibold mb-6">Response</h2>
{response && (
<div className="h-full overflow-auto">
<SyntaxHighlighter
language="json"
style={vscDarkPlus}
className="rounded-lg"
>
{JSON.stringify(response, null, 2)}
</SyntaxHighlighter>
</div>
)}
{!response && (
<div className="flex items-center justify-center h-64 text-gray-500">
Make a request to see the response here
</div>
)}
</div>
</div>
);
};
Real-time Features¶
WebSocket Integration¶
The FARM stack GUI leverages WebSocket connections for real-time data updates and collaborative features:
Real-time Data Streaming:
# Backend WebSocket handler
from fastapi import WebSocket, WebSocketDisconnect
from typing import Dict, List
import asyncio
import json
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, List[WebSocket]] = {}
async def connect(self, websocket: WebSocket, room: str):
await websocket.accept()
if room not in self.active_connections:
self.active_connections[room] = []
self.active_connections[room].append(websocket)
def disconnect(self, websocket: WebSocket, room: str):
if room in self.active_connections:
self.active_connections[room].remove(websocket)
async def broadcast_to_room(self, message: dict, room: str):
if room in self.active_connections:
for connection in self.active_connections[room]:
try:
await connection.send_text(json.dumps(message))
except:
# Remove dead connections
self.active_connections[room].remove(connection)
manager = ConnectionManager()
@app.websocket("/ws/resistance/{organism}/{antibiotic}")
async def websocket_resistance_updates(
websocket: WebSocket,
organism: str,
antibiotic: str
):
room = f"{organism}_{antibiotic}"
await manager.connect(websocket, room)
try:
while True:
# Send periodic updates
latest_data = await get_latest_resistance_data(organism, antibiotic)
await manager.broadcast_to_room(latest_data, room)
await asyncio.sleep(60) # Update every minute
except WebSocketDisconnect:
manager.disconnect(websocket, room)
Frontend WebSocket Hook:
// src/hooks/useWebSocket.ts
import { useEffect, useRef, useState } from 'react';
interface UseWebSocketOptions {
shouldReconnect?: () => boolean;
reconnectAttempts?: number;
reconnectInterval?: number;
}
export const useWebSocket = (
url: string,
options: UseWebSocketOptions = {}
) => {
const [lastMessage, setLastMessage] = useState<MessageEvent | null>(null);
const [connectionStatus, setConnectionStatus] = useState<'Connecting' | 'Open' | 'Closed'>('Connecting');
const ws = useRef<WebSocket | null>(null);
const reconnectAttempts = useRef(0);
const connect = () => {
try {
ws.current = new WebSocket(url);
ws.current.onopen = () => {
setConnectionStatus('Open');
reconnectAttempts.current = 0;
};
ws.current.onmessage = (event) => {
setLastMessage(event);
};
ws.current.onclose = () => {
setConnectionStatus('Closed');
if (
options.shouldReconnect?.() &&
reconnectAttempts.current < (options.reconnectAttempts ?? 5)
) {
setTimeout(() => {
reconnectAttempts.current++;
connect();
}, options.reconnectInterval ?? 3000);
}
};
} catch (error) {
console.error('WebSocket connection failed:', error);
setConnectionStatus('Closed');
}
};
useEffect(() => {
connect();
return () => {
ws.current?.close();
};
}, [url]);
const sendMessage = (message: string) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(message);
}
};
return { lastMessage, connectionStatus, sendMessage };
};
Progressive Web App Features¶
Service Worker Configuration:
// public/sw.js
const CACHE_NAME = 'amrnet-farm-v1';
const urlsToCache = [
'/',
'/manifest.json',
'/static/js/bundle.js',
'/static/css/main.css',
'/api/v2/organisms',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
// Cache-first strategy for static assets
if (event.request.url.includes('/static/')) {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
}
// Network-first strategy for API calls
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(event.request))
);
}
});
Offline Data Management:
// src/utils/offlineStorage.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb';
interface AMRnetDB extends DBSchema {
organisms: {
key: string;
value: {
id: string;
data: any;
timestamp: number;
};
};
resistance_data: {
key: string;
value: {
organism: string;
filters: string;
data: any;
timestamp: number;
};
};
}
class OfflineStorage {
private db: IDBPDatabase<AMRnetDB> | null = null;
async init() {
this.db = await openDB<AMRnetDB>('amrnet-cache', 1, {
upgrade(db) {
db.createObjectStore('organisms');
db.createObjectStore('resistance_data');
},
});
}
async cacheOrganismData(organismId: string, data: any) {
if (!this.db) await this.init();
await this.db!.put('organisms', {
id: organismId,
data,
timestamp: Date.now(),
}, organismId);
}
async getCachedOrganismData(organismId: string) {
if (!this.db) await this.init();
const cached = await this.db!.get('organisms', organismId);
// Return cached data if less than 1 hour old
if (cached && Date.now() - cached.timestamp < 3600000) {
return cached.data;
}
return null;
}
async clearExpiredCache() {
if (!this.db) await this.init();
const cutoff = Date.now() - 86400000; // 24 hours
const tx = this.db!.transaction(['organisms', 'resistance_data'], 'readwrite');
// Clear expired organism data
const organismCursor = await tx.objectStore('organisms').openCursor();
while (organismCursor) {
if (organismCursor.value.timestamp < cutoff) {
await organismCursor.delete();
}
await organismCursor.continue();
}
await tx.done;
}
}
export const offlineStorage = new OfflineStorage();
Accessibility Features¶
The FARM stack GUI prioritizes accessibility to ensure the platform is usable by all researchers, regardless of their abilities:
WCAG 2.1 AA Compliance:
// src/components/atoms/AccessibleButton.tsx
import React, { forwardRef } from 'react';
import { motion } from 'framer-motion';
interface AccessibleButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
icon?: React.ReactNode;
children: React.ReactNode;
}
export const AccessibleButton = forwardRef<
HTMLButtonElement,
AccessibleButtonProps
>(({
variant = 'primary',
size = 'md',
loading = false,
icon,
children,
disabled,
...props
}, ref) => {
const baseClasses = `
inline-flex items-center justify-center
font-medium rounded-lg transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
${loading ? 'cursor-wait' : 'cursor-pointer'}
`;
const variantClasses = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900 focus:ring-gray-500',
danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<motion.button
ref={ref}
whileHover={{ scale: disabled || loading ? 1 : 1.02 }}
whileTap={{ scale: disabled || loading ? 1 : 0.98 }}
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
disabled={disabled || loading}
aria-busy={loading}
aria-describedby={loading ? 'loading-text' : undefined}
{...props}
>
{loading && (
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
className="opacity-25"
/>
<path
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
className="opacity-75"
/>
</svg>
)}
{icon && !loading && <span className="mr-2">{icon}</span>}
<span>{children}</span>
{loading && (
<span id="loading-text" className="sr-only">
Loading, please wait
</span>
)}
</motion.button>
);
});
Screen Reader Support:
// src/components/molecules/DataTable.tsx
import React from 'react';
interface DataTableProps {
data: any[];
columns: Array<{
key: string;
label: string;
sortable?: boolean;
format?: (value: any) => string;
}>;
caption: string;
sortBy?: string;
sortDirection?: 'asc' | 'desc';
onSort?: (column: string) => void;
}
export const DataTable: React.FC<DataTableProps> = ({
data,
columns,
caption,
sortBy,
sortDirection,
onSort,
}) => {
return (
<div className="overflow-x-auto">
<table
className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"
role="table"
aria-label={caption}
>
<caption className="sr-only">{caption}</caption>
<thead className="bg-gray-50 dark:bg-gray-800">
<tr role="row">
{columns.map((column) => (
<th
key={column.key}
scope="col"
className={`
px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider
${column.sortable ? 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700' : ''}
`}
onClick={column.sortable ? () => onSort?.(column.key) : undefined}
onKeyDown={(e) => {
if (column.sortable && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
onSort?.(column.key);
}
}}
tabIndex={column.sortable ? 0 : -1}
role={column.sortable ? 'columnheader button' : 'columnheader'}
aria-sort={
sortBy === column.key
? sortDirection === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
<div className="flex items-center space-x-1">
<span>{column.label}</span>
{column.sortable && (
<span className="ml-2">
{sortBy === column.key ? (
sortDirection === 'asc' ? '↑' : '↓'
) : (
<span className="text-gray-400">↕</span>
)}
</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{data.map((row, rowIndex) => (
<tr
key={rowIndex}
role="row"
className="hover:bg-gray-50 dark:hover:bg-gray-800"
>
{columns.map((column) => (
<td
key={column.key}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"
role="gridcell"
>
{column.format
? column.format(row[column.key])
: row[column.key]
}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
Deployment and Performance¶
The FARM stack is optimized for production deployment with modern DevOps practices:
Docker Configuration:
# Dockerfile.frontend
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
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;"]
# Dockerfile.backend
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Performance Optimization:
// src/utils/performance.ts
import { memo, useMemo, useCallback } from 'react';
// Memoized component example
export const MemoizedResistanceChart = memo(ResistanceChart, (prevProps, nextProps) => {
return (
prevProps.organism === nextProps.organism &&
prevProps.antibiotic === nextProps.antibiotic &&
prevProps.country === nextProps.country
);
});
// Virtual scrolling for large datasets
import { FixedSizeList as List } from 'react-window';
export const VirtualizedTable: React.FC<{
data: any[];
height: number;
rowHeight: number;
}> = ({ data, height, rowHeight }) => {
const Row = useCallback(({ index, style }: any) => (
<div style={style}>
{/* Row content */}
</div>
), []);
return (
<List
height={height}
itemCount={data.length}
itemSize={rowHeight}
overscanCount={5}
>
{Row}
</List>
);
};
Getting Started¶
Development Setup¶
Set up the FARM stack development environment:
1. Clone and Install Dependencies:
# Clone the repository
git clone https://github.com/amrnet/amrnet-farm.git
cd amrnet-farm
# Install frontend dependencies
cd frontend
npm install
# Install backend dependencies
cd ../backend
pip install -r requirements.txt
2. Environment Configuration:
# Frontend environment (.env.local)
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_MAPBOX_TOKEN=your_mapbox_token
NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws
# Backend environment (.env)
MONGODB_URI=mongodb://localhost:27017/amrnet
JWT_SECRET=your_jwt_secret
CORS_ORIGINS=["http://localhost:3000"]
3. Start Development Servers:
# Terminal 1: Backend server
cd backend
uvicorn main:app --reload --port 8000
# Terminal 2: Frontend server
cd frontend
npm run dev
4. Access the Application:
Frontend: http://localhost:3000
Backend API: http://localhost:8000
API Documentation: http://localhost:8000/docs
Production Deployment¶
Deploy using Docker Compose for production:
# docker-compose.prod.yml
version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
ports:
- "80:80"
- "443:443"
environment:
- NODE_ENV=production
volumes:
- ./nginx/ssl:/etc/nginx/ssl
backend:
build:
context: ./backend
dockerfile: Dockerfile.prod
ports:
- "8000:8000"
environment:
- MONGODB_URI=${MONGODB_URI}
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongodb
mongodb:
image: mongo:6.0
restart: always
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
volumes:
- mongodb_data:/data/db
redis:
image: redis:7-alpine
restart: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
mongodb_data:
redis_data:
Support and Community¶
Get help with the FARM stack implementation:
Resources¶
📖 Component Library: Storybook documentation at storybook.amrnet.org
🎨 Design System: Figma design tokens and components
🧪 Testing Guide: Jest and Cypress testing patterns
🚀 Deployment Guide: Production deployment best practices
Contributing¶
Contribute to the FARM stack development:
Frontend Issues: React components, UI/UX improvements
Backend Issues: FastAPI endpoints, performance optimization
Documentation: User guides and API documentation
Testing: Unit tests, integration tests, e2e tests
Development Workflow:
# Create feature branch
git checkout -b feature/new-visualization
# Make changes and test
npm run test
npm run lint
npm run type-check
# Commit and push
git commit -m "Add new resistance trend visualization"
git push origin feature/new-visualization
# Create pull request
Future Roadmap¶
Planned enhancements for the FARM stack GUI:
Q1 2025: - 🤖 AI-powered data insights and recommendations - 📱 Native mobile applications (React Native) - 🔍 Advanced search with natural language queries - 📊 Custom dashboard builder interface
Q2 2025: - 🌐 Multi-language support expansion (Chinese, Arabic, Hindi) - 🔒 Advanced security features (2FA, SSO integration) - 📈 Machine learning model explanations - 🎮 Gamification for data exploration
Q3 2025: - 🗣️ Voice interface for accessibility - 🔗 Enhanced data integration (HL7 FHIR, SNOMED CT) - 📋 Collaborative workspaces and sharing - 🎯 Personalized research recommendations
Long-term Vision: - Virtual reality data exploration environments - Blockchain-based data provenance tracking - Quantum computing integration for complex analyses - Global real-time surveillance network integration