From 8a965a3a3994480b874747785e54f25748aeaa04 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 30 May 2024 10:32:54 -0700 Subject: [PATCH] Add a `Tools` collapsible panel (#162) Signed-off-by: Tyler Ohlsen --- .../workflow_detail/components/header.tsx | 3 +- .../workflow_detail/resizable_workspace.tsx | 240 +++++++++--------- .../workflow_detail/resources/resources.tsx | 38 --- .../{resources => tools}/index.ts | 2 +- .../{ => tools}/resources/columns.tsx | 2 +- .../workflow_detail/tools/resources/index.ts | 6 + .../{ => tools}/resources/resource_list.tsx | 2 +- .../tools/resources/resources.tsx | 52 ++++ public/pages/workflow_detail/tools/tools.tsx | 105 ++++++++ .../workflow_inputs/workflow_inputs.tsx | 6 +- .../workspace/workspace-styles.scss | 2 +- .../workflow_detail/workspace/workspace.tsx | 36 +-- 12 files changed, 299 insertions(+), 195 deletions(-) delete mode 100644 public/pages/workflow_detail/resources/resources.tsx rename public/pages/workflow_detail/{resources => tools}/index.ts (67%) rename public/pages/workflow_detail/{ => tools}/resources/columns.tsx (93%) create mode 100644 public/pages/workflow_detail/tools/resources/index.ts rename public/pages/workflow_detail/{ => tools}/resources/resource_list.tsx (95%) create mode 100644 public/pages/workflow_detail/tools/resources/resources.tsx create mode 100644 public/pages/workflow_detail/tools/tools.tsx diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx index b4854043..4a5ee2a6 100644 --- a/public/pages/workflow_detail/components/header.tsx +++ b/public/pages/workflow_detail/components/header.tsx @@ -41,6 +41,7 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { return ( {getTitle()} @@ -55,7 +56,7 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { Export , ]} - bottomBorder={true} + bottomBorder={false} /> ); } diff --git a/public/pages/workflow_detail/resizable_workspace.tsx b/public/pages/workflow_detail/resizable_workspace.tsx index 5901680e..9a759952 100644 --- a/public/pages/workflow_detail/resizable_workspace.tsx +++ b/public/pages/workflow_detail/resizable_workspace.tsx @@ -6,17 +6,20 @@ import React, { useRef, useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { useReactFlow } from 'reactflow'; import { Form, Formik, FormikProps } from 'formik'; import * as yup from 'yup'; -import { EuiFlexGroup, EuiFlexItem, EuiResizableContainer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiResizableContainer, + EuiTitle, +} from '@elastic/eui'; import { getCore } from '../../services'; import { Workflow, - ReactFlowComponent, WORKFLOW_STATE, - ReactFlowEdge, WorkflowFormValues, WorkflowSchema, WorkflowConfig, @@ -35,13 +38,14 @@ import { updateWorkflow, useAppDispatch, } from '../../store'; +import { WorkflowInputs } from './workflow_inputs'; +import { configToTemplateFlows } from './utils'; import { Workspace } from './workspace'; // styling import './workspace/workspace-styles.scss'; import '../../global-styles.scss'; -import { WorkflowInputs } from './workflow_inputs'; -import { configToTemplateFlows } from './utils'; +import { Tools } from './tools'; interface ResizableWorkspaceProps { isNewWorkflow: boolean; @@ -49,6 +53,7 @@ interface ResizableWorkspaceProps { } const WORKFLOW_INPUTS_PANEL_ID = 'workflow_inputs_panel_id'; +const TOOLS_PANEL_ID = 'tools_panel_id'; /** * The overall workspace component that maintains state related to the 2 resizable @@ -75,21 +80,29 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { // Validation states const [formValidOnSubmit, setFormValidOnSubmit] = useState(true); - // Component details side panel state - const [isDetailsPanelOpen, setisDetailsPanelOpen] = useState(true); - const collapseFn = useRef( + // Workflow inputs side panel state + const [isWorkflowInputsPanelOpen, setIsWorkflowInputsPanelOpen] = useState< + boolean + >(true); + const collapseFnHorizontal = useRef( (id: string, options: { direction: 'left' | 'right' }) => {} ); - const onToggleChange = () => { - collapseFn.current(WORKFLOW_INPUTS_PANEL_ID, { direction: 'left' }); - setisDetailsPanelOpen(!isDetailsPanelOpen); + const onToggleWorkflowInputsChange = () => { + collapseFnHorizontal.current(WORKFLOW_INPUTS_PANEL_ID, { + direction: 'left', + }); + setIsWorkflowInputsPanelOpen(!isWorkflowInputsPanelOpen); }; - // Selected component state - const reactFlowInstance = useReactFlow(); - const [selectedComponent, setSelectedComponent] = useState< - ReactFlowComponent - >(); + // Tools side panel state + const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(true); + const collapseFnVertical = useRef( + (id: string, options: { direction: 'top' | 'bottom' }) => {} + ); + const onToggleToolsChange = () => { + collapseFnVertical.current(TOOLS_PANEL_ID, { direction: 'bottom' }); + setIsToolsPanelOpen(!isToolsPanelOpen); + }; // Save/provision/deprovision button state const isSaveable = @@ -116,31 +129,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { const isLoadingGlobal = loading || isProvisioning || isDeprovisioning || isSaving || isCreating; - /** - * Custom listener on when nodes are selected / de-selected. Passed to - * downstream ReactFlow components you can listen using - * the out-of-the-box useOnSelectionChange hook. - * - populate panel content appropriately - * - open the panel if a node is selected and the panel is closed - * - it is assumed that only one node can be selected at once - */ - function onSelectionChange({ - nodes, - edges, - }: { - nodes: ReactFlowComponent[]; - edges: ReactFlowEdge[]; - }) { - if (nodes && nodes.length > 0) { - setSelectedComponent(nodes[0]); - if (!isDetailsPanelOpen) { - onToggleChange(); - } - } else { - setSelectedComponent(undefined); - } - } - // Hook to update some default values for the workflow, if applicable. // We need to handle different scenarios: // 1. Rendering backend-only-created workflow / an already-created workflow with no ui_metadata. @@ -171,19 +159,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { } }, [props.workflow]); - // Hook to updated the selected ReactFlow component - useEffect(() => { - reactFlowInstance?.setNodes((nodes: ReactFlowComponent[]) => - nodes.map((node) => { - node.data = { - ...node.data, - selected: node.id === selectedComponent?.id ? true : false, - }; - return node; - }) - ); - }, [selectedComponent]); - // Initialize the form state to an existing workflow, if applicable. useEffect(() => { if (workflow?.ui_metadata?.config) { @@ -194,10 +169,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { } }, [workflow]); - // TODO: leave as a placeholder for now. Current functionality is the workflow - // is readonly and only reacts/changes when the underlying form is updated. - function onNodesChange(nodes: ReactFlowComponent[]): void {} - /** * Function to pass down to the Formik
components as a listener to propagate * form changes to this parent component to re-enable save button, etc. @@ -275,41 +246,20 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { > {(formikProps) => ( - {/* - TODO: finalize where/how to show invalidations - */} - {/* {!formValidOnSubmit && ( - - Please address the highlighted fields and try saving again. - - )} - {isDeprovisionable && isDirty && ( - - Changes cannot be saved until the workflow has first been - deprovisioned. - - )} */} {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { if (togglePanel) { - collapseFn.current = (panelId: string, { direction }) => - togglePanel(panelId, { direction }); + collapseFnHorizontal.current = ( + panelId: string, + { direction } + ) => togglePanel(panelId, { direction }); } return ( @@ -320,46 +270,106 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { initialSize={50} minSize="25%" paddingSize="s" - onToggleCollapsedInternal={() => onToggleChange()} + onToggleCollapsedInternal={() => + onToggleWorkflowInputsChange() + } > - - - - - + - - - - - + {( + EuiResizablePanel, + EuiResizableButton, + { togglePanel } + ) => { + if (togglePanel) { + collapseFnVertical.current = ( + panelId: string, + { direction } + ) => + // ignore is added since docs are incorrectly missing "top" and "bottom" + // as valid direction options for vertically-configured resizable panels. + // @ts-ignore + togglePanel(panelId, { direction }); + } + + return ( + <> + + + + + + + + + + onToggleToolsChange() + } + style={{ marginBottom: '-24px' }} + > + + + + + + + + + + ); + }} + ); diff --git a/public/pages/workflow_detail/resources/resources.tsx b/public/pages/workflow_detail/resources/resources.tsx deleted file mode 100644 index cee54acd..00000000 --- a/public/pages/workflow_detail/resources/resources.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPageContent, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { Workflow } from '../../../../common'; -import { ResourceList } from './resource_list'; - -interface ResourcesProps { - workflow?: Workflow; -} - -/** - * A simple resources page to browse created resources for a given Workflow. - */ -export function Resources(props: ResourcesProps) { - return ( - - -

Resources

-
- - - - - - -
- ); -} diff --git a/public/pages/workflow_detail/resources/index.ts b/public/pages/workflow_detail/tools/index.ts similarity index 67% rename from public/pages/workflow_detail/resources/index.ts rename to public/pages/workflow_detail/tools/index.ts index 2132669a..7b285f91 100644 --- a/public/pages/workflow_detail/resources/index.ts +++ b/public/pages/workflow_detail/tools/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { Resources } from './resources'; +export * from './tools'; diff --git a/public/pages/workflow_detail/resources/columns.tsx b/public/pages/workflow_detail/tools/resources/columns.tsx similarity index 93% rename from public/pages/workflow_detail/resources/columns.tsx rename to public/pages/workflow_detail/tools/resources/columns.tsx index a7ffbf55..2ab96f0b 100644 --- a/public/pages/workflow_detail/resources/columns.tsx +++ b/public/pages/workflow_detail/tools/resources/columns.tsx @@ -6,7 +6,7 @@ import { WORKFLOW_STEP_TO_RESOURCE_TYPE_MAP, WORKFLOW_STEP_TYPE, -} from '../../../../common'; +} from '../../../../../common'; export const columns = [ { diff --git a/public/pages/workflow_detail/tools/resources/index.ts b/public/pages/workflow_detail/tools/resources/index.ts new file mode 100644 index 00000000..32618c5f --- /dev/null +++ b/public/pages/workflow_detail/tools/resources/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './resources'; diff --git a/public/pages/workflow_detail/resources/resource_list.tsx b/public/pages/workflow_detail/tools/resources/resource_list.tsx similarity index 95% rename from public/pages/workflow_detail/resources/resource_list.tsx rename to public/pages/workflow_detail/tools/resources/resource_list.tsx index 72a10f1f..98083b64 100644 --- a/public/pages/workflow_detail/resources/resource_list.tsx +++ b/public/pages/workflow_detail/tools/resources/resource_list.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import { Workflow, WorkflowResource } from '../../../../common'; +import { Workflow, WorkflowResource } from '../../../../../common'; import { columns } from './columns'; interface ResourceListProps { diff --git a/public/pages/workflow_detail/tools/resources/resources.tsx b/public/pages/workflow_detail/tools/resources/resources.tsx new file mode 100644 index 00000000..399663c3 --- /dev/null +++ b/public/pages/workflow_detail/tools/resources/resources.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { Workflow } from '../../../../../common'; +import { ResourceList } from './resource_list'; + +interface ResourcesProps { + workflow?: Workflow; +} + +/** + * A simple resources page to browse created resources for a given Workflow. + */ +export function Resources(props: ResourcesProps) { + return ( + <> + {props.workflow?.resourcesCreated && + props.workflow.resourcesCreated.length > 0 ? ( + <> + + + + + + + ) : ( + No resources available} + titleSize="s" + body={ + <> + + Provision the workflow to generate resources in order to start + prototyping. + + + } + /> + )} + + ); +} diff --git a/public/pages/workflow_detail/tools/tools.tsx b/public/pages/workflow_detail/tools/tools.tsx new file mode 100644 index 00000000..3e0308c7 --- /dev/null +++ b/public/pages/workflow_detail/tools/tools.tsx @@ -0,0 +1,105 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTab, + EuiTabs, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { Workflow } from '../../../../common'; +import { Resources } from './resources'; + +interface ToolsProps { + workflow?: Workflow; +} + +enum TAB_ID { + INGEST = 'ingest', + QUERY = 'query', + ERRORS = 'errors', + RESOURCES = 'resources', +} + +const inputTabs = [ + { + id: TAB_ID.INGEST, + name: 'Run ingestion', + disabled: false, + }, + { + id: TAB_ID.QUERY, + name: 'Run queries', + disabled: false, + }, + { + id: TAB_ID.ERRORS, + name: 'Errors', + disabled: false, + }, + { + id: TAB_ID.RESOURCES, + name: 'Resources', + disabled: false, + }, +]; + +/** + * The base Tools component for performing ingest and search, viewing resources, and debugging. + */ +export function Tools(props: ToolsProps) { + const [selectedTabId, setSelectedTabId] = useState(TAB_ID.INGEST); + return ( + <> + +

Tools

+
+ + <> + + {inputTabs.map((tab, idx) => { + return ( + setSelectedTabId(tab.id)} + isSelected={tab.id === selectedTabId} + disabled={tab.disabled} + key={idx} + > + {tab.name} + + ); + })} + + + + {selectedTabId === TAB_ID.INGEST && ( + + TODO: Run ingestion placeholder + + )} + {selectedTabId === TAB_ID.QUERY && ( + + TODO: Run queries placeholder + + )} + {selectedTabId === TAB_ID.ERRORS && ( + + TODO: View errors placeholder + + )} + {selectedTabId === TAB_ID.RESOURCES && ( + + + + )} + + + + ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 69e28fd9..f851651b 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -18,6 +18,9 @@ import { IngestInputs } from './ingest_inputs'; import { SearchInputs } from './search_inputs'; import { FormikProps } from 'formik'; +// styling +import '../workspace/workspace-styles.scss'; + interface WorkflowInputsProps { workflow: Workflow | undefined; formikProps: FormikProps; @@ -43,7 +46,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { useEffect(() => {}, [selectedStep]); return ( - + {props.workflow === undefined ? ( ) : ( @@ -64,7 +67,6 @@ export function WorkflowInputs(props: WorkflowInputsProps) { style={{ overflowY: 'scroll', overflowX: 'hidden', - maxHeight: '55vh', }} > {selectedStep === CREATE_STEP.INGEST ? ( diff --git a/public/pages/workflow_detail/workspace/workspace-styles.scss b/public/pages/workflow_detail/workspace/workspace-styles.scss index cd514574..9694f2d6 100644 --- a/public/pages/workflow_detail/workspace/workspace-styles.scss +++ b/public/pages/workflow_detail/workspace/workspace-styles.scss @@ -1,3 +1,3 @@ .workspace-panel { - height: 90%; + height: 95%; } diff --git a/public/pages/workflow_detail/workspace/workspace.tsx b/public/pages/workflow_detail/workspace/workspace.tsx index 4a024918..031f34b8 100644 --- a/public/pages/workflow_detail/workspace/workspace.tsx +++ b/public/pages/workflow_detail/workspace/workspace.tsx @@ -11,20 +11,11 @@ import ReactFlow, { useEdgesState, addEdge, BackgroundVariant, - useStore, - useReactFlow, - useOnSelectionChange, MarkerType, } from 'reactflow'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { setDirty, useAppDispatch } from '../../../store'; -import { - IComponentData, - ReactFlowComponent, - ReactFlowEdge, - Workflow, - WorkflowConfig, -} from '../../../../common'; +import { IComponentData, Workflow, WorkflowConfig } from '../../../../common'; import { IngestGroupComponent, SearchGroupComponent, @@ -42,15 +33,7 @@ import './workspace_edge/deletable-edge-styles.scss'; interface WorkspaceProps { workflow?: Workflow; readonly: boolean; - onNodesChange: (nodes: ReactFlowComponent[]) => void; id: string; - onSelectionChange: ({ - nodes, - edges, - }: { - nodes: ReactFlowComponent[]; - edges: ReactFlowEdge[]; - }) => void; } const nodeTypes = { @@ -65,26 +48,9 @@ export function Workspace(props: WorkspaceProps) { // ReactFlow state const reactFlowWrapper = useRef(null); - const reactFlowInstance = useReactFlow(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); - // Listener for node additions or deletions to propagate to parent component - const nodesLength = useStore( - (state) => Array.from(state.nodeInternals.values()).length || 0 - ); - useEffect(() => { - props.onNodesChange(nodes); - }, [nodesLength]); - - /** - * Hook provided by reactflow to listen on when nodes are selected / de-selected. - * Trigger the callback fn to propagate changes to parent components. - */ - useOnSelectionChange({ - onChange: props.onSelectionChange, - }); - const onConnect = useCallback( (params) => { const edge = {