diff --git a/CHANGELOG.md b/CHANGELOG.md index ee95eda2f2f..0a29821c6e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ For more information and how-to, see [RFC: Keep A Changelog](https://github.com/ - Add helm annotations for Artifact Hub [#3355](https://github.com/chaos-mesh/chaos-mesh/pull/3355) - Add implementation of blockchaos in chaos-daemon [#2907](https://github.com/chaos-mesh/chaos-mesh/pull/2907) - Bump chaos-tproxy to v0.5.1 [#3412](https://github.com/chaos-mesh/chaos-mesh/pull/3412) +- Allow importing external workflows and copying flow nodes in next generation `New Workflow` [#3368](https://github.com/chaos-mesh/chaos-mesh/pull/3368) ### Changed diff --git a/ui/.gitignore b/ui/.gitignore index 554dfe693c5..1cd33076608 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -26,3 +26,7 @@ package-lock.json # eslint .eslintcache + +# yarn +.yarn +.yarnrc diff --git a/ui/app/package.json b/ui/app/package.json index 2000d684ecc..78cc1baa67c 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -32,12 +32,13 @@ "js-yaml": "^4.0.0", "lodash": "^4.17.21", "luxon": "^1.25.0", + "re-resizable": "^6.9.9", "react": "^17.0.1", "react-ace": "^9.2.1", "react-dnd": "^15.1.1", "react-dnd-html5-backend": "^15.1.2", "react-dom": "^17.0.1", - "react-flow-renderer": "^10.1.2", + "react-flow-renderer": "^10.3.0", "react-helmet": "^6.1.0", "react-intl": "^5.22.0", "react-redux": "^7.2.2", diff --git a/ui/app/src/components/AutoForm/Info.tsx b/ui/app/src/components/AutoForm/Info.tsx index 999f0e13bf6..ae57f90085a 100644 --- a/ui/app/src/components/AutoForm/Info.tsx +++ b/ui/app/src/components/AutoForm/Info.tsx @@ -81,7 +81,7 @@ export default function Info({ belong, kind, action }: InfoProps) { name="name" label={} helperText={ - getIn(errors, 'name') && getIn(touched, 'name') ? getIn(errors, 'name') : + getIn(errors, 'name') && getIn(touched, 'name') ? getIn(errors, 'name') : } error={getIn(errors, 'name') && getIn(touched, 'name')} /> diff --git a/ui/app/src/components/AutoForm/data.ts b/ui/app/src/components/AutoForm/data.ts index a598d68dc31..9be30218103 100644 --- a/ui/app/src/components/AutoForm/data.ts +++ b/ui/app/src/components/AutoForm/data.ts @@ -30,7 +30,14 @@ export const scopeInitialValues = { value: undefined, } -export const scheduleInitialValues = { +export interface Schedule { + schedule: string + historyLimit?: number + concurrencyPolicy?: 'Forbid' | 'Allow' + startingDeadlineSeconds?: number +} + +export const scheduleInitialValues: Schedule = { schedule: '', historyLimit: 1, concurrencyPolicy: 'Forbid', diff --git a/ui/app/src/components/AutoForm/index.tsx b/ui/app/src/components/AutoForm/index.tsx index 4e8ec8390bb..d7f34b51e97 100644 --- a/ui/app/src/components/AutoForm/index.tsx +++ b/ui/app/src/components/AutoForm/index.tsx @@ -18,6 +18,7 @@ import { Box, Divider, FormHelperText, MenuItem, Typography } from '@mui/materia import { eval as expEval, parse } from 'expression-eval' import { Form, Formik, getIn } from 'formik' import type { FormikConfig, FormikErrors, FormikTouched, FormikValues } from 'formik' +import _ from 'lodash' import { useEffect, useState } from 'react' import Checkbox from '@ui/mui-extends/esm/Checkbox' @@ -26,6 +27,7 @@ import Space from '@ui/mui-extends/esm/Space' import { useStoreSelector } from 'store' import { AutocompleteField, SelectField, Submit, TextField, TextTextField } from 'components/FormField' +import { SpecialTemplateType } from 'components/NewWorkflowNext/utils/convert' import Scope from 'components/Scope' import { T } from 'components/T' @@ -62,12 +64,17 @@ export interface AtomFormData { } const AutoForm: React.FC = ({ belong = Belong.Experiment, id, kind, act: action, formikProps }) => { + const kindAction = concatKindAction(kind, action) const [initialValues, setInitialValues] = useState>({ id, kind, action, ...(kind === 'NetworkChaos' && { target: scopeInitialValues }), - ...(kind !== 'PhysicalMachineChaos' && kind !== 'Suspend' && scopeInitialValues), + ...(kind !== 'PhysicalMachineChaos' && + kind !== SpecialTemplateType.Suspend && + kind !== SpecialTemplateType.Serial && + kind !== SpecialTemplateType.Parallel && + scopeInitialValues), ...(belong === Belong.Workflow && { ...workflowNodeInfoInitialValues, templateType: kind }), }) const [form, setForm] = useState([]) @@ -89,11 +96,12 @@ const AutoForm: React.FC = ({ belong = Belong.Experiment, id, kin } async function loadData() { - if (kind === 'Suspend') { - setInitialValues((oldValues) => ({ - ...oldValues, - ...formikProps.initialValues, - })) + if ( + kind === SpecialTemplateType.Suspend || + kind === SpecialTemplateType.Serial || + kind === SpecialTemplateType.Parallel + ) { + setInitialValues((oldValues) => _.merge({}, oldValues, formikProps.initialValues)) return } @@ -116,10 +124,7 @@ const AutoForm: React.FC = ({ belong = Belong.Experiment, id, kin }) : data - setInitialValues((oldValues) => ({ - ...oldValues, - ...(formikProps.initialValues || formToRecords(form)), - })) + setInitialValues((oldValues) => _.merge({}, oldValues, formikProps.initialValues || formToRecords(form))) setForm(form) } @@ -246,7 +251,7 @@ const AutoForm: React.FC = ({ belong = Belong.Experiment, id, kin
- {concatKindAction(kind, action)} + {kindAction} {action && ( = ({ belong = Belong.Experiment, id, kin scope="target.selector" modeScope="target" podsPreviewTitle={} - podsPreviewDesc={} /> )} - {kind !== 'Suspend' && ( - <> - - - - - {kind !== 'PhysicalMachineChaos' && } - - + {kind !== SpecialTemplateType.Suspend && + kind !== SpecialTemplateType.Serial && + kind !== SpecialTemplateType.Parallel && ( + <> + - Schedule + - - - {scheduled && } - - - )} + {kind !== 'PhysicalMachineChaos' && } + + + + Schedule + + + + {scheduled && } + + + )} Info diff --git a/ui/app/src/components/NewExperimentNext/form/TargetGenerated.tsx b/ui/app/src/components/NewExperimentNext/form/TargetGenerated.tsx index d0d79d6db2f..7904d3d310a 100644 --- a/ui/app/src/components/NewExperimentNext/form/TargetGenerated.tsx +++ b/ui/app/src/components/NewExperimentNext/form/TargetGenerated.tsx @@ -220,7 +220,6 @@ const TargetGenerated: React.FC = ({ env, kind, data, vali scope="target.selector" modeScope="target" podsPreviewTitle={i18n('newE.target.network.target.podsPreview')} - podsPreviewDesc={i18n('newE.target.network.target.podsPreviewHelper')} /> )} diff --git a/ui/app/src/components/NewWorkflowNext/AdjustableEdge.tsx b/ui/app/src/components/NewWorkflowNext/AdjustableEdge.tsx index 6dd8ad28410..3aa3c6f0399 100644 --- a/ui/app/src/components/NewWorkflowNext/AdjustableEdge.tsx +++ b/ui/app/src/components/NewWorkflowNext/AdjustableEdge.tsx @@ -17,7 +17,7 @@ import { getBezierPath, getEdgeCenter } from 'react-flow-renderer' import type { EdgeProps } from 'react-flow-renderer' -import CustomTooltip from './CustomTooltip' +import FlowTooltip from './FlowTooltip' const foreignObjectSize = 24 @@ -56,9 +56,9 @@ export default function SuspendEdge({ x={edgeCenterX - foreignObjectSize / 2} y={edgeCenterY - foreignObjectSize / 2} > - +
- + ) diff --git a/ui/app/src/components/NewWorkflowNext/BareNode.tsx b/ui/app/src/components/NewWorkflowNext/BareNode.tsx index 3d7cfd3d188..a4c2c90a87a 100644 --- a/ui/app/src/components/NewWorkflowNext/BareNode.tsx +++ b/ui/app/src/components/NewWorkflowNext/BareNode.tsx @@ -14,27 +14,28 @@ * limitations under the License. * */ -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined' -import { Button } from '@mui/material' +import { Box, Button } from '@mui/material' import type { ButtonProps } from '@mui/material' import { forwardRef } from 'react' import { iconByKind } from 'lib/byKind' export type BareNodeProps = ButtonProps & { - kind?: string + kind: string } -export default forwardRef(({ kind, sx, ...rest }: BareNodeProps, ref) => ( +export default forwardRef(({ kind, sx, children, name, ...rest }, ref) => ( - )} + + 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 = ({ callback, buttonProps }) => { - const intl = useIntl() +const YAML: React.FC = ({ children, callback, ButtonProps }) => { + const [loading, setLoading] = useState(false) const dispatch = useStoreDispatch() const handleUploadYAML = (e: React.ChangeEvent) => { + setLoading(true) + const f = e.target.files![0] const reader = new FileReader() @@ -44,18 +48,27 @@ const YAML: React.FC = ({ callback, buttonProps }) => { dispatch( setAlert({ type: 'success', - message: i18n('confirm.success.load', intl), + message: , }) ) + + setLoading(false) } reader.readAsText(f) } return ( - + ) } diff --git a/ui/app/src/i18n/en.json b/ui/app/src/i18n/en.json index 5d5a7542738..e95ad695bab 100644 --- a/ui/app/src/i18n/en.json +++ b/ui/app/src/i18n/en.json @@ -46,7 +46,7 @@ "byYAML": "By YAML", "byYAMLDesc": "Create an experiment or schedule by filling in or uploading YAML", "basic": { - "nameHelper": "The experiment name", + "nameHelper": "Fill the experiment name.", "namespaceHelper": "Select the namespace" }, "scope": { @@ -94,8 +94,7 @@ "title": "Network Attack", "target": { "title": "Target", - "podsPreview": "Target Pods Preview", - "podsPreviewHelper": "Checking or unchecking Pods to further limit the scope of the target" + "podsPreview": "Target Pods Preview" } }, "http": { @@ -145,11 +144,11 @@ "http": "HTTP Request", "number": "Number", "child": "Child task", - "nameHelper": "The task name", - "nameValidation": "The task name is required", + "nameHelper": "Fill the node name.", + "nameValidation": "The node name is required.", "deadline": "Deadline", - "deadlineHelper": "The task will end within the deadline. Supported formats of the deadline are: ms / s / m / h.", - "deadlineValidation": "The task deadline is required", + "deadlineHelper": "The node will end within the deadline. Supported formats of the deadline are: ms / s / m / h.", + "deadlineValidation": "The node deadline is required.", "container": { "title": "Container", "nameHelper": "The container name", diff --git a/ui/app/src/i18n/zh.json b/ui/app/src/i18n/zh.json index 1416ba80737..9c257cbea20 100644 --- a/ui/app/src/i18n/zh.json +++ b/ui/app/src/i18n/zh.json @@ -91,8 +91,7 @@ "title": "网络攻击", "target": { "title": "目标", - "podsPreview": "预览目标 Pod", - "podsPreviewHelper": "通过勾选 Pod 来进一步限制目标的范围" + "podsPreview": "预览目标 Pod" } }, "http": { diff --git a/ui/app/src/lib/byKind.tsx b/ui/app/src/lib/byKind.tsx index f833742e36a..22082e35688 100644 --- a/ui/app/src/lib/byKind.tsx +++ b/ui/app/src/lib/byKind.tsx @@ -14,6 +14,7 @@ * limitations under the License. * */ +import LinearScaleIcon from '@mui/icons-material/LinearScale' import TimelapseIcon from '@mui/icons-material/Timelapse' import { SvgIcon } from '@mui/material' @@ -36,7 +37,7 @@ import { ReactComponent as ClockIcon } from 'images/chaos/time.svg' import { ReactComponent as K8SIcon } from 'images/k8s.svg' import { ReactComponent as PhysicIcon } from 'images/physic.svg' -export function iconByKind(kind: string, size: 'small' | 'medium' | 'large' = 'medium') { +export function iconByKind(kind: string, size: 'small' | 'inherit' | 'medium' | 'large' = 'medium') { let icon switch (kind) { @@ -91,9 +92,12 @@ export function iconByKind(kind: string, size: 'small' | 'medium' | 'large' = 'm case 'Schedule': icon = break + case 'Serial': + return + case 'Parallel': + return case 'Suspend': - icon = - break + return } return {icon} diff --git a/ui/app/src/lib/utils.ts b/ui/app/src/lib/utils.ts index e171d1d2680..10182a7ca04 100644 --- a/ui/app/src/lib/utils.ts +++ b/ui/app/src/lib/utils.ts @@ -19,23 +19,20 @@ import _ from 'lodash' export function objToArrBySep(obj: Record, separator: string, filters?: string[]) { return Object.entries(obj) .filter((d) => !filters?.includes(d[0])) - .reduce( - (acc: string[], [key, val]) => - acc.concat(Array.isArray(val) ? val.map((d) => `${key}${separator}${d}`) : `${key}${separator}${val}`), + .reduce( + (acc, [k, v]) => acc.concat(Array.isArray(v) ? v.map((d) => `${k}${separator}${d}`) : `${k}${separator}${v}`), [] ) } export function arrToObjBySep(arr: string[], sep: string): Record { - const result: any = {} + return arr.reduce>((acc, d) => { + const [k, v] = d.split(sep) - arr.forEach((d) => { - const split = d.split(sep) + acc[k] = v - result[split[0]] = split[1] - }) - - return result + return acc + }, {}) } /** diff --git a/ui/app/src/pages/Dashboard/Predefined.tsx b/ui/app/src/pages/Dashboard/Predefined.tsx index 3e7a608fdb8..0f1d7b9f328 100644 --- a/ui/app/src/pages/Dashboard/Predefined.tsx +++ b/ui/app/src/pages/Dashboard/Predefined.tsx @@ -14,26 +14,30 @@ * limitations under the License. * */ +import loadable from '@loadable/component' import { Box, Button, Card, Modal, Typography } from '@mui/material' -import { PreDefinedValue, getDB } from 'lib/idb' -import { setAlert, setConfirm } from 'slices/globalStatus' +import { makeStyles } from '@mui/styles' +import { Ace } from 'ace-builds' +import api from 'api' +import clsx from 'clsx' +import yaml from 'js-yaml' import { useEffect, useRef, useState } from 'react' +import { useIntl } from 'react-intl' -import { Ace } from 'ace-builds' import Paper from '@ui/mui-extends/esm/Paper' import PaperTop from '@ui/mui-extends/esm/PaperTop' import Space from '@ui/mui-extends/esm/Space' -import YAML from 'components/YAML' -import api from 'api' -import clsx from 'clsx' + +import { useStoreDispatch } from 'store' + +import { setAlert, setConfirm } from 'slices/globalStatus' + import i18n from 'components/T' +import YAML from 'components/YAML' + import { iconByKind } from 'lib/byKind' -import loadable from '@loadable/component' -import { makeStyles } from '@mui/styles' -import { useIntl } from 'react-intl' -import { useStoreDispatch } from 'store' -import yaml from 'js-yaml' +import { PreDefinedValue, getDB } from 'lib/idb' const YAMLEditor = loadable(() => import('components/YAMLEditor')) @@ -155,7 +159,7 @@ const Predefined = () => { {experiments.map((d) => ( diff --git a/ui/app/src/slices/workflows.ts b/ui/app/src/slices/workflows.ts index e2325fe681b..bf387975fb3 100644 --- a/ui/app/src/slices/workflows.ts +++ b/ui/app/src/slices/workflows.ts @@ -79,23 +79,21 @@ const workflowSlice = createSlice({ name: 'workflows', initialState, reducers: { - insertWorkflowNode(state, action: PayloadAction) { - const { id, experiment } = action.payload - - state.nodes[id] = experiment + importNodes(state, action: PayloadAction>) { + state.nodes = action.payload }, updateWorkflowNode(state, action) { const payload = action.payload - state.nodes[payload.id] = payload + state.nodes[payload.name] = payload }, removeWorkflowNode(state, action: PayloadAction) { delete state.nodes[action.payload] }, - LoadRecentlyUsedExperiments(state) { + loadRecentlyUsedExperiments(state) { state.recentUse = LS.getObj('new-workflow-recently-used-experiments') }, - SetRecentlyUsedExperiments(state, action: PayloadAction) { + setRecentlyUsedExperiments(state, action: PayloadAction) { const exp = action.payload state.recentUse = [...state.recentUse, exp] @@ -128,15 +126,15 @@ const workflowSlice = createSlice({ }) export const { - insertWorkflowNode, + importNodes, updateWorkflowNode, removeWorkflowNode, - LoadRecentlyUsedExperiments, - SetRecentlyUsedExperiments, + loadRecentlyUsedExperiments, + setRecentlyUsedExperiments, + resetWorkflow, setTemplate, updateTemplate, deleteTemplate, - resetWorkflow, } = workflowSlice.actions export default workflowSlice.reducer diff --git a/ui/packages/mui-extends/src/Menu/index.tsx b/ui/packages/mui-extends/src/Menu/index.tsx index 7666f9a2dbc..200e89f7b17 100644 --- a/ui/packages/mui-extends/src/Menu/index.tsx +++ b/ui/packages/mui-extends/src/Menu/index.tsx @@ -14,32 +14,49 @@ * limitations under the License. * */ - -import { IconButton, Menu as MUIMenu, MenuProps } from '@mui/material' - import MoreVertIcon from '@mui/icons-material/MoreVert' +import { IconButton, IconButtonProps, MenuProps, Menu as MuiMenu, SvgIconProps, styled } from '@mui/material' import { useState } from 'react' -const Menu: React.FC> = ({ title, children, ...rest }) => { +const StyledMenu = styled((props: MenuProps) => )(({ theme }) => ({ + '& .MuiPaper-root': { + border: `1px solid ${theme.palette.divider}`, + }, + '& .MuiMenu-list': { + padding: '4px 0', + }, + '& .MuiMenuItem-root': { + padding: '3px 8px', + }, +})) + +const Menu: React.FC< + Omit & { IconButtonProps?: IconButtonProps; IconProps?: SvgIconProps } +> = ({ IconButtonProps, IconProps, children, ...rest }) => { const [anchorEl, setAnchorEl] = useState(null) - const onClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget) + const onClick = (e: React.SyntheticEvent) => { + e.stopPropagation() + + setAnchorEl(e.currentTarget) } - const onClose = () => { + const onClose = (e: React.SyntheticEvent) => { + e && e.stopPropagation() // Allow no event. + setAnchorEl(null) } return ( -
- - + <> + + - - {children} - -
+ + {/* If `children` is a function, the return type must be an array of React elements. */} + {typeof children === 'function' ? children({ onClose }) : children} + + ) } diff --git a/ui/yarn.lock b/ui/yarn.lock index 952332b446e..b9596868e3d 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1144,10 +1144,10 @@ core-js-pure "^3.19.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.7.7", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" - integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.7.7", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.0.tgz#6d77142a19cb6088f0af662af1ada37a604d34ae" + integrity sha512-YMQvx/6nKEaucl0MY56mwIG483xk8SDNdlUwb2Ts6FUpr7fm85DxEmsY18LXBNhcTz6tO6JwZV8w1W06v8UKeg== dependencies: regenerator-runtime "^0.13.4" @@ -6760,9 +6760,9 @@ d3-chord@2: integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== "d3-color@1 - 3": - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" - integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== d3-contour@2: version "2.0.0" @@ -6796,7 +6796,7 @@ d3-drag@2: d3-dispatch "1 - 2" d3-selection "2" -"d3-drag@2 - 3": +"d3-drag@2 - 3", d3-drag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== @@ -13308,6 +13308,11 @@ raw-loader@^4.0.2: loader-utils "^2.0.0" schema-utils "^3.0.0" +re-resizable@^6.9.9: + version "6.9.9" + resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216" + integrity sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA== + react-ace@^9.2.1: version "9.5.0" resolved "https://registry.npmjs.org/react-ace/-/react-ace-9.5.0.tgz" @@ -13414,7 +13419,7 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" -react-draggable@^4.4.3, react-draggable@^4.4.4: +react-draggable@^4.4.3: version "4.4.4" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f" integrity sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA== @@ -13446,17 +13451,17 @@ react-fast-compare@^3.0.1, react-fast-compare@^3.1.1, react-fast-compare@^3.2.0: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== -react-flow-renderer@^10.1.2: - version "10.1.2" - resolved "https://registry.yarnpkg.com/react-flow-renderer/-/react-flow-renderer-10.1.2.tgz#633dabeb7f42308726056b45b93c9153964d58b9" - integrity sha512-gWDYmXSz0pT3fbUoDI4L49GSzKJNBhp0z1h1hN4L9YbzqBITowCQodxKHHkIQHW9V103xdd+SSZOmRvrA9uybg== +react-flow-renderer@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/react-flow-renderer/-/react-flow-renderer-10.3.0.tgz#4ce415c7039ace4f92ba1f0ec281f4c6049724ff" + integrity sha512-LdjFpiyG6Dbf7rsCLzYvY6/XV9IpfIy9sM3RC7BpMdcUmMPOsQ4JRXKHgNz+NepYDVxUmiBlkHUPYUax7CKqSA== dependencies: - "@babel/runtime" "^7.17.8" + "@babel/runtime" "^7.18.0" classcat "^5.0.3" + d3-drag "^3.0.0" d3-selection "^3.0.0" d3-zoom "^3.0.0" - react-draggable "^4.4.4" - zustand "^3.7.1" + zustand "^3.7.2" react-helmet-async@^1.0.7: version "1.2.3" @@ -16676,10 +16681,10 @@ yup@^0.29.1: synchronous-promise "^2.0.13" toposort "^2.0.2" -zustand@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.1.tgz#7388f0a7175a6c2fd9a2880b383a4bf6cdf6b7c6" - integrity sha512-wHBCZlKj+bg03/hP+Tzv24YhnqqP8MCeN9ECPDXoF01062SIbnfl3j9O0znkDw1lNTY0a8WN3F///a0UhhaEqg== +zustand@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" + integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA== zwitch@^1.0.0: version "1.0.5"