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
Expand Up @@ -30,6 +30,7 @@ export const ResourceVisualizationDiagram: React.FC<
const setReactFlowInstance = useLayoutAndFitView(nodes, {
direction: "LR",
extraEdgeLength: 50,
focusedNodeId: relationships.resource.id,
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { NodeProps } from "reactflow";
import { Handle, Position } from "reactflow";

import { cn } from "@ctrlplane/ui";

import { useTargetDrawer } from "~/app/[workspaceSlug]/(app)/_components/target-drawer/useTargetDrawer";
import { TargetIcon as ResourceIcon } from "~/app/[workspaceSlug]/(app)/_components/TargetIcon";

Expand All @@ -10,14 +12,18 @@ type ResourceNodeProps = NodeProps<{
id: string;
kind: string;
version: string;
isBaseNode: boolean;
}>;
export const ResourceNode: React.FC<ResourceNodeProps> = (node) => {
const { data } = node;
const { setTargetId: setResourceId } = useTargetDrawer();
return (
<>
<div
className="flex w-[250px] cursor-pointer flex-col gap-2 rounded-md border bg-neutral-900/30 px-4 py-3"
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",
)}
onClick={() => setResourceId(data.id)}
>
<div className="flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ const getResourceNodes = (relationships: Relationships) =>
relationships.nodes.map((r) => ({
id: r.id,
type: NodeType.Resource,
data: { ...r, label: r.identifier },
data: {
...r,
label: r.identifier,
isBaseNode: r.id === relationships.resource.id,
},
position: { x: 0, y: 0 },
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type LayoutConfig = {
nodesep?: number;
padding?: number;
maxZoom?: number;
focusedNodeId?: string;
};

export const useLayoutAndFitView = (nodes: Node[], config?: LayoutConfig) => {
Expand Down Expand Up @@ -99,20 +100,51 @@ export const useLayoutAndFitView = (nodes: Node[], config?: LayoutConfig) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reactFlowInstance]);

const setDefaultView = useCallback(() => {
reactFlowInstance?.fitView({
padding: config?.padding ?? 0.12,
maxZoom: config?.maxZoom ?? 1,
});
setIsViewFitted(true);
}, [reactFlowInstance, config]);

useEffect(() => {
if (
reactFlowInstance != null &&
nodes.length &&
isLayouted &&
!isViewFitted
) {
if (config?.focusedNodeId == null) {
setDefaultView();
return;
}

const focusedNode = nodes.find((n) => n.id === config.focusedNodeId);
if (focusedNode == null) {
setDefaultView();
return;
}

reactFlowInstance.fitView({
padding: config?.padding ?? 0.12,
maxZoom: config?.maxZoom ?? 1,
padding: config.padding ?? 0.12,
maxZoom: config.maxZoom ?? 2,
});
reactFlowInstance.setCenter(
focusedNode.position.x + 100,
focusedNode.position.y + 100,
{ zoom: 1.5, duration: 500 },
);
setIsViewFitted(true);
}
}, [reactFlowInstance, nodes, isLayouted, isViewFitted, config]);
}, [
reactFlowInstance,
nodes,
isLayouted,
isViewFitted,
config,
setDefaultView,
]);

return setReactFlowInstance;
};
99 changes: 81 additions & 18 deletions packages/api/src/router/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ const getNodeDataForResource = async (

type Node = Awaited<ReturnType<typeof getNodeDataForResource>>;

const getNodesRecursivelyHelper = async (
const getChildrenNodesRecursivelyHelper = async (
db: Tx,
node: Node,
nodes: NonNullable<Node>[],
Expand Down Expand Up @@ -286,15 +286,76 @@ const getNodesRecursivelyHelper = async (
const children = await Promise.all(childrenPromises);

const childrenNodesPromises = children.map((c) =>
getNodesRecursivelyHelper(db, c, []),
getChildrenNodesRecursivelyHelper(db, c, []),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential issue with node accumulation in recursive function

In getChildrenNodesRecursivelyHelper, you are passing an empty array [] as the nodes parameter in the recursive calls at line 289. This may result in only immediate child nodes being collected, without accumulating nodes from deeper levels of the tree. Consider passing the existing nodes array to properly accumulate all descendant nodes.

Apply this diff to pass the nodes array in recursive calls:

const childrenNodesPromises = children.map((c) =>
-  getChildrenNodesRecursivelyHelper(db, c, []),
+  getChildrenNodesRecursivelyHelper(db, c, nodes),
);

Committable suggestion skipped: line range outside the PR's diff.

);
const childrenNodes = (await Promise.all(childrenNodesPromises)).flat();
return [...nodes, node, ...childrenNodes].filter(isPresent);
};

const getNodesRecursively = async (db: Tx, resourceId: string) => {
const getChildrenNodesRecursively = async (db: Tx, resourceId: string) => {
const baseNode = await getNodeDataForResource(db, resourceId);
return getNodesRecursivelyHelper(db, baseNode, []);
return getChildrenNodesRecursivelyHelper(db, baseNode, []);
};

type ParentNodesResult = {
parentNodes: NonNullable<Node>[];
node: Node;
};

const getParentNodesRecursivelyHelper = async (
db: Tx,
node: Node,
nodes: NonNullable<Node>[],
): Promise<ParentNodesResult> => {
if (node == null) return { parentNodes: nodes, node };

const parentJob = await db
.select()
.from(schema.jobResourceRelationship)
.innerJoin(
schema.job,
eq(schema.jobResourceRelationship.jobId, schema.job.id),
)
.innerJoin(
schema.releaseJobTrigger,
eq(schema.releaseJobTrigger.jobId, schema.job.id),
)
.innerJoin(
schema.resource,
eq(schema.releaseJobTrigger.resourceId, schema.resource.id),
)
.where(
and(
eq(schema.jobResourceRelationship.resourceIdentifier, node.identifier),
isNull(schema.resource.deletedAt),
eq(schema.resource.workspaceId, node.workspaceId),
),
)
.orderBy(desc(schema.releaseJobTrigger.createdAt))
.limit(1)
.then(takeFirstOrNull);

if (parentJob == null) return { parentNodes: nodes, node };

const parentNode = await getNodeDataForResource(db, parentJob.resource.id);
if (parentNode == null) return { parentNodes: nodes, node };

const { job_resource_relationship: parentRelationship } = parentJob;

const { parentNodes, node: parentNodeWithData } =
await getParentNodesRecursivelyHelper(db, parentNode, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential issue with node accumulation in recursive parent function

In getParentNodesRecursivelyHelper, you are passing an empty array [] as the nodes parameter in the recursive call at line 346. This could result in only immediate parent nodes being collected, without accumulating nodes from higher levels in the hierarchy. Consider passing the existing nodes array to properly accumulate all ancestor nodes.

Apply this diff to pass the nodes array in the recursive call:

const { parentNodes, node: parentNodeWithData } =
-  await getParentNodesRecursivelyHelper(db, parentNode, []);
+  await getParentNodesRecursivelyHelper(db, parentNode, nodes);

Committable suggestion skipped: line range outside the PR's diff.


const nodeWithParent = { ...node, parent: parentRelationship };

return {
parentNodes: [...parentNodes, parentNodeWithData].filter(isPresent),
node: nodeWithParent,
};
};

const getParentNodesRecursively = async (db: Tx, resourceId: string) => {
const baseNode = await getNodeDataForResource(db, resourceId);
return getParentNodesRecursivelyHelper(db, baseNode, []);
};

export const resourceRouter = createTRPCRouter({
Expand Down Expand Up @@ -361,9 +422,17 @@ export const resourceRouter = createTRPCRouter({
where: eq(schema.resource.id, input),
});
if (resource == null) return null;
const childrenNodes = await getNodesRecursively(ctx.db, input);
const childrenNodes = await getChildrenNodesRecursively(ctx.db, input);
const { parentNodes, node } = await getParentNodesRecursively(
ctx.db,
input,
);

const childrenNodesUpdated = childrenNodes.map((n) =>
n.id === node?.id ? node : n,
);

const fromNodesPromises = ctx.db
const nodesQuery = ctx.db
.select()
.from(schema.resourceRelationship)
.innerJoin(
Expand All @@ -372,7 +441,9 @@ export const resourceRouter = createTRPCRouter({
schema.resourceRelationship.fromIdentifier,
schema.resource.identifier,
),
)
);

const fromNodesPromises = nodesQuery
.where(
and(
eq(schema.resourceRelationship.workspaceId, resource.workspaceId),
Expand All @@ -387,16 +458,7 @@ export const resourceRouter = createTRPCRouter({
)
.then((promises) => Promise.all(promises));

const toNodesPromises = ctx.db
.select()
.from(schema.resourceRelationship)
.innerJoin(
schema.resource,
eq(
schema.resourceRelationship.toIdentifier,
schema.resource.identifier,
),
)
const toNodesPromises = nodesQuery
.where(
and(
eq(schema.resourceRelationship.workspaceId, resource.workspaceId),
Expand All @@ -419,7 +481,8 @@ export const resourceRouter = createTRPCRouter({
return {
resource,
nodes: [
...childrenNodes,
...parentNodes,
...childrenNodesUpdated,
...fromNodes.map((n) => n.node),
...toNodes.map((n) => n.node),
].filter(isPresent),
Expand Down
Loading