Skip to content

Commit

Permalink
improve react flow graphs (#1964)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsladerman authored Mar 4, 2025
1 parent 70cee62 commit 18ac999
Show file tree
Hide file tree
Showing 26 changed files with 876 additions and 1,079 deletions.
4 changes: 2 additions & 2 deletions assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@
"@nivo/radial-bar": "0.88.0",
"@nivo/tooltip": "0.88.0",
"@nivo/treemap": "0.88.0",
"@pluralsh/design-system": "5.8.0",
"@pluralsh/design-system": "5.8.1",
"@react-hooks-library/core": "0.6.0",
"@saas-ui/use-hotkeys": "1.1.3",
"@tanstack/react-table": "8.20.5",
"@tanstack/react-virtual": "3.0.1",
"@types/lodash": "4.17.13",
"@xyflow/react": "12.4.4",
"anser": "2.1.1",
"apollo-absinthe-upload-link": "1.7.0",
"apollo-boost": "0.4.9",
Expand Down Expand Up @@ -102,7 +103,6 @@
"react-window": "1.8.10",
"react-window-infinite-loader": "1.0.9",
"react-window-reversed": "1.4.1",
"reactflow": "11.10.1",
"regenerator-runtime": "0.13.11",
"semver": "7.5.4",
"styled-components": "6.1.15",
Expand Down
4 changes: 2 additions & 2 deletions assets/src/components/cd/logs/DateTimeFormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from 'react'
import styled, { useTheme } from 'styled-components'
import { DateParam, formatDateTime, isValidDateTime } from 'utils/datetime'
import { runAfterLayout } from '../pipelines/utils/nodeLayouter'
import { runAfterBrowserLayout } from '../pipelines/utils/nodeLayouter'

const EMPTY_DATE_STR = '//'
const EMPTY_TIME_STR = '::'
Expand Down Expand Up @@ -97,7 +97,7 @@ export function DateTimeFormInput({
setIsEnteringTimestamp(false)
const date = formatDateTime(timestamp, DATE_FORMAT, true)
const time = formatDateTime(timestamp, TIME_FORMAT, true)
runAfterLayout(() => {
runAfterBrowserLayout(() => {
dateInputRef.current?.setValue(date)
timeInputRef.current?.setValue(time)
})
Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/cd/pipelines/PipelineDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@pluralsh/design-system'
import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { ReactFlowProvider } from 'reactflow'
import { ReactFlowProvider } from '@xyflow/react'
import { useTheme } from 'styled-components'

import {
Expand Down
2 changes: 0 additions & 2 deletions assets/src/components/cd/pipelines/PipelineGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { PipelineFragment } from 'generated/graphql'
import { useMemo } from 'react'

import 'reactflow/dist/style.css'

import { ReactFlowGraph } from '../../utils/reactflow/graph'

import { ApprovalNode } from './nodes/ApprovalNode'
Expand Down
16 changes: 7 additions & 9 deletions assets/src/components/cd/pipelines/nodes/ApprovalNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import { Confirm } from 'components/utils/Confirm'
import { ApolloError } from '@apollo/client'

import {
BaseNode,
EdgeNode,
PipelineBaseNode,
IconHeading,
NodeCardList,
StatusCard,
gateStateToCardStatus,
gateStateToSeverity,
} from './BaseNode'
PipelineGateNodeProps,
} from './PipelineBaseNode'

export const gateStateToApprovalText = {
[GateState.Open]: 'Approved',
Expand Down Expand Up @@ -80,15 +80,13 @@ export function ApproverCard({
)
}

export function ApprovalNode(props: EdgeNode) {
const {
data: { meta, ...edge },
} = props
export function ApprovalNode({ id, data }: PipelineGateNodeProps) {
const { meta, ...edge } = data

const gates = edge?.gates

return (
<BaseNode {...props}>
<PipelineBaseNode id={id}>
<div className="headerArea">
<h2 className="heading">Action</h2>
{meta.state && (
Expand Down Expand Up @@ -120,7 +118,7 @@ export function ApprovalNode(props: EdgeNode) {
)
)}
</NodeCardList>
</BaseNode>
</PipelineBaseNode>
)
}

Expand Down
16 changes: 7 additions & 9 deletions assets/src/components/cd/pipelines/nodes/JobNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import { Link } from 'react-router-dom'
import { PIPELINES_ABS_PATH } from 'routes/cdRoutesConsts'

import {
BaseNode,
EdgeNode,
PipelineBaseNode,
IconHeading,
NodeCardList,
StatusCard,
gateStateToCardStatus,
gateStateToSeverity,
} from './BaseNode'
PipelineGateNodeProps,
} from './PipelineBaseNode'

export const gateStateToJobText = {
[GateState.Open]: 'Approved',
Expand Down Expand Up @@ -118,22 +118,20 @@ export function ContainerCard({
)
}

const JobNodeSC = styled(BaseNode)(({ theme }) => ({
const JobNodeSC = styled(PipelineBaseNode)(({ theme }) => ({
'.headerArea2': { display: 'flex', columnGap: theme.spacing.medium },
}))

export function JobNode(props: EdgeNode) {
const {
data: { meta, ...edge },
} = props
export function JobNode({ id, data }: PipelineGateNodeProps) {
const { meta, ...edge } = data

const gate = edge?.gates?.[0]
const containers = gate?.spec?.job?.containers

if (!gate) return null

return (
<JobNodeSC {...props}>
<JobNodeSC id={id}>
<div className="headerArea">
<h2 className="heading">Action</h2>
{meta.state && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ import {
StatusOkIcon,
Tooltip,
} from '@pluralsh/design-system'
import { GateState, PipelineStageEdgeFragment } from 'generated/graphql'
import { type Node, type NodeProps } from '@xyflow/react'
import {
GateState,
PipelineStageEdgeFragment,
PipelineStageFragment,
} from 'generated/graphql'
import isEmpty from 'lodash/isEmpty'
import {
cloneElement,
ComponentProps,
ComponentPropsWithoutRef,
ComponentPropsWithRef,
ReactElement,
ReactNode,
use,
useMemo,
} from 'react'
import { type Node, type NodeProps, useNodes } from 'reactflow'
import styled, { useTheme } from 'styled-components'

import { useNodeEdges } from 'components/hooks/reactFlowHooks'
Expand All @@ -26,10 +30,19 @@ import { GraphLayoutCtx } from 'components/utils/reactflow/graph'
import {
directionToSourcePosition,
directionToTargetPosition,
NodeBaseCard,
NodeHandle,
NodeBaseCardSC,
NodeHandleSC,
} from '../../../utils/reactflow/nodes'
import { reduceGateStates } from '../utils/reduceGateStatuses'
import { StageStatus } from './StageNode'

type PipelineGateNodeMeta = { meta: { state: GateState } }
type PipelineStageNodeMeta = { meta: { stageStatus: StageStatus } }

type PipelineStageNode = Node<PipelineStageFragment & PipelineStageNodeMeta>
type PipelineGateNode = Node<PipelineStageEdgeFragment & PipelineGateNodeMeta>

export type PipelineStageNodeProps = NodeProps<PipelineStageNode>
export type PipelineGateNodeProps = NodeProps<PipelineGateNode>

export type CardStatus = 'ok' | 'closed' | 'pending' | 'running'

Expand All @@ -46,7 +59,7 @@ export const NodeCardList = styled.ul(({ theme }) => ({
gap: theme.spacing.xsmall,
}))

export const BaseNodeSC = styled(NodeBaseCard)(({ theme }) => ({
export const BaseNodeSC = styled(NodeBaseCardSC)(({ theme }) => ({
'&&': {
position: 'relative',
padding: theme.spacing.small,
Expand Down Expand Up @@ -84,45 +97,28 @@ export const BaseNodeSC = styled(NodeBaseCard)(({ theme }) => ({
},
}))

export function BaseNode({
export function PipelineBaseNode({
id,
data: { meta },
children,
...props
}: NodeProps<NodeMeta> & { children: ReactNode } & ComponentProps<
typeof BaseNodeSC
>) {
}: Pick<PipelineStageNodeProps | PipelineGateNodeProps, 'id'> &
ComponentPropsWithRef<typeof BaseNodeSC>) {
const { incomers, outgoers } = useNodeEdges(id)
const nodes = useNodes()
const { rankdir = 'LR' } = use(GraphLayoutCtx) ?? {}

const reducedInState = useMemo(() => {
const incomingNodes = nodes.filter((node) =>
incomers.some((incomer) => incomer.source === node.id)
)

return reduceGateStates(
incomingNodes.map((inNode) => ({
state: (inNode as Node<NodeMeta>)?.data?.meta?.state,
}))
)
}, [incomers, nodes])

return (
<BaseNodeSC {...props}>
<NodeHandle
<NodeHandleSC
type="target"
isConnectable={false}
$isConnected={!isEmpty(incomers)}
$isOpen={reducedInState === GateState.Open}
position={directionToTargetPosition[rankdir]}
/>
{children}
<NodeHandle
<NodeHandleSC
type="source"
isConnectable={false}
$isConnected={!isEmpty(outgoers)}
$isOpen={meta.state === GateState.Open}
position={directionToSourcePosition[rankdir]}
/>
</BaseNodeSC>
Expand Down Expand Up @@ -164,10 +160,6 @@ export function IconHeading({
)
}

export type NodeMeta = { meta: { state: GateState } }

export type EdgeNode = NodeProps<PipelineStageEdgeFragment & NodeMeta>

const StatusCardSC = styled(Card)(({ theme }) => ({
'&&': {
padding: `${theme.spacing.xsmall}px ${theme.spacing.small}px`,
Expand Down
25 changes: 8 additions & 17 deletions assets/src/components/cd/pipelines/nodes/StageNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import {
useState,
} from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { type NodeProps } from 'reactflow'
import styled, { useTheme } from 'styled-components'
import { MergeDeep } from 'type-fest'

import {
PipelineContextsDocument,
Expand All @@ -51,13 +49,13 @@ import { PipelinePullRequestsModal } from '../PipelinePullRequests'
import { CountBadge } from '../../../utils/CountBadge'

import {
BaseNode,
CardStatus,
IconHeading,
NodeCardList,
NodeMeta,
PipelineBaseNode,
PipelineStageNodeProps,
StatusCard,
} from './BaseNode'
} from './PipelineBaseNode'

const serviceStateToCardStatus = {
[ServiceDeploymentStatus.Healthy]: 'ok',
Expand Down Expand Up @@ -109,7 +107,7 @@ export function getStageStatus(stage: PipelineStageFragment) {
: StageStatus.Pending
}

const StageNodeSC = styled(BaseNode)(() => ({
const StageNodeSC = styled(PipelineBaseNode)(() => ({
'&&': { minWidth: 240 },
}))

Expand Down Expand Up @@ -209,20 +207,13 @@ function HeaderChip({ stage, isOpen, setIsOpen, status }) {
)
}

export function StageNode(
props: NodeProps<
PipelineStageFragment &
MergeDeep<NodeMeta, { meta: { stageStatus: StageStatus } }>
>
) {
export function StageNode({ id, data }: PipelineStageNodeProps) {
const navigate = useNavigate()
const { incomers, outgoers } = useNodeEdges(props.id)
const { incomers, outgoers } = useNodeEdges(id)
const pipelineId = useParams().pipelineId!
const [isOpen, setIsOpen] = useState(false)

const {
data: { meta, ...stage },
} = props
const { meta, ...stage } = data
const status = meta.stageStatus

const isRootStage = isEmpty(incomers) && !isEmpty(outgoers)
Expand All @@ -242,7 +233,7 @@ export function StageNode(
)

return (
<StageNodeSC {...props}>
<StageNodeSC id={id}>
<div className="headerArea">
<h2 className="heading">STAGE</h2>
<HeaderChip
Expand Down
20 changes: 9 additions & 11 deletions assets/src/components/cd/pipelines/nodes/TestsNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Chip, TestTubeIcon } from '@pluralsh/design-system'
import { GateState } from 'generated/graphql'

import {
BaseNode,
EdgeNode,
gateStateToCardStatus,
gateStateToSeverity,
IconHeading,
NodeCardList,
PipelineBaseNode,
PipelineGateNodeProps,
StatusCard,
gateStateToCardStatus,
gateStateToSeverity,
} from './BaseNode'
} from './PipelineBaseNode'

const gateStateToTestText = {
[GateState.Open]: 'Passed',
Expand All @@ -18,14 +18,12 @@ const gateStateToTestText = {
[GateState.Running]: 'Running',
} as const satisfies Record<GateState, string>

export function TestsNode(props: EdgeNode) {
const {
data: { meta, ...edge },
} = props
export function TestsNode({ id, data }: PipelineGateNodeProps) {
const { meta, ...edge } = data
const gates = edge?.gates

return (
<BaseNode {...props}>
<PipelineBaseNode id={id}>
{meta.state && (
<div className="headerArea">
<h2 className="heading">Action</h2>
Expand Down Expand Up @@ -59,6 +57,6 @@ export function TestsNode(props: EdgeNode) {
)
)}
</NodeCardList>
</BaseNode>
</PipelineBaseNode>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
PipelineStageEdgeFragment,
PipelineStageFragment,
} from 'generated/graphql'
import { type Edge } from 'reactflow'
import { type Edge } from '@xyflow/react'
import isEmpty from 'lodash/isEmpty'
import { isNonNullable } from 'utils/isNonNullable'
import { groupBy } from 'lodash'
Expand Down
Loading

0 comments on commit 18ac999

Please sign in to comment.