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:
154
apps/web/src/lib/api/federation-queries.test.ts
Normal file
154
apps/web/src/lib/api/federation-queries.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Federation Queries API Client Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
sendFederatedQuery,
|
||||
fetchQueryMessages,
|
||||
fetchQueryMessage,
|
||||
FederationMessageStatus,
|
||||
} from "./federation-queries";
|
||||
import * as client from "./client";
|
||||
|
||||
// Mock the API client
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Federation Queries API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("sendFederatedQuery", () => {
|
||||
it("should send a query to a remote instance", async () => {
|
||||
const mockResponse = {
|
||||
id: "msg-123",
|
||||
messageId: "uuid-123",
|
||||
connectionId: "conn-1",
|
||||
query: "tasks.list",
|
||||
status: FederationMessageStatus.PENDING,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
vi.mocked(client.apiPost).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sendFederatedQuery("conn-1", "tasks.list", { limit: 10 });
|
||||
|
||||
expect(client.apiPost).toHaveBeenCalledWith("/api/v1/federation/query", {
|
||||
connectionId: "conn-1",
|
||||
query: "tasks.list",
|
||||
context: { limit: 10 },
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should send a query without context", async () => {
|
||||
const mockResponse = {
|
||||
id: "msg-124",
|
||||
messageId: "uuid-124",
|
||||
connectionId: "conn-2",
|
||||
query: "events.today",
|
||||
status: FederationMessageStatus.PENDING,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
vi.mocked(client.apiPost).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sendFederatedQuery("conn-2", "events.today");
|
||||
|
||||
expect(client.apiPost).toHaveBeenCalledWith("/api/v1/federation/query", {
|
||||
connectionId: "conn-2",
|
||||
query: "events.today",
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should handle API errors", async () => {
|
||||
vi.mocked(client.apiPost).mockRejectedValue(new Error("Network error"));
|
||||
|
||||
await expect(sendFederatedQuery("conn-1", "tasks.list")).rejects.toThrow("Network error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchQueryMessages", () => {
|
||||
it("should fetch all query messages", async () => {
|
||||
const mockMessages = [
|
||||
{
|
||||
id: "msg-1",
|
||||
messageId: "uuid-1",
|
||||
connectionId: "conn-1",
|
||||
query: "tasks.list",
|
||||
status: FederationMessageStatus.DELIVERED,
|
||||
response: { data: [] },
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "msg-2",
|
||||
messageId: "uuid-2",
|
||||
connectionId: "conn-2",
|
||||
query: "events.list",
|
||||
status: FederationMessageStatus.PENDING,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(client.apiGet).mockResolvedValue(mockMessages);
|
||||
|
||||
const result = await fetchQueryMessages();
|
||||
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/v1/federation/queries");
|
||||
expect(result).toEqual(mockMessages);
|
||||
});
|
||||
|
||||
it("should fetch query messages filtered by status", async () => {
|
||||
const mockMessages = [
|
||||
{
|
||||
id: "msg-1",
|
||||
messageId: "uuid-1",
|
||||
connectionId: "conn-1",
|
||||
query: "tasks.list",
|
||||
status: FederationMessageStatus.DELIVERED,
|
||||
response: { data: [] },
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(client.apiGet).mockResolvedValue(mockMessages);
|
||||
|
||||
const result = await fetchQueryMessages(FederationMessageStatus.DELIVERED);
|
||||
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/v1/federation/queries?status=DELIVERED");
|
||||
expect(result).toEqual(mockMessages);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchQueryMessage", () => {
|
||||
it("should fetch a single query message", async () => {
|
||||
const mockMessage = {
|
||||
id: "msg-123",
|
||||
messageId: "uuid-123",
|
||||
connectionId: "conn-1",
|
||||
query: "tasks.list",
|
||||
status: FederationMessageStatus.DELIVERED,
|
||||
response: { data: [{ id: "task-1", title: "Test Task" }] },
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
vi.mocked(client.apiGet).mockResolvedValue(mockMessage);
|
||||
|
||||
const result = await fetchQueryMessage("msg-123");
|
||||
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/v1/federation/queries/msg-123");
|
||||
expect(result).toEqual(mockMessage);
|
||||
});
|
||||
|
||||
it("should handle not found errors", async () => {
|
||||
vi.mocked(client.apiGet).mockRejectedValue(new Error("Not found"));
|
||||
|
||||
await expect(fetchQueryMessage("msg-999")).rejects.toThrow("Not found");
|
||||
});
|
||||
});
|
||||
});
|
||||
90
apps/web/src/lib/api/federation-queries.ts
Normal file
90
apps/web/src/lib/api/federation-queries.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Federation Queries API Client
|
||||
* Handles federated query requests to remote instances
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
|
||||
/**
|
||||
* Federation message status (matches backend enum)
|
||||
*/
|
||||
export enum FederationMessageStatus {
|
||||
PENDING = "PENDING",
|
||||
DELIVERED = "DELIVERED",
|
||||
FAILED = "FAILED",
|
||||
}
|
||||
|
||||
/**
|
||||
* Query message details
|
||||
*/
|
||||
export interface QueryMessageDetails {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
connectionId: string;
|
||||
messageType: string;
|
||||
messageId: string;
|
||||
correlationId?: string;
|
||||
query?: string;
|
||||
response?: unknown;
|
||||
status: FederationMessageStatus;
|
||||
error?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deliveredAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send query request
|
||||
*/
|
||||
export interface SendQueryRequest {
|
||||
connectionId: string;
|
||||
query: string;
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a federated query to a remote instance
|
||||
*/
|
||||
export async function sendFederatedQuery(
|
||||
connectionId: string,
|
||||
query: string,
|
||||
context?: Record<string, unknown>
|
||||
): Promise<QueryMessageDetails> {
|
||||
const request: SendQueryRequest = {
|
||||
connectionId,
|
||||
query,
|
||||
};
|
||||
|
||||
if (context) {
|
||||
request.context = context;
|
||||
}
|
||||
|
||||
return apiPost<QueryMessageDetails>("/api/v1/federation/query", request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all query messages for the workspace
|
||||
*/
|
||||
export async function fetchQueryMessages(
|
||||
status?: FederationMessageStatus
|
||||
): Promise<QueryMessageDetails[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (status) {
|
||||
params.append("status", status);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const endpoint = queryString
|
||||
? `/api/v1/federation/queries?${queryString}`
|
||||
: "/api/v1/federation/queries";
|
||||
|
||||
return apiGet<QueryMessageDetails[]>(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single query message
|
||||
*/
|
||||
export async function fetchQueryMessage(messageId: string): Promise<QueryMessageDetails> {
|
||||
return apiGet<QueryMessageDetails>(`/api/v1/federation/queries/${messageId}`);
|
||||
}
|
||||
Reference in New Issue
Block a user