feat(#92): implement Aggregated Dashboard View
Implement unified dashboard to display tasks and events from multiple federated Mosaic Stack instances with clear provenance indicators. Backend Integration: - Extended federation API client with query support (sendFederatedQuery) - Added query message fetching functions - Integrated with existing QUERY message type from Phase 3 Components Created: - ProvenanceIndicator: Shows which instance data came from - FederatedTaskCard: Task display with provenance - FederatedEventCard: Event display with provenance - AggregatedDataGrid: Unified grid for multiple data types - Dashboard page at /federation/dashboard Key Features: - Query all ACTIVE federated connections on load - Display aggregated tasks and events in unified view - Clear provenance indicators (instance name badges) - PDA-friendly language throughout (no demanding terms) - Loading states and error handling - Empty state when no connections available Technical Implementation: - Uses POST /api/v1/federation/query to send queries - Queries each connection for tasks.list and events.list - Aggregates responses with provenance metadata - Handles connection failures gracefully - 86 tests passing with >85% coverage - TypeScript strict mode compliant - ESLint compliant PDA-Friendly Design: - "Unable to reach" instead of "Connection failed" - "No data available" instead of "No results" - "Loading data from instances..." instead of "Fetching..." - Calm color palette (soft blues, greens, grays) - Status indicators: 🟢 Active, 📋 No data, ⚠️ Error Files Added: - apps/web/src/lib/api/federation-queries.ts - apps/web/src/lib/api/federation-queries.test.ts - apps/web/src/components/federation/types.ts - apps/web/src/components/federation/ProvenanceIndicator.tsx - apps/web/src/components/federation/ProvenanceIndicator.test.tsx - apps/web/src/components/federation/FederatedTaskCard.tsx - apps/web/src/components/federation/FederatedTaskCard.test.tsx - apps/web/src/components/federation/FederatedEventCard.tsx - apps/web/src/components/federation/FederatedEventCard.test.tsx - apps/web/src/components/federation/AggregatedDataGrid.tsx - apps/web/src/components/federation/AggregatedDataGrid.test.tsx - apps/web/src/app/(authenticated)/federation/dashboard/page.tsx - docs/scratchpads/92-aggregated-dashboard.md Testing: - 86 total tests passing - Unit tests for all components - Integration tests for API client - PDA-friendly language verified - TypeScript type checking passing - ESLint passing Ready for code review and QA testing. Related Issues: - Depends on #85 (FED-005: QUERY Message Type) - COMPLETED - Depends on #91 (FED-008: Connection Manager UI) - COMPLETED - Uses #90 (FED-007: EVENT Subscriptions) infrastructure Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
113
apps/web/src/components/federation/FederatedTaskCard.tsx
Normal file
113
apps/web/src/components/federation/FederatedTaskCard.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* FederatedTaskCard Component
|
||||
* Displays a task from a federated instance with provenance indicator
|
||||
*/
|
||||
|
||||
import { TaskStatus, TaskPriority } from "@mosaic/shared";
|
||||
import type { FederatedTask } from "./types";
|
||||
import { ProvenanceIndicator } from "./ProvenanceIndicator";
|
||||
|
||||
interface FederatedTaskCardProps {
|
||||
federatedTask: FederatedTask;
|
||||
compact?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDA-friendly status text and color
|
||||
*/
|
||||
function getStatusDisplay(status: TaskStatus): { text: string; colorClass: string } {
|
||||
switch (status) {
|
||||
case TaskStatus.NOT_STARTED:
|
||||
return { text: "Not Started", colorClass: "bg-gray-100 text-gray-700" };
|
||||
case TaskStatus.IN_PROGRESS:
|
||||
return { text: "In Progress", colorClass: "bg-blue-100 text-blue-700" };
|
||||
case TaskStatus.COMPLETED:
|
||||
return { text: "Completed", colorClass: "bg-green-100 text-green-700" };
|
||||
case TaskStatus.PAUSED:
|
||||
return { text: "Paused", colorClass: "bg-yellow-100 text-yellow-700" };
|
||||
case TaskStatus.ARCHIVED:
|
||||
return { text: "Archived", colorClass: "bg-gray-100 text-gray-600" };
|
||||
default:
|
||||
return { text: "Unknown", colorClass: "bg-gray-100 text-gray-700" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority text and color
|
||||
*/
|
||||
function getPriorityDisplay(priority: TaskPriority): { text: string; colorClass: string } {
|
||||
switch (priority) {
|
||||
case TaskPriority.LOW:
|
||||
return { text: "Low", colorClass: "text-gray-600" };
|
||||
case TaskPriority.MEDIUM:
|
||||
return { text: "Medium", colorClass: "text-blue-600" };
|
||||
case TaskPriority.HIGH:
|
||||
return { text: "High", colorClass: "text-orange-600" };
|
||||
default:
|
||||
return { text: "Unknown", colorClass: "text-gray-600" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
function formatDate(date: Date | null): string | null {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(new Date(date));
|
||||
}
|
||||
|
||||
export function FederatedTaskCard({
|
||||
federatedTask,
|
||||
compact = false,
|
||||
onClick,
|
||||
}: FederatedTaskCardProps): React.JSX.Element {
|
||||
const { task, provenance } = federatedTask;
|
||||
const status = getStatusDisplay(task.status);
|
||||
const priority = getPriorityDisplay(task.priority);
|
||||
const dueDate = formatDate(task.dueDate);
|
||||
|
||||
const paddingClass = compact ? "p-3" : "p-4";
|
||||
const clickableClass = onClick ? "cursor-pointer hover:border-gray-300" : "";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border border-gray-200 rounded-lg ${paddingClass} ${clickableClass} transition-colors`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Header with title and provenance */}
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-medium text-gray-900">{task.title}</h3>
|
||||
{task.description && <p className="text-sm text-gray-600 mt-1">{task.description}</p>}
|
||||
</div>
|
||||
<ProvenanceIndicator
|
||||
instanceId={provenance.instanceId}
|
||||
instanceName={provenance.instanceName}
|
||||
instanceUrl={provenance.instanceUrl}
|
||||
compact={compact}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Metadata row */}
|
||||
<div className="flex items-center gap-3 text-sm flex-wrap">
|
||||
{/* Status badge */}
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${status.colorClass}`}>
|
||||
{status.text}
|
||||
</span>
|
||||
|
||||
{/* Priority */}
|
||||
<span className={`text-xs font-medium ${priority.colorClass}`}>{priority.text}</span>
|
||||
|
||||
{/* Due date */}
|
||||
{dueDate && <span className="text-xs text-gray-600">Due: {dueDate}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user