feat: add datasource and definition graph contracts
This commit is contained in:
147
webui/src/definitionGraph.ts
Normal file
147
webui/src/definitionGraph.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
export type DefinitionGraphPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type DefinitionGraphPort = {
|
||||
id: string;
|
||||
label: string;
|
||||
required: boolean;
|
||||
multiple: boolean;
|
||||
minimum_connections: number;
|
||||
};
|
||||
|
||||
export type DefinitionGraphConfigField = {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: string;
|
||||
required: boolean;
|
||||
description?: string | null;
|
||||
options: [string, string][];
|
||||
};
|
||||
|
||||
export type DefinitionGraphNodeType = {
|
||||
type: string;
|
||||
category: string;
|
||||
category_label: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
input_ports: DefinitionGraphPort[];
|
||||
output_ports: DefinitionGraphPort[];
|
||||
config_fields: DefinitionGraphConfigField[];
|
||||
default_config: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DefinitionGraphNode = {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
position: DefinitionGraphPosition;
|
||||
config: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DefinitionGraphEdge = {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
source_port?: string;
|
||||
target_port?: string;
|
||||
};
|
||||
|
||||
export type DefinitionGraph = {
|
||||
schema_version: number;
|
||||
nodes: DefinitionGraphNode[];
|
||||
edges: DefinitionGraphEdge[];
|
||||
};
|
||||
|
||||
export type DefinitionGraphConnection = {
|
||||
source: string;
|
||||
target: string;
|
||||
sourcePort?: string | null;
|
||||
targetPort?: string | null;
|
||||
};
|
||||
|
||||
export function createDefinitionGraphNode<TNode extends DefinitionGraphNode = DefinitionGraphNode>(
|
||||
type: string,
|
||||
position: DefinitionGraphPosition,
|
||||
library: DefinitionGraphNodeType[]
|
||||
): TNode {
|
||||
const definition = library.find((item) => item.type === type);
|
||||
return {
|
||||
id: `${type.replace(/\./g, "-")}-${crypto.randomUUID()}`,
|
||||
type,
|
||||
label: definition?.label ?? type,
|
||||
position,
|
||||
config: structuredClone(definition?.default_config ?? {})
|
||||
} as TNode;
|
||||
}
|
||||
|
||||
export function definitionConnectionError(
|
||||
graph: DefinitionGraph,
|
||||
library: DefinitionGraphNodeType[],
|
||||
connection: DefinitionGraphConnection,
|
||||
options: { allowCycles?: boolean } = {}
|
||||
): string | null {
|
||||
if (!connection.source || !connection.target) return "Both endpoints are required.";
|
||||
if (connection.source === connection.target) return "A node cannot connect to itself.";
|
||||
const source = graph.nodes.find((node) => node.id === connection.source);
|
||||
const target = graph.nodes.find((node) => node.id === connection.target);
|
||||
if (!source || !target) return "The connection references an unknown node.";
|
||||
const sourceDefinition = library.find((item) => item.type === source.type);
|
||||
const targetDefinition = library.find((item) => item.type === target.type);
|
||||
if (!sourceDefinition || !targetDefinition) return "The node type is not in this library.";
|
||||
const sourcePort = connection.sourcePort ?? "output";
|
||||
const targetPort = connection.targetPort ?? "input";
|
||||
if (!sourceDefinition.output_ports.some((port) => port.id === sourcePort)) {
|
||||
return "The source port is not available.";
|
||||
}
|
||||
const port = targetDefinition.input_ports.find((item) => item.id === targetPort);
|
||||
if (!port) return "The target port is not available.";
|
||||
if (graph.edges.some(
|
||||
(edge) =>
|
||||
edge.source === connection.source
|
||||
&& edge.target === connection.target
|
||||
&& (edge.source_port ?? "output") === sourcePort
|
||||
&& (edge.target_port ?? "input") === targetPort
|
||||
)) {
|
||||
return "This connection already exists.";
|
||||
}
|
||||
if (!port.multiple && graph.edges.some(
|
||||
(edge) =>
|
||||
edge.target === connection.target
|
||||
&& (edge.target_port ?? "input") === targetPort
|
||||
)) {
|
||||
return "The target port accepts only one connection.";
|
||||
}
|
||||
if (!options.allowCycles && wouldCreateDefinitionCycle(
|
||||
graph,
|
||||
connection.source,
|
||||
connection.target
|
||||
)) {
|
||||
return "This connection would create a cycle.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function wouldCreateDefinitionCycle(
|
||||
graph: DefinitionGraph,
|
||||
source: string,
|
||||
target: string
|
||||
): boolean {
|
||||
const outgoing = new Map<string, string[]>();
|
||||
graph.edges.forEach((edge) => {
|
||||
outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge.target]);
|
||||
});
|
||||
const pending = [target];
|
||||
const visited = new Set<string>();
|
||||
while (pending.length) {
|
||||
const nodeId = pending.pop();
|
||||
if (!nodeId || visited.has(nodeId)) continue;
|
||||
if (nodeId === source) return true;
|
||||
visited.add(nodeId);
|
||||
pending.push(...(outgoing.get(nodeId) ?? []));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, Folder, Form, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, Users, Waypoints, type LucideIcon } from "lucide-react";
|
||||
import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, Form, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, Users, Waypoints, type LucideIcon } from "lucide-react";
|
||||
import installedWebModules from "virtual:govoplan-installed-modules";
|
||||
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformWebModule } from "../types";
|
||||
import {
|
||||
@@ -49,6 +49,7 @@ const iconByName: Record<string, LucideIcon> = {
|
||||
campaign: Mails,
|
||||
"clipboard-pen-line": ClipboardPenLine,
|
||||
dashboard: LayoutDashboard,
|
||||
"database-zap": DatabaseZap,
|
||||
file: Folder,
|
||||
files: Folder,
|
||||
folder: Folder,
|
||||
|
||||
Reference in New Issue
Block a user