From 64f2a1f3c9594c00b8f1501199de9b0254eab26e Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 10 May 2024 12:37:31 -0700 Subject: [PATCH] Initial refactoring of workflow editor page (#149) * Refactor resizableworkspace; add workflowinputs component Signed-off-by: Tyler Ohlsen * Add stub components for the basic ingest and search configs Signed-off-by: Tyler Ohlsen * More cleanup Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Tyler Ohlsen (cherry picked from commit c74260fd038cd57812dcff9407208f46e14655f5) --- .../workflow_detail/components/header.tsx | 13 +- .../{workspace => }/resizable_workspace.tsx | 176 +++--------------- .../pages/workflow_detail/workflow_detail.tsx | 100 +--------- .../workflow_inputs/footer.tsx | 65 +++++++ .../workflow_detail/workflow_inputs/index.ts | 6 + .../ingest_inputs/enrich_data.tsx | 27 +++ .../workflow_inputs/ingest_inputs/index.ts | 6 + .../ingest_inputs/ingest_data.tsx | 27 +++ .../ingest_inputs/ingest_inputs.tsx | 37 ++++ .../ingest_inputs/source_data.tsx | 27 +++ .../configure_search_request.tsx | 27 +++ .../search_inputs/enrich_search_request.tsx | 27 +++ .../search_inputs/enrich_search_response.tsx | 27 +++ .../workflow_inputs/search_inputs/index.ts | 6 + .../search_inputs/search_inputs.tsx | 37 ++++ .../workflow_inputs/workflow_inputs.tsx | 62 ++++++ .../pages/workflow_detail/workspace/index.ts | 2 +- public/pages/workflows/new_workflow/utils.ts | 3 + 18 files changed, 421 insertions(+), 254 deletions(-) rename public/pages/workflow_detail/{workspace => }/resizable_workspace.tsx (71%) create mode 100644 public/pages/workflow_detail/workflow_inputs/footer.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/index.ts create mode 100644 public/pages/workflow_detail/workflow_inputs/ingest_inputs/enrich_data.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/ingest_inputs/index.ts create mode 100644 public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_request.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_response.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/search_inputs/index.ts create mode 100644 public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx create mode 100644 public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx index 3d1e4035..b4854043 100644 --- a/public/pages/workflow_detail/components/header.tsx +++ b/public/pages/workflow_detail/components/header.tsx @@ -18,7 +18,6 @@ import { } from '../../../../common'; interface WorkflowDetailHeaderProps { - tabs: any[]; isNewWorkflow: boolean; workflow?: Workflow; } @@ -51,17 +50,11 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { } rightSideItems={[ - // TODO: finalize if this is needed - {}} - > - Delete + // TODO: implement export functionality + {}}> + Export , ]} - tabs={props.tabs} bottomBorder={true} /> ); diff --git a/public/pages/workflow_detail/workspace/resizable_workspace.tsx b/public/pages/workflow_detail/resizable_workspace.tsx similarity index 71% rename from public/pages/workflow_detail/workspace/resizable_workspace.tsx rename to public/pages/workflow_detail/resizable_workspace.tsx index 10641a4a..62f634c0 100644 --- a/public/pages/workflow_detail/workspace/resizable_workspace.tsx +++ b/public/pages/workflow_detail/resizable_workspace.tsx @@ -11,14 +11,12 @@ import { Form, Formik, FormikProps } from 'formik'; import * as yup from 'yup'; import { cloneDeep } from 'lodash'; import { - EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiPageHeader, EuiResizableContainer, } from '@elastic/eui'; -import { getCore } from '../../../services'; +import { getCore } from '../../services'; import { Workflow, @@ -29,39 +27,28 @@ import { WorkspaceFlowState, WORKFLOW_STATE, ReactFlowEdge, -} from '../../../../common'; +} from '../../../common'; import { componentDataToFormik, getComponentSchema, processNodes, - reduceToTemplate, APP_PATH, -} from '../../../utils'; -import { validateWorkspaceFlow, toTemplateFlows } from '../utils'; -import { - AppState, - createWorkflow, - deprovisionWorkflow, - getWorkflowState, - provisionWorkflow, - removeDirty, - setDirty, - updateWorkflow, - useAppDispatch, -} from '../../../store'; -import { Workspace } from './workspace'; -import { ComponentDetails } from '../component_details'; +} from '../../utils'; +import { validateWorkspaceFlow, toTemplateFlows } from './utils'; +import { AppState, setDirty, useAppDispatch } from '../../store'; +import { Workspace } from './workspace/workspace'; // styling -import './workspace-styles.scss'; -import '../../../global-styles.scss'; +import './workspace/workspace-styles.scss'; +import '../../global-styles.scss'; +import { WorkflowInputs } from './workflow_inputs'; interface ResizableWorkspaceProps { isNewWorkflow: boolean; workflow?: Workflow; } -const COMPONENT_DETAILS_PANEL_ID = 'component_details_panel_id'; +const WORKFLOW_INPUTS_PANEL_ID = 'workflow_inputs_panel_id'; /** * The overall workspace component that maintains state related to the 2 resizable @@ -337,114 +324,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { deprovisioned. )} - { - if (workflow?.id) { - setIsDeprovisioning(true); - dispatch(deprovisionWorkflow(workflow.id)) - .unwrap() - .then(async (result) => { - await new Promise((f) => setTimeout(f, 3000)); - dispatch(getWorkflowState(workflow.id as string)); - setIsDeprovisioning(false); - }) - .catch((error: any) => { - setIsDeprovisioning(false); - }); - } else { - // This case should not happen - console.debug( - 'Deprovisioning triggered on an invalid workflow. Ignoring.' - ); - } - }} - > - Deprovision - , - { - if (workflow?.id) { - setIsProvisioning(true); - dispatch(provisionWorkflow(workflow.id)) - .unwrap() - .then(async (result) => { - await new Promise((f) => setTimeout(f, 3000)); - dispatch(getWorkflowState(workflow.id as string)); - setIsProvisioning(false); - }) - .catch((error: any) => { - setIsProvisioning(false); - }); - } else { - // This case should not happen - console.debug( - 'Provisioning triggered on an invalid workflow. Ignoring.' - ); - } - }} - > - Provision - , - { - setIsSaving(true); - dispatch(removeDirty()); - if (isFirstSave) { - setIsFirstSave(false); - } - validateFormAndFlow( - formikProps, - // The callback fn to run if everything is valid. - (updatedWorkflow) => { - if (updatedWorkflow.id) { - dispatch( - updateWorkflow({ - workflowId: updatedWorkflow.id, - workflowTemplate: reduceToTemplate(updatedWorkflow), - }) - ) - .unwrap() - .then((result) => { - setIsSaving(false); - }) - .catch((error: any) => { - setIsSaving(false); - }); - } else { - dispatch(createWorkflow(updatedWorkflow)) - .unwrap() - .then((result) => { - const { workflow } = result; - history.replace( - `${APP_PATH.WORKFLOWS}/${workflow.id}` - ); - history.go(0); - }) - .catch((error: any) => { - setIsSaving(false); - }); - } - } - ); - }} - > - {props.isNewWorkflow || isCreating ? 'Create' : 'Save'} - , - ]} - bottomBorder={false} - /> onToggleChange()} > - + onToggleChange()} > - diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index cc795ecb..4c1c4e6a 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -3,11 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; -import { RouteComponentProps, useLocation } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { ReactFlowProvider } from 'reactflow'; -import queryString from 'query-string'; import { EuiPage, EuiPageBody } from '@elastic/eui'; import { BREADCRUMBS } from '../../utils'; import { getCore } from '../../services'; @@ -24,8 +23,6 @@ import { FETCH_ALL_QUERY_BODY, NEW_WORKFLOW_ID_URL, } from '../../../common'; -import { Resources } from './resources'; -import { Prototype } from './prototype'; // styling import './workflow-detail-styles.scss'; @@ -38,29 +35,6 @@ export interface WorkflowDetailRouterProps { interface WorkflowDetailProps extends RouteComponentProps {} -enum WORKFLOW_DETAILS_TAB { - EDITOR = 'editor', - // TODO: temporarily adding a resources tab until UX is finalized. - // This gives clarity into what has been done on the cluster on behalf - // of the frontend provisioning workflows. - RESOURCES = 'resources', - // TODO: temporarily adding a prototype tab until UX is finalized. - // This allows simple UI for executing ingest and search against - // created workflow resources - PROTOTYPE = 'prototype', -} - -const ACTIVE_TAB_PARAM = 'tab'; - -function replaceActiveTab(activeTab: string, props: WorkflowDetailProps) { - props.history.replace({ - ...history, - search: queryString.stringify({ - [ACTIVE_TAB_PARAM]: activeTab, - }), - }); -} - /** * The workflow details page. This is where users will configure, create, and * test their created workflows. Additionally, can be used to load existing workflows @@ -84,25 +58,6 @@ export function WorkflowDetail(props: WorkflowDetailProps) { ? DEFAULT_NEW_WORKFLOW_NAME : ''; - // tab state - const tabFromUrl = queryString.parse(useLocation().search)[ - ACTIVE_TAB_PARAM - ] as WORKFLOW_DETAILS_TAB; - const [selectedTabId, setSelectedTabId] = useState( - tabFromUrl - ); - - // Default to editor tab if there is none or invalid tab ID specified via url. - useEffect(() => { - if ( - !selectedTabId || - !Object.values(WORKFLOW_DETAILS_TAB).includes(selectedTabId) - ) { - setSelectedTabId(WORKFLOW_DETAILS_TAB.EDITOR); - replaceActiveTab(WORKFLOW_DETAILS_TAB.EDITOR, props); - } - }, []); - useEffect(() => { getCore().chrome.setBreadcrumbs([ BREADCRUMBS.FLOW_FRAMEWORK, @@ -129,36 +84,6 @@ export function WorkflowDetail(props: WorkflowDetailProps) { } }, [errorMessage]); - const tabs = [ - { - id: WORKFLOW_DETAILS_TAB.EDITOR, - label: 'Editor', - isSelected: selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR, - onClick: () => { - setSelectedTabId(WORKFLOW_DETAILS_TAB.EDITOR); - replaceActiveTab(WORKFLOW_DETAILS_TAB.EDITOR, props); - }, - }, - { - id: WORKFLOW_DETAILS_TAB.RESOURCES, - label: 'Resources', - isSelected: selectedTabId === WORKFLOW_DETAILS_TAB.RESOURCES, - onClick: () => { - setSelectedTabId(WORKFLOW_DETAILS_TAB.RESOURCES); - replaceActiveTab(WORKFLOW_DETAILS_TAB.RESOURCES, props); - }, - }, - { - id: WORKFLOW_DETAILS_TAB.PROTOTYPE, - label: 'Prototype', - isSelected: selectedTabId === WORKFLOW_DETAILS_TAB.PROTOTYPE, - onClick: () => { - setSelectedTabId(WORKFLOW_DETAILS_TAB.PROTOTYPE); - replaceActiveTab(WORKFLOW_DETAILS_TAB.PROTOTYPE, props); - }, - }, - ]; - return ( @@ -166,22 +91,13 @@ export function WorkflowDetail(props: WorkflowDetailProps) { - {selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR && ( - - - - )} - {selectedTabId === WORKFLOW_DETAILS_TAB.RESOURCES && ( - - )} - {selectedTabId === WORKFLOW_DETAILS_TAB.PROTOTYPE && ( - - )} + + + diff --git a/public/pages/workflow_detail/workflow_inputs/footer.tsx b/public/pages/workflow_detail/workflow_inputs/footer.tsx new file mode 100644 index 00000000..d66b3496 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/footer.tsx @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, +} from '@elastic/eui'; +import { CREATE_STEP } from './workflow_inputs'; + +interface FooterProps { + selectedStep: CREATE_STEP; + setSelectedStep: (step: CREATE_STEP) => void; +} + +/** + * The footer component containing the navigation buttons. + */ +export function Footer(props: FooterProps) { + return ( + + + + + + + {props.selectedStep === CREATE_STEP.INGEST ? ( + + props.setSelectedStep(CREATE_STEP.SEARCH)} + > + Next + + + ) : ( + <> + + props.setSelectedStep(CREATE_STEP.INGEST)} + > + Back + + + + + // TODO: implement creation + console.log('Placeholder for workflow creation...') + } + > + Create + + + + )} + + + + ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/index.ts b/public/pages/workflow_detail/workflow_inputs/index.ts new file mode 100644 index 00000000..bf513c09 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './workflow_inputs'; diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/enrich_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/enrich_data.tsx new file mode 100644 index 00000000..6b7f4cad --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/enrich_data.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; + +interface EnrichDataProps {} + +/** + * Input component for configuring any data enrichment for ingest (ingest pipeline processors etc.) + */ +export function EnrichData(props: EnrichDataProps) { + return ( + + + +

Enrich data

+
+
+ + TODO + +
+ ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/index.ts b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/index.ts new file mode 100644 index 00000000..44de94bf --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './ingest_inputs'; diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx new file mode 100644 index 00000000..ccbdcf04 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; + +interface IngestDataProps {} + +/** + * Input component for configuring the data ingest (the OpenSearch index) + */ +export function IngestData(props: IngestDataProps) { + return ( + + + +

Ingest data

+
+
+ + TODO + +
+ ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx new file mode 100644 index 00000000..29d5c203 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { SourceData } from './source_data'; +import { EnrichData } from './enrich_data'; +import { IngestData } from './ingest_data'; + +interface IngestInputsProps {} + +/** + * The base component containing all of the ingest-related inputs + */ +export function IngestInputs(props: IngestInputsProps) { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx new file mode 100644 index 00000000..35d6af9a --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; + +interface SourceDataProps {} + +/** + * Input component for configuring the source data for ingest. + */ +export function SourceData(props: SourceDataProps) { + return ( + + + +

Source data

+
+
+ + TODO + +
+ ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx new file mode 100644 index 00000000..c25c7810 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; + +interface ConfigureSearchRequestProps {} + +/** + * Input component for configuring a search request + */ +export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) { + return ( + + + +

Configure search request

+
+
+ + TODO + +
+ ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_request.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_request.tsx new file mode 100644 index 00000000..0144dd6b --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_request.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; + +interface EnrichSearchRequestProps {} + +/** + * Input component for enriching a search request (configuring search request processors, etc.) + */ +export function EnrichSearchRequest(props: EnrichSearchRequestProps) { + return ( + + + +

Enrich search request

+
+
+ + TODO + +
+ ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_response.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_response.tsx new file mode 100644 index 00000000..420702b1 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/enrich_search_response.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; + +interface EnrichSearchResponseProps {} + +/** + * Input component for enriching a search response (configuring search response processors, etc.) + */ +export function EnrichSearchResponse(props: EnrichSearchResponseProps) { + return ( + + + +

Enrich search response

+
+
+ + TODO + +
+ ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/index.ts b/public/pages/workflow_detail/workflow_inputs/search_inputs/index.ts new file mode 100644 index 00000000..ad4755d6 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './search_inputs'; diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx new file mode 100644 index 00000000..9427c09a --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { ConfigureSearchRequest } from './configure_search_request'; +import { EnrichSearchRequest } from './enrich_search_request'; +import { EnrichSearchResponse } from './enrich_search_response'; + +interface SearchInputsProps {} + +/** + * The base component containing all of the search-related inputs + */ +export function SearchInputs(props: SearchInputsProps) { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx new file mode 100644 index 00000000..b7321251 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { Workflow } from '../../../../common'; +import { Footer } from './footer'; +import { IngestInputs } from './ingest_inputs'; +import { SearchInputs } from './search_inputs'; + +interface WorkflowInputsProps { + workflow: Workflow | undefined; +} + +export enum CREATE_STEP { + INGEST = 'Step 1: Define ingestion pipeline', + SEARCH = 'Step 2: Define search pipeline', +} + +/** + * The workflow inputs component containing the multi-step flow to create ingest + * and search flows for a particular workflow. + */ + +export function WorkflowInputs(props: WorkflowInputsProps) { + const [selectedStep, setSelectedStep] = useState( + CREATE_STEP.INGEST + ); + + useEffect(() => {}, [selectedStep]); + + return ( + + + + +

{selectedStep}

+
+
+ + {selectedStep === CREATE_STEP.INGEST ? ( + + ) : ( + + )} + + +