diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-drawer/JobDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-drawer/JobDrawer.tsx index 5903915f..0b677d7e 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-drawer/JobDrawer.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-drawer/JobDrawer.tsx @@ -20,7 +20,6 @@ import { JobAgent } from "./JobAgent"; import { JobMetadata } from "./JobMetadata"; import { JobPropertiesTable } from "./JobProperties"; import { JobVariables } from "./JobVariables"; -import { DependenciesDiagram } from "./RelationshipsDiagramDependencies"; import { useJobDrawer } from "./useJobDrawer"; export const JobDrawer: React.FC = () => { @@ -128,13 +127,6 @@ export const JobDrawer: React.FC = () => { - - )} diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-drawer/RelationshipsDiagramDependencies.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-drawer/RelationshipsDiagramDependencies.tsx deleted file mode 100644 index 2bbfc05d..00000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-drawer/RelationshipsDiagramDependencies.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client"; - -import type * as schema from "@ctrlplane/db/schema"; -import type { EdgeTypes, NodeTypes, ReactFlowInstance } from "reactflow"; -import { useCallback, useEffect, useState } from "react"; -import ReactFlow, { - MarkerType, - ReactFlowProvider, - useEdgesState, - useNodesState, - useReactFlow, -} from "reactflow"; -import colors from "tailwindcss/colors"; - -import { Card } from "@ctrlplane/ui/card"; -import { Label } from "@ctrlplane/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@ctrlplane/ui/select"; - -import { getLayoutedElementsDagre } from "~/app/[workspaceSlug]/(app)/_components/reactflow/layout"; -import { DepEdge } from "~/app/[workspaceSlug]/(app)/_components/relationships/DepEdge"; -import { TargetNode } from "~/app/[workspaceSlug]/(app)/_components/relationships/TargetNode"; - -const nodeTypes: NodeTypes = { target: TargetNode }; -const edgeTypes: EdgeTypes = { default: DepEdge }; - -const useOnLayout = () => { - const { getNodes, fitView, setNodes, setEdges, getEdges } = useReactFlow(); - return useCallback(() => { - const layouted = getLayoutedElementsDagre( - getNodes(), - getEdges(), - "BT", - 200, - 50, - ); - setNodes([...layouted.nodes]); - setEdges([...layouted.edges]); - - fitView({ padding: 0.12, maxZoom: 1 }); - }, [getNodes, getEdges, setNodes, setEdges, fitView]); -}; - -const getDFSPath = ( - startId: string, - goalId: string, - graph: Record, - visited: Set = new Set(), - path: string[] = [], -): string[] | null => { - if (startId === goalId) return path; - visited.add(startId); - - for (const neighbor of graph[startId] ?? []) { - if (visited.has(neighbor)) continue; - path.push(neighbor); - const result = getDFSPath(neighbor, goalId, graph, visited, path); - if (result !== null) return result; - path.pop(); - } - - return null; -}; - -const getUndirectedGraph = ( - relationships: Array, -) => { - const graph: Record> = {}; - - for (const relationship of relationships) { - if (!graph[relationship.fromIdentifier]) - graph[relationship.fromIdentifier] = new Set(); - if (!graph[relationship.toIdentifier]) - graph[relationship.toIdentifier] = new Set(); - graph[relationship.fromIdentifier]!.add(relationship.toIdentifier); - graph[relationship.toIdentifier]!.add(relationship.fromIdentifier); - } - return Object.fromEntries( - Object.entries(graph).map(([key, value]) => [key, Array.from(value)]), - ); -}; - -type DependenciesDiagramProps = { - targetId: string; - relationships: Array; - targets: Array; - releaseDependencies: (schema.ReleaseDependency & { - deploymentName: string; - target?: string; - })[]; -}; - -const TargetDiagramDependencies: React.FC = ({ - targetId, - relationships, - targets, - releaseDependencies, -}) => { - const [nodes, _, onNodesChange] = useNodesState( - targets.map((t) => ({ - id: t.identifier, - type: "target", - position: { x: 100, y: 100 }, - data: { - ...t, - targetId, - isOrphanNode: !relationships.some( - (r) => - r.toIdentifier === t.identifier || - r.fromIdentifier === t.identifier, - ), - }, - })), - ); - const [edges, setEdges, onEdgesChange] = useEdgesState( - relationships.map((t) => ({ - id: `${t.fromIdentifier}-${t.toIdentifier}`, - source: t.fromIdentifier, - target: t.toIdentifier, - markerEnd: { - type: MarkerType.Arrow, - color: colors.neutral[700], - }, - style: { - stroke: colors.neutral[700], - }, - label: t.type, - })), - ); - const onLayout = useOnLayout(); - - const graph = getUndirectedGraph(relationships); - - const resetEdges = () => - setEdges( - relationships.map((t) => ({ - id: `${t.fromIdentifier}-${t.toIdentifier}`, - source: t.fromIdentifier, - target: t.toIdentifier, - markerEnd: { - type: MarkerType.Arrow, - color: colors.neutral[700], - }, - style: { - stroke: colors.neutral[700], - }, - label: t.type, - })), - ); - - const getHighlightedEdgesFromPath = (path: string[]) => { - const highlightedEdges: string[] = []; - for (let i = 0; i < path.length - 1; i++) { - highlightedEdges.push(`${path[i]}-${path[i + 1]}`); - highlightedEdges.push(`${path[i + 1]}-${path[i]}`); - } - return highlightedEdges; - }; - - const onDependencySelect = (value: string) => { - const goalId = releaseDependencies.find((rd) => rd.id === value)?.target; - if (goalId == null) { - resetEdges(); - return; - } - const nodesInPath = getDFSPath(targetId, goalId, graph, new Set(), [ - targetId, - ]); - if (nodesInPath == null) { - resetEdges(); - return; - } - const highlightedEdges = getHighlightedEdgesFromPath(nodesInPath); - const newEdges = relationships.map((t) => { - const isHighlighted = highlightedEdges.includes( - `${t.fromIdentifier}-${t.toIdentifier}`, - ); - const color = isHighlighted ? colors.blue[500] : colors.neutral[700]; - - return { - id: `${t.fromIdentifier}-${t.toIdentifier}`, - source: t.fromIdentifier, - target: t.toIdentifier, - markerEnd: { type: MarkerType.Arrow, color }, - style: { stroke: color }, - label: t.type, - }; - }); - setEdges(newEdges); - }; - - const [reactFlowInstance, setReactFlowInstance] = - useState(null); - useEffect(() => { - if (reactFlowInstance != null) onLayout(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reactFlowInstance]); - return ( -
-
- -
- -
- ); -}; - -export const DependenciesDiagram: React.FC = ( - props, -) => ( -
- - - - - - -
-); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-drawer/TargetDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-drawer/TargetDrawer.tsx index 9305ec13..a9d6b42c 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-drawer/TargetDrawer.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-drawer/TargetDrawer.tsx @@ -27,7 +27,6 @@ import { TabButton } from "../TabButton"; import { DeploymentsContent } from "./DeploymentContent"; import { JobsContent } from "./JobsContent"; import { OverviewContent } from "./OverviewContent"; -import { RelationshipsContent } from "./relationships/RelationshipContent"; import { TargetActionsDropdown } from "./TargetActionsDropdown"; import { useTargetDrawer } from "./useTargetDrawer"; import { VariableContent } from "./VariablesContent"; @@ -198,12 +197,6 @@ export const TargetDrawer: React.FC = () => { icon={} label="Variables" /> - setActiveTab("relationships")} - icon={} - label="Relationships" - />
{activeTab === "deployments" && ( @@ -212,9 +205,6 @@ export const TargetDrawer: React.FC = () => { {activeTab === "overview" && ( )} - {activeTab === "relationships" && ( - - )} {activeTab === "jobs" && } {activeTab === "variables" && ( = ({ target }) => { - return ( -
-
Hierarchy
- - - -
- ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-drawer/relationships/RelationshipsDiagram.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-drawer/relationships/RelationshipsDiagram.tsx deleted file mode 100644 index 684a6148..00000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-drawer/relationships/RelationshipsDiagram.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"use client"; - -import type * as schema from "@ctrlplane/db/schema"; -import type { EdgeTypes, NodeTypes, ReactFlowInstance } from "reactflow"; -import { useCallback, useEffect, useState } from "react"; -import ReactFlow, { - MarkerType, - ReactFlowProvider, - useEdgesState, - useNodesState, - useReactFlow, -} from "reactflow"; -import colors from "tailwindcss/colors"; - -import { getLayoutedElementsDagre } from "~/app/[workspaceSlug]/(app)/_components/reactflow/layout"; -import { DepEdge } from "~/app/[workspaceSlug]/(app)/_components/relationships/DepEdge"; -import { TargetNode } from "~/app/[workspaceSlug]/(app)/_components/relationships/TargetNode"; -import { api } from "~/trpc/react"; - -const nodeTypes: NodeTypes = { target: TargetNode }; -const edgeTypes: EdgeTypes = { default: DepEdge }; - -const useOnLayout = () => { - const { getNodes, fitView, setNodes, setEdges, getEdges } = useReactFlow(); - return useCallback(() => { - const layouted = getLayoutedElementsDagre( - getNodes(), - getEdges(), - "BT", - 0, - 50, - ); - setNodes([...layouted.nodes]); - setEdges([...layouted.edges]); - - fitView({ padding: 0.12, maxZoom: 1 }); - }, [getNodes, getEdges, setNodes, setEdges, fitView]); -}; - -const TargetDiagram: React.FC<{ - relationships: Array; - targets: Array; - targetId: string; -}> = ({ relationships, targets, targetId }) => { - const [nodes, _, onNodesChange] = useNodesState( - targets.map((t) => ({ - id: t.identifier, - type: "target", - position: { x: 100, y: 100 }, - data: { - ...t, - targetId, - isOrphanNode: !relationships.some( - (r) => - r.toIdentifier === t.identifier || - r.fromIdentifier === t.identifier, - ), - }, - })), - ); - const [edges, __, onEdgesChange] = useEdgesState( - relationships.map((t) => ({ - id: `${t.fromIdentifier}-${t.toIdentifier}`, - source: t.fromIdentifier, - target: t.toIdentifier, - markerEnd: { type: MarkerType.Arrow, color: colors.neutral[700] }, - style: { stroke: colors.neutral[700] }, - label: t.type, - })), - ); - const onLayout = useOnLayout(); - - const [reactFlowInstance, setReactFlowInstance] = - useState(null); - useEffect(() => { - if (reactFlowInstance != null) onLayout(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reactFlowInstance]); - return ( - - ); -}; - -export const TargetHierarchyRelationshipsDiagram: React.FC<{ - targetId: string; -}> = ({ targetId }) => { - const hierarchy = api.resource.relations.hierarchy.useQuery(targetId); - - if (hierarchy.data == null) return null; - const { relationships, resources } = hierarchy.data; - return ( - - - - ); -}; diff --git a/packages/api/src/router/job.ts b/packages/api/src/router/job.ts index 5fb8d4ae..1ef5a52b 100644 --- a/packages/api/src/router/job.ts +++ b/packages/api/src/router/job.ts @@ -24,7 +24,6 @@ import { countDistinct, desc, eq, - inArray, isNull, notInArray, sql, @@ -44,7 +43,6 @@ import { release, releaseDependency, releaseJobTrigger, - releaseMatchesCondition, resource, system, updateJob, @@ -348,13 +346,6 @@ const releaseJobTriggerRouter = createTRPCRouter({ canUser.perform(Permission.JobGet).on({ type: "job", id: input }), }) .query(async ({ ctx, input }) => { - const rel = await ctx.db - .select() - .from(release) - .innerJoin(deployment, eq(release.deploymentId, deployment.id)) - .innerJoin(system, eq(deployment.systemId, system.id)) - .then(takeFirst); - const deploymentName = ctx.db .select({ deploymentName: deployment.name, @@ -387,132 +378,7 @@ const releaseJobTriggerRouter = createTRPCRouter({ .then(processReleaseJobTriggerWithAdditionalDataRows) .then(takeFirst); - const { releaseDependencies } = data; - - const results = await ctx.db.execute( - sql` - WITH RECURSIVE reachable_relationships(id, visited, tr_id, source_id, target_id, type) AS ( - -- Base case: start with the given ID and no relationship - SELECT - ${data.resource.identifier}::uuid AS identifier, - ARRAY[${data.resource.identifier}::uuid] AS visited, - NULL::uuid AS tr_id, - NULL::uuid AS source_identifier, - NULL::uuid AS target_identifier, - NULL::resource_relationship_type AS type - UNION ALL - -- Recursive case: find all relationships connected to the current set of IDs - SELECT - CASE - WHEN tr.source_identifier = rr.identifier THEN tr.target_identifier - ELSE tr.source_id - END AS id, - rr.visited || CASE - WHEN tr.source_id = rr.id THEN tr.target_id - ELSE tr.source_id - END, - tr.id AS tr_id, - tr.source_identifier, - tr.target_identifier, - tr.type - FROM reachable_relationships rr - JOIN resource_relationship tr ON tr.source_identifier = rr.identifier OR tr.target_identifier = rr.identifier - WHERE - NOT CASE - WHEN tr.source_id = rr.id THEN tr.target_id - ELSE tr.source_id - END = ANY(rr.visited) - AND tr.target_identifier != ${data.resource.identifier} - ) - SELECT DISTINCT tr_id AS id, source_identifier, target_identifier, type - FROM reachable_relationships - WHERE tr_id IS NOT NULL; - `, - ); - - // db.execute does not return the types even if the sql`` is annotated with the type - // so we need to cast them here - const relationships = results.rows.map((r) => ({ - id: String(r.id), - workspaceId: rel.system.workspaceId, - fromIdentifier: String(r.source_identifier), - toIdentifier: String(r.target_identifier), - type: r.type as "associated_with" | "depends_on", - })); - - const fromIdentifiers = relationships.map((r) => r.fromIdentifier); - const toIdentifiers = relationships.map((r) => r.toIdentifier); - - const allIdentifiers = _.uniq([ - ...fromIdentifiers, - ...toIdentifiers, - data.resource.identifier, - ]); - - const resources = await ctx.db - .select() - .from(resource) - .where( - and( - inArray(resource.identifier, allIdentifiers), - isNull(resource.deletedAt), - ), - ); - - const releaseDependenciesWithResourcePromises = releaseDependencies.map( - async (rd) => { - const latestJobSubquery = ctx.db - .select({ - id: releaseJobTrigger.id, - resourceId: releaseJobTrigger.resourceId, - releaseId: releaseJobTrigger.releaseId, - status: job.status, - createdAt: job.createdAt, - rank: sql`ROW_NUMBER() OVER ( - PARTITION BY ${releaseJobTrigger.resourceId}, ${releaseJobTrigger.releaseId} - ORDER BY ${job.createdAt} DESC - )`.as("rank"), - }) - .from(job) - .innerJoin(releaseJobTrigger, eq(releaseJobTrigger.jobId, job.id)) - .as("latest_job"); - - const resourceFulfillingDependency = await ctx.db - .select() - .from(release) - .innerJoin(deployment, eq(release.deploymentId, deployment.id)) - .innerJoin( - latestJobSubquery, - eq(latestJobSubquery.releaseId, release.id), - ) - .where( - and( - releaseMatchesCondition(ctx.db, rd.releaseFilter), - eq(deployment.id, rd.deploymentId), - inArray( - latestJobSubquery.resourceId, - resources.map((r) => r.id), - ), - eq(latestJobSubquery.rank, 1), - eq(latestJobSubquery.status, JobStatus.Completed), - ), - ); - - return { - ...rd, - resource: resourceFulfillingDependency.at(0)?.latest_job.resourceId, - }; - }, - ); - - return { - ...data, - releaseDependencies: await Promise.all( - releaseDependenciesWithResourcePromises, - ), - relationships, - relatedResources: resources, - }; + return data; }), }); diff --git a/packages/api/src/router/resources.ts b/packages/api/src/router/resources.ts index b2931afd..060d2220 100644 --- a/packages/api/src/router/resources.ts +++ b/packages/api/src/router/resources.ts @@ -39,88 +39,6 @@ import { resourceProviderRouter } from "./target-provider"; const isNotDeleted = isNull(schema.resource.deletedAt); -const resourceRelations = createTRPCRouter({ - hierarchy: protectedProcedure - .input(z.string().uuid()) - .query(async ({ ctx, input }) => { - const isResource = eq(schema.resource.id, input); - const where = and(isResource, isNotDeleted); - const r = await ctx.db.query.resource.findFirst({ where }); - if (r == null) return null; - - const results = await ctx.db.execute( - sql` - WITH RECURSIVE reachable_relationships(id, visited, tr_id, source_identifier, target_identifier, type) AS ( - -- Base case: start with the given ID and no relationship - SELECT - ${input}::uuid AS id, - ARRAY[${input}::uuid] AS visited, - NULL::uuid AS tr_id, - NULL::uuid AS source_identifier, - NULL::uuid AS target_identifier, - NULL::resource_relationship_type AS type - UNION ALL - -- Recursive case: find all relationships connected to the current set of IDs - SELECT - CASE - WHEN tr.source_identifier = rr.id THEN tr.target_identifier - ELSE tr.source_identifier - END AS id, - rr.visited || CASE - WHEN tr.source_identifier = rr.id THEN tr.target_identifier - ELSE tr.source_identifier - END, - tr.id AS tr_id, - tr.source_identifier, - tr.target_identifier, - tr.type - FROM reachable_relationships rr - JOIN resource_relationship tr ON tr.source_id = rr.id OR tr.target_id = rr.id - WHERE - NOT CASE - WHEN tr.source_identifier = rr.id THEN tr.target_identifier - ELSE tr.source_identifier - END = ANY(rr.visited) - ) - SELECT DISTINCT tr_id AS id, source_identifier, target_identifier, type - FROM reachable_relationships - WHERE tr_id IS NOT NULL; - `, - ); - - // db.execute does not return the types even if the sql`` is annotated with the type - // so we need to cast them here - const relationships = results.rows.map((r) => ({ - id: String(r.id), - fromIdentifier: String(r.source_identifier), - toIdentifier: String(r.target_identifier), - workspaceId: String(r.workspace_id), - type: r.type as "associated_with" | "depends_on", - })); - - const fromIdentifiers = relationships.map((r) => r.fromIdentifier); - const toIdentifiers = relationships.map((r) => r.toIdentifier); - - const allIdentifiers = _.uniq([ - ...fromIdentifiers, - ...toIdentifiers, - input, - ]); - - const resources = await ctx.db - .select() - .from(schema.resource) - .where( - and( - inArray(schema.resource.identifier, allIdentifiers), - isNotDeleted, - ), - ); - - return { relationships, resources }; - }), -}); - type _StringStringRecord = Record; const resourceQuery = (db: Tx, checks: Array>) => db @@ -382,7 +300,6 @@ const getNodesRecursively = async (db: Tx, resourceId: string) => { export const resourceRouter = createTRPCRouter({ metadataGroup: resourceMetadataGroupRouter, provider: resourceProviderRouter, - relations: resourceRelations, view: resourceViews, variable: resourceVariables, @@ -508,16 +425,16 @@ export const resourceRouter = createTRPCRouter({ ].filter(isPresent), associations: { from: fromNodes - .filter((n) => n.node != null) + .filter((n) => isPresent(n.node)) .map((n) => ({ ...n.resource_relationship, - resource: n.node!, + resource: n.node, })), to: toNodes - .filter((n) => n.node != null) + .filter((n) => isPresent(n.node)) .map((n) => ({ ...n.resource_relationship, - resource: n.node!, + resource: n.node, })), }, }; diff --git a/packages/job-dispatch/src/policy-checker.ts b/packages/job-dispatch/src/policy-checker.ts index a0558f85..65824c04 100644 --- a/packages/job-dispatch/src/policy-checker.ts +++ b/packages/job-dispatch/src/policy-checker.ts @@ -7,7 +7,6 @@ import { isPassingLockingPolicy } from "./lock-checker.js"; import { isPassingConcurrencyPolicy } from "./policies/concurrency-policy.js"; import { isPassingJobRolloutPolicy } from "./policies/gradual-rollout.js"; import { isPassingApprovalPolicy } from "./policies/manual-approval.js"; -import { isPassingReleaseDependencyPolicy } from "./policies/release-dependency.js"; import { isPassingNewerThanLastActiveReleasePolicy, isPassingNoActiveJobsPolicy, @@ -25,7 +24,6 @@ export const isPassingAllPolicies = async ( isPassingApprovalPolicy, isPassingCriteriaPolicy, isPassingConcurrencyPolicy, - isPassingReleaseDependencyPolicy, isPassingJobRolloutPolicy, isPassingNoActiveJobsPolicy, isPassingNewerThanLastActiveReleasePolicy, @@ -48,7 +46,6 @@ export const isPassingAllPoliciesExceptNewerThanLastActive = async ( isPassingApprovalPolicy, isPassingCriteriaPolicy, isPassingConcurrencyPolicy, - isPassingReleaseDependencyPolicy, isPassingJobRolloutPolicy, isPassingNoActiveJobsPolicy, isPassingReleaseWindowPolicy,