diff --git a/package.json b/package.json index 8a6afd474c..5ad624d674 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "memory-fs": "^0.4.1", "morgan": "^1.8.2", "react-chartjs-2": "^4.0.0", - "react-flow-renderer": "^9.6.3", + "react-flow-renderer": "10.0.0-next.30", "react-ga4": "^1.4.1", "react-helmet": "^5.1.3", "react-responsive": "^4.1.0", diff --git a/src/client.tsx b/src/client.tsx index 6488252476..c35fc9e90f 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -6,29 +6,29 @@ import * as ReactDOM from 'react-dom'; import ReactGA from 'react-ga4'; const render = (Component: React.FC) => { - ReactDOM.render(, document.getElementById('react-app')); + ReactDOM.render(, document.getElementById('react-app')); }; const initializeApp = () => { - const App = require('./components/App/App').App; + const App = require('./components/App/App').App; - const { ENABLE_GA, GA_TRACKING_ID } = env; + const { ENABLE_GA, GA_TRACKING_ID } = env; - if (ENABLE_GA != 'false') { - ReactGA.initialize(GA_TRACKING_ID as string); - } + if (ENABLE_GA != 'false') { + ReactGA.initialize(GA_TRACKING_ID as string); + } - if (env.NODE_ENV === 'development') { - // We use style-loader in dev mode, but it causes a FOUC and some initial styling issues - // so we'll give it time to add the styles before initial render. - setTimeout(() => render(App), 500); - } else { - render(App); - } + if (env.NODE_ENV === 'development') { + // We use style-loader in dev mode, but it causes a FOUC and some initial styling issues + // so we'll give it time to add the styles before initial render. + setTimeout(() => render(App), 500); + } else { + render(App); + } }; if (document.body) { - initializeApp(); + initializeApp(); } else { - window.addEventListener('DOMContentLoaded', initializeApp, false); + window.addEventListener('DOMContentLoaded', initializeApp, false); } diff --git a/src/components/Entities/EntityDetails.tsx b/src/components/Entities/EntityDetails.tsx index ec16e8a99b..df1c1f7a60 100644 --- a/src/components/Entities/EntityDetails.tsx +++ b/src/components/Entities/EntityDetails.tsx @@ -14,35 +14,35 @@ import { EntityVersions } from './EntityVersions'; import { EntityExecutionsBarChart } from './EntityExecutionsBarChart'; const useStyles = makeStyles((theme: Theme) => ({ - metadataContainer: { - display: 'flex', - marginBottom: theme.spacing(5), - marginTop: theme.spacing(2), - width: '100%' - }, - descriptionContainer: { - flex: '2 1 auto', - marginRight: theme.spacing(2) - }, - executionsContainer: { - display: 'flex', - flex: '1 1 auto', - flexDirection: 'column', - margin: `0 -${theme.spacing(contentMarginGridUnits)}px`, - flexBasis: theme.spacing(80) - }, - versionsContainer: { - display: 'flex', - flexDirection: 'column' - }, - schedulesContainer: { - flex: '1 2 auto', - marginRight: theme.spacing(30) - } + metadataContainer: { + display: 'flex', + marginBottom: theme.spacing(5), + marginTop: theme.spacing(2), + width: '100%' + }, + descriptionContainer: { + flex: '2 1 auto', + marginRight: theme.spacing(2) + }, + executionsContainer: { + display: 'flex', + flex: '1 1 auto', + flexDirection: 'column', + margin: `0 -${theme.spacing(contentMarginGridUnits)}px`, + flexBasis: theme.spacing(80) + }, + versionsContainer: { + display: 'flex', + flexDirection: 'column' + }, + schedulesContainer: { + flex: '1 2 auto', + marginRight: theme.spacing(30) + } })); interface EntityDetailsProps { - id: ResourceIdentifier; + id: ResourceIdentifier; } /** @@ -52,53 +52,41 @@ interface EntityDetailsProps { * @param id */ export const EntityDetails: React.FC = ({ id }) => { - const sections = entitySections[id.resourceType]; - const project = useProject(id.project); - const styles = useStyles(); - const { chartIds, onToggle, clearCharts } = useChartState(); + const sections = entitySections[id.resourceType]; + const project = useProject(id.project); + const styles = useStyles(); + const { chartIds, onToggle, clearCharts } = useChartState(); - return ( - - + return ( + + -
- {sections.description ? ( -
- -
- ) : null} - {sections.schedules ? ( -
- -
- ) : null} -
+
+ {sections.description ? ( +
+ +
+ ) : null} + {sections.schedules ? ( +
+ +
+ ) : null} +
- {sections.versions ? ( -
- -
- ) : null} + {sections.versions ? ( +
+ +
+ ) : null} - + - {sections.executions ? ( -
- -
- ) : null} -
- ); + {sections.executions ? ( +
+ +
+ ) : null} +
+ ); }; diff --git a/src/components/Entities/EntityVersions.tsx b/src/components/Entities/EntityVersions.tsx index 1d69aaa2db..e66c10acac 100644 --- a/src/components/Entities/EntityVersions.tsx +++ b/src/components/Entities/EntityVersions.tsx @@ -19,30 +19,30 @@ import { WorkflowVersionsTablePageSize } from './constants'; import t from './strings'; const useStyles = makeStyles((theme: Theme) => ({ - headerContainer: { - display: 'flex' - }, - collapseButton: { - marginTop: theme.spacing(-0.5) - }, - header: { - flexGrow: 1, - marginBottom: theme.spacing(1), - marginRight: theme.spacing(1) - }, - viewAll: { - color: interactiveTextColor, - cursor: 'pointer' - }, - divider: { - borderBottom: `1px solid ${theme.palette.divider}`, - marginBottom: theme.spacing(1) - } + headerContainer: { + display: 'flex' + }, + collapseButton: { + marginTop: theme.spacing(-0.5) + }, + header: { + flexGrow: 1, + marginBottom: theme.spacing(1), + marginRight: theme.spacing(1) + }, + viewAll: { + color: interactiveTextColor, + cursor: 'pointer' + }, + divider: { + borderBottom: `1px solid ${theme.palette.divider}`, + marginBottom: theme.spacing(1) + } })); export interface EntityVersionsProps { - id: ResourceIdentifier; - showAll?: boolean; + id: ResourceIdentifier; + showAll?: boolean; } /** @@ -50,86 +50,63 @@ export interface EntityVersionsProps { * @param id * @param showAll - shows all available entity versions */ -export const EntityVersions: React.FC = ({ - id, - showAll = false -}) => { - const { domain, project, resourceType, name } = id; - const [showTable, setShowTable] = useLocalCache( - LocalCacheItem.ShowWorkflowVersions - ); - const styles = useStyles(); - const sort = { - key: executionSortFields.createdAt, - direction: SortDirection.DESCENDING - }; +export const EntityVersions: React.FC = ({ id, showAll = false }) => { + const { domain, project, resourceType, name } = id; + const [showTable, setShowTable] = useLocalCache(LocalCacheItem.ShowWorkflowVersions); + const styles = useStyles(); + const sort = { + key: executionSortFields.createdAt, + direction: SortDirection.DESCENDING + }; - const baseFilters = React.useMemo( - () => executionFilterGenerator[resourceType](id), - [id, resourceType] - ); + const baseFilters = React.useMemo(() => executionFilterGenerator[resourceType](id), [id, resourceType]); - const versions = useWorkflowVersions( - { domain, project }, - { - sort, - filter: baseFilters, - limit: showAll ? 100 : WorkflowVersionsTablePageSize - } - ); + const versions = useWorkflowVersions( + { domain, project }, + { + sort, + filter: baseFilters, + limit: showAll ? 100 : WorkflowVersionsTablePageSize + } + ); - const preventDefault = e => e.preventDefault(); - const handleViewAll = React.useCallback(() => { - history.push( - Routes.WorkflowVersionDetails.makeUrl( - project, - domain, - name, - versions.value[0].id.version ?? '' - ) - ); - }, [project, domain, name, versions]); + const preventDefault = e => e.preventDefault(); + const handleViewAll = React.useCallback(() => { + history.push(Routes.WorkflowVersionDetails.makeUrl(project, domain, name, versions.value[0].id.version ?? '')); + }, [project, domain, name, versions]); - return ( - <> - {!showAll && ( -
- setShowTable(!showTable)} - onMouseDown={preventDefault} - size="small" - aria-label="" - title={t('collapseButton', showTable)} - > - {showTable ? : } - - - {t('workflowVersionsTitle')} - - - {t('viewAll')} - -
- )} - - {showTable || showAll ? ( - - ) : ( -
- )} - - - ); + return ( + <> + {!showAll && ( +
+ setShowTable(!showTable)} + onMouseDown={preventDefault} + size="small" + aria-label="" + title={t('collapseButton', showTable)} + > + {showTable ? : } + + + {t('workflowVersionsTitle')} + + + {t('viewAll')} + +
+ )} + + {showTable || showAll ? ( + + ) : ( +
+ )} + + + ); }; diff --git a/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 285fe4a1e6..43438b050a 100644 --- a/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -6,7 +6,7 @@ import ChartDataLabels from 'chartjs-plugin-datalabels'; import { makeStyles, Typography } from '@material-ui/core'; import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { transformerWorkflowToPlainNodes } from 'components/WorkflowGraph/transformerWorkflowToDag'; +import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { isEndNode, isStartNode, isExpanded } from 'components/WorkflowGraph/utils'; import { tableHeaderColor } from 'components/Theme/constants'; import { NodeExecution } from 'models/Execution/types'; @@ -89,9 +89,7 @@ export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTime const { compiledWorkflowClosure } = useNodeExecutionContext(); React.useEffect(() => { - const nodes: dNode[] = compiledWorkflowClosure - ? transformerWorkflowToPlainNodes(compiledWorkflowClosure).nodes - : []; + const nodes: dNode[] = compiledWorkflowClosure ? transformerWorkflowToDag(compiledWorkflowClosure).dag.nodes : []; // we remove start/end node info in the root dNode list during first assignment const initializeNodes = convertToPlainNodes(nodes); setOriginalNodes(initializeNodes); diff --git a/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx b/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx index 71aa5dca1e..3733f48eac 100644 --- a/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx +++ b/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx @@ -1,8 +1,5 @@ import { render, waitFor } from '@testing-library/react'; -import { - cacheStatusMessages, - viewSourceExecutionString -} from 'components/Executions/constants'; +import { cacheStatusMessages, viewSourceExecutionString } from 'components/Executions/constants'; import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { Core } from 'flyteidl'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; @@ -23,87 +20,73 @@ jest.mock('components/Workflow/workflowQueries'); const { fetchWorkflow } = require('components/Workflow/workflowQueries'); describe('NodeExecutionDetails', () => { - let fixture: ReturnType; - let execution: NodeExecution; - let queryClient: QueryClient; + let fixture: ReturnType; + let execution: NodeExecution; + let queryClient: QueryClient; - beforeEach(() => { - fixture = basicPythonWorkflow.generate(); - execution = - fixture.workflowExecutions.top.nodeExecutions.pythonNode.data; - insertFixture(mockServer, fixture); - fetchWorkflow.mockImplementation(() => - Promise.resolve(fixture.workflows.top) - ); - queryClient = createTestQueryClient(); - }); + beforeEach(() => { + fixture = basicPythonWorkflow.generate(); + execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data; + insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => Promise.resolve(fixture.workflows.top)); + queryClient = createTestQueryClient(); + }); - const renderComponent = () => - render( - - - - - - - - ); + const renderComponent = () => + render( + + + + + + + + ); - it('renders name for task nodes', async () => { - const { name } = fixture.tasks.python.id; - const { getByText } = renderComponent(); - await waitFor(() => expect(getByText(name))); - }); + it('renders name for task nodes', async () => { + const { name } = fixture.tasks.python.id; + const { getByText } = renderComponent(); + await waitFor(() => expect(getByText(name))); + }); - describe('with cache information', () => { - let taskNodeMetadata: TaskNodeMetadata; - beforeEach(() => { - taskNodeMetadata = { - cacheStatus: Core.CatalogCacheStatus.CACHE_MISS, - catalogKey: { - datasetId: makeIdentifier({ - resourceType: Core.ResourceType.DATASET - }), - sourceTaskExecution: { ...mockTaskExecution.id } - } - }; - execution.closure.taskNodeMetadata = taskNodeMetadata; - mockServer.insertNodeExecution(execution); - }); + describe('with cache information', () => { + let taskNodeMetadata: TaskNodeMetadata; + beforeEach(() => { + taskNodeMetadata = { + cacheStatus: Core.CatalogCacheStatus.CACHE_MISS, + catalogKey: { + datasetId: makeIdentifier({ + resourceType: Core.ResourceType.DATASET + }), + sourceTaskExecution: { ...mockTaskExecution.id } + } + }; + execution.closure.taskNodeMetadata = taskNodeMetadata; + mockServer.insertNodeExecution(execution); + }); - [ - Core.CatalogCacheStatus.CACHE_DISABLED, - Core.CatalogCacheStatus.CACHE_HIT, - Core.CatalogCacheStatus.CACHE_LOOKUP_FAILURE, - Core.CatalogCacheStatus.CACHE_MISS, - Core.CatalogCacheStatus.CACHE_POPULATED, - Core.CatalogCacheStatus.CACHE_PUT_FAILURE - ].forEach(cacheStatusValue => - it(`renders correct status for ${Core.CatalogCacheStatus[cacheStatusValue]}`, async () => { - taskNodeMetadata.cacheStatus = cacheStatusValue; - mockServer.insertNodeExecution(execution); - const { getByText } = renderComponent(); - await waitFor(() => - expect(getByText(cacheStatusMessages[cacheStatusValue])) - ); - }) - ); + [ + Core.CatalogCacheStatus.CACHE_DISABLED, + Core.CatalogCacheStatus.CACHE_HIT, + Core.CatalogCacheStatus.CACHE_LOOKUP_FAILURE, + Core.CatalogCacheStatus.CACHE_MISS, + Core.CatalogCacheStatus.CACHE_POPULATED, + Core.CatalogCacheStatus.CACHE_PUT_FAILURE + ].forEach(cacheStatusValue => + it(`renders correct status for ${Core.CatalogCacheStatus[cacheStatusValue]}`, async () => { + taskNodeMetadata.cacheStatus = cacheStatusValue; + mockServer.insertNodeExecution(execution); + const { getByText } = renderComponent(); + await waitFor(() => expect(getByText(cacheStatusMessages[cacheStatusValue]))); + }) + ); - it('renders source execution link for cache hits', async () => { - taskNodeMetadata.cacheStatus = Core.CatalogCacheStatus.CACHE_HIT; - const sourceWorkflowExecutionId = taskNodeMetadata.catalogKey! - .sourceTaskExecution.nodeExecutionId.executionId; - const { getByText } = renderComponent(); - const linkEl = await waitFor(() => - getByText(viewSourceExecutionString) - ); - expect(linkEl.getAttribute('href')).toBe( - Routes.ExecutionDetails.makeUrl(sourceWorkflowExecutionId) - ); - }); + it('renders source execution link for cache hits', async () => { + taskNodeMetadata.cacheStatus = Core.CatalogCacheStatus.CACHE_HIT; + const sourceWorkflowExecutionId = taskNodeMetadata.catalogKey!.sourceTaskExecution.nodeExecutionId.executionId; + const { getByText } = renderComponent(); + const linkEl = await waitFor(() => getByText(viewSourceExecutionString)); + expect(linkEl.getAttribute('href')).toBe(Routes.ExecutionDetails.makeUrl(sourceWorkflowExecutionId)); }); + }); }); diff --git a/src/components/Executions/contextProvider/NodeExecutionDetails/createExecutionArray.tsx b/src/components/Executions/contextProvider/NodeExecutionDetails/createExecutionArray.tsx index 041227cd52..12ebe4a85f 100644 --- a/src/components/Executions/contextProvider/NodeExecutionDetails/createExecutionArray.tsx +++ b/src/components/Executions/contextProvider/NodeExecutionDetails/createExecutionArray.tsx @@ -1,4 +1,4 @@ -import { transformerWorkflowToPlainNodes } from 'components/WorkflowGraph/transformerWorkflowToDag'; +import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { NodeExecutionDetails, NodeExecutionDisplayType } from 'components/Executions/types'; import { Workflow } from 'models/Workflow/types'; import { Identifier } from 'models/Common/types'; @@ -83,7 +83,7 @@ export function createExecutionDetails(workflow: Workflow): CurrentExecutionDeta const compiledWorkflow = workflow.closure?.compiledWorkflow; const { tasks = [] } = compiledWorkflow; - let dNodes = transformerWorkflowToPlainNodes(compiledWorkflow).nodes ?? []; + let dNodes = transformerWorkflowToDag(compiledWorkflow).dag.nodes ?? []; dNodes = convertToPlainNodes(dNodes); dNodes.forEach(n => { diff --git a/src/components/Executions/nodeExecutionQueries.ts b/src/components/Executions/nodeExecutionQueries.ts index d76dbd8bd4..e1e498a749 100644 --- a/src/components/Executions/nodeExecutionQueries.ts +++ b/src/components/Executions/nodeExecutionQueries.ts @@ -1,5 +1,6 @@ import { compareTimestampsAscending } from 'common/utils'; import { QueryInput, QueryType } from 'components/data/types'; +import { retriesToZero } from 'components/flytegraph/ReactFlow/utils'; import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; import { isEqual } from 'lodash'; import { PaginatedEntityResponse, RequestConfig } from 'models/AdminEntity/types'; @@ -75,15 +76,23 @@ export function makeNodeExecutionListQuery( id: WorkflowExecutionIdentifier, config?: RequestConfig ): QueryInput { + /** + * Note on scopedId: + * We use scopedId as a key between various UI elements built from static data + * (eg, CompiledWorkflowClosure for the graph) that need to be mapped to runtime + * values like nodeExecutions; rendering from a static entity has no way to know + * the actual retry value so we use '0' for this key -- the actual value of retries + * remains as the nodeId. + */ return { queryKey: [QueryType.NodeExecutionList, id, config], queryFn: async () => { const nodeExecutions = removeSystemNodes((await listNodeExecutions(id, config)).entities); nodeExecutions.map(exe => { - if (exe.metadata) { - return (exe.scopedId = exe.metadata.specNodeId); + if (exe.metadata?.specNodeId) { + return (exe.scopedId = retriesToZero(exe.metadata.specNodeId)); } else { - return (exe.scopedId = exe.id.nodeId); + return (exe.scopedId = retriesToZero(exe.id.nodeId)); } }); cacheNodeExecutions(queryClient, nodeExecutions); @@ -210,6 +219,7 @@ async function fetchGroupsForParentNodeExecution( nodeExecution.scopedId = parentScopeId; const children = await fetchNodeExecutionList(queryClient, nodeExecution.id.executionId, finalConfig); + const groupsByName = children.reduce>((out, child) => { const retryAttempt = formatRetryAttempt(child.metadata?.retryGroup); let group = out.get(retryAttempt); @@ -217,11 +227,9 @@ async function fetchGroupsForParentNodeExecution( group = { name: retryAttempt, nodeExecutions: [] }; out.set(retryAttempt, group); } - /** - * GraphUX uses workflowClosure which uses scopedId - * This builds a scopedId via parent nodeExecution - * to enable mapping between graph and other components - */ + + /** GraphUX uses workflowClosure which uses scopedId. This builds a scopedId via parent + * nodeExecution to enable mapping between graph and other components */ let scopedId = parentScopeId; if (scopedId != undefined) { scopedId += `-${child.metadata?.retryGroup}-${child.metadata?.specNodeId}`; @@ -229,9 +237,11 @@ async function fetchGroupsForParentNodeExecution( } else { child['scopedId'] = child.metadata?.specNodeId; } + child['fromUniqueParentId'] = nodeExecution.id.nodeId; group.nodeExecutions.push(child); return out; }, new Map()); + return Array.from(groupsByName.values()); } @@ -253,20 +263,37 @@ function fetchChildNodeExecutionGroups(queryClient: QueryClient, nodeExecution: /** * Query returns all children for a list of `nodeExecutions` - * Note: diffrent from fetchGroupsForParentNodeExecution in that it expects a - * list of nodeExecitions + * Will recursively gather all children for anyone that isParent() */ async function fetchAllChildNodeExecutions( queryClient: QueryClient, nodeExecutions: NodeExecution[], config: RequestConfig ): Promise> { - const executions: Array = await Promise.all( - nodeExecutions.map(exe => { - return fetchChildNodeExecutionGroups(queryClient, exe, config); + const executionGroups: Array = await Promise.all( + nodeExecutions.map(exe => fetchChildNodeExecutionGroups(queryClient, exe, config)) + ); + + /** Recursive check for nested/dynamic nodes */ + const childrenFromChildrenNodes: NodeExecution[] = []; + executionGroups.map(group => + group.map(attempt => { + attempt.nodeExecutions.map(execution => { + if (isParentNode(execution)) { + childrenFromChildrenNodes.push(execution); + } + }); }) ); - return executions; + + /** Request and concact data from children */ + if (childrenFromChildrenNodes.length > 0) { + const childGroups = await fetchAllChildNodeExecutions(queryClient, childrenFromChildrenNodes, config); + for (const group in childGroups) { + executionGroups.push(childGroups[group]); + } + } + return executionGroups; } /** @@ -281,12 +308,10 @@ export function useAllChildNodeExecutionGroupsQuery( ): QueryObserverResult, Error> { const queryClient = useQueryClient(); const shouldEnableFn = groups => { - if (nodeExecutions[0] && groups.length > 0) { - if (!nodeExecutionIsTerminal(nodeExecutions[0])) { - return true; - } + if (groups.length > 0) { return groups.some(group => { if (group.nodeExecutions?.length > 0) { + /* Return true is any executions are not yet terminal (ie, they can change) */ return group.nodeExecutions.some(ne => { return !nodeExecutionIsTerminal(ne); }); diff --git a/src/components/Workflow/StaticGraphContainer.tsx b/src/components/Workflow/StaticGraphContainer.tsx index 3ecaa396a5..93171379d7 100644 --- a/src/components/Workflow/StaticGraphContainer.tsx +++ b/src/components/Workflow/StaticGraphContainer.tsx @@ -6,56 +6,45 @@ import { WaitForQuery } from 'components/common/WaitForQuery'; import { DataError } from 'components/Errors/DataError'; import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { ReactFlowWrapper } from 'components/flytegraph/ReactFlow/ReactFlowWrapper'; -import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformerDAGToReactFlow'; -import { dNode } from 'models/Graph/types'; +import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; import { getRFBackground } from 'components/flytegraph/ReactFlow/utils'; -import { - ConvertDagProps, - RFGraphTypes, - RFWrapperProps -} from 'components/flytegraph/ReactFlow/types'; +import { ConvertDagProps, RFWrapperProps } from 'components/flytegraph/ReactFlow/types'; export const renderStaticGraph = props => { - const workflow = props.closure.compiledWorkflow; - const version = props.id.version; + const workflow = props.closure.compiledWorkflow; + const { dag } = transformerWorkflowToDag(workflow); + const rfGraphJson = ConvertFlyteDagToReactFlows({ + root: dag, + maxRenderDepth: 0, + currentNestedView: [], + isStaticGraph: true + } as ConvertDagProps); - const dag: dNode = transformerWorkflowToDag(workflow); - const rfGraphJson = ConvertFlyteDagToReactFlows({ - root: dag, - maxRenderDepth: 0, - isStaticGraph: true - } as ConvertDagProps); - const backgroundStyle = getRFBackground().static; - const ReactFlowProps: RFWrapperProps = { - backgroundStyle, - rfGraphJson: rfGraphJson, - type: RFGraphTypes.static, - version: version - }; - return ; + const backgroundStyle = getRFBackground().static; + const ReactFlowProps: RFWrapperProps = { + backgroundStyle, + rfGraphJson: rfGraphJson, + currentNestedView: [] + }; + return ; }; export interface StaticGraphContainerProps { - workflowId: WorkflowId; + workflowId: WorkflowId; } -export const StaticGraphContainer: React.FC = ({ - workflowId -}) => { - const containerStyle: React.CSSProperties = { - height: 300, - minHeight: 300, - padding: '1rem 0' - }; - const workflowQuery = useQuery( - makeWorkflowQuery(useQueryClient(), workflowId) - ); +export const StaticGraphContainer: React.FC = ({ workflowId }) => { + const containerStyle: React.CSSProperties = { + display: 'flex', + width: '100%' + }; + const workflowQuery = useQuery(makeWorkflowQuery(useQueryClient(), workflowId)); - return ( -
- - {renderStaticGraph} - -
- ); + return ( +
+ + {renderStaticGraph} + +
+ ); }; diff --git a/src/components/Workflow/WorkflowVersionDetails.tsx b/src/components/Workflow/WorkflowVersionDetails.tsx index f8a44f969f..24bce5058e 100644 --- a/src/components/Workflow/WorkflowVersionDetails.tsx +++ b/src/components/Workflow/WorkflowVersionDetails.tsx @@ -11,18 +11,33 @@ import { EntityDetailsHeader } from 'components/Entities/EntityDetailsHeader'; import { EntityVersions } from 'components/Entities/EntityVersions'; const useStyles = makeStyles((_theme: Theme) => ({ - versionsContainer: { - display: 'flex', - flexDirection: 'column', - flex: '1 1 auto' - } + verionDetailsContatiner: { + display: 'flex', + flexDirection: 'column', + flexWrap: 'nowrap', + overflow: 'hidden', + height: `calc(100vh - ${_theme.spacing(1)}rem)` + }, + staticGraphContainer: { + display: 'flex', + height: '60%', + width: '100%', + flex: '1' + }, + versionsContainer: { + display: 'flex', + flex: '0 1 auto', + height: '40%', + flexDirection: 'column', + overflowY: 'scroll' + } })); interface WorkflowVersionDetailsRouteParams { - projectId: string; - domainId: string; - workflowName: string; - workflowVersion: string; + projectId: string; + domainId: string; + workflowName: string; + workflowVersion: string; } /** @@ -32,43 +47,42 @@ interface WorkflowVersionDetailsRouteParams { * @param workflowName */ const WorkflowVersionDetailsContainer: React.FC = ({ - projectId, - domainId, - workflowName, - workflowVersion + projectId, + domainId, + workflowName, + workflowVersion }) => { - const workflowId = React.useMemo( - () => ({ - resourceType: ResourceType.WORKFLOW, - project: projectId, - domain: domainId, - name: workflowName, - version: workflowVersion - }), - [projectId, domainId, workflowName, workflowVersion] - ); + const workflowId = React.useMemo( + () => ({ + resourceType: ResourceType.WORKFLOW, + project: projectId, + domain: domainId, + name: workflowName, + version: workflowVersion + }), + [projectId, domainId, workflowName, workflowVersion] + ); - const id = workflowId as ResourceIdentifier; - const sections = entitySections[ResourceType.WORKFLOW]; - const project = useProject(workflowId.project); - const styles = useStyles(); + const id = workflowId as ResourceIdentifier; + const sections = entitySections[ResourceType.WORKFLOW]; + const project = useProject(workflowId.project); + const styles = useStyles(); - return ( - - - -
- -
-
- ); + return ( + + +
+
+ +
+
+ +
+
+
+ ); }; -export const WorkflowVersionDetails = withRouteParams< - WorkflowVersionDetailsRouteParams ->(WorkflowVersionDetailsContainer); +export const WorkflowVersionDetails = withRouteParams( + WorkflowVersionDetailsContainer +); diff --git a/src/components/Workflow/workflowQueries.ts b/src/components/Workflow/workflowQueries.ts index 4418325114..926da09148 100644 --- a/src/components/Workflow/workflowQueries.ts +++ b/src/components/Workflow/workflowQueries.ts @@ -1,8 +1,10 @@ import { QueryInput, QueryType } from 'components/data/types'; +import { DataError } from 'components/Errors/DataError'; import { extractTaskTemplates } from 'components/hooks/utils'; +import { getNodeExecutionData } from 'models/Execution/api'; import { getWorkflow } from 'models/Workflow/api'; import { Workflow, WorkflowId } from 'models/Workflow/types'; -import { QueryClient } from 'react-query'; +import { QueryClient, QueryObserverResult } from 'react-query'; export function makeWorkflowQuery( queryClient: QueryClient, @@ -29,6 +31,37 @@ export function makeWorkflowQuery( }; } +export function makeNodeExecutionDynamicWorkflowQuery( + queryClient: QueryClient, + parentsToFetch +): QueryInput<{ [key: string]: any }> { + return { + queryKey: [QueryType.DynamicWorkflowFromNodeExecution, parentsToFetch], + queryFn: async () => { + return await Promise.all( + Object.keys(parentsToFetch).map(id => { + const executionId = parentsToFetch[id]; + const data = getNodeExecutionData(executionId.id).then( + value => { + return { key: id, value: value }; + } + ); + return data; + }) + ).then(values => { + const output: { [key: string]: any } = {}; + for (let i = 0; i < values.length; i++) { + /* Filter to only include dynamicWorkflow */ + if (values[i].value.dynamicWorkflow) { + output[values[i].key] = values[i].value; + } + } + return output; + }); + } + }; +} + export async function fetchWorkflow(queryClient: QueryClient, id: WorkflowId) { return queryClient.fetchQuery(makeWorkflowQuery(queryClient, id)); } diff --git a/src/components/WorkflowGraph/WorkflowGraph.tsx b/src/components/WorkflowGraph/WorkflowGraph.tsx index 3f96917ab1..49ad5c04f2 100644 --- a/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -4,63 +4,120 @@ import { Workflow } from 'models/Workflow/types'; import * as React from 'react'; import ReactFlowGraphComponent from 'components/flytegraph/ReactFlow/ReactFlowGraphComponent'; import { Error } from 'models/Common/types'; +import { NonIdealState } from 'components/common/NonIdealState'; +import { DataError } from 'components/Errors/DataError'; +import { NodeExecutionsContext } from 'components/Executions/contexts'; +import { WaitForQuery } from 'components/common/WaitForQuery'; +import { useQuery, useQueryClient } from 'react-query'; +import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; +import { createDebugLogger } from 'components/flytegraph/utils'; +import { CompiledNode } from 'models/Node/types'; export interface WorkflowGraphProps { - onNodeSelectionChanged: (selectedNodes: string[]) => void; - selectedNodes?: string[]; - workflow: Workflow; - nodeExecutionsById?: any; -} - -interface WorkflowGraphState { - dag: dNode | null; - error?: Error; + onNodeSelectionChanged: (selectedNodes: string[]) => void; + selectedNodes?: string[]; + workflow: Workflow; + nodeExecutionsById?: any; } interface PrepareDAGResult { - dag: dNode | null; - error?: Error; + dag: dNode | null; + staticExecutionIdsMap?: any; + error?: Error; } +const debug = createDebugLogger('@WorkflowGraph'); + function workflowToDag(workflow: Workflow): PrepareDAGResult { - try { - if (!workflow.closure) { - throw new Error('Workflow has no closure'); - } - if (!workflow.closure.compiledWorkflow) { - throw new Error('Workflow closure missing a compiled workflow'); - } - const { compiledWorkflow } = workflow.closure; - const dag: dNode = transformerWorkflowToDag(compiledWorkflow); - return { dag }; - } catch (e) { - return { - dag: null, - error: e as Error - }; + try { + if (!workflow.closure) { + throw new Error('Workflow has no closure'); + } + if (!workflow.closure.compiledWorkflow) { + throw new Error('Workflow closure missing a compiled workflow'); } + const { compiledWorkflow } = workflow.closure; + const { dag, staticExecutionIdsMap } = transformerWorkflowToDag(compiledWorkflow); + return { dag, staticExecutionIdsMap }; + } catch (e) { + return { + dag: null, + error: e as Error + }; + } } -export class WorkflowGraph extends React.Component< - WorkflowGraphProps, - WorkflowGraphState -> { - constructor(props) { - super(props); - const { dag, error } = workflowToDag(this.props.workflow); - this.state = { dag, error }; +export interface DynamicWorkflowMapping { + rootGraphNodeId: CompiledNode; + dynamicWorkflow: any; + dynamicExecutions: any[]; +} +export const WorkflowGraph: React.FC = props => { + const { onNodeSelectionChanged, nodeExecutionsById, workflow } = props; + const { dag, staticExecutionIdsMap, error } = workflowToDag(workflow); + /** + * Note: + * Dynamic nodes are deteremined at runtime and thus do not come + * down as part of the workflow closure. We can detect and place + * dynamic nodes by finding orphan execution id's and then mapping + * those executions into the dag by using the executions 'uniqueParentId' + * to render that node as a subworkflow + */ + const checkForDynamicExeuctions = (allExecutions, staticExecutions) => { + const parentsToFetch = {}; + for (const executionId in allExecutions) { + if (!staticExecutions[executionId]) { + const dynamicExecution = allExecutions[executionId]; + const dynamicExecutionId = dynamicExecution.metadata.specNodeId || dynamicExecution.id; + const uniqueParentId = dynamicExecution.fromUniqueParentId; + if (parentsToFetch[uniqueParentId]) { + parentsToFetch[uniqueParentId].push(dynamicExecutionId); + } else { + parentsToFetch[uniqueParentId] = [dynamicExecutionId]; + } + } + } + const result = {}; + for (const parentId in parentsToFetch) { + result[parentId] = allExecutions[parentId]; } + return result; + }; - render() { - const { dag } = this.state; - const { onNodeSelectionChanged, nodeExecutionsById } = this.props; + const dynamicParents = checkForDynamicExeuctions(nodeExecutionsById, staticExecutionIdsMap); - return ( - - ); + const dynamicWorkflowQuery = useQuery(makeNodeExecutionDynamicWorkflowQuery(useQueryClient(), dynamicParents)); + const renderReactFlowGraph = dynamicWorkflows => { + debug('DynamicWorkflows:', dynamicWorkflows); + let mergedDag = dag; + for (const dynamicId in dynamicWorkflows) { + if (staticExecutionIdsMap[dynamicId]) { + if (workflow.closure?.compiledWorkflow) { + const dynamicWorkflow = transformerWorkflowToDag(workflow.closure?.compiledWorkflow, dynamicWorkflows); + mergedDag = dynamicWorkflow.dag; + } + } } -} + const merged = mergedDag; + return ( + + ); + }; + + if (error) { + return ; + } else { + return ( + + + {renderReactFlowGraph} + + + ); + } +}; diff --git a/src/components/WorkflowGraph/test/utils.test.ts b/src/components/WorkflowGraph/test/utils.test.ts index 4b3f298dc5..beb6931308 100644 --- a/src/components/WorkflowGraph/test/utils.test.ts +++ b/src/components/WorkflowGraph/test/utils.test.ts @@ -1,117 +1,109 @@ import { - mockCompiledWorkflow, - mockCompiledEndNode, - mockCompiledStartNode, - mockCompiledTaskNode + mockCompiledWorkflow, + mockCompiledEndNode, + mockCompiledStartNode, + mockCompiledTaskNode } from 'models/__mocks__/graphWorkflowData'; import { dTypes } from 'models/Graph/types'; import { - DISPLAY_NAME_START, - DISPLAY_NAME_END, - checkIfObjectsAreSame, - getDisplayName, - getNodeTypeFromCompiledNode, - isStartNode, - isEndNode + DISPLAY_NAME_START, + DISPLAY_NAME_END, + checkIfObjectsAreSame, + getDisplayName, + getNodeTypeFromCompiledNode, + isStartNode, + isEndNode } from '../../WorkflowGraph/utils'; describe('getDisplayName', () => { - it('should return correct name', () => { - expect(getDisplayName(mockCompiledStartNode)).toBe(DISPLAY_NAME_START); - expect(getDisplayName(mockCompiledEndNode)).toBe(DISPLAY_NAME_END); - expect(getDisplayName(mockCompiledTaskNode)).toBe('DEADBEEF'); - expect(getDisplayName(mockCompiledWorkflow)).toBe('myWorkflowName'); - }); + it('should return correct name', () => { + expect(getDisplayName(mockCompiledStartNode)).toBe(DISPLAY_NAME_START); + expect(getDisplayName(mockCompiledEndNode)).toBe(DISPLAY_NAME_END); + expect(getDisplayName(mockCompiledTaskNode)).toBe('DEADBEEF'); + expect(getDisplayName(mockCompiledWorkflow)).toBe('myWorkflowName'); + }); }); describe('checkIfObjectsAreSame', () => { - const a = { - red: 'red', - blue: 'blue', - green: 'green' - }; - const b = { - red: 'red', - blue: 'blue', - green: 'green' - }; - const fail_a = { - red: 'red', - blue: 'not blue', - green: 'green' - }; - const fail_b = { - red: 'red', - green: 'green', - orange: 'orange' - }; - it('should return true when a-keys match b-values', () => { - expect(checkIfObjectsAreSame(a, b)).toEqual(true); - }); - it("should return false when a-keys don't match b-values", () => { - expect(checkIfObjectsAreSame(fail_a, b)).toEqual(false); - expect(checkIfObjectsAreSame(a, fail_b)).toEqual(false); - }); + const a = { + red: 'red', + blue: 'blue', + green: 'green' + }; + const b = { + red: 'red', + blue: 'blue', + green: 'green' + }; + const fail_a = { + red: 'red', + blue: 'not blue', + green: 'green' + }; + const fail_b = { + red: 'red', + green: 'green', + orange: 'orange' + }; + it('should return true when a-keys match b-values', () => { + expect(checkIfObjectsAreSame(a, b)).toEqual(true); + }); + it("should return false when a-keys don't match b-values", () => { + expect(checkIfObjectsAreSame(fail_a, b)).toEqual(false); + expect(checkIfObjectsAreSame(a, fail_b)).toEqual(false); + }); }); describe('getNodeTypeFromCompiledNode', () => { - const branchNode = { - branchNode: {} - }; - const workflowNode = { - workflowNode: {} - }; - const mockBranchNode = { ...mockCompiledTaskNode, ...branchNode }; - const mockWorkflowNode = { ...mockCompiledTaskNode, ...workflowNode }; + const branchNode = { + branchNode: {} + }; + const workflowNode = { + workflowNode: {} + }; + const mockBranchNode = { ...mockCompiledTaskNode, ...branchNode }; + const mockWorkflowNode = { ...mockCompiledTaskNode, ...workflowNode }; - it('should return dTypes.start when is start-node', () => { - expect(getNodeTypeFromCompiledNode(mockCompiledStartNode)).toBe( - dTypes.start - ); - }); - it('should return dTypes.end when is end-node', () => { - expect(getNodeTypeFromCompiledNode(mockCompiledEndNode)).toBe( - dTypes.end - ); - }); - it('should return dTypes.branch when is node has branchNodes', () => { - expect(getNodeTypeFromCompiledNode(mockBranchNode)).toBe(dTypes.branch); - }); - it('should return dTypes.subworkflow when is node has workflowNode', () => { - expect(getNodeTypeFromCompiledNode(mockWorkflowNode)).toBe( - dTypes.subworkflow - ); - }); - it('should return dTypes.task when is node is taskNode', () => { - expect(getNodeTypeFromCompiledNode(mockCompiledTaskNode)).toBe( - dTypes.task - ); - }); + it('should return dTypes.start when is start-node', () => { + expect(getNodeTypeFromCompiledNode(mockCompiledStartNode)).toBe(dTypes.start); + }); + it('should return dTypes.end when is end-node', () => { + expect(getNodeTypeFromCompiledNode(mockCompiledEndNode)).toBe(dTypes.end); + }); + it('should return *dTypes.subworkflow (branch is typed as subworkflow for graph) when is node has branchNodes', () => { + expect(getNodeTypeFromCompiledNode(mockBranchNode)).toBe(dTypes.subworkflow); + }); + it('should return dTypes.subworkflow when is node has workflowNode', () => { + expect(getNodeTypeFromCompiledNode(mockWorkflowNode)).toBe(dTypes.subworkflow); + }); + it('should return dTypes.task when is node is taskNode', () => { + expect(getNodeTypeFromCompiledNode(mockCompiledTaskNode)).toBe(dTypes.task); + }); }); describe('isStartNode', () => { - it('should return true when start-node', () => { - expect(isStartNode(mockCompiledStartNode)).toBe(true); - }); - it('should return false when not start-node', () => { - expect(isStartNode(mockCompiledTaskNode)).toBe(false); - }); + it('should return true when start-node', () => { + expect(isStartNode(mockCompiledStartNode)).toBe(true); + }); + it('should return false when not start-node', () => { + expect(isStartNode(mockCompiledTaskNode)).toBe(false); + }); }); describe('isEndNode', () => { - it('should return true when start-node', () => { - expect(isEndNode(mockCompiledEndNode)).toBe(true); - }); - it('should return false when not start-node', () => { - expect(isEndNode(mockCompiledTaskNode)).toBe(false); - }); + it('should return true when start-node', () => { + expect(isEndNode(mockCompiledEndNode)).toBe(true); + }); + it('should return false when not start-node', () => { + expect(isEndNode(mockCompiledTaskNode)).toBe(false); + }); }); describe('getSubWorkflowFromId', () => { - it('should return subworkflow from id', () => { - expect(isStartNode(mockCompiledStartNode)).toBe(true); - }); - it('should return false when not start-node', () => { - expect(isStartNode(mockCompiledTaskNode)).toBe(false); - }); + it('should return subworkflow from id', () => { + expect(isStartNode(mockCompiledStartNode)).toBe(true); + }); + it('should return false when not start-node', () => { + expect(isStartNode(mockCompiledTaskNode)).toBe(false); + }); }); diff --git a/src/components/WorkflowGraph/transformerWorkflowToDag.tsx b/src/components/WorkflowGraph/transformerWorkflowToDag.tsx index 666b80a58e..c47961077a 100644 --- a/src/components/WorkflowGraph/transformerWorkflowToDag.tsx +++ b/src/components/WorkflowGraph/transformerWorkflowToDag.tsx @@ -1,289 +1,338 @@ import { DISPLAY_NAME_END, DISPLAY_NAME_START } from 'components/flytegraph/ReactFlow/utils'; +import { createDebugLogger } from 'components/flytegraph/utils'; import { dTypes, dEdge, dNode } from 'models/Graph/types'; import { startNodeId, endNodeId } from 'models/Node/constants'; import { CompiledNode, ConnectionSet, TaskNode } from 'models/Node/types'; import { CompiledTask } from 'models/Task/types'; -import { CompiledWorkflow, CompiledWorkflowClosure, WorkflowTemplate } from 'models/Workflow/types'; +import { CompiledWorkflow, CompiledWorkflowClosure } from 'models/Workflow/types'; import { - isEndNode, - isStartNode, + isStartOrEndNode, getDisplayName, getSubWorkflowFromId, getNodeTypeFromCompiledNode, getTaskTypeFromCompiledNode } from './utils'; +export interface staticNodeExecutionIds { + staticNodeId: string; +} + +const debug = createDebugLogger('@transformerWorkflowToDag'); + /** * Returns a DAG from Flyte workflow request data * @param context input can be either CompiledWorkflow or CompiledNode * @returns Display name */ -export const transformerWorkflowToDag = (workflow: CompiledWorkflowClosure): dNode => { +export const transformerWorkflowToDag = (workflow: CompiledWorkflowClosure, dynamicToMerge: any | null = null): any => { const { primary } = workflow; - const root = buildDAG(null, primary, dTypes.primary, workflow); - return root; -}; - -export const transformerWorkflowToPlainNodes = (workflow: CompiledWorkflowClosure): dNode => { - return buildDAG(null, workflow.primary, dTypes.primary, workflow); -}; + const staticExecutionIdsMap = {}; -const createDNode = ( - compiledNode: CompiledNode, - parentDNode?: dNode | null, - taskTemplate?: CompiledTask | null, - typeOverride?: dTypes | null -): dNode => { - const nodeValue = taskTemplate == null ? compiledNode : { ...compiledNode, ...taskTemplate }; + interface CreateDEdgeProps { + sourceId: string; + targetId: string; + } + const createDEdge = ({ sourceId, targetId }: CreateDEdgeProps): dEdge => { + const id = `${sourceId}->${targetId}`; + const edge: dEdge = { + sourceId: sourceId, + targetId: targetId, + id: id + }; + return edge; + }; - /* scopedId is used for requests; this creates format used by contract */ - let scopedId = ''; + interface CreateDNodeProps { + compiledNode: CompiledNode; + parentDNode?: dNode; + taskTemplate?: CompiledTask; + typeOverride?: dTypes; + } + const createDNode = (props: CreateDNodeProps): dNode => { + const { compiledNode, parentDNode, taskTemplate, typeOverride } = props; + const nodeValue = taskTemplate == null ? compiledNode : { ...compiledNode, ...taskTemplate }; - if (parentDNode && parentDNode.type != dTypes.start) { - if (parentDNode.type == dTypes.branch || parentDNode.type == dTypes.subworkflow) { - /* Note: request contract indicates nested (subworkflow, branch) with -0- */ - scopedId = `${parentDNode.scopedId}-0-${compiledNode.id}`; - } else { + /** + * Note on scopedId: + * We need to be able to map nodeExecution's to their corresponding nodes. The problem is that nodeExecutions come + * back with a scoped id's (eg, {parentId}-{retry}-{childId}) while nodes are contextual (eg, 'n3' vs 'n0-0-n1-0-n3'). + * Further, even if we try to construct these values here we cannot know the actual retry value until run-time. + * + * To mitigate this we've added a new property on NodeExecutions that is the same as an executions scopedId but + * assuming '0' for each retry. We then construct that same scopedId here with the same solution of '0' for retries + * which allows us to map them regardless of what the actual retry value is. + */ + let scopedId = ''; + if (isStartOrEndNode(compiledNode) && parentDNode && !isStartOrEndNode(parentDNode)) { scopedId = `${parentDNode.scopedId}-${compiledNode.id}`; + } else if (parentDNode && parentDNode.type != dTypes.start) { + if (parentDNode.type == dTypes.branch || parentDNode.type == dTypes.subworkflow) { + scopedId = `${parentDNode.scopedId}-0-${compiledNode.id}`; + } else { + scopedId = `${parentDNode.scopedId}-${compiledNode.id}`; + } + } else { + /* Case: primary workflow nodes won't have parents */ + scopedId = compiledNode.id; } - } else { - /* Case: primary workflow nodes won't have parents */ - scopedId = compiledNode.id; - } + const type = typeOverride == null ? getNodeTypeFromCompiledNode(compiledNode) : typeOverride; - /** - * @TODO decide if we want to nested/standard start/end in - * UX; saving untilthat is decided. - */ - const type = typeOverride == null ? getNodeTypeFromCompiledNode(compiledNode) : typeOverride; + const output = { + id: compiledNode.id, + scopedId: scopedId, + value: nodeValue, + type: type, + name: getDisplayName(compiledNode), + nodes: [], + edges: [] + } as dNode; - const output = { - id: compiledNode.id, - scopedId: scopedId, - value: nodeValue, - type: type, - name: getDisplayName(compiledNode), - nodes: [], - edges: [] - } as dNode; - return output; -}; + staticExecutionIdsMap[output.scopedId] = compiledNode; + return output; + }; -export const buildBranchStartEndNodes = (root: dNode) => { - const startNode = createDNode( - { - id: `${root.id}-${startNodeId}`, - metadata: { - name: DISPLAY_NAME_START - } - } as CompiledNode, - null, - null, - dTypes.nestedStart - ); + const buildBranchStartEndNodes = (root: dNode) => { + const startNode = createDNode({ + compiledNode: { + id: `${root.id}-${startNodeId}`, + metadata: { + name: DISPLAY_NAME_START + } + } as CompiledNode, + typeOverride: dTypes.nestedStart + }); - const endNode = createDNode( - { - id: `${root.id}-${endNodeId}`, - metadata: { - name: DISPLAY_NAME_END - } - } as CompiledNode, - null, - null, - dTypes.nestedEnd - ); + const endNode = createDNode({ + compiledNode: { + id: `${root.id}-${endNodeId}`, + metadata: { + name: DISPLAY_NAME_END + } + } as CompiledNode, + typeOverride: dTypes.nestedEnd + }); - return { - startNode, - endNode + return { + startNode, + endNode + }; }; -}; -export const buildBranchNodeWidthType = (node, root, workflow) => { - const taskNode = node.taskNode as TaskNode; - let taskType: CompiledTask | null = null; - if (taskNode) { - taskType = getTaskTypeFromCompiledNode(taskNode, workflow.tasks) as CompiledTask; - } - const dNode = createDNode(node as CompiledNode, root, taskType); - root.nodes.push(dNode); -}; + const buildWorkflowEdges = (root, context: ConnectionSet, ingress, nodeMap) => { + const list = context.downstream[ingress].ids; -/** - * Will parse values when dealing with a Branch and recursively find and build - * any other node types. - * @param root Parent root for Branch; will render independent DAG and - * add as a child node of root. - * @param parentCompiledNode CompiledNode of origin - */ -export const parseBranch = (root: dNode, parentCompiledNode: CompiledNode, workflow: CompiledWorkflowClosure) => { - const otherNode = parentCompiledNode.branchNode?.ifElse?.other; - const thenNode = parentCompiledNode.branchNode?.ifElse?.case?.thenNode as CompiledNode; - const elseNode = parentCompiledNode.branchNode?.ifElse?.elseNode as CompiledNode; + for (let i = 0; i < list.length; i++) { + const source = nodeMap[ingress]?.dNode.scopedId; + const target = nodeMap[list[i]]?.dNode.scopedId; + if (source && target) { + const edge: dEdge = createDEdge({ + sourceId: source, + targetId: target + }); + root.edges.push(edge); + if (context.downstream[list[i]]) { + buildWorkflowEdges(root, context, list[i], nodeMap); + } + } + } + }; - /* Check: if thenNode has branch : else add theNode */ - if (thenNode.branchNode) { - const thenNodeDNode = createDNode(thenNode, root); - buildDAG(thenNodeDNode, thenNode, dTypes.branch, workflow); - root.nodes.push(thenNodeDNode); - } else { - buildBranchNodeWidthType(thenNode, root, workflow); + /** + * Handles parsing CompiledNode + * + * @param node CompiledNode to parse + * @param root Root node for the graph that will be rendered + * @param workflow Main/root workflow + */ + interface ParseNodeProps { + node: CompiledNode; + root?: dNode; } + const parseNode = ({ node, root }: ParseNodeProps) => { + let dNode; + /** + * Note: if node is dynamic we must add dynamicWorkflow + * as a subworkflow on the root workflow. We also need to check + * if the dynamic workflow has any subworkflows and add them too. + */ + if (dynamicToMerge) { + const scopedId = `${root?.scopedId}-0-${node.id}`; + const id = dynamicToMerge[scopedId] != null ? scopedId : node.id; + if (dynamicToMerge[id]) { + const dynamicWorkflow = dynamicToMerge[id].dynamicWorkflow; - /* Check: else case */ - if (elseNode) { - buildBranchNodeWidthType(elseNode, root, workflow); - } + if (dynamicWorkflow) { + const dWorkflowId = dynamicWorkflow.metadata?.specNodeId || dynamicWorkflow.id; + const dPrimaryWorkflow = dynamicWorkflow.compiledWorkflow.primary; - /* Check: other case */ - if (otherNode) { - otherNode.map(otherItem => { - const otherCompiledNode: CompiledNode = otherItem.thenNode as CompiledNode; - if (otherCompiledNode.branchNode) { - const otherDNodeBranch = createDNode(otherCompiledNode, root); - buildDAG(otherDNodeBranch, otherCompiledNode, dTypes.branch, workflow); - } else { - buildBranchNodeWidthType(otherCompiledNode, root, workflow); - } - }); - } + node['workflowNode'] = { + subWorkflowRef: dWorkflowId + }; - /* Add edges and add start/end nodes */ - const { startNode, endNode } = buildBranchStartEndNodes(root); - for (let i = 0; i < root.nodes.length; i++) { - const startEdge: dEdge = { - sourceId: startNode.id, - targetId: root.nodes[i].scopedId - }; - const endEdge: dEdge = { - sourceId: root.nodes[i].scopedId, - targetId: endNode.id - }; - root.edges.push(startEdge); - root.edges.push(endEdge); - } - root.nodes.push(startNode); - root.nodes.push(endNode); -}; + /* 1. Add primary workflow as subworkflow on root */ + if (getSubWorkflowFromId(dWorkflowId, workflow) === false) { + workflow.subWorkflows?.push(dPrimaryWorkflow); + } -export const buildOutNodesFromContext = ( - root: dNode, - contextWf: WorkflowTemplate, - type: dTypes, - workflow: CompiledWorkflowClosure -): void => { - for (let i = 0; i < contextWf.nodes.length; i++) { - const compiledNode: CompiledNode = contextWf.nodes[i]; - let dNode: dNode; + /* 2. Add subworkflows as subworkflows on root */ + const dSubWorkflows = dynamicWorkflow.compiledWorkflow.subWorkflows; - if (isStartNode(compiledNode) && type == dTypes.subworkflow) { - /** @TODO Decide if we should implement this */ - /* Case: override type as nestedStart node */ - dNode = createDNode(compiledNode); - } else if (isEndNode(compiledNode) && type == dTypes.subworkflow) { - /** @TODO Decide if we should implement this */ - /* Case: override type as nestedEnd node */ - dNode = createDNode(compiledNode); - } else if (compiledNode.branchNode) { - /* Case: recurse on branch node */ - dNode = createDNode(compiledNode, null); - buildDAG(dNode, compiledNode, dTypes.branch, workflow); - } else if (compiledNode.workflowNode) { - /* Case: recurse on workflow node */ - const id = compiledNode.workflowNode.subWorkflowRef; - const subworkflow = getSubWorkflowFromId(id, workflow); - if (!isStartNode(root)) { - dNode = createDNode(compiledNode, root); - } else { - /** - * @TODO may not need this else case - */ - dNode = createDNode(compiledNode, null); + for (let i = 0; i < dSubWorkflows.length; i++) { + const subworkflow = dSubWorkflows[i]; + const subId = subworkflow.template.id; + if (getSubWorkflowFromId(subId, workflow) === false) { + workflow.subWorkflows?.push(subworkflow); + } + } + } + /* Remove entry when done to prevent infinite loop */ + delete dynamicToMerge[node.id]; } - buildDAG(dNode, subworkflow, dTypes.subworkflow, workflow); - } else if (compiledNode.taskNode) { - /* Case: build task node */ - const taskType = getTaskTypeFromCompiledNode(compiledNode.taskNode, workflow.tasks); - dNode = createDNode(compiledNode, root, taskType); + } + + if (node.branchNode) { + dNode = createDNode({ + compiledNode: node, + parentDNode: root + }); + buildDAG(dNode, node, dTypes.branch); + } else if (node.workflowNode) { + const id = node.workflowNode.subWorkflowRef; + const subworkflow = getSubWorkflowFromId(id, workflow); + dNode = createDNode({ + compiledNode: node, + parentDNode: root + }); + buildDAG(dNode, subworkflow, dTypes.subworkflow); + } else if (node.taskNode) { + const taskNode = node.taskNode as TaskNode; + const taskType: CompiledTask = getTaskTypeFromCompiledNode(taskNode, workflow.tasks) as CompiledTask; + dNode = createDNode({ + compiledNode: node as CompiledNode, + parentDNode: root, + taskTemplate: taskType + }); } else { - /* Else: primary start/finish nodes */ - dNode = createDNode(compiledNode, root); + dNode = createDNode({ + compiledNode: node, + parentDNode: root + }); } + root?.nodes.push(dNode); + }; - root.nodes.push(dNode); + /** + * Handles parsing branch from CompiledNode + * + * @param root Root node for the branch that will be rendered + * @param context Current branch node being parsed + */ + interface ParseBranchProps { + root: dNode; + context: CompiledNode; } -}; + const parseBranch = ({ root, context }: ParseBranchProps) => { + const otherNode = context.branchNode?.ifElse?.other; + const thenNode = context.branchNode?.ifElse?.case?.thenNode as CompiledNode; + const elseNode = context.branchNode?.ifElse?.elseNode as CompiledNode; -export const buildOutWorkflowEdges = (root, context: ConnectionSet, ingress, nodeMap) => { - const list = context.downstream[ingress].ids; - for (let i = 0; i < list.length; i++) { - const edge: dEdge = { - sourceId: nodeMap[ingress] && nodeMap[ingress].dNode.scopedId, - targetId: nodeMap[list[i]]?.dNode.scopedId ?? '' - }; - root.edges.push(edge); - if (context.downstream[list[i]]) { - buildOutWorkflowEdges(root, context, list[i], nodeMap); + /* Check: then (if) case */ + if (thenNode) { + parseNode({ node: thenNode, root: root }); } - } -}; -/** - * Handles parsing CompiledWorkflow data objects - * - * @param root Root node for the graph that will be rendered - * @param context The current workflow (could be child of main workflow) - * @param type Type (sub or primrary) - * @param workflow Main parent workflow - */ -export const parseWorkflow = (root, context: CompiledWorkflow, type: dTypes, workflow: CompiledWorkflowClosure) => { - /* Note: only Primary workflow is null, all others have root */ - let contextualRoot; - if (root) { - contextualRoot = root; - } else { - const primaryStart = createDNode({ id: startNodeId } as CompiledNode); - contextualRoot = primaryStart; - } + /* Check: else case */ + if (elseNode) { + parseNode({ node: elseNode, root: root }); + } - /* Build Nodes */ - buildOutNodesFromContext(contextualRoot, context.template, type, workflow); + /* Check: other (else-if) case */ + if (otherNode) { + otherNode.map(otherItem => { + const otherCompiledNode: CompiledNode = otherItem.thenNode as CompiledNode; + parseNode({ + node: otherCompiledNode, + root: root + }); + }); + } - const nodesList = context.template.nodes; - const nodeMap = {}; + /* Add edges and add start/end nodes */ + const { startNode, endNode } = buildBranchStartEndNodes(root); + for (let i = 0; i < root.nodes.length; i++) { + const startEdge: dEdge = createDEdge({ + sourceId: startNode.id, + targetId: root.nodes[i].scopedId + }); + const endEdge: dEdge = createDEdge({ + sourceId: root.nodes[i].scopedId, + targetId: endNode.id + }); + root.edges.push(startEdge); + root.edges.push(endEdge); + } + root.nodes.push(startNode); + root.nodes.push(endNode); + }; - /* Create mapping of id => dNode for all child nodes of root to build edges */ - for (let i = 0; i < contextualRoot.nodes.length; i++) { - const dNode = contextualRoot.nodes[i]; - nodeMap[dNode.id] = { - dNode: dNode, - compiledNode: nodesList[i] - }; - } + /** + * Handles parsing CompiledWorkflow + * + * @param root Root node for the graph that will be rendered + * @param context The current workflow being parsed + */ + const parseWorkflow = (root, context: CompiledWorkflow) => { + /* Build Nodes from template */ + for (let i = 0; i < context.template.nodes.length; i++) { + const compiledNode: CompiledNode = context.template.nodes[i]; + parseNode({ + node: compiledNode, + root: root + }); + } - /* Build Edges */ - buildOutWorkflowEdges(contextualRoot, context.connections, startNodeId, nodeMap); - return contextualRoot; -}; + const nodesList = context.template.nodes; + const nodeMap = {}; -/** - * Mutates root (if passed) by recursively rendering DAG of given context. - * - * @param root Root node of DAG - * @param graphType DAG type (eg, branch, workflow) - * @param context Pointer to current context of response - */ -export const buildDAG = (root: dNode | null, context: any, graphType: dTypes, workflow: CompiledWorkflowClosure) => { - switch (graphType) { - case dTypes.branch: - parseBranch(root as dNode, context, workflow); - break; - case dTypes.subworkflow: - parseWorkflow(root, context, graphType, workflow); - break; - case dTypes.primary: - return parseWorkflow(root, context, graphType, workflow); - break; - } + /* Create mapping of CompiledNode.id => dNode.id to build edges */ + for (let i = 0; i < root.nodes.length; i++) { + const dNode = root.nodes[i]; + nodeMap[dNode.id] = { + dNode: dNode, + compiledNode: nodesList[i] + }; + } + + /* Build Edges */ + buildWorkflowEdges(root, context.connections, startNodeId, nodeMap); + return root; + }; + + /** + * Recursively renders DAG of given context. + * + * @param root Root node of DAG (note: will mutate root) + * @param graphType DAG type (eg, branch, workflow) + * @param context Pointer to current context of response + */ + const buildDAG = (root: dNode, context: any, graphType: dTypes) => { + switch (graphType) { + case dTypes.branch: + parseBranch({ root, context }); + break; + case dTypes.subworkflow: + parseWorkflow(root, context); + break; + case dTypes.primary: + return parseWorkflow(root, context); + } + }; + const primaryWorkflowRoot = createDNode({ + compiledNode: { + id: startNodeId + } as CompiledNode + }); + const dag: dNode = buildDAG(primaryWorkflowRoot, primary, dTypes.primary); + debug('output:', dag); + return { dag, staticExecutionIdsMap }; }; diff --git a/src/components/WorkflowGraph/utils.ts b/src/components/WorkflowGraph/utils.ts index 592d6f1273..f3225e1eeb 100644 --- a/src/components/WorkflowGraph/utils.ts +++ b/src/components/WorkflowGraph/utils.ts @@ -11,6 +11,10 @@ import { transformerWorkflowToDag } from './transformerWorkflowToDag'; export const DISPLAY_NAME_START = 'start'; export const DISPLAY_NAME_END = 'end'; +export const isStartOrEndNode = (node: any) => { + return node.id === startNodeId || node.id === endNodeId; +}; + export function isStartNode(node: any) { return node.id === startNodeId; } @@ -44,28 +48,32 @@ export const checkIfObjectsAreSame = (a, b) => { * @param context input can be either CompiledWorkflow or CompiledNode * @returns Display name */ -export const getDisplayName = (context: any): string => { - let fullName; +export const getDisplayName = (context: any, truncate = true): string => { + let displayName = ''; if (context.metadata) { // Compiled Node with Meta - fullName = context.metadata.name; + displayName = context.metadata.name; + } else if (context.displayId) { + // NodeExecutionDetails + displayName = context.displayId; + } else if (context.template?.id?.name) { + // CompiledWorkflow + displayName = context.template.id.name; } else if (context.id) { // Compiled Node (start/end) - fullName = context.id; - } else { - // CompiledWorkflow - fullName = context.template.id.name; + displayName = context.id; } - if (fullName == startNodeId) { + if (displayName == startNodeId) { return DISPLAY_NAME_START; - } else if (fullName == endNodeId) { + } else if (displayName == endNodeId) { return DISPLAY_NAME_END; - } else if (fullName.indexOf('.') > 0) { - return fullName.substr(fullName.lastIndexOf('.') + 1, fullName.length - 1); - } else { - return fullName; + } else if (displayName.indexOf('.') > 0 && truncate) { + /* Note: for displaying truncated task name */ + return displayName.substring(displayName.lastIndexOf('.') + 1, displayName.length); } + + return displayName; }; /** @@ -77,13 +85,23 @@ export const getWorkflowId = (workflow: CompiledWorkflow): string => { return workflow.template.id.name; }; +export const createWorkflowNodeFromDynamic = dw => { + return { + subWorkflowRef: { + domain: 'development', + name: '', + project: '' + } + }; +}; + export const getNodeTypeFromCompiledNode = (node: CompiledNode): dTypes => { if (isStartNode(node)) { return dTypes.start; } else if (isEndNode(node)) { return dTypes.end; } else if (node.branchNode) { - return dTypes.branch; + return dTypes.subworkflow; } else if (node.workflowNode) { return dTypes.subworkflow; } else { @@ -140,9 +158,9 @@ export const getNodeTemplateName = (node: dNode) => { export const transformWorkflowToKeyedDag = (workflow: Workflow) => { if (!workflow.closure?.compiledWorkflow) return {}; - const dagData = transformerWorkflowToDag(workflow.closure?.compiledWorkflow); + const { dag } = transformerWorkflowToDag(workflow.closure?.compiledWorkflow); const data = {}; - dagData.nodes.forEach(node => { + dag.nodes.forEach(node => { data[`${node.id}`] = node; }); return data; diff --git a/src/components/data/types.ts b/src/components/data/types.ts index d7dd979744..5cf8f2d966 100644 --- a/src/components/data/types.ts +++ b/src/components/data/types.ts @@ -2,6 +2,7 @@ import { InfiniteQueryObserverOptions, QueryObserverOptions } from 'react-query' export enum QueryType { NodeExecutionDetails = 'NodeExecutionDetails', + DynamicWorkflowFromNodeExecution = 'DynamicWorkflowFromNodeExecution', NodeExecution = 'nodeExecution', NodeExecutionList = 'nodeExecutionList', NodeExecutionChildList = 'nodeExecutionChildList', diff --git a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 3500918165..ab4bc03e4c 100644 --- a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,31 +1,124 @@ -import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformerDAGToReactFlow'; import * as React from 'react'; +import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; +import { useState, useEffect } from 'react'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; import { Legend } from './NodeStatusLegend'; +import { createDebugLogger } from '../utils'; + +const debug = createDebugLogger('@ReactFlowGraphComponent'); + +const nodeExecutionStatusChanged = (previous, nodeExecutionsById) => { + for (const exe in nodeExecutionsById) { + const oldStatus = previous[exe]?.closure.phase; + const newStatus = nodeExecutionsById[exe]?.closure.phase; + if (oldStatus != newStatus) { + return true; + } + } + return false; +}; + +const graphNodeCountChanged = (previous, data) => { + if (previous.nodes.length !== data.nodes.length) { + return true; + } else { + return false; + } +}; -/** - * Renders workflow graph using React Flow. - * @param props.data DAG from transformerWorkflowToDag - * @returns ReactFlow Graph as - */ const ReactFlowGraphComponent = props => { - const { data, onNodeSelectionChanged, nodeExecutionsById } = props; - const rfGraphJson = ConvertFlyteDagToReactFlows({ - root: data, + const { + data, + onNodeSelectionChanged, + nodeExecutionsById, + dynamicWorkflows + } = props; + const [state, setState] = useState({ + data: data, + dynamicWorkflows: dynamicWorkflows, + currentNestedView: {}, nodeExecutionsById: nodeExecutionsById, onNodeSelectionChanged: onNodeSelectionChanged, - maxRenderDepth: 1 - } as ConvertDagProps); + rfGraphJson: null + }); - const backgroundStyle = getRFBackground().nested; - const ReactFlowProps: RFWrapperProps = { - backgroundStyle, - rfGraphJson: rfGraphJson, - type: RFGraphTypes.main + const onAddNestedView = view => { + debug('@addNestedView:', view); + const currentView = state.currentNestedView[view.parent] || []; + const newView = { + [view.parent]: [...currentView, view.view] + }; + setState(state => ({ + ...state, + currentNestedView: { ...newView } + })); + }; + + const onRemoveNestedView = (viewParent, viewIndex) => { + const currentNestedView: any = { ...state.currentNestedView }; + currentNestedView[viewParent] = currentNestedView[viewParent]?.filter( + (item, i) => i <= viewIndex + ); + if (currentNestedView[viewParent]?.length < 1) { + delete currentNestedView[viewParent]; + } + setState(state => ({ + ...state, + currentNestedView + })); }; + const buildReactFlowGraphData = () => { + return ConvertFlyteDagToReactFlows({ + root: state.data, + nodeExecutionsById: state.nodeExecutionsById, + onNodeSelectionChanged: state.onNodeSelectionChanged, + onAddNestedView: onAddNestedView, + onRemoveNestedView: onRemoveNestedView, + currentNestedView: state.currentNestedView, + maxRenderDepth: 1 + } as ConvertDagProps); + }; + + useEffect(() => { + const newRFGraphData = buildReactFlowGraphData(); + setState(state => ({ + ...state, + rfGraphJson: newRFGraphData + })); + }, [state.currentNestedView]); + + useEffect(() => { + if (graphNodeCountChanged(state.data, data)) { + setState(state => ({ + ...state, + data: data + })); + } + if ( + nodeExecutionStatusChanged( + state.nodeExecutionsById, + nodeExecutionsById + ) + ) { + setState(state => ({ + ...state, + nodeExecutionsById: nodeExecutionsById + })); + } + }, [data, nodeExecutionsById]); + + useEffect(() => { + setState(state => ({ + ...state, + onNodeSelectionChanged: onNodeSelectionChanged + })); + }, [onNodeSelectionChanged]); + + const backgroundStyle = getRFBackground().nested; + const containerStyle: React.CSSProperties = { display: 'flex', flex: `1 1 100%`, @@ -34,12 +127,23 @@ const ReactFlowGraphComponent = props => { minWidth: '200px' }; - return ( -
- - -
- ); + const renderGraph = () => { + const ReactFlowProps: RFWrapperProps = { + backgroundStyle, + rfGraphJson: state.rfGraphJson, + type: RFGraphTypes.main, + nodeExecutionsById: nodeExecutionsById, + currentNestedView: state.currentNestedView + }; + return ( +
+ + +
+ ); + }; + + return state.rfGraphJson ? renderGraph() : <>; }; export default ReactFlowGraphComponent; diff --git a/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index e897721d47..de021fa325 100644 --- a/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -1,31 +1,28 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; -import ReactFlow, { - Background, - ReactFlowProvider, - useStoreState -} from 'react-flow-renderer'; -import { RFWrapperProps, LayoutRCProps } from './types'; +import { useState, useEffect, useCallback } from 'react'; +import ReactFlow, { Background } from 'react-flow-renderer'; +import { RFWrapperProps } from './types'; import { - ReactFlowCustomBranchNode, ReactFlowCustomEndNode, ReactFlowCustomNestedPoint, ReactFlowCustomStartNode, ReactFlowCustomTaskNode, - ReactFlowCustomSubworkflowNode, + ReactFlowSubWorkflowContainer, ReactFlowCustomMaxNested, ReactFlowStaticNested, ReactFlowStaticNode } from './customNodeComponents'; -import setReactFlowGraphLayout from './utils'; +import { getPositionedNodes, ReactFlowIdHash } from './utils'; +import { createDebugLogger } from '../utils'; + +const debug = createDebugLogger('@ReactFlowWrapper'); /** * Mapping for using custom nodes inside ReactFlow */ const CustomNodeTypes = { FlyteNode_task: ReactFlowCustomTaskNode, - FlyteNode_subworkflow: ReactFlowCustomSubworkflowNode, - FlyteNode_branch: ReactFlowCustomBranchNode, + FlyteNode_subworkflow: ReactFlowSubWorkflowContainer, FlyteNode_start: ReactFlowCustomStartNode, FlyteNode_end: ReactFlowCustomEndNode, FlyteNode_nestedStart: ReactFlowCustomNestedPoint, @@ -35,104 +32,74 @@ const CustomNodeTypes = { FlyteNode_staticNestedNode: ReactFlowStaticNested }; -/** - * Renderless component waits for ReactFlow to give rendered - * dimensions before computing layout - * @param props:LayoutRC - * @returns: void - */ -const LayoutRC: React.FC = ({ - setElements, - setLayout, - hasLayout -}: LayoutRCProps) => { - /* strore is only populated onLoad for each flow */ - const nodes = useStoreState(store => store.nodes); - const edges = useStoreState(store => store.edges); - - const [computeLayout, setComputeLayout] = useState(true); - - if (nodes.length > 0 && computeLayout) { - if (nodes[0].__rf.width) { - setComputeLayout(false); - } - } - - useEffect(() => { - if (!hasLayout && !computeLayout) { - setComputeLayout(true); - } - }, [hasLayout, computeLayout]); - - useEffect(() => { - if (!computeLayout) { - const nodesAndEdges = (nodes as any[]).concat(edges); - const { graph } = setReactFlowGraphLayout(nodesAndEdges, 'LR'); - setElements(graph); - setLayout(true); - } - }, [computeLayout]); - - return null; -}; - -/** - * Notes: - * To support nested graphs we wrap each flow inside its own provider/store - * which allows us to contextualize fitView to only render when its own - * elements change (not parents/children) - * - * Workflow: - * - set initial (unpositioned) elements and wait for onload - * - position elements (with rendered dimensions) in - * - fit view - * - * @see https://reactflow.dev/docs/ - * @param props:ReactFlowWrapperProps - * @returns rendered component - */ export const ReactFlowWrapper: React.FC = ({ rfGraphJson, backgroundStyle, + currentNestedView, version }) => { - const [elements, setElements] = useState(rfGraphJson); - const [currentVersion, setCurrentVersion] = useState(version); - const [hasLayout, setHasLayout] = useState(false); + const [state, setState] = useState({ + shouldUpdate: true, + nodes: rfGraphJson.nodes, + edges: rfGraphJson.edges, + version: version + }); + const [reactFlowInstance, setReactFlowInstance] = useState( null ); + useEffect(() => { + if (reactFlowInstance && state.shouldUpdate == false) { + reactFlowInstance?.fitView(); + } + }, [state.shouldUpdate, reactFlowInstance]); + + useEffect(() => { + setState(state => ({ + ...state, + shouldUpdate: true, + nodes: rfGraphJson.nodes, + edges: rfGraphJson.edges + })); + }, [rfGraphJson]); + const onLoad = (rf: any) => { setReactFlowInstance(rf); }; - useEffect(() => { - if (version != currentVersion) { - setHasLayout(false); - setElements(rfGraphJson); - } - }, [version, rfGraphJson, currentVersion]); + const onNodesChange = useCallback( + changes => { + if (changes.length == state.nodes.length && state.shouldUpdate) { + const nodesWithDimensions: any[] = []; + for (let i = 0; i < changes.length; i++) { + nodesWithDimensions.push({ + ...state.nodes[i], + ['dimensions']: changes[i].dimensions + }); + } + const positionedNodes = getPositionedNodes( + nodesWithDimensions, + state.edges, + currentNestedView, + 'LR' + ); + const { hashGraph, hashEdges } = ReactFlowIdHash( + positionedNodes, + state.edges + ); - /** - * Note: setLayout passed/called by - */ - useEffect(() => { - if (hasLayout && reactFlowInstance) { - reactFlowInstance?.fitView({ padding: 0 }); - setCurrentVersion(version); - } - }, [hasLayout, reactFlowInstance]); - /** - * STEPS: - * - have each node click return nodes {text.data} - * - Then figure out the input needed for the slide out (execution id? node id?) - * - Append that to the rf nodes {data} object. - */ + setState(state => ({ + ...state, + shouldUpdate: false, + nodes: hashGraph, + edges: hashEdges + })); + } + }, + [state.shouldUpdate] + ); - /** - * React Flow's min height to make it render - */ const reactFlowStyle: React.CSSProperties = { display: 'flex', flex: `1 1 100%`, @@ -140,24 +107,20 @@ export const ReactFlowWrapper: React.FC = ({ }; return ( - - - - - - + + + ); }; diff --git a/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 9e865e71ce..504cf60300 100644 --- a/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -1,59 +1,15 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; -import { - Handle, - getBezierPath, - getMarkerEnd, - Position -} from 'react-flow-renderer'; +import { Handle, Position } from 'react-flow-renderer'; import { dTypes } from 'models/Graph/types'; -import { ReactFlowWrapper } from './ReactFlowWrapper'; -import setReactFlowGraphLayout, { +import { COLOR_TASK_TYPE, COLOR_GRAPH_BACKGROUND, getGraphHandleStyle, getGraphNodeStyle, - getRFBackground, - getNestedContainerStyle, - getNestedGraphContainerStyle + getNestedContainerStyle } from './utils'; -import { RFGraphTypes, RFHandleProps } from './types'; - -export const customEdge = ( - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - style = {}, - data, - arrowHeadType, - markerEndId -) => { - const edgePath = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition - }); - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); - - return ( - <> - - - ); -}; +import { RFHandleProps } from './types'; export const renderDefaultHandles = ( id: string, @@ -61,14 +17,14 @@ export const renderDefaultHandles = ( targetStyle: any ) => { const leftHandleProps: RFHandleProps = { - id: `rf-handle-left-${id}`, + id: `edge-left-${id}`, type: 'target', position: Position.Left, style: targetStyle }; const rightHandleProps: RFHandleProps = { - id: `rf-handle-right-${id}`, + id: `edge-right-${id}`, type: 'source', position: Position.Right, style: sourceStyle @@ -105,11 +61,7 @@ export const renderStardEndHandles = (data: any) => { style: style }; - return ( - <> - - - ); + return ; }; /** @@ -158,8 +110,12 @@ export const ReactFlowCustomMaxNested = ({ data }: any) => { ); }; + const onClick = e => { + data.onAddNestedView(); + }; + return ( -
+
{data.taskType ? renderTaskType() : null}
{data.text}
{renderDefaultHandles( @@ -252,9 +208,6 @@ export const ReactFlowStaticNode = ({ data }: any) => { */ export const ReactFlowCustomTaskNode = ({ data }: any) => { - // console.log(`\n\n@ReactFlowCustomTaskNode: ${data.text}`); - // console.log('\t data.nodeType:', dTypes[data.nodeType]); - // console.log('\t data.nodeExecutionStatus:', data.nodeExecutionStatus); const styles = getGraphNodeStyle(data.nodeType, data.nodeExecutionStatus); const onNodeSelectionChanged = data.onNodeSelectionChanged; const [selectedNode, setSelectedNode] = useState(false); @@ -290,20 +243,7 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => {
); }; - - /** - * @TODO - * Decide if we want to make all nodes clickable - */ - // const isClickable = - // data.nodeExecutionStatus == RFNodeExecutionStatus.executed; - return ( - //
{data.taskType ? renderTaskType() : null}
{data.text}
@@ -321,61 +261,126 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { * and any edge handles. * @param props.data data property of ReactFlowGraphNodeData */ -export const ReactFlowCustomSubworkflowNode = ({ data }: any) => { - const { dag } = data; - const backgroundStyle = getRFBackground().nested; +export const ReactFlowSubWorkflowContainer = ({ data }: any) => { + const BREAD_FONT_SIZE = '9px'; + const BREAD_COLOR_ACTIVE = '#8B37FF'; + const BREAD_COLOR_INACTIVE = '#000'; const borderStyle = getNestedContainerStyle(data.nodeExecutionStatus); - const { estimatedDimensions } = setReactFlowGraphLayout(dag, 'LR', true); - const graphContainer = getNestedGraphContainerStyle(estimatedDimensions); - return ( - <> - {renderDefaultHandles( - data.scopedId, - getGraphHandleStyle('source'), - getGraphHandleStyle('target') - )} -
-
- -
-
- - ); -}; -/** - * Custom component renders Branch nodes as indepdenet flow - * and any edge handles. - * @param props.data data property of ReactFlowGraphNodeData - */ -export const ReactFlowCustomBranchNode = ({ data }: any) => { - const { dag } = data; - console.log('@ReactFlowCustomBranchNode: data', data); - const backgroundStyle = getRFBackground().nested; - const borderStyle = getNestedContainerStyle(data.nodeExecutionStatus); - const { estimatedDimensions } = setReactFlowGraphLayout(dag, 'LR', true); - const graphContainer = getNestedGraphContainerStyle(estimatedDimensions); + const handleNestedViewClick = e => { + const index = e.target.id.substr( + e.target.id.indexOf('_') + 1, + e.target.id.length + ); + data.onRemoveNestedView(data.scopedId, index); + }; + + const handleRootClick = () => { + data.onRemoveNestedView(data.scopedId, -1); + }; + + const currentNestedDepth = data.currentNestedView?.length || 0; + + const BreadElement = ({ nestedView, index }) => { + const liStyles: React.CSSProperties = { + cursor: 'pointer', + fontSize: BREAD_FONT_SIZE, + color: BREAD_COLOR_ACTIVE + }; + + const liStyleInactive: React.CSSProperties = { ...liStyles }; + liStyleInactive['color'] = BREAD_COLOR_INACTIVE; + + const beforeStyle: React.CSSProperties = { + cursor: 'pointer', + color: BREAD_COLOR_ACTIVE, + padding: '0 .2rem', + fontSize: BREAD_FONT_SIZE + }; + const onClick = + currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; + return ( +
  • + {index == 0 ? {'>'} : null} + {nestedView} + {index < currentNestedDepth - 1 ? ( + {'>'} + ) : null} +
  • + ); + }; + + const BorderElement = props => { + return
    {props.children}
    ; + }; + + const BorderContainer = props => { + let output = BorderElement(props); + for (let i = 0; i < currentNestedDepth; i++) { + output = {output}; + } + return output; + }; + + const renderBreadCrumb = () => { + const breadContainerStyle: React.CSSProperties = { + position: 'absolute', + display: 'flex', + width: '100%', + marginTop: '-1rem' + }; + const olStyles: React.CSSProperties = { + margin: 0, + padding: 0, + display: 'flex', + listStyle: 'none', + listStyleImage: 'none', + minWidth: '1rem' + }; + const headerStyle: React.CSSProperties = { + color: BREAD_COLOR_ACTIVE, + fontSize: BREAD_FONT_SIZE, + margin: 0, + padding: 0 + }; + + const rootClick = currentNestedDepth > 0 ? handleRootClick : undefined; + return ( +
    +
    + {data.text} +
    +
      + {data.currentNestedView?.map((nestedView, i) => { + return ( + + ); + })} +
    +
    + ); + }; return ( <> - {renderDefaultHandles( - data.scopedId, - getGraphHandleStyle('source'), - getGraphHandleStyle('target') - )} -
    -
    - -
    -
    + {renderBreadCrumb()} + + {renderDefaultHandles( + data.scopedId, + getGraphHandleStyle('source'), + getGraphHandleStyle('target') + )} + ); }; diff --git a/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx new file mode 100644 index 0000000000..ecb54a6e07 --- /dev/null +++ b/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -0,0 +1,397 @@ +import { dEdge, dNode, dTypes } from 'models/Graph/types'; +import { ReactFlowGraphConfig } from './utils'; +import { Edge, Node, Position } from 'react-flow-renderer'; +import { NodeExecutionPhase } from 'models/Execution/enums'; +import { ConvertDagProps } from './types'; +import { createDebugLogger } from '../utils'; + +interface rfNode extends Node { + isRootParentNode?: boolean; +} + +interface rfEdge extends Edge { + parent?: string; +} + +interface ReactFlowGraph { + nodes: any; + edges: any; +} + +const debug = createDebugLogger('@TransformerDAGToReactFlow'); + +export interface ReactFlowGraphMapping { + root: ReactFlowGraph; + rootParentMap: { + [key: string]: { + [key: string]: ReactFlowGraph; + }; + }; +} + +export const buildCustomNodeName = (type: dTypes) => { + return `${ReactFlowGraphConfig.customNodePrefix}_${dTypes[type]}`; +}; + +export const nodeHasChildren = (node: dNode) => { + return node.nodes.length > 0; +}; + +export const isStartOrEndEdge = edge => { + return edge.sourceId == 'start-node' || edge.targetId == 'end-node'; +}; + +interface BuildDataProps { + node: dNode; + nodeExecutionsById: any; + onNodeSelectionChanged: any; + onAddNestedView: any; + onRemoveNestedView: any; + rootParentNode: dNode; + currentNestedView: string[]; +} +export const buildReactFlowDataProps = (props: BuildDataProps) => { + const { + node, + nodeExecutionsById, + onNodeSelectionChanged, + onAddNestedView, + onRemoveNestedView, + rootParentNode, + currentNestedView + } = props; + + const taskType = node.value?.template ? node.value.template.type : null; + const displayName = node.name; + const mapNodeExecutionStatus = () => { + if (nodeExecutionsById) { + if (nodeExecutionsById[node.scopedId]) { + return nodeExecutionsById[node.scopedId].closure + .phase as NodeExecutionPhase; + } else { + return NodeExecutionPhase.SKIPPED; + } + } else { + return NodeExecutionPhase.UNDEFINED; + } + }; + const nodeExecutionStatus = mapNodeExecutionStatus(); + + const dataProps = { + nodeExecutionStatus: nodeExecutionStatus, + text: displayName, + handles: [], + nodeType: node.type, + scopedId: node.scopedId, + taskType: taskType, + onNodeSelectionChanged: () => { + if (onNodeSelectionChanged) { + onNodeSelectionChanged([node.scopedId]); + } + }, + onAddNestedView: () => { + onAddNestedView({ + parent: rootParentNode.scopedId, + view: node.scopedId + }); + }, + onRemoveNestedView + }; + + for (const rootParentId in currentNestedView) { + if (node.scopedId == rootParentId) { + dataProps['currentNestedView'] = currentNestedView[rootParentId]; + } + } + + return dataProps; +}; + +interface BuildNodeProps { + node: dNode; + dataProps: any; + rootParentNode?: dNode; + parentNode?: dNode; + typeOverride?: dTypes; +} +export const buildReactFlowNode = ({ + node, + dataProps, + rootParentNode, + parentNode, + typeOverride +}: BuildNodeProps): rfNode => { + const output: rfNode = { + id: node.scopedId, + type: buildCustomNodeName(typeOverride || node.type), + data: { text: node.scopedId }, + position: { x: 0, y: 0 }, + style: {}, + sourcePosition: Position.Right, + targetPosition: Position.Left + }; + if (rootParentNode) { + output['parentNode'] = rootParentNode.scopedId; + } else if (parentNode) { + output['parentNode'] = parentNode.scopedId; + } + + if (output.parentNode == node.scopedId) { + delete output.parentNode; + } + + output['data'] = buildReactFlowDataProps({ + ...dataProps, + node, + rootParentNode + }); + return output; +}; +interface BuildEdgeProps { + edge: dEdge; + rootParentNode?: dNode; + parentNode?: dNode; +} +export const buildReactFlowEdge = ({ + edge, + rootParentNode +}: BuildEdgeProps): rfEdge => { + const output = { + id: `${edge.sourceId}->${edge.targetId}`, + source: edge.sourceId, + target: edge.targetId, + arrowHeadType: ReactFlowGraphConfig.arrowHeadType, + type: ReactFlowGraphConfig.edgeType + } as rfEdge; + + if (rootParentNode) { + output['parent'] = rootParentNode.scopedId; + output['zIndex'] = 1; + } + + return output; +}; + +export const edgesToArray = edges => { + return Object.values(edges); +}; + +export const nodesToArray = nodes => { + return Object.values(nodes); +}; + +export const buildGraphMapping = (props): ReactFlowGraphMapping => { + const dag: dNode = props.root; + const { + nodeExecutionsById, + onNodeSelectionChanged, + onAddNestedView, + onRemoveNestedView, + currentNestedView, + isStaticGraph + } = props; + const nodeDataProps = { + nodeExecutionsById, + onNodeSelectionChanged, + onAddNestedView, + onRemoveNestedView, + currentNestedView + }; + const root: ReactFlowGraph = { + nodes: {}, + edges: {} + }; + const rootParentMap = {}; + + interface ParseProps { + nodeDataProps: any; + contextNode: dNode; + contextParent?: dNode; + rootParentNode?: dNode; + } + const parse = (props: ParseProps) => { + const { + contextNode, + contextParent, + rootParentNode, + nodeDataProps + } = props; + let context: ReactFlowGraph | null = null; + contextNode.nodes.map((node: dNode) => { + /* Case: node has children => recurse */ + if (nodeHasChildren(node)) { + if (rootParentNode) { + parse({ + contextNode: node, + contextParent: node, + rootParentNode: rootParentNode, + nodeDataProps: nodeDataProps + }); + } else { + parse({ + contextNode: node, + contextParent: node, + rootParentNode: node, + nodeDataProps: nodeDataProps + }); + } + } + + if (rootParentNode) { + const rootParentId = rootParentNode.scopedId; + const contextParentId = contextParent?.scopedId; + rootParentMap[rootParentId] = rootParentMap[rootParentId] || {}; + rootParentMap[rootParentId][contextParentId] = rootParentMap[ + rootParentId + ][contextParentId] || { + nodes: {}, + edges: {} + }; + context = rootParentMap[rootParentId][ + contextParentId + ] as ReactFlowGraph; + const reactFlowNode = buildReactFlowNode({ + node: node, + dataProps: nodeDataProps, + rootParentNode: rootParentNode, + parentNode: contextParent, + typeOverride: + isStaticGraph == true ? dTypes.staticNode : undefined + }); + context.nodes[reactFlowNode.id] = reactFlowNode; + } else { + const reactFlowNode = buildReactFlowNode({ + node: node, + dataProps: nodeDataProps, + typeOverride: + isStaticGraph == true ? dTypes.staticNode : undefined + }); + root.nodes[reactFlowNode.id] = reactFlowNode; + } + }); + contextNode.edges.map((edge: dEdge) => { + const reactFlowEdge = buildReactFlowEdge({ edge, rootParentNode }); + if (rootParentNode && context) { + context.edges[reactFlowEdge.id] = reactFlowEdge; + } else { + root.edges[reactFlowEdge.id] = reactFlowEdge; + } + }); + }; + + parse({ contextNode: dag, nodeDataProps: nodeDataProps }); + + return { + root, + rootParentMap + }; +}; + +export interface RenderGraphProps { + graphMapping: any; + currentNestedView?: any[]; + maxRenderDepth?: number; + isStaticGraph?: boolean; +} +export const renderGraph = ({ + graphMapping, + currentNestedView, + maxRenderDepth = 0, + isStaticGraph = false +}) => { + debug('\t graphMapping:', graphMapping); + debug('\t currentNestedView:', currentNestedView); + if (maxRenderDepth > 0 && !isStaticGraph) { + const nestedChildGraphs: ReactFlowGraph = { + nodes: {}, + edges: {} + }; + const nestedContent: string[] = []; + + /** + * Compute which nested content will be populated into a subworkflow container. + * + * Function returns array of id's. These id's are then matched to rootParentMap + * values for determining which nodes to show + * + * Note: currentNestedView is a mapping where + * k: rootParentId + * v: array of nested depth with last value as current view + */ + for (const nestedParentId in graphMapping.rootParentMap) { + const rootParent = currentNestedView[nestedParentId]; + if (rootParent) { + const currentView = rootParent[rootParent.length - 1]; + nestedContent.push(currentView); + } else { + nestedContent.push(nestedParentId); + } + } + + for (const rootParentId in graphMapping.rootParentMap) { + const parentMapContext = graphMapping.rootParentMap[rootParentId]; + for (let i = 0; i < nestedContent.length; i++) { + const nestedChildGraphId = nestedContent[i]; + if (parentMapContext[nestedChildGraphId]) { + nestedChildGraphs.nodes = { + ...nestedChildGraphs.nodes, + ...parentMapContext[nestedChildGraphId].nodes + }; + nestedChildGraphs.edges = { + ...nestedChildGraphs.edges, + ...parentMapContext[nestedChildGraphId].edges + }; + } + } + for (const parentKey in graphMapping.root.nodes) { + const parentNode = graphMapping.root.nodes[parentKey]; + if (parentNode.id == rootParentId) { + parentNode['isRootParentNode'] = true; + parentNode['style'] = {}; + } + } + /** + * @TODO refactor this; we need this step but can prob be done better + * The issue is that somehow/somewhere root-level nodes are being added + * to these nestedGraphs and if the appear in the output they break + * reactFlow because a node can have a self-referencing "parentNode" + * + * eg. { id: "n0", parentNode: "n0"} will break + * + */ + for (const nodeId in nestedChildGraphs.nodes) { + const node = nestedChildGraphs.nodes[nodeId]; + for (const rootId in graphMapping.rootParentMap) { + if (node.id == rootId) { + delete nestedChildGraphs.nodes[nodeId]; + } else { + if (node.type == 'FlyteNode_subworkflow') { + node.type = 'FlyteNode_nestedMaxDepth'; + } + } + } + } + } + const output = { ...graphMapping.root }; + output.nodes = { ...output.nodes, ...nestedChildGraphs.nodes }; + output.edges = { ...output.edges, ...nestedChildGraphs.edges }; + output.nodes = nodesToArray(output.nodes); + output.edges = edgesToArray(output.edges); + return output; + } else { + const output = { ...graphMapping.root }; + output.nodes = nodesToArray(output.nodes); + output.edges = edgesToArray(output.edges); + return output; + } +}; + +export const ConvertFlyteDagToReactFlows = (props: ConvertDagProps) => { + const graphMapping: ReactFlowGraphMapping = buildGraphMapping(props); + return renderGraph({ + graphMapping: graphMapping, + currentNestedView: props.currentNestedView, + maxRenderDepth: props.maxRenderDepth, + isStaticGraph: props.isStaticGraph + }); +}; diff --git a/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx b/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx deleted file mode 100644 index a693853b0f..0000000000 --- a/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { dEdge, dTypes } from 'models/Graph/types'; -import { ReactFlowGraphConfig } from './utils'; -import { Edge, Elements, Node, Position } from 'react-flow-renderer'; -import { NodeExecutionPhase } from 'models/Execution/enums'; -import { BuildRFNodeProps, ConvertDagProps, DagToFRProps } from './types'; - -export const buildCustomNodeName = (type: dTypes) => { - return `${ReactFlowGraphConfig.customNodePrefix}_${dTypes[type]}`; -}; - -export const buildReactFlowEdge = (edge: dEdge): Edge => { - return { - id: `[${edge.sourceId}]->[${edge.targetId}]`, - source: edge.sourceId, - target: edge.targetId, - sourceHandle: 'left-handle', - arrowHeadType: ReactFlowGraphConfig.arrowHeadType, - type: ReactFlowGraphConfig.edgeType - } as Edge; -}; - -export const buildReactFlowNode = (props: BuildRFNodeProps): Node => { - const { - dNode, - dag, - nodeExecutionsById, - typeOverride, - onNodeSelectionChanged - } = props; - - const type = typeOverride ? typeOverride : dNode.type; - const taskType = dNode?.value?.template ? dNode.value.template.type : null; - const displayName = dNode.name; - - const mapNodeExecutionStatus = () => { - if (nodeExecutionsById) { - if (nodeExecutionsById[dNode.scopedId]) { - return nodeExecutionsById[dNode.scopedId].closure - .phase as NodeExecutionPhase; - } else { - return NodeExecutionPhase.SKIPPED; - } - } else { - return NodeExecutionPhase.UNDEFINED; - } - }; - const nodeExecutionStatus = mapNodeExecutionStatus(); - - const dataProps = { - nodeExecutionStatus: nodeExecutionStatus, - text: displayName, - handles: [], - nodeType: type, - scopedId: dNode.scopedId, - dag: dag, - taskType: taskType, - onNodeSelectionChanged: () => { - if (onNodeSelectionChanged) { - onNodeSelectionChanged([dNode.scopedId]); - } - } - }; - - return { - id: dNode.scopedId, - type: buildCustomNodeName(type), - data: dataProps, - position: { x: 0, y: 0 }, - sourcePosition: Position.Right, - targetPosition: Position.Left - } as Node; -}; - -export const nodeMapToArr = map => { - const output: any[] = []; - for (const k in map) { - output.push(map[k]); - } - return output; -}; - -export const dagToReactFlow = (props: DagToFRProps) => { - const { - root, - nodeExecutionsById, - currentDepth, - onNodeSelectionChanged, - maxRenderDepth, - isStaticGraph - } = props; - - const nodes: any = {}; - const edges: any = {}; - - root.nodes?.map(dNode => { - /* Base props to build RF Node */ - const buildNodeProps = { - dNode: dNode, - dag: [], - nodeExecutionsById: nodeExecutionsById, - typeOverride: null, - onNodeSelectionChanged: onNodeSelectionChanged, - isStaticGraph: isStaticGraph - } as BuildRFNodeProps; - if (dNode.nodes?.length > 0 && currentDepth <= maxRenderDepth) { - /* Note: currentDepth will be replaced once nested toggle */ - if (currentDepth == maxRenderDepth) { - buildNodeProps.typeOverride = isStaticGraph - ? dTypes.staticNestedNode - : dTypes.nestedMaxDepth; - } else { - const nestedDagProps: DagToFRProps = { - root: dNode, - nodeExecutionsById: nodeExecutionsById, - currentDepth: currentDepth + 1, - onNodeSelectionChanged: onNodeSelectionChanged, - maxRenderDepth: maxRenderDepth, - isStaticGraph: isStaticGraph - }; - buildNodeProps.dag = dagToReactFlow(nestedDagProps); - buildNodeProps.typeOverride = isStaticGraph - ? dTypes.staticNode - : null; - } - } else { - buildNodeProps.typeOverride = isStaticGraph - ? dTypes.staticNode - : null; - } - /* Build and add node to map */ - nodes[dNode.id] = buildReactFlowNode(buildNodeProps); - }); - root.edges?.map(edge => { - const rfEdge = buildReactFlowEdge(edge); - edges[rfEdge.id] = rfEdge; - }); - const output = nodeMapToArr(nodes).concat(nodeMapToArr(edges)); - return output; -}; - -export const ConvertFlyteDagToReactFlows = ( - props: ConvertDagProps -): Elements => { - const dagProps = { ...props, currentDepth: 0 } as DagToFRProps; - const rfJson = dagToReactFlow(dagProps); - return rfJson; -}; diff --git a/src/components/flytegraph/ReactFlow/types.ts b/src/components/flytegraph/ReactFlow/types.ts index 49412edcf8..7d99e7a9fd 100644 --- a/src/components/flytegraph/ReactFlow/types.ts +++ b/src/components/flytegraph/ReactFlow/types.ts @@ -1,12 +1,14 @@ import { NodeExecutionsById } from 'models/Execution/types'; import { dNode, dTypes } from 'models/Graph/types'; -import { Elements, HandleProps } from 'react-flow-renderer'; +import { HandleProps } from 'react-flow-renderer'; export interface RFWrapperProps { - rfGraphJson: Elements; + rfGraphJson: any; backgroundStyle: RFBackgroundProps; - type: RFGraphTypes; + type?: RFGraphTypes; + currentNestedView?: any; onNodeSelectionChanged?: any; + nodeExecutionsById?: any; version?: string; } @@ -22,9 +24,9 @@ export enum RFGraphTypes { } export interface LayoutRCProps { - setElements: any; - setLayout: any; - hasLayout: boolean; + setPositionedElements: any; + graphData: any; + nodeExecutionsById: any; } /* React Flow params and styles (background is styles) */ @@ -40,6 +42,9 @@ export interface BuildRFNodeProps { nodeExecutionsById: any; typeOverride: dTypes | null; onNodeSelectionChanged: any; + onAddNestedView?: any; + onRemoveNestedView?: any; + currentNestedView?: any; isStaticGraph: boolean; } @@ -47,12 +52,16 @@ export interface ConvertDagProps { root: dNode; nodeExecutionsById: any; onNodeSelectionChanged: any; + onRemoveNestedView?: any; + onAddNestedView?: any; + currentNestedView?: any; maxRenderDepth: number; - isStaticGraph: boolean; + isStaticGraph?: boolean; } -export interface DagToFRProps extends ConvertDagProps { +export interface DagToReactFlowProps extends ConvertDagProps { currentDepth: number; + parents: any; } export interface RFCustomData { @@ -64,4 +73,6 @@ export interface RFCustomData { dag: any; taskType?: dTypes; onNodeSelectionChanged?: any; + onAddNestedView: any; + onRemoveNestedView: any; } diff --git a/src/components/flytegraph/ReactFlow/utils.tsx b/src/components/flytegraph/ReactFlow/utils.tsx index 01d017c9fb..e9b34e25a7 100644 --- a/src/components/flytegraph/ReactFlow/utils.tsx +++ b/src/components/flytegraph/ReactFlow/utils.tsx @@ -1,7 +1,6 @@ import { NodeExecutionPhase } from 'models/Execution/enums'; import { dTypes } from 'models/Graph/types'; import { CSSProperties } from 'react'; -import { Elements, isNode, Position } from 'react-flow-renderer'; import { RFBackgroundProps } from './types'; const dagre = require('dagre'); @@ -10,6 +9,7 @@ export const COLOR_EXECUTED = '#2892f4'; export const COLOR_NOT_EXECUTED = '#c6c6c6'; export const COLOR_TASK_TYPE = '#666666'; export const COLOR_GRAPH_BACKGROUND = '#666666'; +export const GRAPH_PADDING_FACTOR = 50; export const DISPLAY_NAME_START = 'start'; export const DISPLAY_NAME_END = 'end'; @@ -22,6 +22,17 @@ export const ReactFlowGraphConfig = { edgeType: 'default' }; +/** + * Function replaces all retry values with '0' to be used a key between static/runtime + * values. + * @param id NodeExcution nodeId. + * @returns nodeId with all retry values replaces with '0' + */ +export const retriesToZero = (id: string): string => { + const output = id.replace(/(-[0-9]-)/g, '-0-'); + return output; +}; + export const getGraphHandleStyle = ( handleType: string, type?: dTypes @@ -133,7 +144,10 @@ export const getNestedContainerStyle = nodeExecutionStatus => { const style = { border: `1px dashed ${getStatusColor(nodeExecutionStatus)}`, borderRadius: '8px', - background: 'rgba(255,255,255,.9)' + background: 'rgba(255,255,255,.9)', + width: '100%', + height: '100%', + padding: '.25rem' } as React.CSSProperties; return style; }; @@ -154,7 +168,9 @@ export const getGraphNodeStyle = ( minWidth: '.5rem', minHeight: '.5rem', height: 'auto', - width: 'auto' + width: 'auto', + zIndex: 100000, + position: 'relative' }; const nestedPoint = { @@ -237,43 +253,87 @@ export const getRFBackground = () => { border: '1px solid #444', backgroundColor: 'rgba(255,255,255,1)' }, - gridColor: '#ccc', - gridSpacing: 20 + gridColor: 'none' } as RFBackgroundProps, nested: { - gridColor: 'none', - gridSpacing: 1 + gridColor: 'none' } as RFBackgroundProps, static: { background: { border: 'none', backgroundColor: 'rgb(255,255,255)' }, - gridColor: 'none', - gridSpacing: 20 + gridColor: 'none' } as RFBackgroundProps }; }; +export interface PositionProps { + nodes: any; + edges: any; + parentMap?: any; + direction?: string; +} + /** - * Uses dagree/graphlib to compute graph layout - * @see https://github.com/dagrejs/dagre/wiki - * @param elements Graph elements (nodes/edges) in JSON format - * @param direction Direction to render graph + * Computes positions for provided nodes + * @param PositionProps * @returns */ -export const setReactFlowGraphLayout = ( - elements: Elements, - direction: string, - estimate = false -) => { +export const computeChildNodePositions = ({ + nodes, + edges, + direction = 'LR' +}: PositionProps) => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); - const isHorizontal = direction === 'LR'; - - const ESTIMATE_HEIGHT = 25; - const ESTIMATE_WIDTH_FACTOR = 6; + dagreGraph.setGraph({ + rankdir: direction, + edgesep: 60, + nodesep: 30, + ranker: 'longest-path', + acyclicer: 'greedy' + }); + nodes.map(n => { + dagreGraph.setNode(n.id, n.dimensions); + }); + edges.map(e => { + dagreGraph.setEdge(e.source, e.target); + }); + dagre.layout(dagreGraph); + const dimensions = { + width: dagreGraph.graph().width, + height: dagreGraph.graph().height + }; + const graph = nodes.map(el => { + const node = dagreGraph.node(el.id); + const x = node.x - node.width / 2; + const y = node.y - node.height / 2; + return { + ...el, + position: { + x: x, + y: y + } + }; + }); + return { graph, dimensions }; +}; +/** + * Computes positions for root-level nodes in a graph by filtering out + * all children (nodes that have parents). + * @param PositionProps + * @returns + */ +export const computeRootNodePositions = ({ + nodes, + edges, + parentMap, + direction = 'LR' +}: PositionProps) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); dagreGraph.setGraph({ rankdir: direction, edgesep: 60, @@ -282,69 +342,189 @@ export const setReactFlowGraphLayout = ( acyclicer: 'greedy' }); - /** - * Note: this waits/assumes rendered dimensions from ReactFlow as .__rf - */ - elements.forEach(el => { - if (isNode(el)) { - const nodeWidth = estimate - ? el.data.text.length * ESTIMATE_WIDTH_FACTOR - : el.__rf.width; - const nodeHeight = estimate ? ESTIMATE_HEIGHT : el.__rf.height; - dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight }); - } else { - dagreGraph.setEdge(el.source, el.target); + /* Filter all children from creating dagree nodes */ + nodes.map(n => { + if (n.isRootParentNode) { + dagreGraph.setNode(n.id, { + width: parentMap[n.id].childGraphDimensions.width, + height: parentMap[n.id].childGraphDimensions.height + }); + } else if (!n.parentNode) { + dagreGraph.setNode(n.id, n.dimensions); } }); + /* Filter all children from creating dagree edges */ + edges.map(e => { + if (!e.parent) { + dagreGraph.setEdge(e.source, e.target); + } + }); + + /* Compute graph posistions for root-level nodes */ dagre.layout(dagreGraph); - const graphWidth = dagreGraph.graph().width; - const graphHeight = dagreGraph.graph().height; - if (estimate) { - return { - estimatedDimensions: { - width: graphWidth, - height: graphHeight + const dimensions = { + width: dagreGraph.graph().width, + height: dagreGraph.graph().height + }; + + /* Merge dagre positions to rf elements*/ + const graph = nodes.map(el => { + const node = dagreGraph.node(el.id); + if (node) { + const x = node.x - node.width / 2; + const y = node.y - node.height / 2; + if (parentMap && el.isRootParentNode) { + el.style = parentMap[el.id].childGraphDimensions; } - }; - } else { - return { - graph: elements.map(el => { - if (isNode(el)) { - el.targetPosition = isHorizontal - ? Position.Left - : Position.Top; - el.sourcePosition = isHorizontal - ? Position.Right - : Position.Bottom; - const nodeWidth = estimate - ? el.data.text.length * ESTIMATE_WIDTH_FACTOR - : el.__rf.width; - const nodeHeight = estimate - ? ESTIMATE_HEIGHT - : el.__rf.height; - const nodeWithPosition = dagreGraph.node(el.id); - - /** Keep both position and .__rf.position in sync */ - const x = nodeWithPosition.x - nodeWidth / 2; - const y = nodeWithPosition.y - nodeHeight / 2; - el.position = { - x: x, - y: y - }; - el.__rf.position = { - x: x, - y: y + return { + ...el, + position: { + x: x, + y: y + } + }; + } else if (parentMap) { + /* Case: Overwrite children positions with computed values */ + const parent = parentMap[el.parentNode]; + for (let i = 0; i < parent.nodes.length; i++) { + const node = parent.nodes[i]; + if (node.id == el.id) { + return { + ...el, + position: { ...node.position } }; } - return el; - }), - dimensions: { - width: graphWidth, - height: graphHeight } + } + }); + return { graph, dimensions }; +}; + +/** + * Returns positioned nodes and edges. + * + * Note: Handles nesting by first "rendering" all child graphs to calculate their rendered + * dimensions and setting those values as the dimentions (width/height) for parent/container. + * Once those dimensions have been set (for parent/container nodes) we can set root-level node + * positions. + * + * @param nodes + * @param edges + * @param currentNestedView + * @param direction + * @returns Array of ReactFlow nodes + */ +export const getPositionedNodes = ( + nodes, + edges, + currentNestedView, + direction = 'LR' +) => { + const parentMap = {}; + /* (1) Collect all child graphs in parentMap */ + nodes.forEach(node => { + if (node.isRootParentNode) { + parentMap[node.id] = { + nodes: [], + edges: [], + childGraphDimensions: { + width: 0, + height: 0 + }, + self: node + }; + } + if (node.parentNode) { + if (parentMap[node.parentNode]) { + if (parentMap[node.parentNode].nodes) { + parentMap[node.parentNode].nodes.push(node); + } else { + parentMap[node.parentNode].nodes = [node]; + } + } + } + }); + edges.forEach(edge => { + if (edge.parent) { + if (parentMap[edge.parent]) { + if (parentMap[edge.parent].edges) { + parentMap[edge.parent].edges.push(edge); + } else { + parentMap[edge.parent].edges = [edge]; + } + } + } + }); + + /* (2) Compute child graph positiions/dimensions */ + for (const parentId in parentMap) { + const children = parentMap[parentId]; + const childGraph = computeChildNodePositions({ + nodes: children.nodes, + edges: children.edges, + direction: direction + }); + let nestedDepth = 1; + if ( + currentNestedView && + currentNestedView[parentId] && + currentNestedView[parentId].length > 0 + ) { + nestedDepth = currentNestedView[parentId].length; + } + const borderPadding = GRAPH_PADDING_FACTOR * nestedDepth; + const width = childGraph.dimensions.width + borderPadding; + const height = childGraph.dimensions.height + borderPadding; + + parentMap[parentId].childGraphDimensions = { + width: width, + height: height }; + const relativePosNodes = childGraph.graph.map(node => { + const position = node.position; + position.y = position.y + GRAPH_PADDING_FACTOR / 2; + position.x = position.x + GRAPH_PADDING_FACTOR / 2; + return { + ...node, + position + }; + }); + parentMap[parentId].nodes = relativePosNodes; + parentMap[parentId].self.dimensions.width = width; + parentMap[parentId].self.dimensions.height = height; } + /* (3) Compute positions of root-level nodes */ + const { graph, dimensions } = computeRootNodePositions({ + nodes: nodes, + edges: edges, + direction: direction, + parentMap: parentMap + }); + return graph; }; -export default setReactFlowGraphLayout; +export const ReactFlowIdHash = (nodes, edges) => { + const key = Math.floor(Math.random() * 10000).toString(); + const properties = ['id', 'source', 'target', 'parent', 'parentNode']; + const hashGraph = nodes.map(node => { + const updates = {}; + properties.forEach(prop => { + if (node[prop]) { + updates[prop] = `${key}-${node[prop]}`; + } + }); + return { ...node, ...updates }; + }); + + const hashEdges = edges.map(edge => { + const updates = {}; + properties.forEach(prop => { + if (edge[prop]) { + updates[prop] = `${key}-${edge[prop]}`; + } + }); + return { ...edge, ...updates }; + }); + return { hashGraph, hashEdges }; +}; diff --git a/src/models/Common/constants.ts b/src/models/Common/constants.ts index fbd8b3c218..0fab59b181 100644 --- a/src/models/Common/constants.ts +++ b/src/models/Common/constants.ts @@ -9,6 +9,7 @@ export const endpointPrefixes = { launchPlan: '/launch_plans', namedEntity: '/named_entities', nodeExecution: '/node_executions', + dynamicWorkflowExecution: '/data/node_executions', project: '/projects', relaunchExecution: '/executions/relaunch', recoverExecution: '/executions/recover', diff --git a/src/models/Execution/api.ts b/src/models/Execution/api.ts index 8778719928..450c280404 100644 --- a/src/models/Execution/api.ts +++ b/src/models/Execution/api.ts @@ -256,7 +256,8 @@ export const getNodeExecution = ( config ); -/** Fetches data URLs for a NodeExecution */ +/** Fetches data URLs for a NodeExecution (used when fetching Dynamicworkflows + * from nodeExeecutions at runtime) */ export const getNodeExecutionData = ( id: NodeExecutionIdentifier, config?: RequestConfig diff --git a/src/models/Execution/types.ts b/src/models/Execution/types.ts index a78b3561fb..0720008618 100644 --- a/src/models/Execution/types.ts +++ b/src/models/Execution/types.ts @@ -1,5 +1,6 @@ import { Admin, Core, Protobuf } from 'flyteidl'; import { Identifier, LiteralMap, LiteralMapBlob, TaskLog, UrlBlob } from 'models/Common/types'; +import { CompiledWorkflow } from 'models/Workflow/types'; import { ExecutionMode, NodeExecutionPhase, TaskExecutionPhase, WorkflowExecutionPhase } from './enums'; export type WorkflowExecutionIdentifier = RequiredNonNullable; @@ -126,4 +127,5 @@ export interface ExecutionData { outputs: UrlBlob; fullInputs: LiteralMap | null; fullOutputs: LiteralMap | null; + dynamicWorkflow?: CompiledWorkflow; } diff --git a/src/models/Graph/types.ts b/src/models/Graph/types.ts index 307f6cf6a9..fbc5fcdd26 100644 --- a/src/models/Graph/types.ts +++ b/src/models/Graph/types.ts @@ -34,6 +34,7 @@ export enum dTypes { * @targetId dNode.id */ export interface dEdge { + id: string; sourceId: string; targetId: string; } diff --git a/src/server.tsx b/src/server.tsx index 8b25f66b24..7555ac704d 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -7,59 +7,48 @@ import { Helmet } from 'react-helmet'; import { processEnv } from '../env'; interface Stats { - publicPath: string; - assets: Array<{ name: string }>; + publicPath: string; + assets: Array<{ name: string }>; } interface ServerRendererArguments { - clientStats: Stats; - fileSystem: MemoryFileSystem; - currentDirectory: string; + clientStats: Stats; + fileSystem: MemoryFileSystem; + currentDirectory: string; } /** * Universal render function in development mode */ -export default function serverRenderer({ - fileSystem, - currentDirectory -}: ServerRendererArguments) { - const env = process.env.NODE_ENV || 'development'; - const isDev = env === 'development'; - const isProd = env === 'production'; - let html = ''; - if (isProd) { - html = fs - .readFileSync(path.join(currentDirectory, 'dist/index.html')) - .toString(); +export default function serverRenderer({ fileSystem, currentDirectory }: ServerRendererArguments) { + const env = process.env.NODE_ENV || 'development'; + const isDev = env === 'development'; + const isProd = env === 'production'; + let html = ''; + if (isProd) { + html = fs.readFileSync(path.join(currentDirectory, 'dist/index.html')).toString(); + } + + return (_req: express.Request, res: express.Response) => { + if (isDev) { + const indexPath = path.join(currentDirectory, 'dist', 'index.html'); + html = fileSystem.readFileSync(indexPath).toString(); } - return (_req: express.Request, res: express.Response) => { - if (isDev) { - const indexPath = path.join(currentDirectory, 'dist', 'index.html'); - html = fileSystem.readFileSync(indexPath).toString(); - } - - if (html === '') { - throw new ReferenceError('Could not find index.html to render'); - } + if (html === '') { + throw new ReferenceError('Could not find index.html to render'); + } - // populate the app content... - const $ = cheerio.load(html); + // populate the app content... + const $ = cheerio.load(html); - // populate Helmet content - const helmet = Helmet.renderStatic(); - $('head').append( - $.parseHTML( - `${helmet.title.toString()} ${helmet.meta.toString()} ${helmet.link.toString()}` - ) - ); + // populate Helmet content + const helmet = Helmet.renderStatic(); + $('head').append($.parseHTML(`${helmet.title.toString()} ${helmet.meta.toString()} ${helmet.link.toString()}`)); - // Populate process.env into window.env - $('head').append( - $(``) - ); + // Populate process.env into window.env + $('head').append($(``)); - res.status(200).send($.html()); - }; + res.status(200).send($.html()); + }; } diff --git a/yarn.lock b/yarn.lock index 64efbbe647..2b8f3a622b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1346,7 +1346,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.7", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.5", "@babel/runtime@^7.16.7", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -1360,13 +1360,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.14.6": - version "7.14.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" - integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -3831,109 +3824,109 @@ integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== "@types/d3-array@*": - version "2.12.1" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.12.1.tgz#bee6857b812f1ecfd5e6832fd67f617b667dd024" - integrity sha512-kVHqB3kfLpU0WYEmx5Y2hi3LRhUGIEIQXFdGazNNWQhyhzHx8xrgLtpAOKYzpfS3a+GjFMdKsI82QUH4q5dACQ== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.2.tgz#71c35bca8366a40d1b8fce9279afa4a77fb0065d" + integrity sha512-5mjGjz6XOXKOCdTajXTZ/pMsg236RdiwKPrRPWAEf/2S/+PzwY+LLYShUpeysWaMvsdS7LArh6GdUefoxpchsQ== "@types/d3-axis@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-2.1.0.tgz#5314adec80e4303d4eb02d23bdb5b8c600f6ee2c" - integrity sha512-6ekm+D+EG/LVT3oiwwU9wsm0+SBAQpxSSOsZq92fp+tnpaa19YMHpj8sRZQAeksSBcqNWzEMjuRPXR9s38YFaw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.1.tgz#6afc20744fa5cc0cbc3e2bd367b140a79ed3e7a8" + integrity sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw== dependencies: "@types/d3-selection" "*" "@types/d3-brush@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-2.1.0.tgz#c51ad1ab93887b23be7637d2100540f1df0dac00" - integrity sha512-rLQqxQeXWF4ArXi81GlV8HBNwJw9EDpz0jcWvvzv548EDE4tXrayBTOHYi/8Q4FZ/Df8PGXFzxpAVQmJMjOtvQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.1.tgz#ae5f17ce391935ca88b29000e60ee20452c6357c" + integrity sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw== dependencies: "@types/d3-selection" "*" "@types/d3-chord@*": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-2.0.1.tgz#45c72b28c9686eb4b02c8a1bb0fe174723053890" - integrity sha512-mqGww8qDtGZRnDsFizzobAVizd85hgaYNEri095ZI7/aYtW7hxa9a20enwuoVTWm0YqdCtLPoyV9ZPYgfyaTZw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.1.tgz#54c8856c19c8e4ab36a53f73ba737de4768ad248" + integrity sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw== "@types/d3-color@*": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.1.tgz#570ea7f8b853461301804efa52bd790a640a26db" - integrity sha512-u7LTCL7RnaavFSmob2rIAJLNwu50i6gFwY9cHFr80BrQURYQBRkJ+Yv47nA3Fm7FeRhdWTiVTeqvSeOuMAOzBQ== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" + integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== "@types/d3-contour@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-2.0.0.tgz#6e079f281b29a8df3fcbd3ec193f2cf1d0b4a584" - integrity sha512-PS9UO6zBQqwHXsocbpdzZFONgK1oRUgWtjjh/iz2vM06KaXLInLiKZ9e3OLBRerc1cU2uJYpO+8zOnb6frvCGQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.1.tgz#9ff4e2fd2a3910de9c5097270a7da8a6ef240017" + integrity sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ== dependencies: "@types/d3-array" "*" "@types/geojson" "*" "@types/d3-delaunay@*": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-5.3.0.tgz#416169bb5c67a510c87b55d092a404fcab49def3" - integrity sha512-gJYcGxLu0xDZPccbUe32OUpeaNtd1Lz0NYJtko6ZLMyG2euF4pBzrsQXms67LHZCDFzzszw+dMhSL/QAML3bXw== + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz#c09953ac7e5460997f693d2d7bf3522e0d4a88e6" + integrity sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w== "@types/d3-dispatch@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-2.0.0.tgz#1f8803041b73b81f2c751e026b7bb63dd5f24ce0" - integrity sha512-Sh0KW6z/d7uxssD7K4s4uCSzlEG/+SP+U47q098NVdOfFvUKNTvKAIV4XqjxsUuhE/854ARAREHOxkr9gQOCyg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz#a1b18ae5fa055a6734cb3bd3cbc6260ef19676e3" + integrity sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw== "@types/d3-drag@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-2.0.0.tgz#ef66acc422576fbe10b8bd66af45a9fb8525199a" - integrity sha512-VaUJPjbMnDn02tcRqsHLRAX5VjcRIzCjBfeXTLGe6QjMn5JccB5Cz4ztMRXMJfkbC45ovgJFWuj6DHvWMX1thA== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.1.tgz#fb1e3d5cceeee4d913caa59dedf55c94cb66e80f" + integrity sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA== dependencies: "@types/d3-selection" "*" "@types/d3-dsv@*": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-2.0.1.tgz#44ce09b025cf365d27cbe11fc13cd10954369627" - integrity sha512-wovgiG9Mgkr/SZ/m/c0m+RwrIT4ozsuCWeLxJyoObDWsie2DeQT4wzMdHZPR9Ya5oZLQT3w3uSl0NehG0+0dCA== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.0.tgz#f3c61fb117bd493ec0e814856feb804a14cfc311" + integrity sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A== "@types/d3-ease@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-2.0.0.tgz#798cbd9908d26cfe9f1a295a3a75164da9a3666e" - integrity sha512-6aZrTyX5LG+ptofVHf+gTsThLRY1nhLotJjgY4drYqk1OkJMu2UvuoZRlPw2fffjRHeYepue3/fxTufqKKmvsA== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" + integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== "@types/d3-fetch@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-2.0.0.tgz#580846256ed0011b36a08ebb36924e0dff70e27e" - integrity sha512-WnLepGtxepFfXRdPI8I5FTgNiHn9p4vMTTqaNCzJJfAswXx0rOY2jjeolzEU063em3iJmGZ+U79InnEeFOrCRw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.1.tgz#f9fa88b81aa2eea5814f11aec82ecfddbd0b8fe0" + integrity sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw== dependencies: "@types/d3-dsv" "*" "@types/d3-force@*": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-2.1.1.tgz#a18b6f029d056eb0f8f84a09471e6228e4469b14" - integrity sha512-3r+CQv2K/uDTAVg0DGxsbBjV02vgOxb8RhPIv3gd6cp3pdPAZ7wEXpDjUZSoqycAQLSDOxG/AZ54Vx6YXZSbmQ== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.3.tgz#76cb20d04ae798afede1ea6e41750763ff5a9c82" + integrity sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA== "@types/d3-format@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-2.0.0.tgz#607d261cb268f0a027f100575491031539a40ee6" - integrity sha512-uagdkftxnGkO4pZw5jEYOM5ZnZOEsh7z8j11Qxk85UkB2RzfUUxRl7R9VvvJZHwKn8l+x+rpS77Nusq7FkFmIg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" + integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg== "@types/d3-geo@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-2.0.0.tgz#6f179512343c2d30e06acde190abfacf44b2d264" - integrity sha512-DHHgYXW36lnAEQMYU2udKVOxxljHrn2EdOINeSC9jWCAXwOnGn7A19B8sNsHqgpu4F7O2bSD7//cqBXD3W0Deg== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.2.tgz#e7ec5f484c159b2c404c42d260e6d99d99f45d9a" + integrity sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ== dependencies: "@types/geojson" "*" "@types/d3-hierarchy@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz#92079d9dbcec1dfe2736fb050a8bf916e5850a1c" - integrity sha512-YxdskUvwzqggpnSnDQj4KVkicgjpkgXn/g/9M9iGsiToLS3nG6Ytjo1FoYhYVAAElV/fJBGVL3cQ9Hb7tcv+lw== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz#ca63f2f4da15b8f129c5b7dffd71d904cba6aca2" + integrity sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ== "@types/d3-interpolate@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-2.0.0.tgz#325029216dc722c1c68c33ccda759f1209d35823" - integrity sha512-Wt1v2zTlEN8dSx8hhx6MoOhWQgTkz0Ukj7owAEIOF2QtI0e219paFX9rf/SLOr/UExWb1TcUzatU8zWwFby6gg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== dependencies: "@types/d3-color" "*" "@types/d3-path@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-2.0.0.tgz#dcc7f5ecadf52b0c0c39f6c1def3733195e4b199" - integrity sha512-tXcR/9OtDdeCIsyl6eTNHC3XOAOdyc6ceF3QGBXOd9jTcK+ex/ecr00p9L9362e/op3UEPpxrToi1FHrtTSj7Q== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" + integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== "@types/d3-path@^1": version "1.0.9" @@ -3941,43 +3934,43 @@ integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== "@types/d3-polygon@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-2.0.0.tgz#8b1df0a1358016e62c4961b01e8dc8e5ab4c64e5" - integrity sha512-fISnMd8ePED1G4aa4V974Jmt+ajHSgPoxMa2D0ULxMybpx0Vw4WEzhQEaMIrL3hM8HVRcKTx669I+dTy/4PhAw== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93" + integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw== "@types/d3-quadtree@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-2.0.0.tgz#b17e953dc061e083966075bba0d3a9a259812150" - integrity sha512-YZuJuGBnijD0H+98xMJD4oZXgv/umPXy5deu3IimYTPGH3Kr8Th6iQUff0/6S80oNBD7KtOuIHwHUCymUiRoeQ== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5" + integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw== "@types/d3-random@*": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-2.2.0.tgz#fc44cabb966917459490b758f31f5359adeabe5b" - integrity sha512-Hjfj9m68NmYZzushzEG7etPvKH/nj9b9s9+qtkNG3/dbRBjQZQg1XS6nRuHJcCASTjxXlyXZnKu2gDxyQIIu9A== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953" + integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ== "@types/d3-scale-chromatic@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#8d4a6f07cbbf2a9f2a4bec9c9476c27ed76a96ea" - integrity sha512-Y62+2clOwZoKua84Ha0xU77w7lePiaBoTjXugT4l8Rd5LAk+Mn/ZDtrgs087a+B5uJ3jYUHHtKw5nuEzp0WBHw== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" + integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw== "@types/d3-scale@*": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-3.3.0.tgz#7ab91db0186bac0f24834ceb33f970e829f2fba1" - integrity sha512-rJj4nh/71Rw5bZgTF5cA5rW60WT3x8RbivEsScgQ66sqFnYZRmuyKSayyo7JiP+c9KJJiQhY9JXBmY16FZa3+g== + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.2.tgz#41be241126af4630524ead9cb1008ab2f0f26e69" + integrity sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA== dependencies: "@types/d3-time" "*" "@types/d3-selection@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-2.0.0.tgz#59df94a8e47ed1050a337d4ffb4d4d213aa590a8" - integrity sha512-EF0lWZ4tg7oDFg4YQFlbOU3936e3a9UmoQ2IXlBy1+cv2c2Pv7knhKUzGlH5Hq2sF/KeDTH1amiRPey2rrLMQA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59" + integrity sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ== "@types/d3-shape@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.0.tgz#cc7bbc9fc2c25f092bd457887a3224a21a55ca55" - integrity sha512-xTMEs8eITRksXclcVxMHIONRdyjj2TjDIwO4XFOPTVBNK9/oC4ZOhUbvTz1IpcsEsS/mClwuulP+OoawSAbSGA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" + integrity sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw== dependencies: - "@types/d3-path" "^1" + "@types/d3-path" "*" "@types/d3-shape@^1.2.6": version "1.3.5" @@ -3987,39 +3980,39 @@ "@types/d3-path" "^1" "@types/d3-time-format@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-3.0.0.tgz#913e984362a59792dc8d8b122dd17625991eade2" - integrity sha512-UpLg1mn/8PLyjr+J/JwdQJM/GzysMvv2CS8y+WYAL5K0+wbvXv/pPSLEfdNaprCZsGcXTxPsFMy8QtkYv9ueew== + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946" + integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw== "@types/d3-time@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-2.1.0.tgz#95708e5c92b199959806fd2116eeb3dfa0e9661c" - integrity sha512-qVCiT93utxN0cawScyQuNx8H82vBvZXSClZfgOu3l3dRRlRO6FjKEZlaPgXG9XUFjIAOsA4kAJY101vobHeJLQ== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" + integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== "@types/d3-timer@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-2.0.0.tgz#9901bb02af38798764674df17d66b07329705632" - integrity sha512-l6stHr1VD1BWlW6u3pxrjLtJfpPZq9I3XmKIQtq7zHM/s6fwEtI1Yn6Sr5/jQTrUDCC5jkS6gWqlFGCDArDqNg== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" + integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== "@types/d3-transition@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-2.0.0.tgz#6f073f0b567c13b7a3dcd1d54214c89f48c5a873" - integrity sha512-UJDzI98utcZQUJt3uIit/Ho0/eBIANzrWJrTmi4+TaKIyWL2iCu7ShP0o4QajCskhyjOA7C8+4CE3b1YirTzEQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.1.tgz#c9a96125567173d6163a6985b874f79154f4cc3d" + integrity sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g== dependencies: "@types/d3-selection" "*" "@types/d3-zoom@*": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-2.0.1.tgz#6f0d993042124947314053c937784e24d8b003cf" - integrity sha512-FuiGLfaHmp84b9wsj0dG03E/aJl5k98OLnJ2/5p7bQOHEpWqR+z5WCoWYMAbdGxaca7VNd9tCT5i6AJnpauNTQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" + integrity sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ== dependencies: "@types/d3-interpolate" "*" "@types/d3-selection" "*" -"@types/d3@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.0.0.tgz#d102ec6ea5741e51a1ff7b8228850db0665ccd27" - integrity sha512-7rMMuS5unvbvFCJXAkQXIxWTo2OUlmVXN5q7sfQFesuVICY55PSP6hhbUhWjTTNpfTTB3iLALsIYDFe7KUNABw== +"@types/d3@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.1.0.tgz#8f32a7e7f434d8f920c8b1ebdfed55e18c033720" + integrity sha512-gYWvgeGjEl+zmF8c+U1RNIKqe7sfQwIXeLXO5Os72TjDjCEtgpvGBvZ8dXlAuSS1m6B90Y1Uo6Bm36OGR/OtCA== dependencies: "@types/d3-array" "*" "@types/d3-axis" "*" @@ -4082,9 +4075,9 @@ "@types/serve-static" "*" "@types/geojson@*": - version "7946.0.7" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" - integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== + version "7946.0.8" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" + integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== "@types/glob@*", "@types/glob@^7.1.1": version "7.2.0" @@ -4113,14 +4106,6 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/html-minifier-terser@^5.0.0": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57" @@ -4380,16 +4365,6 @@ dependencies: "@types/react" "*" -"@types/react-redux@^7.1.16": - version "7.1.16" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" - integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - "@types/react-responsive@^3.0.1": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-3.0.3.tgz#a31b599c7cfe4135c5cc2f45d0b71df64803b23f" @@ -6787,7 +6762,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -13109,7 +13084,7 @@ lodash@4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@^4.15.0, lodash@^4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -15857,6 +15832,14 @@ react-draggable@^4.4.3: classnames "^2.2.5" prop-types "^15.6.0" +react-draggable@^4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f" + integrity sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA== + dependencies: + clsx "^1.1.1" + prop-types "^15.6.0" + react-element-to-jsx-string@^14.3.4: version "14.3.4" resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8" @@ -15876,21 +15859,18 @@ react-fast-compare@^3.0.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@^9.6.3: - version "9.6.3" - resolved "https://registry.yarnpkg.com/react-flow-renderer/-/react-flow-renderer-9.6.3.tgz#3dc4bf941a5fa5691057248aec838746238a3f9c" - integrity sha512-/2J1Lil8eOq3G0spVJPTIyhVUfnzDyV/JGRQC7DEFzwqzjL+qTj+ssyCblSxCSidxx1Q7zaza+ghnuMTr4E8QA== +react-flow-renderer@10.0.0-next.30: + version "10.0.0-next.30" + resolved "https://registry.yarnpkg.com/react-flow-renderer/-/react-flow-renderer-10.0.0-next.30.tgz#eb105270da2cc1c67362b12845760c5530312317" + integrity sha512-ZBaI0ti2g1TTKjeN0O2JHNQUD9sT5l6AMGTaPtuWOLJZRx0xUBbOmBR4HL936Gq1BzbiZv09OWZnf2qEyfC7yw== dependencies: - "@babel/runtime" "^7.14.6" - "@types/d3" "^7.0.0" - "@types/react-redux" "^7.1.16" + "@babel/runtime" "^7.16.5" + "@types/d3" "^7.1.0" classcat "^5.0.3" d3-selection "^3.0.0" d3-zoom "^3.0.0" - fast-deep-equal "^3.1.3" - react-draggable "^4.4.3" - react-redux "^7.2.4" - redux "^4.1.0" + react-draggable "^4.4.4" + zustand "^3.6.7" react-ga4@^1.4.1: version "1.4.1" @@ -16014,18 +15994,6 @@ react-query@^3.3.0: "@babel/runtime" "^7.5.5" match-sorter "^6.0.2" -react-redux@^7.2.4: - version "7.2.4" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" - integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== - dependencies: - "@babel/runtime" "^7.12.1" - "@types/react-redux" "^7.1.16" - hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.13.1" - react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" @@ -16358,13 +16326,6 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -redux@^4.0.0, redux@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" - integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== - dependencies: - "@babel/runtime" "^7.9.2" - refractor@^3.1.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" @@ -19971,6 +19932,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zustand@^3.6.7: + version "3.7.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.1.tgz#7388f0a7175a6c2fd9a2880b383a4bf6cdf6b7c6" + integrity sha512-wHBCZlKj+bg03/hP+Tzv24YhnqqP8MCeN9ECPDXoF01062SIbnfl3j9O0znkDw1lNTY0a8WN3F///a0UhhaEqg== + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"