Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"use client";

import type * as schema from "@ctrlplane/db/schema";
import type { EdgeTypes, NodeTypes } from "reactflow";
import ReactFlow, {
MarkerType,
ReactFlowProvider,
useEdgesState,
useNodesState,
} from "reactflow";
import colors from "tailwindcss/colors";

import { useLayoutAndFitView } from "~/app/[workspaceSlug]/(app)/_components/reactflow/layout";
import { DepEdge } from "./DepEdge";
import { ResourceNode } from "./ResourceNode";

type ParentRelationship = {
ruleId: string;
type: schema.ResourceDependencyType;
target: schema.Resource;
reference: string;
};

type ChildRelationship = {
ruleId: string;
type: schema.ResourceDependencyType;
source: schema.Resource;
reference: string;
};

type RelationshipsDiagramProps = {
resource: schema.Resource;
parents: ParentRelationship[];
children: ChildRelationship[];
};

const getNodes = (resources: schema.Resource[]) =>
resources.map((r) => ({
id: r.id,
type: "resource",
data: { ...r, label: r.identifier },
position: { x: 0, y: 0 },
}));

const markerEnd = {
type: MarkerType.Arrow,
color: colors.neutral[800],
};

const nodeTypes: NodeTypes = { resource: ResourceNode };
const edgeTypes: EdgeTypes = { default: DepEdge };

const getParentEdges = (
parents: ParentRelationship[],
resource: schema.Resource,
) =>
parents.map((p) => ({
id: `${p.ruleId}-${p.target.id}`,
source: p.target.id,
target: resource.id,
style: { stroke: colors.neutral[800] },
markerEnd,
label: p.type,
}));

const getChildEdges = (
children: ChildRelationship[],
resource: schema.Resource,
) =>
children.map((c) => ({
id: `${c.ruleId}-${c.source.id}`,
source: resource.id,
target: c.source.id,
style: { stroke: colors.neutral[800] },
markerEnd,
label: c.type,
}));

export const RelationshipsDiagram: React.FC<RelationshipsDiagramProps> = ({
resource,
parents,
children,
}) => {
const [nodes, _, onNodesChange] = useNodesState<{ label: string }>(
getNodes([
resource,
...parents.map((p) => p.target),
...children.map((c) => c.source),
]),
);

const [edges, __, onEdgesChange] = useEdgesState([
...getParentEdges(parents, resource),
...getChildEdges(children, resource),
]);

const { setReactFlowInstance } = useLayoutAndFitView(nodes, {
direction: "LR",
extraEdgeLength: 50,
focusedNodeId: resource.id,
});

return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
fitView
proOptions={{ hideAttribution: true }}
onInit={setReactFlowInstance}
nodesDraggable
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
/>
);
};

export const RelationshipsDiagramProvider: React.FC<
RelationshipsDiagramProps
> = ({ resource, parents, children }) => {
return (
<ReactFlowProvider>
<RelationshipsDiagram
resource={resource}
parents={parents}
children={children}
/>
</ReactFlowProvider>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"use client";

import type { NodeProps } from "reactflow";
import { useParams } from "next/navigation";
import { Handle, Position } from "reactflow";

import { cn } from "@ctrlplane/ui";
Expand All @@ -12,17 +15,17 @@ type ResourceNodeProps = NodeProps<{
id: string;
kind: string;
version: string;
isBaseNode: boolean;
}>;
export const ResourceNode: React.FC<ResourceNodeProps> = (node) => {
const { data } = node;
const { resourceId } = useParams<{ resourceId: string }>();
const { setResourceId } = useResourceDrawer();
return (
<>
<div
className={cn(
"flex w-[250px] cursor-pointer flex-col gap-2 rounded-md border bg-neutral-900/30 px-4 py-3",
data.isBaseNode && "bg-neutral-800/60",
data.id === resourceId && "bg-neutral-800/60",
)}
onClick={() => setResourceId(data.id)}
>
Comment on lines 25 to 31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace clickable <div> with a semantic <button> for a11y & keyboard support

The node is an interactive element but is rendered as a bare div.
Screen-reader users won’t recognise it as clickable and keyboard users cannot focus it.

-<div
-  className={cn(
-    "flex w-[250px] cursor-pointer flex-col gap-2 rounded-md border bg-neutral-900/30 px-4 py-3",
-    data.id === resourceId && "bg-neutral-800/60",
-  )}
-  onClick={() => setResourceId(data.id)}
->
+<button
+  type="button"
+  className={cn(
+    "flex w-[250px] cursor-pointer flex-col gap-2 rounded-md border bg-neutral-900/30 px-4 py-3 text-left",
+    data.id === resourceId && "bg-neutral-800/60",
+  )}
+  onClick={() => setResourceId(data.id)}
+>
 ...
-</div>
+</button>

Benefits: built-in focus handling, space/enter activation, and correct ARIA semantics without extra work.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className={cn(
"flex w-[250px] cursor-pointer flex-col gap-2 rounded-md border bg-neutral-900/30 px-4 py-3",
data.isBaseNode && "bg-neutral-800/60",
data.id === resourceId && "bg-neutral-800/60",
)}
onClick={() => setResourceId(data.id)}
>
<button
type="button"
className={cn(
"flex w-[250px] cursor-pointer flex-col gap-2 rounded-md border bg-neutral-900/30 px-4 py-3 text-left",
data.id === resourceId && "bg-neutral-800/60",
)}
onClick={() => setResourceId(data.id)}
>
{/* existing children/content */}
</button>
🤖 Prompt for AI Agents
In
apps/webservice/src/app/[workspaceSlug]/(app)/resources/(raw)/[resourceId]/visualize/ResourceNode.tsx
around lines 25 to 31, replace the clickable <div> element with a semantic
<button> element to improve accessibility. This change ensures the element is
focusable and operable via keyboard, and provides correct ARIA semantics
automatically. Update the className and onClick handler to the <button> while
removing any non-button attributes that are invalid for <button>.

Expand Down

This file was deleted.

This file was deleted.

Loading
Loading