Issue #73 - Entry-Centered Graph View: - Added GET /api/knowledge/entries/:id/graph endpoint with depth parameter - Returns entry + connected nodes with link relationships - Created GraphService for graph traversal using BFS - Added EntryGraphViewer component for frontend - Integrated graph view tab into entry detail page Issue #74 - Graph Statistics Dashboard: - Added GET /api/knowledge/stats endpoint - Returns overview stats (entries, tags, links by status) - Includes most connected entries, recent activity, tag distribution - Created StatsDashboard component with visual stats - Added route at /knowledge/stats Backend: - GraphService: BFS-based graph traversal with configurable depth - StatsService: Parallel queries for comprehensive statistics - GraphQueryDto: Validation for depth parameter (1-5) - Entity types for graph nodes/edges and statistics - Unit tests for both services Frontend: - EntryGraphViewer: Entry-centered graph visualization - StatsDashboard: Statistics overview with charts - Graph view tab on entry detail page - API client functions for new endpoints - TypeScript strict typing throughout
384 lines
9.6 KiB
TypeScript
384 lines
9.6 KiB
TypeScript
/**
|
|
* Knowledge API Client
|
|
* Handles knowledge entry-related API requests
|
|
*/
|
|
|
|
import type { KnowledgeEntryWithTags, KnowledgeTag } from "@mosaic/shared";
|
|
import { EntryStatus, Visibility } from "@mosaic/shared";
|
|
import { apiGet, apiPost, apiPatch, apiDelete, type ApiResponse } from "./client";
|
|
|
|
export interface EntryFilters {
|
|
status?: EntryStatus;
|
|
visibility?: Visibility;
|
|
tag?: string;
|
|
tags?: string[];
|
|
page?: number;
|
|
limit?: number;
|
|
search?: string;
|
|
sortBy?: "updatedAt" | "createdAt" | "title";
|
|
sortOrder?: "asc" | "desc";
|
|
}
|
|
|
|
export interface EntriesResponse {
|
|
data: KnowledgeEntryWithTags[];
|
|
meta?: {
|
|
total?: number;
|
|
page?: number;
|
|
limit?: number;
|
|
};
|
|
}
|
|
|
|
export interface CreateEntryData {
|
|
title: string;
|
|
content: string;
|
|
summary?: string;
|
|
status?: EntryStatus;
|
|
visibility?: Visibility;
|
|
tags?: string[];
|
|
}
|
|
|
|
export interface UpdateEntryData {
|
|
title?: string;
|
|
content?: string;
|
|
summary?: string;
|
|
status?: EntryStatus;
|
|
visibility?: Visibility;
|
|
tags?: string[];
|
|
}
|
|
|
|
/**
|
|
* Fetch knowledge entries with optional filters
|
|
*/
|
|
export async function fetchEntries(filters?: EntryFilters): Promise<EntriesResponse> {
|
|
const params = new URLSearchParams();
|
|
|
|
if (filters?.status) {
|
|
params.append("status", filters.status);
|
|
}
|
|
if (filters?.visibility) {
|
|
params.append("visibility", filters.visibility);
|
|
}
|
|
if (filters?.tag) {
|
|
params.append("tag", filters.tag);
|
|
}
|
|
if (filters?.tags && filters.tags.length > 0) {
|
|
filters.tags.forEach((tag) => params.append("tags", tag));
|
|
}
|
|
if (filters?.page) {
|
|
params.append("page", filters.page.toString());
|
|
}
|
|
if (filters?.limit) {
|
|
params.append("limit", filters.limit.toString());
|
|
}
|
|
if (filters?.search) {
|
|
params.append("search", filters.search);
|
|
}
|
|
if (filters?.sortBy) {
|
|
params.append("sortBy", filters.sortBy);
|
|
}
|
|
if (filters?.sortOrder) {
|
|
params.append("sortOrder", filters.sortOrder);
|
|
}
|
|
|
|
const queryString = params.toString();
|
|
const endpoint = queryString
|
|
? `/api/knowledge/entries?${queryString}`
|
|
: "/api/knowledge/entries";
|
|
|
|
const response = await apiGet<EntriesResponse>(endpoint);
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Fetch a single knowledge entry by slug
|
|
*/
|
|
export async function fetchEntry(slug: string): Promise<KnowledgeEntryWithTags> {
|
|
return apiGet<KnowledgeEntryWithTags>(`/api/knowledge/entries/${slug}`);
|
|
}
|
|
|
|
/**
|
|
* Create a new knowledge entry
|
|
*/
|
|
export async function createEntry(data: CreateEntryData): Promise<KnowledgeEntryWithTags> {
|
|
return apiPost<KnowledgeEntryWithTags>("/api/knowledge/entries", data);
|
|
}
|
|
|
|
/**
|
|
* Update an existing knowledge entry
|
|
*/
|
|
export async function updateEntry(
|
|
slug: string,
|
|
data: UpdateEntryData
|
|
): Promise<KnowledgeEntryWithTags> {
|
|
return apiPatch<KnowledgeEntryWithTags>(`/api/knowledge/entries/${slug}`, data);
|
|
}
|
|
|
|
/**
|
|
* Delete (archive) a knowledge entry
|
|
*/
|
|
export async function deleteEntry(slug: string): Promise<void> {
|
|
await apiDelete(`/api/knowledge/entries/${slug}`);
|
|
}
|
|
|
|
/**
|
|
* Fetch all knowledge tags
|
|
*/
|
|
export async function fetchTags(): Promise<KnowledgeTag[]> {
|
|
const response = await apiGet<ApiResponse<KnowledgeTag[]>>("/api/knowledge/tags");
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Fetch entry-centered graph view
|
|
*/
|
|
export async function fetchEntryGraph(slug: string, depth: number = 1) {
|
|
const params = new URLSearchParams();
|
|
params.append("depth", depth.toString());
|
|
return apiGet(`/api/knowledge/entries/${slug}/graph?${params.toString()}`);
|
|
}
|
|
|
|
/**
|
|
* Fetch knowledge base statistics
|
|
*/
|
|
export async function fetchKnowledgeStats() {
|
|
return apiGet("/api/knowledge/stats");
|
|
}
|
|
|
|
/**
|
|
* Mock entries for development (until backend endpoints are ready)
|
|
*/
|
|
export const mockEntries: KnowledgeEntryWithTags[] = [
|
|
{
|
|
id: "entry-1",
|
|
workspaceId: "workspace-1",
|
|
slug: "getting-started",
|
|
title: "Getting Started with Mosaic Stack",
|
|
content: "# Getting Started\n\nWelcome to Mosaic Stack...",
|
|
contentHtml: "<h1>Getting Started</h1><p>Welcome to Mosaic Stack...</p>",
|
|
summary: "A comprehensive guide to getting started with the Mosaic Stack platform.",
|
|
status: EntryStatus.PUBLISHED,
|
|
visibility: Visibility.PUBLIC,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date("2026-01-20"),
|
|
updatedAt: new Date("2026-01-28"),
|
|
tags: [
|
|
{
|
|
id: "tag-1",
|
|
workspaceId: "workspace-1",
|
|
name: "Tutorial",
|
|
slug: "tutorial",
|
|
color: "#3B82F6",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-2",
|
|
workspaceId: "workspace-1",
|
|
name: "Onboarding",
|
|
slug: "onboarding",
|
|
color: "#10B981",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "entry-2",
|
|
workspaceId: "workspace-1",
|
|
slug: "architecture-overview",
|
|
title: "Architecture Overview",
|
|
content: "# Architecture\n\nThe Mosaic Stack architecture...",
|
|
contentHtml: "<h1>Architecture</h1><p>The Mosaic Stack architecture...</p>",
|
|
summary: "Overview of the system architecture and design patterns used in Mosaic Stack.",
|
|
status: EntryStatus.PUBLISHED,
|
|
visibility: Visibility.WORKSPACE,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date("2026-01-15"),
|
|
updatedAt: new Date("2026-01-27"),
|
|
tags: [
|
|
{
|
|
id: "tag-3",
|
|
workspaceId: "workspace-1",
|
|
name: "Architecture",
|
|
slug: "architecture",
|
|
color: "#8B5CF6",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-4",
|
|
workspaceId: "workspace-1",
|
|
name: "Technical",
|
|
slug: "technical",
|
|
color: "#F59E0B",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "entry-3",
|
|
workspaceId: "workspace-1",
|
|
slug: "api-documentation-draft",
|
|
title: "API Documentation (Draft)",
|
|
content: "# API Docs\n\nWork in progress...",
|
|
contentHtml: "<h1>API Docs</h1><p>Work in progress...</p>",
|
|
summary: "Comprehensive API documentation for developers.",
|
|
status: EntryStatus.DRAFT,
|
|
visibility: Visibility.PRIVATE,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date("2026-01-29"),
|
|
updatedAt: new Date("2026-01-29"),
|
|
tags: [
|
|
{
|
|
id: "tag-4",
|
|
workspaceId: "workspace-1",
|
|
name: "Technical",
|
|
slug: "technical",
|
|
color: "#F59E0B",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-5",
|
|
workspaceId: "workspace-1",
|
|
name: "API",
|
|
slug: "api",
|
|
color: "#EF4444",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "entry-4",
|
|
workspaceId: "workspace-1",
|
|
slug: "deployment-guide",
|
|
title: "Deployment Guide",
|
|
content: "# Deployment\n\nHow to deploy Mosaic Stack...",
|
|
contentHtml: "<h1>Deployment</h1><p>How to deploy Mosaic Stack...</p>",
|
|
summary: "Step-by-step guide for deploying Mosaic Stack to production.",
|
|
status: EntryStatus.PUBLISHED,
|
|
visibility: Visibility.WORKSPACE,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date("2026-01-18"),
|
|
updatedAt: new Date("2026-01-25"),
|
|
tags: [
|
|
{
|
|
id: "tag-6",
|
|
workspaceId: "workspace-1",
|
|
name: "DevOps",
|
|
slug: "devops",
|
|
color: "#14B8A6",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-1",
|
|
workspaceId: "workspace-1",
|
|
name: "Tutorial",
|
|
slug: "tutorial",
|
|
color: "#3B82F6",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "entry-5",
|
|
workspaceId: "workspace-1",
|
|
slug: "old-meeting-notes",
|
|
title: "Q4 2025 Meeting Notes",
|
|
content: "# Meeting Notes\n\nOld archived notes...",
|
|
contentHtml: "<h1>Meeting Notes</h1><p>Old archived notes...</p>",
|
|
summary: "Meeting notes from Q4 2025 - archived for reference.",
|
|
status: EntryStatus.ARCHIVED,
|
|
visibility: Visibility.PRIVATE,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date("2025-12-15"),
|
|
updatedAt: new Date("2026-01-05"),
|
|
tags: [
|
|
{
|
|
id: "tag-7",
|
|
workspaceId: "workspace-1",
|
|
name: "Meetings",
|
|
slug: "meetings",
|
|
color: "#6B7280",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export const mockTags: KnowledgeTag[] = [
|
|
{
|
|
id: "tag-1",
|
|
workspaceId: "workspace-1",
|
|
name: "Tutorial",
|
|
slug: "tutorial",
|
|
color: "#3B82F6",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-2",
|
|
workspaceId: "workspace-1",
|
|
name: "Onboarding",
|
|
slug: "onboarding",
|
|
color: "#10B981",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-3",
|
|
workspaceId: "workspace-1",
|
|
name: "Architecture",
|
|
slug: "architecture",
|
|
color: "#8B5CF6",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-4",
|
|
workspaceId: "workspace-1",
|
|
name: "Technical",
|
|
slug: "technical",
|
|
color: "#F59E0B",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-5",
|
|
workspaceId: "workspace-1",
|
|
name: "API",
|
|
slug: "api",
|
|
color: "#EF4444",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-6",
|
|
workspaceId: "workspace-1",
|
|
name: "DevOps",
|
|
slug: "devops",
|
|
color: "#14B8A6",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "tag-7",
|
|
workspaceId: "workspace-1",
|
|
name: "Meetings",
|
|
slug: "meetings",
|
|
color: "#6B7280",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
];
|