From 1b750834113612a6db40b3e65e92e09c1f4968e5 Mon Sep 17 00:00:00 2001 From: Rafal Date: Wed, 13 Sep 2023 03:09:52 +0200 Subject: [PATCH] feat(ui): Helm/Container/Git Repository is shown in DAG (#735) Signed-off-by: Rafal Pelczar --- .../nodes/repo-node.module.less | 31 ++++ .../project-details/nodes/repo-node.tsx | 54 +++++++ .../{ => nodes}/stage-node.module.less | 0 .../project-details/nodes/stage-node.tsx | 85 +++++++++++ .../project-details/project-details.tsx | 135 +++++++++++------- .../project-details/stage-item.module.less | 9 -- .../project/project-details/stage-item.tsx | 15 -- .../project/project-details/stage-node.tsx | 72 ---------- .../features/project/project-details/types.ts | 38 +++++ 9 files changed, 293 insertions(+), 146 deletions(-) create mode 100644 ui/src/features/project/project-details/nodes/repo-node.module.less create mode 100644 ui/src/features/project/project-details/nodes/repo-node.tsx rename ui/src/features/project/project-details/{ => nodes}/stage-node.module.less (100%) create mode 100644 ui/src/features/project/project-details/nodes/stage-node.tsx delete mode 100644 ui/src/features/project/project-details/stage-item.module.less delete mode 100644 ui/src/features/project/project-details/stage-item.tsx delete mode 100644 ui/src/features/project/project-details/stage-node.tsx create mode 100644 ui/src/features/project/project-details/types.ts diff --git a/ui/src/features/project/project-details/nodes/repo-node.module.less b/ui/src/features/project/project-details/nodes/repo-node.module.less new file mode 100644 index 000000000..70f278b00 --- /dev/null +++ b/ui/src/features/project/project-details/nodes/repo-node.module.less @@ -0,0 +1,31 @@ +.node { + background-color: #ddd; + border-radius: 10px; + display: flex; + flex-direction: column; + padding: 2px; + + h3 { + padding: 0.4em 0.5em; + color: #333; + font-size: 15px; + font-weight: 600; + margin-left: 0.25em; + margin-bottom: 0; + } +} + +.body { + background-color: white; + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + padding: 8px; + font-size: 12px; + flex: 1; +} + +.value { + margin-top: 3px; + color: #777; + font-size: 11px; +} diff --git a/ui/src/features/project/project-details/nodes/repo-node.tsx b/ui/src/features/project/project-details/nodes/repo-node.tsx new file mode 100644 index 000000000..8baa805d4 --- /dev/null +++ b/ui/src/features/project/project-details/nodes/repo-node.tsx @@ -0,0 +1,54 @@ +import { faDocker, faGit } from '@fortawesome/free-brands-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from 'antd'; + +import { NodeType, NodesRepoType } from '../types'; + +import * as styles from './repo-node.module.less'; + +const MAX_CHARS = 19; + +type Props = { + nodeData: NodesRepoType; + height: number; +}; + +const name = { + [NodeType.REPO_IMAGE]: 'Image', + [NodeType.REPO_GIT]: 'Git', + [NodeType.REPO_CHART]: 'Chart' +}; + +const ico = { + [NodeType.REPO_IMAGE]: faDocker, + [NodeType.REPO_GIT]: faGit +}; + +export const RepoNode = ({ nodeData, height }: Props) => ( +
+

+ {name[nodeData.type]} + {nodeData.type !== NodeType.REPO_CHART && } +

+
+ {(nodeData.type === NodeType.REPO_IMAGE || nodeData.type === NodeType.REPO_GIT) && ( + + )} + {nodeData.type === NodeType.REPO_CHART && ( + + )} +
+
+); + +const RepoNodeBody = ({ label, value }: { label: string; value: string }) => ( + <> +
{label}
+ +
+ {value.length > MAX_CHARS && '...'} + {value.substring(value.length - MAX_CHARS)} +
+
+ +); diff --git a/ui/src/features/project/project-details/stage-node.module.less b/ui/src/features/project/project-details/nodes/stage-node.module.less similarity index 100% rename from ui/src/features/project/project-details/stage-node.module.less rename to ui/src/features/project/project-details/nodes/stage-node.module.less diff --git a/ui/src/features/project/project-details/nodes/stage-node.tsx b/ui/src/features/project/project-details/nodes/stage-node.tsx new file mode 100644 index 000000000..38aaa477a --- /dev/null +++ b/ui/src/features/project/project-details/nodes/stage-node.tsx @@ -0,0 +1,85 @@ +import { faGear } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Space, Tooltip } from 'antd'; +import { generatePath, useNavigate } from 'react-router-dom'; + +import { paths } from '@ui/config/paths'; +import { HealthStatusIcon } from '@ui/features/common/health-status/health-status-icon'; +import { Stage } from '@ui/gen/v1alpha1/types_pb'; + +import * as styles from './stage-node.module.less'; + +export const StageNode = ({ + stage, + color, + height, + projectName, + onPromoteSubscribersClick +}: { + stage: Stage; + color: string; + height: number; + projectName?: string; + onPromoteSubscribersClick: () => void; +}) => { + const navigate = useNavigate(); + return ( +
+ navigate(generatePath(paths.stage, { name: projectName, stageName: stage.metadata?.name })) + } + > +

+
{stage.metadata?.name}
+ + {stage.status?.currentPromotion && ( + + + + )} + {stage.status?.health && ( + + )} + +

+
+

Current Freight

+

+ {stage.status?.currentFreight?.id?.slice(0, 7) || 'N/A'}{' '} +

+
+ +
+ ); +}; + +const Nodule = (props: { begin?: boolean; nodeHeight: number; onClick?: () => void }) => { + const noduleHeight = 16; + const top = props.nodeHeight / 2 - noduleHeight / 2; + return ( +
{ + e.stopPropagation(); + if (props.onClick) { + props.onClick(); + } + }} + style={{ + top: top, + height: noduleHeight, + width: noduleHeight + }} + className={`z-10 bg-gray-400 hover:bg-blue-400 absolute ${ + props.begin ? '-left-2' : '-right-2' + } rounded-md`} + /> + ); +}; diff --git a/ui/src/features/project/project-details/project-details.tsx b/ui/src/features/project/project-details/project-details.tsx index 7a0294777..ea2c282b4 100644 --- a/ui/src/features/project/project-details/project-details.tsx +++ b/ui/src/features/project/project-details/project-details.tsx @@ -6,9 +6,8 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Empty } from 'antd'; import { graphlib, layout } from 'dagre'; import React from 'react'; -import { generatePath, useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; -import { paths } from '@ui/config/paths'; import { transport } from '@ui/config/transport'; import { LoadingState } from '@ui/features/common'; import { useModal } from '@ui/features/common/modal/use-modal'; @@ -25,8 +24,10 @@ import { Stage } from '@ui/gen/v1alpha1/types_pb'; import { useDocumentEvent } from '@ui/utils/document'; import { Images } from './images'; +import { RepoNode } from './nodes/repo-node'; +import { StageNode } from './nodes/stage-node'; import { PromoteSubscribersModal } from './promote-subscribers-modal'; -import { StageNode } from './stage-node'; +import { NodeType, NodesItemType } from './types'; const lineThickness = 2; const nodeWidth = 144; @@ -34,7 +35,6 @@ const nodeHeight = 100; export const ProjectDetails = () => { const { name, stageName } = useParams(); - const navigate = useNavigate(); const { data, isLoading } = useQuery(listStages.useQuery({ project: name })); const { data: freightData, isLoading: isLoadingFreight } = useQuery( queryFreight.useQuery({ project: name }) @@ -100,39 +100,71 @@ export const ProjectDetails = () => { const g = new graphlib.Graph(); g.setGraph({ rankdir: 'LR' }); g.setDefaultEdgeLabel(() => ({})); - const stageByName = new Map(); - const colorByStage = new Map(); - const stages = data.stages + + const colors = getStageColors(data.stages); + + const myNodes = data.stages .slice() - .sort((a, b) => a.metadata?.name?.localeCompare(b.metadata?.name || '') || 0); - - const colors = getStageColors(stages); - stages?.forEach((stage) => { - const curColor = colors[stage?.metadata?.uid || '']; - colorByStage.set(stage.metadata?.name || '', curColor); - stageByName.set(stage.metadata?.name || '', stage); - g.setNode(stage.metadata?.name || '', { - label: stage.metadata?.name || '', + .sort((a, b) => a.metadata?.name?.localeCompare(b.metadata?.name || '') || 0) + .flatMap((stage) => { + return [ + { + data: stage, + type: NodeType.STAGE, + color: colors[stage?.metadata?.uid || ''] + }, + ...(stage.spec?.subscriptions?.repos?.images || []).map((image) => ({ + data: image, + stageName: stage.metadata?.name, + type: NodeType.REPO_IMAGE + })), + ...(stage.spec?.subscriptions?.repos?.git || []).map((git) => ({ + data: git, + stageName: stage.metadata?.name, + type: NodeType.REPO_GIT + })), + ...(stage.spec?.subscriptions?.repos?.charts || []).map((chart) => ({ + data: chart, + stageName: stage.metadata?.name, + type: NodeType.REPO_CHART + })) + ] as NodesItemType[]; + }); + + myNodes.forEach((item, index) => { + g.setNode(String(index), { width: nodeWidth, height: nodeHeight }); + + if (item.type === NodeType.STAGE) { + item.data?.spec?.subscriptions?.upstreamStages.forEach((upstramStage) => { + const subsIndex = myNodes.findIndex((node) => { + return node.type === NodeType.STAGE && node.data.metadata?.name === upstramStage.name; + }); + + g.setEdge(String(subsIndex), String(index)); + }); + } else { + const subsIndex = myNodes.findIndex((node) => { + return node.type === NodeType.STAGE && node.data.metadata?.name === item.stageName; + }); + + g.setEdge(String(index), String(subsIndex)); + } }); - stages.forEach((stage) => { - stage?.spec?.subscriptions?.upstreamStages.forEach((item) => { - g.setEdge(item.name || '', stage.metadata?.name || ''); - }); - }); - layout(g); - const nodes = g.nodes().map((name) => { - const node = g.node(name); + layout(g, { lablepos: 'c' }); + + const nodes = myNodes.map((node, index) => { + const gNode = g.node(String(index)); + return { - left: node.x - node.width / 2, - top: node.y - node.height / 2, - width: node.width, - height: node.height, - stage: stageByName.get(name) as Stage, - color: colorByStage.get(name) as string + ...node, + left: gNode.x - gNode.width / 2, + top: gNode.y - gNode.height / 2, + width: gNode.width, + height: gNode.height }; }); @@ -157,6 +189,7 @@ export const ProjectDetails = () => { } return lines; }); + const box = nodes.reduce( (acc, node) => ({ width: Math.max(acc.width, node.left + node.width), @@ -216,13 +249,10 @@ export const ProjectDetails = () => { className='relative' style={{ width: box?.width, height: box?.height, margin: '0 auto' }} > - {nodes?.map((node) => ( + {nodes?.map((node, index) => (
- navigate(generatePath(paths.stage, { name, stageName: node.stage.metadata?.name })) - } + key={index} + className='absolute' style={{ left: node.left, top: node.top, @@ -230,20 +260,25 @@ export const ProjectDetails = () => { height: node.height }} > - - showPromoteSubscribersModal((p) => ( - - )) - } - /> + {node.type === NodeType.STAGE ? ( + + showPromoteSubscribersModal((p) => ( + + )) + } + /> + ) : ( + + )}
))} {connectors?.map((connector) => diff --git a/ui/src/features/project/project-details/stage-item.module.less b/ui/src/features/project/project-details/stage-item.module.less deleted file mode 100644 index eb71feee2..000000000 --- a/ui/src/features/project/project-details/stage-item.module.less +++ /dev/null @@ -1,9 +0,0 @@ -.item { - display: flex; - align-items: center; - cursor: pointer; - padding: 1em; - border-radius: 5px; - border: 1px solid rgb(232, 232, 232); - margin-bottom: 1em; -} diff --git a/ui/src/features/project/project-details/stage-item.tsx b/ui/src/features/project/project-details/stage-item.tsx deleted file mode 100644 index ae6f6d60e..000000000 --- a/ui/src/features/project/project-details/stage-item.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { HealthStatusIcon } from '@ui/features/common/health-status/health-status-icon'; -import { Stage } from '@ui/gen/v1alpha1/types_pb'; - -import * as styles from './stage-item.module.less'; - -export const StageItem = (props: { stage: Stage; onClick: () => void }) => { - const { stage } = props; - - return ( -
- - {stage.metadata?.name} -
- ); -}; diff --git a/ui/src/features/project/project-details/stage-node.tsx b/ui/src/features/project/project-details/stage-node.tsx deleted file mode 100644 index 3fbce57f7..000000000 --- a/ui/src/features/project/project-details/stage-node.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { faGear } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Space, Tooltip } from 'antd'; - -import { HealthStatusIcon } from '@ui/features/common/health-status/health-status-icon'; -import { Stage } from '@ui/gen/v1alpha1/types_pb'; - -import * as styles from './stage-node.module.less'; - -export const StageNode = ({ - stage, - color, - height, - onPromoteSubscribersClick -}: { - stage: Stage; - color: string; - height: number; - onPromoteSubscribersClick: () => void; -}) => ( -
-

-
{stage.metadata?.name}
- - {stage.status?.currentPromotion && ( - - - - )} - {stage.status?.health && ( - - )} - -

-
-

Current Freight

-

- {stage.status?.currentFreight?.id?.slice(0, 7) || 'N/A'}{' '} -

-
- -
-); - -const Nodule = (props: { begin?: boolean; nodeHeight: number; onClick?: () => void }) => { - const noduleHeight = 16; - const top = props.nodeHeight / 2 - noduleHeight / 2; - return ( -
{ - e.stopPropagation(); - if (props.onClick) { - props.onClick(); - } - }} - style={{ - top: top, - height: noduleHeight, - width: noduleHeight - }} - className={`z-10 bg-gray-400 hover:bg-blue-400 absolute ${ - props.begin ? '-left-2' : '-right-2' - } rounded-md`} - /> - ); -}; diff --git a/ui/src/features/project/project-details/types.ts b/ui/src/features/project/project-details/types.ts new file mode 100644 index 000000000..793166298 --- /dev/null +++ b/ui/src/features/project/project-details/types.ts @@ -0,0 +1,38 @@ +import { + ChartSubscription, + GitSubscription, + ImageSubscription, + Stage +} from '@ui/gen/v1alpha1/types_pb'; + +export enum NodeType { + STAGE, + REPO_IMAGE, + REPO_GIT, + REPO_CHART +} + +export type NodesRepoType = + | { + type: NodeType.REPO_IMAGE; + data: ImageSubscription; + stageName: string; + } + | { + type: NodeType.REPO_GIT; + data: GitSubscription; + stageName: string; + } + | { + type: NodeType.REPO_CHART; + data: ChartSubscription; + stageName: string; + }; + +export type NodesItemType = + | { + type: NodeType.STAGE; + data: Stage; + color: string; + } + | NodesRepoType;