diff --git a/src/components/TreeGraph/component-node.less b/src/components/TreeGraph/component-node.less new file mode 100644 index 000000000..b4d8c44c9 --- /dev/null +++ b/src/components/TreeGraph/component-node.less @@ -0,0 +1,30 @@ +.label-traits { + z-index: 1000; + display: flex; + flex-direction: row; + align-items: center; + white-space: nowrap; + .trait-num { + display: block; + padding: 2px 4px; + border: rgb(27, 88, 244) solid 1px; + border-radius: 4px; + &.active { + color: #fff; + background-color: rgb(27, 88, 244); + } + } +} + +.trait-graph { + position: absolute; + top: -16px; + left: 260px; + .trait { + height: 24px; + line-height: 24px; + } + .trait-node { + padding-left: 1em; + } +} diff --git a/src/components/TreeGraph/component-node.tsx b/src/components/TreeGraph/component-node.tsx new file mode 100644 index 000000000..0e32a1997 --- /dev/null +++ b/src/components/TreeGraph/component-node.tsx @@ -0,0 +1,235 @@ +import React, { useState } from 'react'; +import * as dagre from 'dagre'; +import classNames from 'classnames'; +import Translation from '../Translation'; +import type { GraphNode, GraphEdge, TraitGraphNode, Line } from './interface'; +import { describeComponents, getGraphSize, ResourceIcon } from './utils'; +import { If } from 'tsx-control-statements/components'; +import { Balloon, Tag } from '@b-design/ui'; + +import './component-node.less'; +import type { TraitStatus } from '../../interface/application'; + +export interface ComponentNodeProps { + node: GraphNode; + showTrait: boolean; +} + +function renderTraitTree(traits: TraitStatus[]) { + const graph = new dagre.graphlib.Graph(); + graph.setGraph({ + nodesep: 20, + rankdir: 'TB', + align: 'UL', + ranksep: 26, + compound: true, + }); + + // set node and make layout + graph.setNode('graph-trait-start', { + width: 5, + height: 40, + x: 0, + y: 0, + }); + traits.map((trait) => { + graph.setEdge('graph-trait-start', trait.type, {}); + graph.setNode(trait.type, { + trait: trait, + width: 130, + height: 30, + x: 0, + y: 0, + }); + }); + dagre.layout(graph); + const edges: { from: string; to: string; lines: Line[] }[] = []; + graph.edges().forEach((edgeInfo) => { + const edge = graph.edge(edgeInfo); + const lines: Line[] = []; + if (edge.points && edge.points.length > 1) { + for (let i = 1; i < edge.points.length; i++) { + lines.push({ + x1: edge.points[i - 1].x, + y1: edge.points[i - 1].y - 14, + x2: edge.points[i].x, + y2: edge.points[i].y - 14, + }); + } + } + edges.push({ + from: edgeInfo.v, + to: edgeInfo.w, + lines: lines, + }); + }); + const size = getGraphSize(graph.nodes().map((key) => graph.node(key))); + return ( +
+ {graph.nodes().map((key) => { + if (key === 'graph-trait-start') { + return; + } + const { trait, x, y, width, height } = graph.node(key); + if (!trait) { + return; + } + const label = trait.type; + const traitNode = ( +
+
+
+ + {label} +
+
+
+ ); + + if (trait.message) { + return ( + +
+

{`Message: ${trait.message}`}

+
+
+ ); + } + return traitNode; + })} + {edges.map((edge) => ( +
+ {edge.lines.map((line) => { + const distance = Math.sqrt( + Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2), + ); + const xMid = (line.x1 + line.x2) / 2; + const yMid = (line.y1 + line.y2) / 2; + const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI; + return ( +
+ ); + })} +
+ ))} +
+ ); +} + +export const ComponentNode = (props: ComponentNodeProps) => { + const { node } = props; + const traits = node.resource.service?.traits || []; + const [showTrait, setShowTrait] = useState(props.showTrait); + const WithBalloon = (graphNode: React.ReactNode) => { + return ( + +
+ {describeComponents(node).map((line: any) => { + return

{line}

; + })} +
+
+ ); + }; + + const graphNode = ( +
+ {WithBalloon( +
+ +
, + )} + {WithBalloon( +
+
{node.resource.name}
+
+ + + Healthy + + + + UnHealthy + +
+
, + )} + + 0}> +
+ {traits && ( + + + {traits[0].type} + + )} + 1}> +
setShowTrait(!showTrait)} + > + {traits?.length > 1 && '+' + (traits?.length - 1)} +
+
+
+
+ {traits.length > 1 && showTrait && renderTraitTree(traits)} +
+ ); + return graphNode; +}; diff --git a/src/components/TreeGraph/index.less b/src/components/TreeGraph/index.less index 75bec9032..46a36dfdd 100644 --- a/src/components/TreeGraph/index.less +++ b/src/components/TreeGraph/index.less @@ -2,6 +2,8 @@ .graph-tree { position: relative; + + margin-top: 32px; overflow: auto; .graph-node { @@ -31,6 +33,9 @@ color: #a6a6a6; font-size: 12px; } + .healthy.success { + color: #28a745; + } } .icon { @@ -59,20 +64,6 @@ color: #666; } } - .trait { - height: 24px; - line-height: 24px; - } - .label-dot { - width: 5px; - height: 5px; - border-radius: 5px; - background: #2DC86D; - display: inline-block; - position: absolute; - bottom: 7px; - left: -9px; - } .additional { position: absolute; right: 8px; diff --git a/src/components/TreeGraph/index.tsx b/src/components/TreeGraph/index.tsx index 286d46d5f..42496fcd1 100644 --- a/src/components/TreeGraph/index.tsx +++ b/src/components/TreeGraph/index.tsx @@ -7,462 +7,356 @@ import type { ResourceTreeNode } from '../../interface/observation'; import './index.less'; import classNames from 'classnames'; import { - nodeKey, describeNode, describeCluster, - describeComponents, treeNodeKey, getGraphSize, getNodeSize, ResourceIcon, + describeTarget, } from './utils'; import kubernetes from '../../assets/kubernetes.svg'; import pod from '../../assets/resources/pod.svg'; import kubevela from '../../assets/KubeVela-01.svg'; - import { Link } from 'dva/router'; import { Dropdown, Icon, Menu, Tag, Balloon } from '@b-design/ui'; import i18n from '../../i18n'; import { If } from 'tsx-control-statements/components'; +import type { GraphNode, TreeNode, GraphEdge, Line } from './interface'; +import { ComponentNode } from './component-node'; type TreeGraphProps = { node: TreeNode; zoom: number; appName: string; envName: string; - graphType: any; - onNodeClick: (nodeKey: string) => void; + nodesep: 50 | number; onResourceDetailClick: (resource: ResourceTreeNode) => void; }; -export interface GraphNode extends TreeNode { - x: number; - y: number; - width: number; - height: number; -} - -export interface GraphEdge { - points?: { x: number; y: number }[]; - [key: string]: any; -} - -export interface Line { - x1: number; - y1: number; - x2: number; - y2: number; -} - -export interface TreeNode { - resource: ResourceTreeNode; - nodeType: 'app' | 'cluster' | 'component' | 'trait' | 'policy' | 'resource' | 'pod'; - leafNodes?: TreeNode[]; -} - -type State = { - traitsShow: boolean +function renderResourceNode(props: TreeGraphProps, id: string, node: GraphNode) { + const graphNode = ( +
+
+ +
+
+
{node.resource.name}
+
{node.resource.kind}
+
+
+ }> + + props.onResourceDetailClick(node.resource)}>Detail + + +
+ +
+ + EIP: {node.resource.additionalInfo?.EIP} + +
+
+
+ ); + return ( + +
+ {describeNode(node).map((line) => { + return

{line}

; + })} +
+
+ ); } -class TreeGraph extends React.Component { - constructor(props: TreeGraphProps) { - super(props); - this.state = { - traitsShow: false, - }; - } - renderComponentNode(props: TreeGraphProps, id: string, node: GraphNode) { - const fullName = nodeKey(node); - const traits = node.resource.service.traits - const labelTraits = traits ? (traits[0].alias ? traits[0].alias + '(' + traits[0].type + ')' : traits[0].type) : undefined - const graphNode = ( -
props.onNodeClick && props.onNodeClick(fullName)} - className={classNames('graph-node', 'graph-node-resource', { - 'warning-status': !node.resource.service.healthy, +function renderAppNode(props: TreeGraphProps, id: string, node: GraphNode) { + const graphNode = ( +
+
+ +
+
+ {node.resource.name} +
+
+ }> + + props.onResourceDetailClick(node.resource)}>Detail + + +
+
+ ); + return ( + +
+ {describeNode(node).map((line) => { + return

{line}

; })} - style={{ - left: node.x, - top: node.y, - width: node.width, - height: node.height, - transform: `translate(-80px, 0px)`, - }} - > -
- -
-
-
{node.resource.name}
-
-
-
- {node.resource.service.healthy ? 'Healthy' : 'Unhealthy'} -
-
- -
-
{labelTraits} {this.setState({ traitsShow: !this.state.traitsShow })}}>{' +' + node.resource.service.traits.length}
-
-
- -
- {traits.map((trait: any, index: any) => { - const label = trait.alias ? trait.alias + '(' + trait.type + ')' : trait.type; - const num = traits.length === 1 ? 0 : Math.trunc(traits.length / 2) - const nodeY = num === index ? -24 : (-24 + (index - num) * 30) - return ( -
-
-
{label}
-
-
- ) - })} - {traits.map((trait: any, index: any) => { - const num = traits.length === 1 ? 0 : Math.trunc(traits.length / 2) - const nodeY = num === index ? 0 : ((index - num) * 30) - const edgeY = ((index - num) * 15) - const distance = Math.sqrt( - Math.pow(30, 2) + Math.pow(nodeY, 2), - ); - const xmid = num === index || num === index - 1 || num === index + 1 ? 0 : -(Math.abs((num - index) * (15))) + 15 - const angle = (Math.atan2(nodeY, 30) * 180) / Math.PI - 180; - return ( -
- ) - })} -
-
- ); - return ( - -
- {describeComponents(node).map((line: any) => { - return

{line}

; - })} -
-
- ); - } + + ); +} - renderResourceNode(props: TreeGraphProps, id: string, node: GraphNode) { - const fullName = nodeKey(node); - const graphNode = ( -
props.onNodeClick && props.onNodeClick(fullName)} - className={classNames('graph-node', 'graph-node-resource', { - 'error-status': node.resource.healthStatus?.statusCode == 'UnHealthy', - 'warning-status': node.resource.healthStatus?.statusCode == 'Progressing', - })} - style={{ - left: node.x, - top: node.y, - width: node.width, - height: node.height, - transform: `translate(-80px, 0px)`, - }} - > -
- -
-
-
{node.resource.name}
-
{node.resource.kind}
-
-
- }> - - props.onResourceDetailClick(node.resource)}>Detail - - -
- -
- - EIP: {node.resource.additionalInfo?.EIP} - -
-
+function renderPodNode(props: TreeGraphProps, id: string, node: GraphNode) { + const { appName, envName } = props; + const graphNode = ( +
+
+ + Pod
- ); - return ( - -
- {describeNode(node).map((line) => { - return

{line}

; - })} -
-
- ); - } - - renderAppNode(props: TreeGraphProps, id: string, node: GraphNode) { - const fullName = nodeKey(node); - - const graphNode = ( -
props.onNodeClick && props.onNodeClick(fullName)} - className={classNames('graph-node', 'graph-node-app')} - style={{ - left: node.x, - top: node.y, - width: node.width, - height: node.height, - transform: `translate(-60px, 0px)`, - }} - > -
- -
-
- {node.resource.name} -
+
+ + {node.resource.name} +
- }> - - props.onResourceDetailClick(node.resource)}>Detail - - -
-
- ); - return ( - -
- {describeNode(node).map((line) => { - return

{line}

; - })} -
-
- ); - } - - renderPodNode(props: TreeGraphProps, id: string, node: GraphNode) { - const fullName = nodeKey(node); - const { appName, envName } = props; - const graphNode = ( -
props.onNodeClick && props.onNodeClick(fullName)} - className={classNames('graph-node', 'graph-node-pod', { - 'error-status': node.resource.healthStatus?.statusCode == 'UnHealthy', - 'warning-status': node.resource.healthStatus?.statusCode == 'Progressing', - })} - style={{ - left: node.x, - top: node.y, - width: node.width, - height: node.height, - transform: `translate(-80px, 0px)`, - }} - > -
- - Pod -
-
- {node.resource.name} + -
- - - -
-
-
- }> - - props.onResourceDetailClick(node.resource)}>Detail - - -
-
- - Ready: {node.resource.additionalInfo?.Ready} -
- ); - return ( - -
- {describeNode(node).map((line) => { - return

{line}

; - })} -
-
- ); - } - - renderClusterNode(props: TreeGraphProps, id: string, node: GraphNode) { - const fullName = nodeKey(node); - - const graphNode = ( -
props.onNodeClick && props.onNodeClick(fullName)} - className={classNames('graph-node', 'graph-node-cluster')} - style={{ - left: node.x, - top: node.y, - width: node.width, - height: node.height, - transform: `translate(-40px, 0px)`, - }} - > -
- -
-
-
{node.resource.name}
-
Cluster
-
+
+ }> + + props.onResourceDetailClick(node.resource)}>Detail + +
- ); - return ( - -
- {describeCluster(node).map((line) => { - return

{line}

; - })} -
-
- ); - } - - setNode(graph: dagre.graphlib.Graph, node: TreeNode) { - const size = getNodeSize(node); - graph.setNode(treeNodeKey(node), { ...node, width: size.width, height: size.height, x: 0, y: 0 }); - - node.leafNodes?.map((subNode) => { - if (treeNodeKey(node) == treeNodeKey(subNode)) { - return; - } - graph.setEdge(treeNodeKey(node), treeNodeKey(subNode), {}); - this.setNode(graph, subNode); - }); - } +
+ + Ready: {node.resource.additionalInfo?.Ready} + +
+
+ ); + return ( + +
+ {describeNode(node).map((line) => { + return

{line}

; + })} +
+
+ ); +} - render() { - // init the graph - const { graphType } = this.props - const graph = new dagre.graphlib.Graph(); - graph.setGraph({ nodesep: 50, rankdir: graphType === 'resource-graph' ? 'LR' : 'TB'}); +function renderClusterNode(props: TreeGraphProps, id: string, node: GraphNode) { + const graphNode = ( +
+
+ +
+
+
{node.resource.name}
+
Cluster
+
+
+ ); + return ( + +
+ {describeCluster(node).map((line) => { + return

{line}

; + })} +
+
+ ); +} - // set node and make layout - this.setNode(graph, this.props.node); - dagre.layout(graph); - - const edges: { from: string; to: string; lines: Line[] }[] = []; - graph.edges().forEach((edgeInfo) => { - const edge = graph.edge(edgeInfo); - const lines: Line[] = []; - if (edge.points && edge.points.length > 1) { - for (let i = 1; i < edge.points.length; i++) { - lines.push({ - x1: edge.points[i - 1].x, - y1: edge.points[i - 1].y, - x2: edge.points[i].x, - y2: edge.points[i].y, - }); - } - } - edges.push({ - from: edgeInfo.v, - to: edgeInfo.w, - lines: lines, - }); - }); - - const graphNodes = graph.nodes(); - - const size = getGraphSize(graphNodes.map((id) => graph.node(id))); - return ( -
- {graphNodes.map((key) => { - const node = graph.node(key); - const nodeType = node.nodeType; - switch (nodeType) { - case 'app': - return {this.renderAppNode(this.props, key, node)}; - case 'cluster': - return {this.renderClusterNode(this.props, key, node)}; - case 'pod': - return {this.renderPodNode(this.props, key, node)}; - case 'component': - return {this.renderComponentNode(this.props, key, node)}; - default: - return ( - {this.renderResourceNode(this.props, key, node)} - ); - } +function renderTargetNode(props: TreeGraphProps, id: string, node: GraphNode) { + const graphNode = ( +
+
+ +
+
+
{node.resource.name}
+
Target
+
+
+ ); + return ( + +
+ {describeTarget(node).map((line) => { + return

{line}

; })} - - {edges.map((edge) => ( -
- {edge.lines.map((line, i) => { - const distance = Math.sqrt( - Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2), - ); - const xMid = (line.x1 + line.x2) / 2; - const yMid = (line.y1 + line.y2) / 2; - const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI; - return ( -
- ); - })} -
- ))}
- ); - }; + + ); +} + +function setNode(graph: dagre.graphlib.Graph, node: TreeNode) { + const size = getNodeSize(node); + graph.setNode(treeNodeKey(node), { + ...node, + width: size.width, + height: size.height, + x: 0, + y: 0, + }); + + node.leafNodes?.map((subNode) => { + if (treeNodeKey(node) == treeNodeKey(subNode)) { + return; + } + graph.setEdge(treeNodeKey(node), treeNodeKey(subNode), {}); + setNode(graph, subNode); + }); } -export default TreeGraph; +export const TreeGraph = (props: TreeGraphProps) => { + // init the graph + const graph = new dagre.graphlib.Graph(); + graph.setGraph({ + nodesep: props.nodesep, + rankdir: 'LR', + }); + + // set node and make layout + setNode(graph, props.node); + dagre.layout(graph); + + const edges: { from: string; to: string; lines: Line[] }[] = []; + graph.edges().forEach((edgeInfo) => { + const edge = graph.edge(edgeInfo); + const lines: Line[] = []; + if (edge.points && edge.points.length > 1) { + for (let i = 1; i < edge.points.length; i++) { + lines.push({ + x1: edge.points[i - 1].x, + y1: edge.points[i - 1].y, + x2: edge.points[i].x, + y2: edge.points[i].y, + }); + } + } + edges.push({ + from: edgeInfo.v, + to: edgeInfo.w, + lines: lines, + }); + }); + const graphNodes = graph.nodes(); + const size = getGraphSize(graphNodes.map((id) => graph.node(id))); + return ( +
+ {graphNodes.map((key) => { + const node = graph.node(key); + const nodeType = node.nodeType; + switch (nodeType) { + case 'app': + return {renderAppNode(props, key, node)}; + case 'cluster': + return {renderClusterNode(props, key, node)}; + case 'target': + return {renderTargetNode(props, key, node)}; + case 'pod': + return {renderPodNode(props, key, node)}; + case 'component': + return ; + default: + return ( + {renderResourceNode(props, key, node)} + ); + } + })} + {edges.map((edge) => ( +
+ {edge.lines.map((line) => { + const distance = Math.sqrt( + Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2), + ); + const xMid = (line.x1 + line.x2) / 2; + const yMid = (line.y1 + line.y2) / 2; + const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI; + return ( +
+ ); + })} +
+ ))} +
+ ); +}; diff --git a/src/components/TreeGraph/interface.tsx b/src/components/TreeGraph/interface.tsx new file mode 100644 index 000000000..785cafe29 --- /dev/null +++ b/src/components/TreeGraph/interface.tsx @@ -0,0 +1,33 @@ +import type { TraitStatus } from '../../interface/application'; +import type { ResourceTreeNode } from '../../interface/observation'; + +export interface TreeNode { + resource: ResourceTreeNode; + nodeType: 'app' | 'cluster' | 'component' | 'trait' | 'policy' | 'resource' | 'pod' | 'target'; + leafNodes?: TreeNode[]; +} + +export interface Node { + x: number; + y: number; + width: number; + height: number; +} + +export interface GraphNode extends Node, TreeNode {} + +export interface TraitGraphNode extends Node { + trait?: TraitStatus; +} + +export interface GraphEdge { + points?: { x: number; y: number }[]; + [key: string]: any; +} + +export interface Line { + x1: number; + y1: number; + x2: number; + y2: number; +} diff --git a/src/components/TreeGraph/utils.tsx b/src/components/TreeGraph/utils.tsx index d7b8ab1a2..c53838f9d 100644 --- a/src/components/TreeGraph/utils.tsx +++ b/src/components/TreeGraph/utils.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { GraphNode, TreeNode } from './index'; +import type { GraphNode, TreeNode, Node } from './interface'; import cm from '../../assets/resources/cm.svg'; import deploy from '../../assets/resources/deploy.svg'; @@ -66,14 +66,26 @@ export function describeCluster(node: GraphNode) { return lines; } +export function describeTarget(node: GraphNode) { + const info = node.resource.name.split('/'); + if (info.length > 1) { + const lines = [`Cluster: ${info[0]}`, `Namespace: ${info[1]}`]; + return lines; + } + return [`Cluster: ${node.resource.name}`]; +} + export function describeComponents(node: GraphNode) { const lines = [ `Name: ${node.resource.name}`, - `Namespace: ${node.resource.service.namespace || '(global)'}`, - `Cluster: ${node.resource.service.cluster || 'local'}` + `Alias: ${node.resource.component?.alias}`, + `Type: ${node.resource.component?.componentType}`, + `DependsOn: ${node.resource.component?.dependsOn || []}`, + `Namespace: ${node.resource?.namespace}`, + `Cluster: ${node.resource.service?.cluster || 'local'}`, ]; - if (node.resource.service.message) { - lines.push(`Message: ${node.resource.service.message}`); + if (node.resource.service?.message) { + lines.push(`Message: ${node.resource.service?.message}`); } return lines; } @@ -95,7 +107,7 @@ export function nodeKey(node: TreeNode) { ].join('/'); } -export function getGraphSize(nodes: GraphNode[]): { width: number; height: number } { +export function getGraphSize(nodes: Node[]): { width: number; height: number } { let width = 0; let height = 0; nodes.forEach((node) => { @@ -112,14 +124,22 @@ export function getNodeSize(node: TreeNode): { width: number; height: number } { width = 140; height = 40; } + if (node.nodeType == 'target') { + width = 200; + height = 40; + } if (node.nodeType == 'app') { width = 180; height = 40; } if (node.nodeType == 'pod') { - width = 180; + width = 220; height = 60; } + if (node.nodeType == 'component') { + width = 320; + height = 40; + } return { width, height }; } diff --git a/src/components/UISchema/index.tsx b/src/components/UISchema/index.tsx index 340895a61..0d5d4027e 100644 --- a/src/components/UISchema/index.tsx +++ b/src/components/UISchema/index.tsx @@ -113,7 +113,7 @@ class UISchema extends Component { onChange: (name: string, value: any) => { const values: any = this.form.getValues(); // Can not assign the empty value for the field with the number type - if (paramKeyMap[name].uiType == 'Number' && value === '') { + if (paramKeyMap[name] && paramKeyMap[name].uiType == 'Number' && value === '') { delete values[name]; } // Can not assign the empty value for the field with the array type diff --git a/src/interface/application.ts b/src/interface/application.ts index b93a05321..aca6e8b8e 100644 --- a/src/interface/application.ts +++ b/src/interface/application.ts @@ -107,28 +107,21 @@ export interface ApplicationStatus { revision: number; revisionHash: string; }; - components?: { - kind: string; - namespace: string; - name: string; - apiVersion: string; - }[]; services?: ComponentStatus[]; appliedResources: Resource[]; } export interface ComponentStatus { name: string; - env?: string; - healthy: string; + namespace: string; + healthy: boolean; message: string; - traits: { - type: string; - healthy: string; - message: string; - }[]; - leafNodes?: any; - cluster?: string; + traits?: TraitStatus[]; + cluster: string; + workloadDefinition: { + apiVersion: string; + kind: string; + }; } export interface Condition { @@ -169,6 +162,12 @@ export interface Trait { updateTime?: string; } +export interface TraitStatus { + type: string; + healthy: string; + message: string; +} + export interface ApplicationComponentBase { name: string; alias?: string; @@ -177,7 +176,7 @@ export interface ApplicationComponentBase { componentType: string; creator?: string; main: boolean; - dependsOn: string[]; + dependsOn?: string[]; createTime?: string; updateTime?: string; input?: InputItem[]; @@ -214,9 +213,6 @@ export interface ApplicationComponent extends ApplicationComponentBase { type: string; }; }; - kind?: string; - service?: ComponentStatus; - leafNodes?: any; } export interface ApplicationRevision { diff --git a/src/interface/observation.ts b/src/interface/observation.ts index 6773fbe0b..d5607a00e 100644 --- a/src/interface/observation.ts +++ b/src/interface/observation.ts @@ -1,3 +1,5 @@ +import type { ApplicationComponent, ComponentStatus } from './application'; + export interface PodBase { cluster: string; component: string; @@ -222,8 +224,10 @@ export interface ResourceTreeNode extends Resource { healthStatus?: ResourceHealthStatus; additionalInfo?: Record; leafNodes?: ResourceTreeNode[]; - service?: any; - componentType?: any + + // For the component node + service?: ComponentStatus; + component?: ApplicationComponent; } export interface ResourceHealthStatus { diff --git a/src/pages/ApplicationStatus/components/ApplicationGraph/index.less b/src/pages/ApplicationStatus/components/ApplicationGraph/index.less index c64433254..7b05e1e2e 100644 --- a/src/pages/ApplicationStatus/components/ApplicationGraph/index.less +++ b/src/pages/ApplicationStatus/components/ApplicationGraph/index.less @@ -17,3 +17,6 @@ z-index: 100; } } +.graph-container.top-center { + justify-content: center; +} diff --git a/src/pages/ApplicationStatus/components/ApplicationGraph/index.tsx b/src/pages/ApplicationStatus/components/ApplicationGraph/index.tsx index 89160cecb..1ba37f0f4 100644 --- a/src/pages/ApplicationStatus/components/ApplicationGraph/index.tsx +++ b/src/pages/ApplicationStatus/components/ApplicationGraph/index.tsx @@ -1,17 +1,19 @@ import { Button, Icon } from '@b-design/ui'; import React from 'react'; import { If } from 'tsx-control-statements/components'; -import type { TreeNode } from '../../../../components/TreeGraph'; -import TreeGraph from '../../../../components/TreeGraph'; +import type { TreeNode } from '../../../../components/TreeGraph/interface'; +import { TreeGraph } from '../../../../components/TreeGraph'; import type { ApplicationDetail, ApplicationStatus, EnvBinding, ApplicationComponent, + ComponentStatus, } from '../../../../interface/application'; import type { AppliedResource, ResourceTreeNode } from '../../../../interface/observation'; import { ShowResource } from './resource-show'; import './index.less'; +import classNames from 'classnames'; type Props = { applicationStatus?: ApplicationStatus; @@ -19,8 +21,7 @@ type Props = { env?: EnvBinding; resources: AppliedResource[]; components?: ApplicationComponent[]; - graphType?: string, - componentsData?: ApplicationComponent[]; + graphType: 'resource-graph' | 'application-graph'; }; type State = { @@ -66,7 +67,55 @@ class ApplicationGraph extends React.Component { return tree; } - buildClusterNode(resources: AppliedResource[], components?: ApplicationComponent[], graphType?: string): TreeNode[] { + convertComponentNode(service: ComponentStatus, component?: ApplicationComponent): TreeNode { + const node: TreeNode = { + nodeType: 'component', + resource: { + name: service.name, + namespace: service.namespace, + kind: 'Component', + component: component, + cluster: service.cluster, + service: service, + }, + }; + return node; + } + + isLeafNode(componentName: string, component: ApplicationComponent): boolean { + return component.dependsOn?.includes(componentName) || false; + } + + // generate the component tree base the dependencies + generateTree(tree: Map, components: ApplicationComponent[]) { + tree.forEach((node) => { + const nodeMap = new Map(); + node.leafNodes?.map((ln) => { + nodeMap.set(ln.resource.name, ln); + }); + const deleteNode: string[] = []; + node.leafNodes?.map((n) => { + let isTop = true; + components.map((c) => { + if (this.isLeafNode(n.resource.name, c)) { + const parentNode = nodeMap.get(c.name); + if (parentNode && !parentNode.leafNodes) { + parentNode.leafNodes = [n]; + } else if (parentNode && parentNode.leafNodes) { + parentNode.leafNodes.push(n); + } + isTop = false; + } + }); + if (!isTop) { + deleteNode.push(n.resource.name); + } + }); + node.leafNodes = node.leafNodes?.filter((n) => !deleteNode.includes(n.resource.name)); + }); + } + + buildClusterNode(resources: AppliedResource[], graphType?: string): TreeNode[] { const clusterTree: Map = new Map(); if (graphType === 'resource-graph') { resources.map((res) => { @@ -85,22 +134,33 @@ class ApplicationGraph extends React.Component { } } }); - } else { - const arr = components?.filter((res) => {return res.dependsOn === null}) - arr?.map((res) => { - const cluster = res?.service?.cluster || 'local' - if (!clusterTree.get(cluster)) { - clusterTree.set(cluster, { resource: { name: cluster }, nodeType: 'cluster' }); + } else if (graphType === 'application-graph') { + const { applicationStatus, components } = this.props; + const services = (applicationStatus && applicationStatus.services) || []; + const componentMap = new Map(); + components?.map((com) => { + componentMap.set(com.name, com); + }); + services.map((s) => { + const cluster = s.cluster || 'local'; + const namespace = s.namespace; + const name = `${cluster}/${namespace}`; + if (!clusterTree.get(name)) { + clusterTree.set(name, { resource: { name: name }, nodeType: 'target' }); } - const node = clusterTree.get(cluster); - if (node) { - if (!node.leafNodes) { - node.leafNodes = this.convertNode(arr); + const clusterNode = clusterTree.get(name); + if (clusterNode) { + const component = componentMap.get(s.name); + if (!clusterNode.leafNodes) { + clusterNode.leafNodes = [this.convertComponentNode(s, component)]; } else { - node.leafNodes = node.leafNodes.concat(this.convertNode(arr)); + clusterNode.leafNodes = clusterNode.leafNodes.concat( + this.convertComponentNode(s, component), + ); } } - }) + }); + //this.generateTree(clusterTree, components || []); } const tree: TreeNode[] = []; clusterTree.forEach((value: TreeNode) => { @@ -110,7 +170,7 @@ class ApplicationGraph extends React.Component { } buildTree(): TreeNode { - const { resources, env, application, graphType, componentsData } = this.props; + const { resources, env, application, graphType } = this.props; const root: TreeNode = { nodeType: 'app', resource: { @@ -119,7 +179,7 @@ class ApplicationGraph extends React.Component { apiVersion: 'core.oam.dev/v1beta1', namespace: env?.appDeployNamespace || '', }, - leafNodes: this.buildClusterNode(resources, graphType === 'resource-graph' ? [] : componentsData, graphType), + leafNodes: this.buildClusterNode(resources, graphType), }; return root; } @@ -133,7 +193,7 @@ class ApplicationGraph extends React.Component { const { showResource, resource, zoom } = this.state; const data = this.buildTree(); return ( -
+