feat(FN-3079): add plugin dashboard views and dependency graph plugin
- Add plugin dashboard view registration and hosting across core, dashboard routes, and plugin SDK exports - Integrate plugin-provided views into app navigation, mobile/header UI, and view state hooks with coverage - Add fusion-plugin-dependency-graph example plugin with persisted storage, dashboard view UI, and manifest wiring - Update plugin authoring and architecture docs for dashboard view extension points - Add a changeset for @runfusion/fusion covering plugin dashboard view support Fusion-Task-Id: FN-3079
This commit is contained in:
20
packages/dashboard/app/plugins/PluginDashboardViewHost.tsx
Normal file
20
packages/dashboard/app/plugins/PluginDashboardViewHost.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { resolvePluginDashboardView, MissingPluginDashboardView, parsePluginTaskViewId } from "./pluginViewRegistry";
|
||||
import type { PluginDashboardHostContext, PluginTaskView } from "./pluginViewRegistry";
|
||||
|
||||
export function PluginDashboardViewHost({
|
||||
taskView,
|
||||
context,
|
||||
}: {
|
||||
taskView: PluginTaskView;
|
||||
context: PluginDashboardHostContext;
|
||||
}) {
|
||||
const parsed = parsePluginTaskViewId(taskView);
|
||||
if (!parsed) return null;
|
||||
|
||||
const ViewComponent = resolvePluginDashboardView(parsed.pluginId, parsed.viewId);
|
||||
if (!ViewComponent) {
|
||||
return <>{MissingPluginDashboardView({ pluginId: parsed.pluginId, viewId: parsed.viewId })}</>;
|
||||
}
|
||||
|
||||
return <ViewComponent context={context} />;
|
||||
}
|
||||
27
packages/dashboard/app/plugins/pluginViewRegistry.css
Normal file
27
packages/dashboard/app/plugins/pluginViewRegistry.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.plugin-dashboard-view-missing {
|
||||
margin: var(--space-lg);
|
||||
padding: var(--space-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.plugin-dashboard-view-missing-title {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.plugin-dashboard-view-missing-description {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.plugin-dashboard-view-missing {
|
||||
margin: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
}
|
||||
}
|
||||
64
packages/dashboard/app/plugins/pluginViewRegistry.tsx
Normal file
64
packages/dashboard/app/plugins/pluginViewRegistry.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import type { ComponentType, ReactNode } from "react";
|
||||
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
|
||||
import { DependencyGraphView } from "@fusion-plugin-examples/dependency-graph/dashboard-view";
|
||||
import "./pluginViewRegistry.css";
|
||||
|
||||
export type PluginTaskView = `plugin:${string}:${string}`;
|
||||
|
||||
export interface PluginDashboardHostContext {
|
||||
projectId?: string;
|
||||
tasks: Task[];
|
||||
workflowSteps: WorkflowStep[];
|
||||
openTaskDetail: (task: Task | TaskDetail, initialTab?: "logs" | "changes") => void;
|
||||
renderTaskCard: (task: Task) => ReactNode;
|
||||
}
|
||||
|
||||
export interface PluginDashboardViewComponentProps {
|
||||
context: PluginDashboardHostContext;
|
||||
}
|
||||
|
||||
export interface PluginDashboardViewRegistration {
|
||||
pluginId: string;
|
||||
viewId: string;
|
||||
component: ComponentType<PluginDashboardViewComponentProps>;
|
||||
}
|
||||
|
||||
const REGISTRY: PluginDashboardViewRegistration[] = [
|
||||
{
|
||||
pluginId: "fusion-plugin-dependency-graph",
|
||||
viewId: "graph",
|
||||
component: DependencyGraphView as ComponentType<PluginDashboardViewComponentProps>,
|
||||
},
|
||||
];
|
||||
|
||||
export function buildPluginTaskViewId(pluginId: string, viewId: string): PluginTaskView {
|
||||
return `plugin:${pluginId}:${viewId}`;
|
||||
}
|
||||
|
||||
export function parsePluginTaskViewId(taskView: string): { pluginId: string; viewId: string } | null {
|
||||
if (!taskView.startsWith("plugin:")) return null;
|
||||
const [, pluginId, ...viewParts] = taskView.split(":");
|
||||
const viewId = viewParts.join(":");
|
||||
if (!pluginId || !viewId) return null;
|
||||
return { pluginId, viewId };
|
||||
}
|
||||
|
||||
export function resolvePluginDashboardView(pluginId: string, viewId: string): ComponentType<PluginDashboardViewComponentProps> | null {
|
||||
const hit = REGISTRY.find((entry) => entry.pluginId === pluginId && entry.viewId === viewId);
|
||||
return hit?.component ?? null;
|
||||
}
|
||||
|
||||
export function MissingPluginDashboardView({ pluginId, viewId }: { pluginId: string; viewId: string }): ReactNode {
|
||||
return (
|
||||
<section className="card plugin-dashboard-view-missing">
|
||||
<h2 className="plugin-dashboard-view-missing-title">
|
||||
<AlertTriangle />
|
||||
Plugin view unavailable
|
||||
</h2>
|
||||
<p className="plugin-dashboard-view-missing-description">
|
||||
The dashboard could not resolve <code>{pluginId}:{viewId}</code> from the host registry.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user