@@ -21,6 +21,9 @@ exports[` renders correctly 1`] = `
+
diff --git a/ui/app/src/components/NewWorkflowNext/data.ts b/ui/app/src/components/NewWorkflowNext/data.ts
new file mode 100644
index 00000000000..9fa20fa1f75
--- /dev/null
+++ b/ui/app/src/components/NewWorkflowNext/data.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 Chaos Mesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+import { ElementTypes } from './Elements/types'
+import { SpecialTemplateType } from './utils/convert'
+
+export const dndAccept = [
+ ElementTypes.Kubernetes,
+ ElementTypes.PhysicalNodes,
+ ElementTypes.Suspend,
+ SpecialTemplateType.Serial,
+ SpecialTemplateType.Parallel,
+]
diff --git a/ui/app/src/components/NewWorkflowNext/index.tsx b/ui/app/src/components/NewWorkflowNext/index.tsx
index 77612b334ff..28c53807a8a 100644
--- a/ui/app/src/components/NewWorkflowNext/index.tsx
+++ b/ui/app/src/components/NewWorkflowNext/index.tsx
@@ -15,6 +15,7 @@
*
*/
import { Badge, Box, Button, Divider, Grow, Typography } from '@mui/material'
+import _ from 'lodash'
import { useEffect, useRef, useState } from 'react'
import type { ReactFlowInstance } from 'react-flow-renderer'
@@ -23,7 +24,9 @@ import Space from '@ui/mui-extends/esm/Space'
import { useStoreDispatch, useStoreSelector } from 'store'
-import { LoadRecentlyUsedExperiments } from 'slices/workflows'
+import { loadRecentlyUsedExperiments } from 'slices/workflows'
+
+import YAML from 'components/YAML'
import FunctionalNodesElements from './Elements/FunctionalNodes'
import KubernetesElements from './Elements/Kubernetes'
@@ -40,21 +43,24 @@ export default function NewWorkflow() {
const dispatch = useStoreDispatch()
useEffect(() => {
- dispatch(LoadRecentlyUsedExperiments())
+ dispatch(loadRecentlyUsedExperiments())
}, [dispatch])
const flowRef = useRef
()
const handleClickElement = (kind: string, act?: string) => {
- ;(flowRef.current as any).initNode({ kind, act }, undefined, { x: 50, y: 50 }) // TODO: calculate the appropriate coordinates automatically
+ ;(flowRef.current as any).initNode({ kind, act }, undefined, { x: 100, y: 100 }) // TODO: calculate the appropriate coordinates automatically
+ }
+
+ const handleImportWorkflow = (workflow: string) => {
+ ;(flowRef.current as any).importWorkflow(workflow)
}
const onFinishWorkflow = () => {
const nds = flowRef.current?.getNodes()!
- const origin = nds.find((n) => n.data.origin)!
const eds = flowRef.current?.getEdges()!
- const workflow = flowToWorkflow(nodes[origin.id], nodes, eds)
+ const workflow = flowToWorkflow(nds, eds, nodes)
setWorkflow(workflow)
setOpenSubmitDialog(true)
@@ -73,11 +79,14 @@ export default function NewWorkflow() {
Use flowchart to create a new workflow.
- {Object.keys(nodes).length > 0 && (
-
- )}
+
+ Import Workflow
+ {!_.isEmpty(nodes) && (
+
+ )}
+
`1px solid ${theme.palette.divider}` }}>
diff --git a/ui/app/src/components/NewWorkflowNext/utils/convert.test.ts b/ui/app/src/components/NewWorkflowNext/utils/convert.test.ts
index 7e40b00fdce..2421a0245c0 100644
--- a/ui/app/src/components/NewWorkflowNext/utils/convert.test.ts
+++ b/ui/app/src/components/NewWorkflowNext/utils/convert.test.ts
@@ -14,77 +14,151 @@
* limitations under the License.
*
*/
-import { ExperimentKind, nodeExperimentToTemplate, templateTypeToFieldName } from './convert'
+import type { Node } from 'react-flow-renderer'
+import { v4 as uuidv4 } from 'uuid'
+
+import {
+ ExperimentKind,
+ Template,
+ connectNodes,
+ nodeExperimentToTemplate,
+ templateToNodeExperiment,
+ templateTypeToFieldName,
+ workflowToFlow,
+} from './convert'
+
+const nodeExperimentPodChaosSample = {
+ kind: 'PodChaos',
+ name: 'p1',
+ templateType: 'PodChaos',
+ deadline: '1m',
+ action: 'pod-failure',
+ selector: {
+ namespaces: ['default'],
+ },
+ mode: 'all',
+}
+
+const templatePodChaosSample: any = {
+ name: 'p1',
+ templateType: ExperimentKind.PodChaos,
+ deadline: '1m',
+ podChaos: {
+ action: 'pod-failure',
+ selector: {
+ namespaces: ['default'],
+ },
+ mode: 'all',
+ },
+}
+
+const nodeExperimentScheduleSample = {
+ kind: 'PodChaos',
+ name: 's1',
+ templateType: 'PodChaos',
+ deadline: '1m',
+ scheduled: true,
+ schedule: '@every 2h',
+ historyLimit: 2,
+ concurrencyPolicy: 'Forbid',
+ action: 'pod-failure',
+ selector: {
+ namespaces: ['default'],
+ },
+ mode: 'all',
+}
+
+const templateScheduleSample: Template = {
+ name: 's1',
+ templateType: 'Schedule',
+ deadline: '1m',
+ schedule: {
+ schedule: '@every 2h',
+ historyLimit: 2,
+ concurrencyPolicy: 'Forbid',
+ type: 'PodChaos',
+ podChaos: {
+ action: 'pod-failure',
+ selector: {
+ namespaces: ['default'],
+ },
+ mode: 'all',
+ },
+ },
+}
+
+const workflowSample1 = `
+apiVersion: chaos-mesh.org/v1alpha1
+kind: Workflow
+metadata:
+ name: try-workflow-parallel
+spec:
+ entry: the-entry
+ templates:
+ - name: the-entry
+ templateType: Parallel
+ deadline: 240s
+ children:
+ - workflow-stress-chaos
+ - workflow-network-chaos
+ - workflow-pod-chaos-schedule
+ - name: workflow-network-chaos
+ templateType: NetworkChaos
+ deadline: 20s
+ networkChaos:
+ direction: to
+ action: delay
+ mode: all
+ selector:
+ labelSelectors:
+ 'app': 'hello-kubernetes'
+ delay:
+ latency: '90ms'
+ correlation: '25'
+ jitter: '90ms'
+ - name: workflow-pod-chaos-schedule
+ templateType: Schedule
+ deadline: 40s
+ schedule:
+ schedule: '@every 2s'
+ type: 'PodChaos'
+ podChaos:
+ action: pod-kill
+ mode: one
+ selector:
+ labelSelectors:
+ 'app': 'hello-kubernetes'
+ - name: workflow-stress-chaos
+ templateType: StressChaos
+ deadline: 20s
+ stressChaos:
+ mode: one
+ selector:
+ labelSelectors:
+ 'app': 'hello-kubernetes'
+ stressors:
+ cpu:
+ workers: 1
+ load: 20
+ options: ['--cpu 1', '--timeout 600']
+`
describe('components/NewWorkflowNext/utils/convert', () => {
describe('templateTypeToFieldName', () => {
it('should return the correct field names', () => {
expect(templateTypeToFieldName(ExperimentKind.AWSChaos)).toBe('awsChaos')
+ expect(templateTypeToFieldName(ExperimentKind.HTTPChaos)).toBe('httpChaos')
expect(templateTypeToFieldName(ExperimentKind.PhysicalMachineChaos)).toBe('physicalmachineChaos')
})
})
describe('nodeExperimentToTemplate', () => {
it('should return the correct PodChaos template', () => {
- const data = {
- name: 'p1',
- templateType: 'PodChaos',
- deadline: '1m',
- action: 'pod-failure',
- selector: {
- namespaces: ['default'],
- },
- mode: 'all',
- }
-
- expect(nodeExperimentToTemplate(data)).toEqual({
- name: 'p1',
- templateType: 'PodChaos',
- deadline: '1m',
- podChaos: {
- action: 'pod-failure',
- selector: {
- namespaces: ['default'],
- },
- mode: 'all',
- },
- })
+ expect(nodeExperimentToTemplate(nodeExperimentPodChaosSample)).toEqual(templatePodChaosSample)
})
it('should return the correct Schedule template', () => {
- const data = {
- name: 's1',
- templateType: 'PodChaos',
- deadline: '1m',
- scheduled: true,
- schedule: '@every 2h',
- historyLimit: 1,
- concurrencyPolicy: 'Forbid',
- action: 'pod-failure',
- selector: {
- namespaces: ['default'],
- },
- mode: 'all',
- }
-
- expect(nodeExperimentToTemplate(data)).toEqual({
- name: 's1',
- templateType: 'Schedule',
- deadline: '1m',
- schedule: {
- schedule: '@every 2h',
- historyLimit: 1,
- concurrencyPolicy: 'Forbid',
- type: 'PodChaos',
- podChaos: {
- action: 'pod-failure',
- selector: {
- namespaces: ['default'],
- },
- mode: 'all',
- },
- },
- })
+ expect(nodeExperimentToTemplate(nodeExperimentScheduleSample)).toEqual(templateScheduleSample)
})
it('should return the correct Suspend template', () => {
@@ -101,4 +175,59 @@ describe('components/NewWorkflowNext/utils/convert', () => {
})
})
})
+
+ describe('templateToNodeExperiment', () => {
+ it('should return the correct PodChaos NodeExperiment', () => {
+ const { id, ...rest } = templateToNodeExperiment(templatePodChaosSample)
+
+ expect(rest).toEqual(nodeExperimentPodChaosSample)
+ })
+
+ it('should return the correct Schedule NodeExperiment', () => {
+ const { id, ...rest } = templateToNodeExperiment(templateScheduleSample, true)
+
+ expect(rest).toEqual({
+ ...nodeExperimentScheduleSample,
+ startingDeadlineSeconds: 0,
+ })
+ })
+ })
+
+ describe('connectNodes', () => {
+ const nodes: Node[] = [
+ {
+ id: uuidv4(),
+ position: { x: 0, y: 0 },
+ data: {},
+ },
+ {
+ id: uuidv4(),
+ position: { x: 0, y: 0 },
+ data: {},
+ },
+ {
+ id: uuidv4(),
+ position: { x: 0, y: 0 },
+ data: {},
+ },
+ ]
+ it('should return the correct connections', () => {
+ const result = connectNodes(nodes)
+
+ expect(result.length).toBe(2)
+ expect(result[0].source).toBe(nodes[0].id)
+ expect(result[0].target).toBe(nodes[1].id)
+ expect(result[1].source).toBe(nodes[1].id)
+ expect(result[1].target).toBe(nodes[2].id)
+ })
+ })
+
+ describe('workflowToFlow', () => {
+ it('test workflow sample 1', () => {
+ const { nodes, edges } = workflowToFlow(workflowSample1)
+
+ expect(nodes.length).toBe(4)
+ expect(edges.length).toBe(0)
+ })
+ })
})
diff --git a/ui/app/src/components/NewWorkflowNext/utils/convert.ts b/ui/app/src/components/NewWorkflowNext/utils/convert.ts
index c8fbd1c8599..db995771451 100644
--- a/ui/app/src/components/NewWorkflowNext/utils/convert.ts
+++ b/ui/app/src/components/NewWorkflowNext/utils/convert.ts
@@ -16,14 +16,14 @@
*/
import yaml from 'js-yaml'
import _ from 'lodash'
-import type { Edge } from 'react-flow-renderer'
+import { Edge, Node, XYPosition, getIncomers } from 'react-flow-renderer'
import { v4 as uuidv4 } from 'uuid'
-import { NodeExperiment } from 'slices/workflows'
+import type { NodeExperiment } from 'slices/workflows'
-import { scheduleInitialValues } from 'components/AutoForm/data'
+import { Schedule, scheduleInitialValues } from 'components/AutoForm/data'
-import { isDeepEmpty } from 'lib/utils'
+import { arrToObjBySep, isDeepEmpty, objToArrBySep } from 'lib/utils'
export enum ExperimentKind {
AWSChaos = 'AWSChaos',
@@ -59,8 +59,8 @@ const mapping = new Map([
[ExperimentKind.PhysicalMachineChaos, 'physicalmachineChaos'],
])
-export function templateTypeToFieldName(templateType: ExperimentKind): string {
- return mapping.get(templateType)!
+export function templateTypeToFieldName(templateType: ExperimentKind) {
+ return mapping.get(templateType)
}
export enum SpecialTemplateType {
@@ -75,40 +75,10 @@ export interface Template {
name: string
templateType: SpecialTemplateType | ExperimentKind | 'Schedule'
deadline?: string
- schedule?: { type: string } & typeof scheduleInitialValues
+ schedule?: { type: string; [key: string]: any } & Schedule
children?: string[]
}
-/**
- * Convert edges to ES6 Map with source node UUID as key and edges array as value.
- *
- * @param {Edge[]} edges
- * @return {Map}
- */
-function edgesToSourceMap(edges: Edge[]): Map {
- const map = new Map()
-
- edges.forEach((edge) => {
- if (map.has(edge.source)) {
- map.set(edge.source, [...map.get(edge.source), edge])
- } else {
- map.set(edge.source, [edge])
- }
- })
-
- return map
-}
-
-function findNextNodeArray(origin: string, result: uuid[], edgesMap: Map): uuid[] {
- if (edgesMap.has(origin)) {
- const target = edgesMap.get(origin)![0].target
-
- return findNextNodeArray(target, [...result, target], edgesMap)
- }
-
- return result
-}
-
export function nodeExperimentToTemplate(node: NodeExperiment): Template {
const { id, kind, name, templateType, deadline, scheduled, ...rest } = JSON.parse(JSON.stringify(node))
@@ -125,7 +95,7 @@ export function nodeExperimentToTemplate(node: NodeExperiment): Template {
concurrencyPolicy,
startingDeadlineSeconds,
type: templateType,
- [templateTypeToFieldName(templateType)]: restrest,
+ [templateTypeToFieldName(templateType)!]: restrest,
},
}
}
@@ -140,151 +110,68 @@ export function nodeExperimentToTemplate(node: NodeExperiment): Template {
}
}
-export function flowToWorkflow(origin: NodeExperiment, nodesMap: Record, edges: Edge[]) {
- const sourceMap = edgesToSourceMap(edges)
- const scannedNodes: uuid[] = []
- const realNexts: uuid[] = []
-
- function genTemplates(origin: NodeExperiment, level: number): Template[] {
- if (scannedNodes.includes(origin.id)) {
- return []
- }
-
- scannedNodes.push(origin.id)
-
- const eds = sourceMap.get(origin.id)
- let nextNodes: NodeExperiment[] = []
- const extraNodes: Template[] = []
-
- eds?.forEach((edge) => {
- if (edge.target) {
- nextNodes.push(nodesMap[edge.target])
- }
- })
-
- // This indicates that the next node is parallel.
- if (nextNodes.length > 1) {
- extraNodes.push({
+export function flowToWorkflow(nodes: Node[], edges: Edge[], storeTemplates: Record) {
+ const origin = nodes
+ .filter((n) => !n.parentNode)
+ .map((n) => ({ ...n, incomers: getIncomers(n, nodes, edges) }))
+ .find((n) => n.incomers.length === 0)!
+ const nodeMap = _.keyBy(nodes, 'id')
+ const sourceMap = _.keyBy(edges, 'source')
+
+ function genTemplates(origin: Node, level: number): Template[] {
+ const originalTemplate = storeTemplates[origin.data.name]
+ let currentTemplate: Template
+ let restTemplates: Template[] = []
+
+ if (
+ originalTemplate.templateType === SpecialTemplateType.Serial ||
+ originalTemplate.templateType === SpecialTemplateType.Parallel
+ ) {
+ const children = nodes
+ .filter((n) => n.parentNode === origin.id)
+ .map((n) => ({ id: n.id, ...storeTemplates[n.data.name] }))
+
+ currentTemplate = {
level,
- name: SpecialTemplateType.Parallel + '-' + uuidv4(),
- templateType: SpecialTemplateType.Parallel,
- children: nextNodes.map((n) => n.name),
- })
-
- let realNext: uuid = ''
- const uniqNexts = _.uniqWith(
- nextNodes.map((n) => {
- const nds = findNextNodeArray(n.id, [], sourceMap)
-
- return { ...n, next: nds }
- }),
- (a, b) => {
- const intersection = _.intersection(a.next, b.next)
-
- if (intersection.length > 0) {
- realNext = intersection[0]
- }
-
- return a.next[0] === b.next[0]
- }
- )
- // If all next nodes have the same next node, then jump to the next node.
- const sameNext = uniqNexts.length === 1 && uniqNexts[0] && nodesMap[realNext]
-
- if (sameNext) {
- nextNodes.forEach((n) => {
- extraNodes.push({ level: level + 1, ...nodeExperimentToTemplate(n) })
- })
-
- nextNodes = [sameNext]
+ name: originalTemplate.name,
+ templateType: originalTemplate.templateType,
+ children: children.map((n) => n.name),
}
- // This indicates that all next nodes have non-direct next node.
- if (realNext && !sameNext) {
- realNexts.push(realNext)
- }
+ restTemplates = children.flatMap((n) => genTemplates(nodeMap[n.id], level + 1))
+ } else {
+ currentTemplate = { level, ...nodeExperimentToTemplate(originalTemplate) }
}
- return [
- { level, ...nodeExperimentToTemplate(origin) },
- ...extraNodes,
- ...nextNodes.flatMap((node) =>
- genTemplates(
- node,
- nextNodes.length > 1
- ? level + 1
- : nextNodes.length === 1 && realNexts.includes(nextNodes[0].id)
- ? level - 1
- : level
- )
- ),
- ]
- }
-
- function findPotentialSerials(nodeName: string, siblings: string[], templates: Template[]) {
- const node = templates.find((t) => t.name === nodeName)!
- let matchedIndex = -1
- const children = []
-
- for (let i = 0; i < templates.length; i++) {
- const name = templates[i].name
-
- if (name === nodeName) {
- matchedIndex = i
- }
-
- if (realNexts.includes(templates[i].id!) || siblings.includes(name) || i === templates.length - 1) {
- return children.length > 1
- ? {
- level: node.level,
- name: SpecialTemplateType.Serial + '-' + uuidv4(),
- templateType: SpecialTemplateType.Serial,
- children,
- }
- : null
- }
-
- if (matchedIndex > 0 && templates[i].level === node.level && !siblings.includes(name)) {
- children.push(name)
- }
+ const edge = sourceMap[origin.id]
+ let nextNode
+ if (edge) {
+ nextNode = nodeMap[edge.target]
}
- }
-
- function genPotentialSerials(templates: Template[]) {
- return templates
- .map((template) => {
- const serials: Template[] = []
- if (template.templateType === SpecialTemplateType.Parallel) {
- template.children = template.children?.map((child, i) => {
- const serial = findPotentialSerials(
- child,
- template.children!.slice(i).filter((name) => name !== child),
- templates
- )
-
- if (serial) {
- serials.push(serial)
-
- return serial.name
- }
-
- return child
- })
- }
-
- return [template, ...serials]
- })
- .flat()
+ return [
+ currentTemplate,
+ ...restTemplates,
+ ...(nextNode && !nextNode.parentNode ? genTemplates(nextNode, level) : []),
+ ]
}
- let templates = genPotentialSerials(genTemplates(origin, 0))
+ let templates = _.uniqBy(genTemplates(origin, 0), 'name')
+ const templatesWithLevel0 = templates.filter((t) => t.level === 0)
+ const hasEntry =
+ templatesWithLevel0.length === 1 &&
+ (templatesWithLevel0[0].templateType === SpecialTemplateType.Serial ||
+ templatesWithLevel0[0].templateType === SpecialTemplateType.Parallel)
templates = [
- {
- name: 'entry',
- templateType: SpecialTemplateType.Serial,
- children: templates.filter((t) => t.level === 0).map((t) => t.name),
- },
+ ...(!hasEntry
+ ? [
+ {
+ name: 'entry',
+ templateType: SpecialTemplateType.Serial,
+ children: templatesWithLevel0.map((t) => t.name),
+ },
+ ]
+ : []),
...templates.map((t) => _.omit(t, 'level')),
]
@@ -294,7 +181,7 @@ export function flowToWorkflow(origin: NodeExperiment, nodesMap: Record>((acc, val) => {
- const [k, v] = val.replace(/\s/g, '').split(':')
- acc[k] = v
-
- return acc
- }, {})
+ return arrToObjBySep(value, ': ')
}
return value
@@ -332,3 +214,255 @@ export function flowToWorkflow(origin: NodeExperiment, nodesMap: Record objToArrBySep(obj, ': '))
+ }
+ if (_.has(result, 'selector.annotationSelectors')) {
+ _.update(result, 'selector.annotationSelectors', (obj) => objToArrBySep(obj, ': '))
+ }
+
+ return result
+}
+
+export function connectNodes(nodes: Node[]) {
+ const edges: Edge[] = []
+
+ for (let i = 1; i < nodes.length; i++) {
+ const prev = nodes[i - 1]
+ const cur = nodes[i]
+
+ const id = uuidv4()
+
+ edges.push({
+ id,
+ type: 'adjustableEdge',
+ source: prev.id,
+ target: cur.id,
+ data: {
+ id,
+ },
+ })
+ }
+
+ return edges
+}
+
+type ParentNode = {
+ id: uuid
+ type: SpecialTemplateType.Serial | SpecialTemplateType.Parallel
+}
+export enum View {
+ NodeWidth = 200,
+ NodeHeight = 30,
+ PaddingX = 30,
+ PaddingY = 15,
+ GroupNodeTypographyHeight = 32,
+}
+
+export function workflowToFlow(workflow: string) {
+ const { entry, templates }: { entry: string; templates: Template[] } = (yaml.load(workflow) as any).spec
+ const templatesMap = _.keyBy(templates, 'name')
+ // Convert templates to store.
+ //
+ // The `name` is used here as the unique id,
+ // because the name of a template inside a Workflow is unique.
+ const store = _.transform>(templatesMap, (acc, t, k) => {
+ if (t.templateType === 'Schedule') {
+ acc[k] = templateToNodeExperiment(t, true)
+ } else {
+ acc[k] = templateToNodeExperiment(t)
+ }
+ })
+ const nodes: Record = {}
+ const edges: Edge[] = []
+
+ function recurInsertNodesAndEdges(
+ entry: Template,
+ relativePos: XYPosition,
+ level: number,
+ index: number,
+ parentNode?: ParentNode
+ ): { id: uuid; width: number; height: number } {
+ function addNode(id: uuid, parentNode?: ParentNode): Node {
+ return {
+ id,
+ type: 'flowNode',
+ position: {
+ x:
+ parentNode?.type === SpecialTemplateType.Serial
+ ? relativePos.x + View.PaddingX * (index + 1)
+ : View.PaddingX,
+ y:
+ View.GroupNodeTypographyHeight +
+ (parentNode?.type === SpecialTemplateType.Parallel
+ ? relativePos.y + View.PaddingY * (index + 1)
+ : View.PaddingY),
+ },
+ data: {
+ name: entry.name,
+ kind: entry.templateType,
+ children: _.truncate(entry.name, { length: 20 }),
+ },
+ ...(parentNode && {
+ parentNode: parentNode.id,
+ extent: 'parent',
+ }),
+ }
+ }
+
+ const id = uuidv4()
+ let width = 0
+ let height = 0
+
+ if (entry.templateType === SpecialTemplateType.Serial || entry.templateType === SpecialTemplateType.Parallel) {
+ const childrenNum = entry.children!.length
+
+ nodes[id] = {
+ id,
+ type: 'groupNode',
+ position: {
+ x:
+ parentNode?.type === SpecialTemplateType.Serial
+ ? relativePos.x + View.PaddingX * (index + 1)
+ : parentNode?.type === SpecialTemplateType.Parallel
+ ? View.PaddingX
+ : relativePos.x,
+ y:
+ View.GroupNodeTypographyHeight +
+ (parentNode?.type === SpecialTemplateType.Parallel
+ ? relativePos.y + View.PaddingY * (index + 1)
+ : parentNode?.type === SpecialTemplateType.Serial
+ ? View.PaddingY
+ : relativePos.y - View.GroupNodeTypographyHeight),
+ },
+ data: {
+ name: entry.name,
+ type: entry.templateType,
+ childrenNum,
+ },
+ ...(parentNode && {
+ parentNode: parentNode.id,
+ extent: 'parent',
+ connectable: parentNode.type === SpecialTemplateType.Serial,
+ }),
+ zIndex: -1, // Make edges visible on the top of the group node.
+ }
+
+ const children = entry.children!.map((child) => templatesMap[child])
+
+ let prevWidth = 0
+ let prevHeight = 0
+ const uuids = children.map((child, i) => {
+ const {
+ id: uuid,
+ width: w,
+ height: h,
+ } = recurInsertNodesAndEdges(
+ child,
+ {
+ x: prevWidth,
+ y: prevHeight,
+ },
+ level + 1,
+ i,
+ {
+ id,
+ type: entry.templateType as any,
+ }
+ )
+
+ if (entry.templateType === SpecialTemplateType.Serial) {
+ width += w
+ height = Math.max(height, h)
+
+ prevWidth += w
+ }
+
+ if (entry.templateType === SpecialTemplateType.Parallel) {
+ width = Math.max(width, w)
+ height += h
+
+ prevHeight += h
+ }
+
+ if (nodes[uuid].type === 'groupNode') {
+ prevHeight += View.GroupNodeTypographyHeight
+ }
+
+ return uuid
+ })
+
+ // If Serial, connect all child nodes.
+ if (entry.templateType === SpecialTemplateType.Serial) {
+ edges.push(...connectNodes(uuids.map((uuid) => nodes[uuid])))
+ }
+
+ // Calculate the padding of the group node.
+ width += entry.templateType === SpecialTemplateType.Serial ? View.PaddingX * (childrenNum + 1) : View.PaddingX * 2
+ height +=
+ entry.templateType === SpecialTemplateType.Parallel ? View.PaddingY * (childrenNum + 1) : View.PaddingY * 2
+
+ // Calculate the height of all headers.
+ const specialUUIDs = _.sumBy(uuids, (uuid) => (nodes[uuid].type === 'groupNode' ? 1 : 0))
+ if (specialUUIDs > 0) {
+ height +=
+ entry.templateType === SpecialTemplateType.Serial
+ ? View.GroupNodeTypographyHeight
+ : View.GroupNodeTypographyHeight * specialUUIDs
+ }
+
+ nodes[id].data.width = width
+ nodes[id].data.height = height
+ } else {
+ nodes[id] = addNode(id, parentNode)
+
+ width = View.NodeWidth
+ height = View.NodeHeight
+ }
+
+ return { id, width, height }
+ }
+
+ recurInsertNodesAndEdges(templatesMap[entry], { x: 100, y: 100 }, 0, 0)
+
+ return { store, nodes: _.values(nodes), edges }
+}
diff --git a/ui/app/src/components/Scope/index.tsx b/ui/app/src/components/Scope/index.tsx
index baa94eaf705..2b81804dd1b 100644
--- a/ui/app/src/components/Scope/index.tsx
+++ b/ui/app/src/components/Scope/index.tsx
@@ -40,17 +40,9 @@ interface ScopeProps {
scope?: string
modeScope?: string
podsPreviewTitle?: string | JSX.Element
- podsPreviewDesc?: string | JSX.Element
}
-const Scope: React.FC = ({
- kind,
- namespaces,
- scope = 'selector',
- modeScope = '',
- podsPreviewTitle,
- podsPreviewDesc,
-}) => {
+const Scope: React.FC = ({ kind, namespaces, scope = 'selector', modeScope = '', podsPreviewTitle }) => {
const { values, setFieldValue, errors, touched } = useFormikContext()
const {
namespaces: currentNamespaces,
@@ -110,6 +102,7 @@ const Scope: React.FC = ({
return (
}
@@ -126,6 +119,7 @@ const Scope: React.FC = ({
/>
}
@@ -136,6 +130,7 @@ const Scope: React.FC = ({
}
@@ -167,7 +162,7 @@ const Scope: React.FC = ({
{podsPreviewTitle || }
- {podsPreviewDesc || }
+
{pods.length > 0 ? (
diff --git a/ui/app/src/components/YAML/index.tsx b/ui/app/src/components/YAML/index.tsx
index f22f1a79925..dabfa426605 100644
--- a/ui/app/src/components/YAML/index.tsx
+++ b/ui/app/src/components/YAML/index.tsx
@@ -14,25 +14,29 @@
* limitations under the License.
*
*/
-import { Button, ButtonProps } from '@mui/material'
+import FileOpenIcon from '@mui/icons-material/FileOpen'
+import LoadingButton, { LoadingButtonProps } from '@mui/lab/LoadingButton'
+import { useState } from 'react'
-import CloudUploadOutlinedIcon from '@mui/icons-material/CloudUploadOutlined'
-import i18n from 'components/T'
-import { setAlert } from 'slices/globalStatus'
-import { useIntl } from 'react-intl'
import { useStoreDispatch } from 'store'
+import { setAlert } from 'slices/globalStatus'
+
+import { T } from 'components/T'
+
interface YAMLProps {
callback: (y: any) => void
- buttonProps?: ButtonProps<'label'>
+ ButtonProps?: LoadingButtonProps<'label'>
}
-const YAML: React.FC