From b45d44805714aee67370b56a649af0265cd50320 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 25 Nov 2024 01:23:01 -0800 Subject: [PATCH 1/4] init recusrive relations --- .../ResourceVisualizationDiagram.tsx | 77 +++--- .../targets/[targetId]/visualize/edges.ts | 36 ++- .../visualize/nodes/EnvironmentNode.tsx | 109 +++++--- .../targets/[targetId]/visualize/page.tsx | 16 +- packages/api/src/router/resources.ts | 241 ++++++++++++++---- packages/db/src/schema/job.ts | 3 +- packages/db/src/schema/resource.ts | 17 +- 7 files changed, 363 insertions(+), 136 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx index 7ba2c3a90..ae7d22c37 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx @@ -1,7 +1,6 @@ "use client"; import type { RouterOutputs } from "@ctrlplane/api"; -import type * as SCHEMA from "@ctrlplane/db/schema"; import type { EdgeTypes, NodeTypes } from "reactflow"; import React from "react"; import { compact } from "lodash"; @@ -15,6 +14,7 @@ import { useLayoutAndFitView } from "~/app/[workspaceSlug]/(app)/_components/rea import { DepEdge } from "./DepEdge"; import { createEdgeFromProviderToResource, + createEdgesFromDeploymentsToResources, createEdgesFromResourceToEnvironments, } from "./edges"; import { EnvironmentNode } from "./nodes/EnvironmentNode"; @@ -24,7 +24,6 @@ import { ResourceNode } from "./nodes/ResourceNode"; type Relationships = NonNullable; type ResourceVisualizationDiagramProps = { - resource: SCHEMA.Resource; relationships: Relationships; }; @@ -43,49 +42,59 @@ const edgeTypes: EdgeTypes = { default: DepEdge }; export const ResourceVisualizationDiagram: React.FC< ResourceVisualizationDiagramProps -> = ({ resource, relationships }) => { - const { workspace, provider } = relationships; - const { systems } = workspace; +> = ({ relationships }) => { const [nodes, _, onNodesChange] = useNodesState<{ label: string }>( compact([ - { - id: resource.id, + ...relationships.map((r) => ({ + id: r.id, type: NodeType.Resource, - data: { ...resource, label: resource.identifier }, + data: { ...r, label: r.identifier }, position: { x: 0, y: 0 }, - }, - ...systems.flatMap((system) => - system.environments.map((env) => ({ - id: env.id, - type: NodeType.Environment, - data: { - environment: { - ...env, - deployments: system.deployments, - resource, + })), + ...relationships.flatMap((r) => + r.provider != null + ? [ + { + id: `${r.provider.id}-${r.id}`, + type: NodeType.Provider, + data: { ...r.provider, label: r.provider.name }, + position: { x: 0, y: 0 }, + }, + ] + : [], + ), + ...relationships.flatMap((r) => + r.workspace.systems.flatMap((s) => + s.environments.map((e) => ({ + id: e.id, + type: NodeType.Environment, + data: { + environment: { + ...e, + deployments: s.deployments, + resource: r, + }, + label: `${s.name}/${e.name}`, }, - label: `${system.name}/${env.name}`, - }, - position: { x: 0, y: 0 }, - })), + position: { x: 0, y: 0 }, + })), + ), ), - provider != null && { - id: provider.id, - type: NodeType.Provider, - data: { ...provider, label: provider.name }, - position: { x: 0, y: 0 }, - }, ]), ); - const resourceToEnvEdges = createEdgesFromResourceToEnvironments( - resource, - systems.flatMap((s) => s.environments), + const resourceToEnvEdges = relationships.flatMap((r) => + createEdgesFromResourceToEnvironments( + r, + r.workspace.systems.flatMap((s) => s.environments), + ), ); - const providerEdge = createEdgeFromProviderToResource(provider, resource); - + const providerEdges = relationships.flatMap((r) => + r.provider != null ? [createEdgeFromProviderToResource(r.provider, r)] : [], + ); + const deploymentEdges = createEdgesFromDeploymentsToResources(relationships); const [edges, __, onEdgesChange] = useEdgesState( - compact([...resourceToEnvEdges, providerEdge]), + compact([...resourceToEnvEdges, ...providerEdges, ...deploymentEdges]), ); const setReactFlowInstance = useLayoutAndFitView(nodes, { direction: "LR" }); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts index 6123ec35f..56654f8de 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts @@ -1,3 +1,4 @@ +import type { RouterOutputs } from "@ctrlplane/api"; import type * as SCHEMA from "@ctrlplane/db/schema"; import { MarkerType } from "reactflow"; import colors from "tailwindcss/colors"; @@ -31,10 +32,43 @@ export const createEdgeFromProviderToResource = ( provider != null ? { id: `${provider.id}-${resource.id}`, - source: provider.id, + source: `${provider.id}-${resource.id}`, target: resource.id, style: { stroke: colors.neutral[700] }, markerEnd, label: "discovered", } : null; + +type Relationships = NonNullable; + +export const createEdgesFromDeploymentsToResources = ( + relationships: Relationships, +) => + relationships.map((resource) => { + const { parent } = resource; + if (parent == null) return null; + + const allReleaseJobTriggers = relationships.flatMap((r) => + r.workspace.systems.flatMap((s) => + s.environments.flatMap((e) => + e.latestActiveRelease.map((rel) => rel.releaseJobTrigger), + ), + ), + ); + + const releaseJobTrigger = allReleaseJobTriggers.find( + (j) => j.jobId === parent.jobId, + ); + if (releaseJobTrigger == null) return null; + + return { + id: `${releaseJobTrigger.jobId}-${resource.id}`, + source: releaseJobTrigger.environmentId, + sourceHandle: releaseJobTrigger.jobId, + target: resource.id, + style: { stroke: colors.neutral[700] }, + markerEnd, + label: "created", + }; + }); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx index 33ee352c7..c08e353fc 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx @@ -1,5 +1,6 @@ "use client"; +import type { RouterOutputs } from "@ctrlplane/api"; import type * as SCHEMA from "@ctrlplane/db/schema"; import type { NodeProps } from "reactflow"; import React from "react"; @@ -11,12 +12,16 @@ import { cn } from "@ctrlplane/ui"; import { Skeleton } from "@ctrlplane/ui/skeleton"; import { JobStatus, JobStatusReadable } from "@ctrlplane/validators/jobs"; +import { useJobDrawer } from "~/app/[workspaceSlug]/(app)/_components/job-drawer/useJobDrawer"; import { api } from "~/trpc/react"; import { ReleaseIcon } from "../../ReleaseCell"; -type Environment = SCHEMA.Environment & { +type Env = NonNullable< + RouterOutputs["resource"]["relationships"][number] +>["workspace"]["systems"][number]["environments"][number]; + +type Environment = Env & { deployments: SCHEMA.Deployment[]; - resource: SCHEMA.Resource; }; type EnvironmentNodeProps = NodeProps<{ @@ -28,25 +33,38 @@ const DeploymentCard: React.FC<{ deploymentName: string; job?: SCHEMA.Job; releaseVersion: string; -}> = ({ deploymentName, job, releaseVersion }) => ( -
-
{deploymentName}
- {job != null && ( -
- -
-
{releaseVersion}
-
{JobStatusReadable[job.status]}
-
-
- )} - {job == null && ( -
- No active job +}> = ({ deploymentName, job, releaseVersion }) => { + const { setJobId } = useJobDrawer(); + return ( + <> +
{ + if (job != null) setJobId(job.id); + }} + > + {deploymentName} + {job != null && ( +
+ +
+
{releaseVersion}
+
{JobStatusReadable[job.status]}
+
+
+ )} + {job == null && ( +
+ No active job +
+ )}
- )} -
-); + + ); +}; export const EnvironmentNode: React.FC = (node) => { const { data } = node; @@ -56,7 +74,7 @@ export const EnvironmentNode: React.FC = (node) => { const latestActiveReleasesQ = api.resource.activeReleases.byResourceAndEnvironmentId.useQuery( { resourceId, environmentId }, - { refetchInterval: 5_000 }, + // { refetchInterval: 5_000 }, ); const latestActiveReleases = latestActiveReleasesQ.data ?? []; @@ -71,11 +89,13 @@ export const EnvironmentNode: React.FC = (node) => { (r) => r.releaseJobTrigger.job.status === JobStatus.Completed, ); + const numDeployments = data.environment.deployments.length; + return ( <>
= (node) => {
{latestActiveReleasesQ.isLoading && - _.range(3).map((i) => ( + _.range(numDeployments).map((i) => ( ))} {!latestActiveReleasesQ.isLoading && @@ -115,14 +135,41 @@ export const EnvironmentNode: React.FC = (node) => {
- + {data.environment.deployments.map((deployment, index) => { + // position handles vertically: header (+ extra accounting for padding) + + // (card height + gap) * index + half card height for centering + const topOffset = 44 + (56 + 16) * index + 56 / 2; + const latestActiveRelease = data.environment.latestActiveRelease.find( + (r) => r.releaseJobTrigger.release.deploymentId === deployment.id, + ); + console.log({ + latestActiveReleaseJobId: + latestActiveRelease?.releaseJobTrigger.job.id, + }); + return ( + + ); + })} ); }; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/page.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/page.tsx index 3da323303..2344699a6 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/page.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/page.tsx @@ -1,5 +1,3 @@ -import { notFound } from "next/navigation"; - import { api } from "~/trpc/server"; import { ResourceVisualizationDiagramProvider } from "./ResourceVisualizationDiagram"; @@ -8,16 +6,6 @@ export default async function VisualizePage({ }: { params: { targetId: string }; }) { - const [resource, relationships] = await Promise.all([ - api.resource.byId(targetId), - api.resource.relationships(targetId), - ]); - if (resource == null || relationships == null) return notFound(); - - return ( - - ); + const relationships = await api.resource.relationships(targetId); + return ; } diff --git a/packages/api/src/router/resources.ts b/packages/api/src/router/resources.ts index 5bbd8797d..4f6bb01c9 100644 --- a/packages/api/src/router/resources.ts +++ b/packages/api/src/router/resources.ts @@ -330,6 +330,193 @@ const environmentHasResource = ( }) .then((matchedResource) => matchedResource != null); +const latestActiveReleaseByResourceAndEnvironmentId = ( + db: Tx, + resourceId: string, + environmentId: string, +) => { + const rankSubquery = db + .select({ + rank: sql`ROW_NUMBER() OVER (PARTITION BY ${schema.release.deploymentId} ORDER BY ${schema.release.createdAt} DESC)`.as( + "rank", + ), + rankReleaseId: schema.release.id, + rankDeploymentId: schema.release.deploymentId, + }) + .from(schema.release) + .as("rank_subquery"); + + return db + .select() + .from(schema.deployment) + .innerJoin(schema.system, eq(schema.system.id, schema.deployment.systemId)) + .innerJoin( + schema.environment, + eq(schema.environment.systemId, schema.system.id), + ) + .innerJoin( + schema.release, + eq(schema.release.deploymentId, schema.deployment.id), + ) + .innerJoin( + rankSubquery, + and( + eq(rankSubquery.rankDeploymentId, schema.release.deploymentId), + eq(rankSubquery.rankReleaseId, schema.release.id), + ), + ) + .innerJoin( + schema.releaseJobTrigger, + and( + eq(schema.releaseJobTrigger.releaseId, schema.release.id), + eq(schema.releaseJobTrigger.environmentId, schema.environment.id), + ), + ) + .innerJoin( + schema.resource, + eq(schema.resource.id, schema.releaseJobTrigger.resourceId), + ) + .innerJoin(schema.job, eq(schema.releaseJobTrigger.jobId, schema.job.id)) + .where( + and( + eq(schema.resource.id, resourceId), + eq(schema.environment.id, environmentId), + isNull(schema.resource.deletedAt), + eq(rankSubquery.rank, 1), + ), + ) + .orderBy(schema.deployment.id, schema.releaseJobTrigger.createdAt) + .then((r) => + r.map((row) => ({ + ...row.deployment, + environment: row.environment, + system: row.system, + releaseJobTrigger: { + ...row.release_job_trigger, + job: row.job, + release: row.release, + resourceId: row.resource.id, + }, + })), + ); +}; + +const getNodeDataForResource = async (db: Tx, resourceId: string) => { + const hasFilter = isNotNull(schema.environment.resourceFilter); + const resource = await db.query.resource.findFirst({ + where: and(eq(schema.resource.id, resourceId), isNotDeleted), + with: { + provider: { with: { google: true } }, + workspace: { + with: { + systems: { + with: { environments: { where: hasFilter }, deployments: true }, + }, + }, + }, + }, + }); + if (resource == null) return null; + + const matchesIdentifier = eq( + schema.jobResourceRelationship.resourceIdentifier, + resource.identifier, + ); + const parent = await db.query.jobResourceRelationship.findFirst({ + where: matchesIdentifier, + with: { job: { with: { releaseTrigger: true } } }, + }); + + const { systems } = resource.workspace; + const systemsWithResource = await _.chain( + systems.map(async (s) => + _.chain(s.environments) + .filter((e) => isPresent(e.resourceFilter)) + .map((e) => + environmentHasResource(db, resource.id, e.resourceFilter!).then( + async (t) => + t + ? { + ...e, + resource, + latestActiveRelease: + await latestActiveReleaseByResourceAndEnvironmentId( + db, + resource.id, + e.id, + ), + } + : null, + ), + ) + .thru((promises) => Promise.all(promises)) + .thru((results) => { + return results; + }) + .value() + .then((t) => t.filter(isPresent)) + .then((t) => (t.length > 0 ? { ...s, environments: t } : null)), + ), + ) + .thru((promises) => Promise.all(promises)) + .value() + .then((t) => t.filter(isPresent)); + + const provider = + resource.provider == null + ? null + : { + ...resource.provider, + google: resource.provider.google[0] ?? null, + }; + + return { + ...resource, + workspace: { ...resource.workspace, systems: systemsWithResource }, + provider, + parent: parent ?? null, + }; +}; + +type Node = Awaited>; + +const getNodesRecursivelyHelper = async ( + db: Tx, + node: Node, + nodes: NonNullable[], +): Promise[]> => { + if (node == null) return nodes; + const activeReleaseJobs = node.workspace.systems.flatMap((s) => + s.environments.flatMap((e) => + e.latestActiveRelease.map((r) => r.releaseJobTrigger.job), + ), + ); + + const relationships = await db.query.jobResourceRelationship.findMany({ + where: inArray( + schema.jobResourceRelationship.jobId, + activeReleaseJobs.map((j) => j.id), + ), + with: { resource: true }, + }); + + const childrenPromises = relationships.map((r) => + getNodeDataForResource(db, r.resource.id), + ); + const children = await Promise.all(childrenPromises); + + const childrenNodesPromises = children.map((c) => + getNodesRecursivelyHelper(db, c, []), + ); + const childrenNodes = (await Promise.all(childrenNodesPromises)).flat(); + return [...nodes, node, ...childrenNodes].filter(isPresent); +}; + +const getNodesRecursively = async (db: Tx, resourceId: string) => { + const baseNode = await getNodeDataForResource(db, resourceId); + return getNodesRecursivelyHelper(db, baseNode, []); +}; + export const resourceRouter = createTRPCRouter({ metadataGroup: resourceMetadataGroupRouter, provider: resourceProviderRouter, @@ -456,59 +643,7 @@ export const resourceRouter = createTRPCRouter({ .on({ type: "resource", id: input }), }) .input(z.string().uuid()) - .query(async ({ ctx, input }) => { - const hasFilter = isNotNull(schema.environment.resourceFilter); - const resource = await ctx.db.query.resource.findFirst({ - where: and(eq(schema.resource.id, input), isNotDeleted), - with: { - provider: { with: { google: true } }, - workspace: { - with: { - systems: { - with: { environments: { where: hasFilter }, deployments: true }, - }, - }, - }, - }, - }); - if (resource == null) return null; - - const { systems } = resource.workspace; - const systemsWithResource = await _.chain( - systems.map(async (s) => - _.chain(s.environments) - .filter((e) => isPresent(e.resourceFilter)) - .map((e) => - environmentHasResource( - ctx.db, - resource.id, - e.resourceFilter!, - ).then((t) => (t ? { ...e, resource } : null)), - ) - .thru((promises) => Promise.all(promises)) - .value() - .then((t) => t.filter(isPresent)) - .then((t) => (t.length > 0 ? { ...s, environments: t } : null)), - ), - ) - .thru((promises) => Promise.all(promises)) - .value() - .then((t) => t.filter(isPresent)); - - const provider = - resource.provider == null - ? null - : { - ...resource.provider, - google: resource.provider.google[0] ?? null, - }; - - return { - ...resource, - workspace: { ...resource.workspace, systems: systemsWithResource }, - provider, - }; - }), + .query(async ({ ctx, input }) => getNodesRecursively(ctx.db, input)), byWorkspaceId: createTRPCRouter({ list: protectedProcedure diff --git a/packages/db/src/schema/job.ts b/packages/db/src/schema/job.ts index 2cb3f423e..4f186ed63 100644 --- a/packages/db/src/schema/job.ts +++ b/packages/db/src/schema/job.ts @@ -47,7 +47,7 @@ import { deployment } from "./deployment.js"; import { environment } from "./environment.js"; import { jobAgent } from "./job-agent.js"; import { release, releaseJobTrigger } from "./release.js"; -import { resource } from "./resource.js"; +import { jobResourceRelationship, resource } from "./resource.js"; // if adding a new status, update the validators package @ctrlplane/validators/src/jobs/index.ts export const jobStatus = pgEnum("job_status", [ @@ -97,6 +97,7 @@ export const job = pgTable("job", { export const jobRelations = relations(job, ({ many }) => ({ releaseTrigger: many(releaseJobTrigger), + jobRelationships: many(jobResourceRelationship), })); export const jobMetadata = pgTable( diff --git a/packages/db/src/schema/resource.ts b/packages/db/src/schema/resource.ts index 68d2dd6e5..c93b74295 100644 --- a/packages/db/src/schema/resource.ts +++ b/packages/db/src/schema/resource.ts @@ -78,6 +78,7 @@ export const resourceRelations = relations(resource, ({ one, many }) => ({ references: [workspace.id], }), releaseTrigger: many(releaseJobTrigger), + jobRelationships: many(jobResourceRelationship), })); export type Resource = InferSelectModel; @@ -283,8 +284,20 @@ export const jobResourceRelationship = pgTable( .notNull(), resourceIdentifier: text("resource_identifier").notNull(), }, - (t) => ({ - uniq: uniqueIndex().on(t.jobId, t.resourceIdentifier), + (t) => ({ uniq: uniqueIndex().on(t.jobId, t.resourceIdentifier) }), +); + +export const jobResourceRelationshipRelations = relations( + jobResourceRelationship, + ({ one }) => ({ + job: one(job, { + fields: [jobResourceRelationship.jobId], + references: [job.id], + }), + resource: one(resource, { + fields: [jobResourceRelationship.resourceIdentifier], + references: [resource.identifier], + }), }), ); From 511b4469881b4c645e9ddffbda46d5ec4ab9df16 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 25 Nov 2024 14:16:08 -0800 Subject: [PATCH 2/4] more updates --- .../ResourceVisualizationDiagram.tsx | 80 +-------- .../targets/[targetId]/visualize/edges.ts | 80 +++++++-- .../visualize/nodes/DeploymentNode.tsx | 107 ++++++++++++ .../visualize/nodes/EnvironmentNode.tsx | 156 ++---------------- .../visualize/nodes/ProviderNode.tsx | 26 +-- .../visualize/nodes/ResourceNode.tsx | 34 ++-- .../[targetId]/visualize/nodes/nodes.ts | 86 ++++++++++ packages/api/src/router/resources.ts | 93 ++--------- 8 files changed, 308 insertions(+), 354 deletions(-) create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/DeploymentNode.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx index ae7d22c37..23f5c2ec4 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/ResourceVisualizationDiagram.tsx @@ -1,9 +1,7 @@ "use client"; import type { RouterOutputs } from "@ctrlplane/api"; -import type { EdgeTypes, NodeTypes } from "reactflow"; import React from "react"; -import { compact } from "lodash"; import ReactFlow, { ReactFlowProvider, useEdgesState, @@ -11,15 +9,8 @@ import ReactFlow, { } from "reactflow"; import { useLayoutAndFitView } from "~/app/[workspaceSlug]/(app)/_components/reactflow/layout"; -import { DepEdge } from "./DepEdge"; -import { - createEdgeFromProviderToResource, - createEdgesFromDeploymentsToResources, - createEdgesFromResourceToEnvironments, -} from "./edges"; -import { EnvironmentNode } from "./nodes/EnvironmentNode"; -import { ProviderNode } from "./nodes/ProviderNode"; -import { ResourceNode } from "./nodes/ResourceNode"; +import { edgeTypes, getEdges } from "./edges"; +import { getNodes, nodeTypes } from "./nodes/nodes"; type Relationships = NonNullable; @@ -27,77 +18,16 @@ type ResourceVisualizationDiagramProps = { relationships: Relationships; }; -enum NodeType { - Resource = "resource", - Environment = "environment", - Provider = "provider", -} - -const nodeTypes: NodeTypes = { - [NodeType.Resource]: ResourceNode, - [NodeType.Environment]: EnvironmentNode, - [NodeType.Provider]: ProviderNode, -}; -const edgeTypes: EdgeTypes = { default: DepEdge }; - export const ResourceVisualizationDiagram: React.FC< ResourceVisualizationDiagramProps > = ({ relationships }) => { const [nodes, _, onNodesChange] = useNodesState<{ label: string }>( - compact([ - ...relationships.map((r) => ({ - id: r.id, - type: NodeType.Resource, - data: { ...r, label: r.identifier }, - position: { x: 0, y: 0 }, - })), - ...relationships.flatMap((r) => - r.provider != null - ? [ - { - id: `${r.provider.id}-${r.id}`, - type: NodeType.Provider, - data: { ...r.provider, label: r.provider.name }, - position: { x: 0, y: 0 }, - }, - ] - : [], - ), - ...relationships.flatMap((r) => - r.workspace.systems.flatMap((s) => - s.environments.map((e) => ({ - id: e.id, - type: NodeType.Environment, - data: { - environment: { - ...e, - deployments: s.deployments, - resource: r, - }, - label: `${s.name}/${e.name}`, - }, - position: { x: 0, y: 0 }, - })), - ), - ), - ]), + getNodes(relationships), ); - const resourceToEnvEdges = relationships.flatMap((r) => - createEdgesFromResourceToEnvironments( - r, - r.workspace.systems.flatMap((s) => s.environments), - ), - ); - const providerEdges = relationships.flatMap((r) => - r.provider != null ? [createEdgeFromProviderToResource(r.provider, r)] : [], - ); - const deploymentEdges = createEdgesFromDeploymentsToResources(relationships); - const [edges, __, onEdgesChange] = useEdgesState( - compact([...resourceToEnvEdges, ...providerEdges, ...deploymentEdges]), - ); + const [edges, __, onEdgesChange] = useEdgesState(getEdges(relationships)); - const setReactFlowInstance = useLayoutAndFitView(nodes, { direction: "LR" }); + const setReactFlowInstance = useLayoutAndFitView(nodes, { direction: "TB" }); return ( @@ -20,12 +26,12 @@ export const createEdgesFromResourceToEnvironments = ( id: `${resource.id}-${environment.id}`, source: resource.id, target: environment.id, - style: { stroke: colors.neutral[700] }, + style: { stroke: colors.neutral[800] }, markerEnd, label: "in", })); -export const createEdgeFromProviderToResource = ( +const createEdgeFromProviderToResource = ( provider: Provider | null, resource: SCHEMA.Resource, ) => @@ -34,7 +40,7 @@ export const createEdgeFromProviderToResource = ( id: `${provider.id}-${resource.id}`, source: `${provider.id}-${resource.id}`, target: resource.id, - style: { stroke: colors.neutral[700] }, + style: { stroke: colors.neutral[800] }, markerEnd, label: "discovered", } @@ -42,33 +48,73 @@ export const createEdgeFromProviderToResource = ( type Relationships = NonNullable; -export const createEdgesFromDeploymentsToResources = ( - relationships: Relationships, +const createEdgesFromEnvironmentToDeployments = ( + environments: SCHEMA.Environment[], + deployments: SCHEMA.Deployment[], ) => + _.chain(environments) + .flatMap((e) => deployments.map((d) => ({ e, d }))) + .map(({ e, d }) => ({ + id: `${e.id}-${d.id}`, + source: e.id, + target: `${e.id}-${d.id}`, + label: "deploys", + style: { stroke: colors.neutral[800] }, + markerEnd, + })) + .value(); + +const createEdgesFromDeploymentsToResources = (relationships: Relationships) => relationships.map((resource) => { const { parent } = resource; if (parent == null) return null; - const allReleaseJobTriggers = relationships.flatMap((r) => - r.workspace.systems.flatMap((s) => - s.environments.flatMap((e) => - e.latestActiveRelease.map((rel) => rel.releaseJobTrigger), - ), - ), - ); + const allReleaseJobTriggers = _.chain(relationships) + .flatMap((r) => r.workspace.systems) + .flatMap((s) => s.environments) + .flatMap((e) => e.latestActiveReleases) + .map((rel) => rel.releaseJobTrigger) + .value(); const releaseJobTrigger = allReleaseJobTriggers.find( (j) => j.jobId === parent.jobId, ); if (releaseJobTrigger == null) return null; + const { deploymentId } = releaseJobTrigger.release; + const { environmentId } = releaseJobTrigger; + return { id: `${releaseJobTrigger.jobId}-${resource.id}`, - source: releaseJobTrigger.environmentId, - sourceHandle: releaseJobTrigger.jobId, + source: `${environmentId}-${deploymentId}`, target: resource.id, - style: { stroke: colors.neutral[700] }, + style: { stroke: colors.neutral[800] }, markerEnd, label: "created", }; }); + +export const getEdges = (relationships: Relationships) => { + const resourceToEnvEdges = relationships.flatMap((r) => + createEdgesFromResourceToEnvironments( + r, + r.workspace.systems.flatMap((s) => s.environments), + ), + ); + const environmentToDeploymentEdges = relationships.flatMap((r) => + r.workspace.systems.flatMap((s) => + createEdgesFromEnvironmentToDeployments(s.environments, s.deployments), + ), + ); + const providerEdges = relationships.flatMap((r) => + r.provider != null ? [createEdgeFromProviderToResource(r.provider, r)] : [], + ); + const deploymentEdges = createEdgesFromDeploymentsToResources(relationships); + + return _.compact([ + ...resourceToEnvEdges, + ...environmentToDeploymentEdges, + ...providerEdges, + ...deploymentEdges, + ]); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/DeploymentNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/DeploymentNode.tsx new file mode 100644 index 000000000..724064008 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/DeploymentNode.tsx @@ -0,0 +1,107 @@ +import type * as SCHEMA from "@ctrlplane/db/schema"; +import type { NodeProps } from "reactflow"; +import React from "react"; +import { Handle, Position } from "reactflow"; + +import { cn } from "@ctrlplane/ui"; +import { JobStatus, JobStatusReadable } from "@ctrlplane/validators/jobs"; + +import { useJobDrawer } from "~/app/[workspaceSlug]/(app)/_components/job-drawer/useJobDrawer"; +import { api } from "~/trpc/react"; +import { ReleaseIcon } from "../../ReleaseCell"; + +type DeploymentNodeProps = NodeProps<{ + label: string; + deployment: SCHEMA.Deployment; + environment: SCHEMA.Environment; + resource: SCHEMA.Resource; +}>; + +export const DeploymentNode: React.FC = ({ data }) => { + const { deployment, environment, resource } = data; + const { setJobId } = useJobDrawer(); + + const resourceId = resource.id; + const environmentId = environment.id; + const latestActiveReleasesQ = + api.resource.activeReleases.byResourceAndEnvironmentId.useQuery( + { resourceId, environmentId }, + { refetchInterval: 5_000 }, + ); + const latestActiveReleases = latestActiveReleasesQ.data ?? []; + const activeRelease = latestActiveReleases.find( + (r) => r.releaseJobTrigger.release.deploymentId === deployment.id, + ); + + const isInProgress = latestActiveReleases.some( + (r) => r.releaseJobTrigger.job.status === JobStatus.InProgress, + ); + const isPending = latestActiveReleases.some( + (r) => r.releaseJobTrigger.job.status === JobStatus.Pending, + ); + const isCompleted = latestActiveReleases.every( + (r) => r.releaseJobTrigger.job.status === JobStatus.Completed, + ); + + const releaseJobTrigger = activeRelease?.releaseJobTrigger; + + return ( + <> +
{ + if (releaseJobTrigger != null) setJobId(releaseJobTrigger.job.id); + }} + > +
+
+ {deployment.name} +
+ {releaseJobTrigger != null && ( +
+ +
+
{releaseJobTrigger.release.version}
+
{JobStatusReadable[releaseJobTrigger.job.status]}
+
+
+ )} + {releaseJobTrigger == null && ( +
+ No active job +
+ )} +
+
+ + + + ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx index c08e353fc..6e1fd195d 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/EnvironmentNode.tsx @@ -1,175 +1,41 @@ "use client"; import type { RouterOutputs } from "@ctrlplane/api"; -import type * as SCHEMA from "@ctrlplane/db/schema"; import type { NodeProps } from "reactflow"; import React from "react"; import { IconPlant } from "@tabler/icons-react"; -import _ from "lodash"; import { Handle, Position } from "reactflow"; -import { cn } from "@ctrlplane/ui"; -import { Skeleton } from "@ctrlplane/ui/skeleton"; -import { JobStatus, JobStatusReadable } from "@ctrlplane/validators/jobs"; - -import { useJobDrawer } from "~/app/[workspaceSlug]/(app)/_components/job-drawer/useJobDrawer"; -import { api } from "~/trpc/react"; -import { ReleaseIcon } from "../../ReleaseCell"; - -type Env = NonNullable< +type Environment = NonNullable< RouterOutputs["resource"]["relationships"][number] >["workspace"]["systems"][number]["environments"][number]; -type Environment = Env & { - deployments: SCHEMA.Deployment[]; -}; - type EnvironmentNodeProps = NodeProps<{ label: string; environment: Environment; }>; -const DeploymentCard: React.FC<{ - deploymentName: string; - job?: SCHEMA.Job; - releaseVersion: string; -}> = ({ deploymentName, job, releaseVersion }) => { - const { setJobId } = useJobDrawer(); - return ( - <> -
{ - if (job != null) setJobId(job.id); - }} - > - {deploymentName} - {job != null && ( -
- -
-
{releaseVersion}
-
{JobStatusReadable[job.status]}
-
-
- )} - {job == null && ( -
- No active job -
- )} -
- - ); -}; - export const EnvironmentNode: React.FC = (node) => { const { data } = node; - - const resourceId = data.environment.resource.id; - const environmentId = data.environment.id; - const latestActiveReleasesQ = - api.resource.activeReleases.byResourceAndEnvironmentId.useQuery( - { resourceId, environmentId }, - // { refetchInterval: 5_000 }, - ); - - const latestActiveReleases = latestActiveReleasesQ.data ?? []; - - const isInProgress = latestActiveReleases.some( - (r) => r.releaseJobTrigger.job.status === JobStatus.InProgress, - ); - const isPending = latestActiveReleases.some( - (r) => r.releaseJobTrigger.job.status === JobStatus.Pending, - ); - const isCompleted = latestActiveReleases.every( - (r) => r.releaseJobTrigger.job.status === JobStatus.Completed, - ); - - const numDeployments = data.environment.deployments.length; - return ( <> -
+
- {data.label} -
-
- {latestActiveReleasesQ.isLoading && - _.range(numDeployments).map((i) => ( - - ))} - {!latestActiveReleasesQ.isLoading && - data.environment.deployments.map((deployment) => { - const latestActiveRelease = latestActiveReleases.find( - (r) => - r.releaseJobTrigger.release.deploymentId === deployment.id, - ); - return ( - - ); - })} + Environment
+
{data.label}
+ - {data.environment.deployments.map((deployment, index) => { - // position handles vertically: header (+ extra accounting for padding) + - // (card height + gap) * index + half card height for centering - const topOffset = 44 + (56 + 16) * index + 56 / 2; - const latestActiveRelease = data.environment.latestActiveRelease.find( - (r) => r.releaseJobTrigger.release.deploymentId === deployment.id, - ); - console.log({ - latestActiveReleaseJobId: - latestActiveRelease?.releaseJobTrigger.job.id, - }); - return ( - - ); - })} ); }; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ProviderNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ProviderNode.tsx index 90f1fe4d2..8e17f34ba 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ProviderNode.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ProviderNode.tsx @@ -4,8 +4,6 @@ import React from "react"; import { IconBrandGoogleFilled, IconCube } from "@tabler/icons-react"; import { Handle, Position } from "reactflow"; -import { cn } from "@ctrlplane/ui"; - type ProviderNodeProps = NodeProps<{ id: string; name: string; @@ -25,8 +23,7 @@ export const ProviderIcon: React.FC<{ node: ProviderNodeProps }> = ({ const ProviderLabel: React.FC<{ node: ProviderNodeProps }> = ({ node }) => { const { google } = node.data; - if (google != null) - return Google Provider; + if (google != null) return Google Provider; return Resource Provider; }; @@ -34,12 +31,7 @@ export const ProviderNode: React.FC = (node) => { const { data } = node; return ( <> -
+
@@ -48,19 +40,13 @@ export const ProviderNode: React.FC = (node) => {
); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx index 7f525939c..fe628e376 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx @@ -15,18 +15,18 @@ type ResourceNodeProps = NodeProps<{ export const ResourceNode: React.FC = (node) => { const { data } = node; - const isKubernetes = data.version.includes("kubernetes"); - const isTerraform = data.version.includes("terraform"); - const isSharedCluster = data.kind.toLowerCase().includes("sharedcluster"); + // const isKubernetes = data.version.includes("kubernetes"); + // const isTerraform = data.version.includes("terraform"); + // const isSharedCluster = data.kind.toLowerCase().includes("sharedcluster"); return ( <>
@@ -39,22 +39,22 @@ export const ResourceNode: React.FC = (node) => { ); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts new file mode 100644 index 000000000..b74841036 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts @@ -0,0 +1,86 @@ +import type { RouterOutputs } from "@ctrlplane/api"; +import type { NodeTypes } from "reactflow"; +import _ from "lodash"; +import { isPresent } from "ts-is-present"; + +import { DeploymentNode } from "./DeploymentNode"; +import { EnvironmentNode } from "./EnvironmentNode"; +import { ProviderNode } from "./ProviderNode"; +import { ResourceNode } from "./ResourceNode"; + +type Relationships = NonNullable; + +enum NodeType { + Resource = "resource", + Environment = "environment", + Provider = "provider", + Deployment = "deployment", +} + +export const nodeTypes: NodeTypes = { + [NodeType.Resource]: ResourceNode, + [NodeType.Environment]: EnvironmentNode, + [NodeType.Provider]: ProviderNode, + [NodeType.Deployment]: DeploymentNode, +}; + +const getResourceNodes = (relationships: Relationships) => + relationships.map((r) => ({ + id: r.id, + type: NodeType.Resource, + data: { ...r, label: r.identifier }, + position: { x: 0, y: 0 }, + })); + +const getProviderNodes = (relationships: Relationships) => + relationships + .map((r) => + r.provider != null + ? { + id: `${r.provider.id}-${r.id}`, + type: NodeType.Provider, + data: { ...r.provider, label: r.provider.name }, + position: { x: 0, y: 0 }, + } + : null, + ) + .filter(isPresent); + +const getEnvironmentNodes = (relationships: Relationships) => + _.chain(relationships) + .flatMap((r) => r.workspace.systems) + .flatMap((s) => s.environments.map((e) => ({ s, e }))) + .map(({ s, e }) => ({ + id: e.id, + type: NodeType.Environment, + data: { environment: e, label: `${s.name}/${e.name}` }, + position: { x: 0, y: 0 }, + })) + .value(); + +const getDeploymentNodes = (relationships: Relationships) => + relationships.flatMap((r) => + r.workspace.systems.flatMap((system) => + system.environments.flatMap((environment) => + system.deployments.map((deployment) => ({ + id: `${environment.id}-${deployment.id}`, + type: NodeType.Deployment, + data: { + deployment, + environment, + resource: r, + label: deployment.name, + }, + position: { x: 0, y: 0 }, + })), + ), + ), + ); + +export const getNodes = (relationships: Relationships) => + _.compact([ + ...getResourceNodes(relationships), + ...getProviderNodes(relationships), + ...getEnvironmentNodes(relationships), + ...getDeploymentNodes(relationships), + ]); diff --git a/packages/api/src/router/resources.ts b/packages/api/src/router/resources.ts index 4f6bb01c9..6d7c37fd6 100644 --- a/packages/api/src/router/resources.ts +++ b/packages/api/src/router/resources.ts @@ -424,7 +424,6 @@ const getNodeDataForResource = async (db: Tx, resourceId: string) => { ); const parent = await db.query.jobResourceRelationship.findFirst({ where: matchesIdentifier, - with: { job: { with: { releaseTrigger: true } } }, }); const { systems } = resource.workspace; @@ -439,7 +438,7 @@ const getNodeDataForResource = async (db: Tx, resourceId: string) => { ? { ...e, resource, - latestActiveRelease: + latestActiveReleases: await latestActiveReleaseByResourceAndEnvironmentId( db, resource.id, @@ -486,11 +485,11 @@ const getNodesRecursivelyHelper = async ( nodes: NonNullable[], ): Promise[]> => { if (node == null) return nodes; - const activeReleaseJobs = node.workspace.systems.flatMap((s) => - s.environments.flatMap((e) => - e.latestActiveRelease.map((r) => r.releaseJobTrigger.job), - ), - ); + const activeReleaseJobs = _.chain(node.workspace.systems) + .flatMap((s) => s.environments) + .flatMap((e) => e.latestActiveReleases) + .map((r) => r.releaseJobTrigger.job) + .value(); const relationships = await db.query.jobResourceRelationship.findMany({ where: inArray( @@ -560,79 +559,13 @@ export const resourceRouter = createTRPCRouter({ .perform(Permission.ResourceGet) .on({ type: "resource", id: input.resourceId }), }) - .query(async ({ ctx, input }) => { - const { resourceId, environmentId } = input; - const rankSubquery = ctx.db - .select({ - rank: sql`ROW_NUMBER() OVER (PARTITION BY ${schema.release.deploymentId} ORDER BY ${schema.release.createdAt} DESC)`.as( - "rank", - ), - rankReleaseId: schema.release.id, - rankDeploymentId: schema.release.deploymentId, - }) - .from(schema.release) - .as("rank_subquery"); - - return ctx.db - .select() - .from(schema.deployment) - .innerJoin( - schema.system, - eq(schema.system.id, schema.deployment.systemId), - ) - .innerJoin( - schema.environment, - eq(schema.environment.systemId, schema.system.id), - ) - .innerJoin( - schema.release, - eq(schema.release.deploymentId, schema.deployment.id), - ) - .innerJoin( - rankSubquery, - and( - eq(rankSubquery.rankDeploymentId, schema.release.deploymentId), - eq(rankSubquery.rankReleaseId, schema.release.id), - ), - ) - .innerJoin( - schema.releaseJobTrigger, - and( - eq(schema.releaseJobTrigger.releaseId, schema.release.id), - eq(schema.releaseJobTrigger.environmentId, schema.environment.id), - ), - ) - .innerJoin( - schema.resource, - eq(schema.resource.id, schema.releaseJobTrigger.resourceId), - ) - .innerJoin( - schema.job, - eq(schema.releaseJobTrigger.jobId, schema.job.id), - ) - .where( - and( - eq(schema.resource.id, resourceId), - eq(schema.environment.id, environmentId), - isNull(schema.resource.deletedAt), - eq(rankSubquery.rank, 1), - ), - ) - .orderBy(schema.deployment.id, schema.releaseJobTrigger.createdAt) - .then((r) => - r.map((row) => ({ - ...row.deployment, - environment: row.environment, - system: row.system, - releaseJobTrigger: { - ...row.release_job_trigger, - job: row.job, - release: row.release, - resourceId: row.resource.id, - }, - })), - ); - }), + .query(({ ctx, input }) => + latestActiveReleaseByResourceAndEnvironmentId( + ctx.db, + input.resourceId, + input.environmentId, + ), + ), }), relationships: protectedProcedure From fdd63a8fa2459116b3ca23609be0358375920f24 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 25 Nov 2024 14:21:25 -0800 Subject: [PATCH 3/4] cleanup --- .../visualize/nodes/ResourceNode.tsx | 29 ++----------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx index fe628e376..ed8da8e84 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/ResourceNode.tsx @@ -1,8 +1,6 @@ import type { NodeProps } from "reactflow"; import { Handle, Position } from "reactflow"; -import { cn } from "@ctrlplane/ui"; - import { TargetIcon as ResourceIcon } from "~/app/[workspaceSlug]/(app)/_components/TargetIcon"; type ResourceNodeProps = NodeProps<{ @@ -15,20 +13,9 @@ type ResourceNodeProps = NodeProps<{ export const ResourceNode: React.FC = (node) => { const { data } = node; - // const isKubernetes = data.version.includes("kubernetes"); - // const isTerraform = data.version.includes("terraform"); - // const isSharedCluster = data.kind.toLowerCase().includes("sharedcluster"); - return ( <> -
+
{data.kind} @@ -38,22 +25,12 @@ export const ResourceNode: React.FC = (node) => { From 7268c54efaba9642633f50658a7650db03e12c53 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 25 Nov 2024 14:41:16 -0800 Subject: [PATCH 4/4] cleanup --- .../targets/[targetId]/visualize/edges.ts | 16 +++++++--------- .../targets/[targetId]/visualize/nodes/nodes.ts | 10 ++++------ packages/api/src/router/resources.ts | 5 ++--- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts index fee575963..d5aa52584 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/edges.ts @@ -1,9 +1,9 @@ import type { RouterOutputs } from "@ctrlplane/api"; import type * as SCHEMA from "@ctrlplane/db/schema"; import type { EdgeTypes } from "reactflow"; -import _ from "lodash"; import { MarkerType } from "reactflow"; import colors from "tailwindcss/colors"; +import { isPresent } from "ts-is-present"; import { DepEdge } from "./DepEdge"; @@ -52,7 +52,7 @@ const createEdgesFromEnvironmentToDeployments = ( environments: SCHEMA.Environment[], deployments: SCHEMA.Deployment[], ) => - _.chain(environments) + environments .flatMap((e) => deployments.map((d) => ({ e, d }))) .map(({ e, d }) => ({ id: `${e.id}-${d.id}`, @@ -61,20 +61,18 @@ const createEdgesFromEnvironmentToDeployments = ( label: "deploys", style: { stroke: colors.neutral[800] }, markerEnd, - })) - .value(); + })); const createEdgesFromDeploymentsToResources = (relationships: Relationships) => relationships.map((resource) => { const { parent } = resource; if (parent == null) return null; - const allReleaseJobTriggers = _.chain(relationships) + const allReleaseJobTriggers = relationships .flatMap((r) => r.workspace.systems) .flatMap((s) => s.environments) .flatMap((e) => e.latestActiveReleases) - .map((rel) => rel.releaseJobTrigger) - .value(); + .map((rel) => rel.releaseJobTrigger); const releaseJobTrigger = allReleaseJobTriggers.find( (j) => j.jobId === parent.jobId, @@ -111,10 +109,10 @@ export const getEdges = (relationships: Relationships) => { ); const deploymentEdges = createEdgesFromDeploymentsToResources(relationships); - return _.compact([ + return [ ...resourceToEnvEdges, ...environmentToDeploymentEdges, ...providerEdges, ...deploymentEdges, - ]); + ].filter(isPresent); }; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts index b74841036..f53657bbc 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/[targetId]/visualize/nodes/nodes.ts @@ -1,6 +1,5 @@ import type { RouterOutputs } from "@ctrlplane/api"; import type { NodeTypes } from "reactflow"; -import _ from "lodash"; import { isPresent } from "ts-is-present"; import { DeploymentNode } from "./DeploymentNode"; @@ -47,7 +46,7 @@ const getProviderNodes = (relationships: Relationships) => .filter(isPresent); const getEnvironmentNodes = (relationships: Relationships) => - _.chain(relationships) + relationships .flatMap((r) => r.workspace.systems) .flatMap((s) => s.environments.map((e) => ({ s, e }))) .map(({ s, e }) => ({ @@ -55,8 +54,7 @@ const getEnvironmentNodes = (relationships: Relationships) => type: NodeType.Environment, data: { environment: e, label: `${s.name}/${e.name}` }, position: { x: 0, y: 0 }, - })) - .value(); + })); const getDeploymentNodes = (relationships: Relationships) => relationships.flatMap((r) => @@ -78,9 +76,9 @@ const getDeploymentNodes = (relationships: Relationships) => ); export const getNodes = (relationships: Relationships) => - _.compact([ + [ ...getResourceNodes(relationships), ...getProviderNodes(relationships), ...getEnvironmentNodes(relationships), ...getDeploymentNodes(relationships), - ]); + ].filter(isPresent); diff --git a/packages/api/src/router/resources.ts b/packages/api/src/router/resources.ts index 6d7c37fd6..677d1c235 100644 --- a/packages/api/src/router/resources.ts +++ b/packages/api/src/router/resources.ts @@ -485,11 +485,10 @@ const getNodesRecursivelyHelper = async ( nodes: NonNullable[], ): Promise[]> => { if (node == null) return nodes; - const activeReleaseJobs = _.chain(node.workspace.systems) + const activeReleaseJobs = node.workspace.systems .flatMap((s) => s.environments) .flatMap((e) => e.latestActiveReleases) - .map((r) => r.releaseJobTrigger.job) - .value(); + .map((r) => r.releaseJobTrigger.job); const relationships = await db.query.jobResourceRelationship.findMany({ where: inArray(