From 886746504647554bcff0f421d81783a4264823bf Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:00:52 -0700 Subject: [PATCH] Add interfaces & types; fetch reactflow state from workflow redux store (#45) (#46) Signed-off-by: Tyler Ohlsen (cherry picked from commit d9e697c6da7eb526530691330e98c979417e9803) Co-authored-by: Tyler Ohlsen --- common/helpers.ts | 74 ------------------ common/index.ts | 1 - common/interfaces.ts | 78 ++++++++++++++++--- eslintrc.json | 7 ++ package.json | 2 +- public/app.tsx | 1 - public/component_types/index.ts | 2 +- public/component_types/indices/knn_index.ts | 2 +- .../{base_interfaces.ts => interfaces.ts} | 0 .../processors/text_embedding_processor.ts | 2 +- .../workflow_detail/components/header.tsx | 4 +- .../pages/workflow_detail/workflow_detail.tsx | 3 +- .../workflow_detail/workspace/workspace.tsx | 42 +++++----- .../workspace_component.tsx | 4 +- public/pages/workflows/components/columns.tsx | 4 +- .../workflows/components/workflow_list.tsx | 5 +- public/store/reducers/opensearch_reducer.ts | 8 +- public/store/reducers/workflows_reducer.ts | 61 ++++++++++++++- public/types.ts | 2 - server/routes/opensearch_routes.ts | 6 +- server/types.ts | 2 - 21 files changed, 176 insertions(+), 134 deletions(-) delete mode 100644 common/helpers.ts create mode 100644 eslintrc.json rename public/component_types/{base_interfaces.ts => interfaces.ts} (100%) diff --git a/common/helpers.ts b/common/helpers.ts deleted file mode 100644 index cb422fff..00000000 --- a/common/helpers.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { Node, Edge } from 'reactflow'; -import { IComponent } from '../public/component_types'; - -/** - * TODO: remove hardcoded nodes/edges. - * - * Converts the stored IComponents into the low-level ReactFlow nodes and edges. - * This may change entirely, depending on how/where the ReactFlow JSON will be - * persisted. Using this stub helper fn in the meantime. - */ -export function convertToReactFlowData(components: IComponent[]) { - const dummyNodes = [ - { - id: 'semantic-search', - position: { x: 40, y: 10 }, - data: { label: 'Semantic Search' }, - type: 'group', - style: { - height: 110, - width: 700, - }, - }, - { - id: 'model', - position: { x: 25, y: 25 }, - data: { label: 'Deployed Model ID' }, - type: 'default', - parentNode: 'semantic-search', - extent: 'parent', - }, - { - id: 'ingest-pipeline', - position: { x: 262, y: 25 }, - data: { label: 'Ingest Pipeline Name' }, - type: 'default', - parentNode: 'semantic-search', - extent: 'parent', - }, - ] as Array< - Node< - { - label: string; - }, - string | undefined - > - >; - - const dummyEdges = [ - { - id: 'e1-2', - source: 'model', - target: 'ingest-pipeline', - style: { - strokeWidth: 2, - stroke: 'black', - }, - markerEnd: { - type: 'arrow', - strokeWidth: 1, - color: 'black', - }, - }, - ] as Array>; - - return { - rfNodes: dummyNodes, - rfEdges: dummyEdges, - }; -} diff --git a/common/index.ts b/common/index.ts index d3ac23ca..47f29dc4 100644 --- a/common/index.ts +++ b/common/index.ts @@ -5,5 +5,4 @@ export * from './constants'; export * from './interfaces'; -export * from './helpers'; export { IComponent } from '../public/component_types'; diff --git a/common/interfaces.ts b/common/interfaces.ts index 39165e35..5487c718 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -3,19 +3,77 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * Interfaces here are primarily used for standardizing the data across - * server & client side - */ +import { Node, Edge } from 'reactflow'; +import { IComponent as IComponentData } from '../public/component_types'; -export interface IIndex { +export type Index = { name: string; health: 'green' | 'yellow' | 'red'; -} +}; -// TODO: this will grow as more fields are defined and what frontend reqts there will be -export interface IWorkflow { - name: string; +/** + ********** REACTFLOW TYPES/INTERFACES ********** + */ + +export type ReactFlowComponent = Node; + +// TODO: we may not need this re-defined type here at all, if we don't add +// any special fields/configuration for an edge. Currently this +// is the same as the default Edge type. +export type ReactFlowEdge = Edge<{}> & {}; + +type ReactFlowViewport = { + x: number; + y: number; + zoom: number; +}; + +export type ReactFlowState = { + nodes: ReactFlowComponent[]; + edges: ReactFlowEdge[]; + viewport?: ReactFlowViewport; +}; + +/** + ********** USE CASE TEMPLATE TYPES/INTERFACES ********** + */ + +type TemplateNode = { id: string; + inputs: {}; +}; + +type TemplateEdge = { + source: string; + target: string; +}; + +type TemplateFlow = { + userParams: {}; + nodes: TemplateNode[]; + edges: TemplateEdge[]; +}; + +type TemplateFlows = { + provision: TemplateFlow; + ingest: TemplateFlow; + query: TemplateFlow; +}; + +export type UseCaseTemplate = { + type: string; + name: string; description: string; -} + userInputs: {}; + workflows: TemplateFlows; +}; + +export type Workflow = { + id: string; + name: string; + description?: string; + // ReactFlow state may not exist if a workflow is created via API/backend-only. + reactFlowState?: ReactFlowState; + template: UseCaseTemplate; + lastUpdated: number; +}; diff --git a/eslintrc.json b/eslintrc.json new file mode 100644 index 00000000..6a4e1aa1 --- /dev/null +++ b/eslintrc.json @@ -0,0 +1,7 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/no-empty-interface": "off", + "react-hooks/exhaustive-deps": "off" + } +} diff --git a/package.json b/package.json index 6ddff479..979c1ad1 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "plugin-helpers": "../../scripts/use_node ../../scripts/plugin_helpers", "osd": "../../scripts/use_node ../../scripts/osd", "opensearch": "../../scripts/use_node ../../scripts/opensearch", - "lint:es": "../../scripts/use_node ../../scripts/eslint", + "lint:es": "../../scripts/use_node ../../scripts/eslint -c eslintrc.json", "lint:es:precommit": "yarn lint:es common/* public/* server/*", "build": "yarn plugin-helpers build && echo Renaming artifact to $npm_package_config_plugin_zip_name-$npm_package_config_plugin_version.zip && mv ./build/$npm_package_config_plugin_name*.zip ./build/$npm_package_config_plugin_zip_name-$npm_package_config_plugin_version.zip" }, diff --git a/public/app.tsx b/public/app.tsx index 0130d4e8..1eb7d547 100644 --- a/public/app.tsx +++ b/public/app.tsx @@ -15,7 +15,6 @@ import { WorkflowDetailRouterProps, } from './pages'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface interface Props extends RouteComponentProps {} export const AiFlowDashboardsApp = (props: Props) => { diff --git a/public/component_types/index.ts b/public/component_types/index.ts index b732283b..bc3be5cb 100644 --- a/public/component_types/index.ts +++ b/public/component_types/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './base_interfaces'; +export * from './interfaces'; export * from './processors'; export * from './indices'; diff --git a/public/component_types/indices/knn_index.ts b/public/component_types/indices/knn_index.ts index 70978071..3a35a595 100644 --- a/public/component_types/indices/knn_index.ts +++ b/public/component_types/indices/knn_index.ts @@ -11,7 +11,7 @@ import { IComponentOutput, UIFlow, BaseClass, -} from '../base_interfaces'; +} from '../interfaces'; /** * A k-NN index UI component diff --git a/public/component_types/base_interfaces.ts b/public/component_types/interfaces.ts similarity index 100% rename from public/component_types/base_interfaces.ts rename to public/component_types/interfaces.ts diff --git a/public/component_types/processors/text_embedding_processor.ts b/public/component_types/processors/text_embedding_processor.ts index 1000bdbe..7356f1fa 100644 --- a/public/component_types/processors/text_embedding_processor.ts +++ b/public/component_types/processors/text_embedding_processor.ts @@ -11,7 +11,7 @@ import { IComponentOutput, UIFlow, BaseClass, -} from '../base_interfaces'; +} from '../interfaces'; /** * A text embedding processor UI component diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx index 324ece40..4c70ef20 100644 --- a/public/pages/workflow_detail/components/header.tsx +++ b/public/pages/workflow_detail/components/header.tsx @@ -5,10 +5,10 @@ import React from 'react'; import { EuiPageHeader, EuiButton } from '@elastic/eui'; -import { IWorkflow } from '../../../../common'; +import { Workflow } from '../../../../common'; interface WorkflowDetailHeaderProps { - workflow: IWorkflow | undefined; + workflow?: Workflow; } export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index 4d0fddda..e110acaf 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -17,7 +17,6 @@ export interface WorkflowDetailRouterProps { workflowId: string; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface interface WorkflowDetailProps extends RouteComponentProps {} @@ -41,7 +40,7 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
- +
); } diff --git a/public/pages/workflow_detail/workspace/workspace.tsx b/public/pages/workflow_detail/workspace/workspace.tsx index 9b883fda..576a8361 100644 --- a/public/pages/workflow_detail/workspace/workspace.tsx +++ b/public/pages/workflow_detail/workspace/workspace.tsx @@ -11,31 +11,25 @@ import ReactFlow, { useEdgesState, addEdge, } from 'reactflow'; -import { useSelector } from 'react-redux'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { AppState, rfContext } from '../../../store'; -import { convertToReactFlowData } from '../../../../common'; +import { rfContext } from '../../../store'; +import { Workflow } from '../../../../common'; +import { getCore } from '../../../services'; // styling import 'reactflow/dist/style.css'; import './reactflow-styles.scss'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface WorkspaceProps {} +interface WorkspaceProps { + workflow?: Workflow; +} export function Workspace(props: WorkspaceProps) { const reactFlowWrapper = useRef(null); const { reactFlowInstance, setReactFlowInstance } = useContext(rfContext); - // Fetching workspace state to populate the initial nodes/edges. - // Where/how the low-level ReactFlow JSON will be persisted is TBD. - // TODO: update when that design is finalized - const storedComponents = useSelector( - (state: AppState) => state.workspace.components - ); - const { rfNodes, rfEdges } = convertToReactFlowData(storedComponents); - const [nodes, setNodes, onNodesChange] = useNodesState(rfNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(rfEdges); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); const onConnect = useCallback( (params) => { @@ -88,16 +82,24 @@ export function Workspace(props: WorkspaceProps) { setNodes((nds) => nds.concat(newNode)); }, - // eslint-disable-next-line react-hooks/exhaustive-deps [reactFlowInstance] ); - // Initialization hook + // Initialization. Set the nodes and edges to an existing workflow, + // if applicable. useEffect(() => { - // TODO: fetch the nodes/edges dynamically (loading existing flow, - // creating fresh from template, creating blank template, etc.) - // Will involve populating and/or fetching from redux store - }, []); + const workflow = props.workflow; + if (workflow) { + if (workflow.reactFlowState) { + setNodes(workflow.reactFlowState.nodes); + setEdges(workflow.reactFlowState.edges); + } else { + getCore().notifications.toasts.addWarning( + `There is no configured UI flow for workflow: ${workflow.name}` + ); + } + } + }, [props.workflow]); return ( diff --git a/public/pages/workflow_detail/workspace_component/workspace_component.tsx b/public/pages/workflow_detail/workspace_component/workspace_component.tsx index 4088abdd..a69a5d0e 100644 --- a/public/pages/workflow_detail/workspace_component/workspace_component.tsx +++ b/public/pages/workflow_detail/workspace_component/workspace_component.tsx @@ -20,8 +20,8 @@ interface WorkspaceComponentProps { } /** - * TODO: This will be the ReactFlow node in the drag-and-drop workspace. It will take in a component - * from the global workflow state and render it appropriately (inputs / params / outputs / etc.) + * TODO: This will be the ReactFlow node in the drag-and-drop workspace. It will take in the data passed + * to it from the workspace and render it appropriately (inputs / params / outputs / etc.) * Similar to Flowise's CanvasNode - see * https://github.com/FlowiseAI/Flowise/blob/main/packages/ui/src/views/canvas/CanvasNode.js */ diff --git a/public/pages/workflows/components/columns.tsx b/public/pages/workflows/components/columns.tsx index a41179c2..07300ca0 100644 --- a/public/pages/workflows/components/columns.tsx +++ b/public/pages/workflows/components/columns.tsx @@ -5,14 +5,14 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; -import { PLUGIN_ID, IWorkflow } from '../../../../common'; +import { PLUGIN_ID, Workflow } from '../../../../common'; export const columns = [ { field: 'name', name: 'Name', sortable: true, - render: (name: string, workflow: IWorkflow) => ( + render: (name: string, workflow: Workflow) => ( {name} ), }, diff --git a/public/pages/workflows/components/workflow_list.tsx b/public/pages/workflows/components/workflow_list.tsx index 93c0a3b8..f182de24 100644 --- a/public/pages/workflows/components/workflow_list.tsx +++ b/public/pages/workflows/components/workflow_list.tsx @@ -7,10 +7,9 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { EuiInMemoryTable, Direction } from '@elastic/eui'; import { AppState } from '../../../store'; -import { IWorkflow } from '../../../../common'; +import { Workflow } from '../../../../common'; import { columns } from './columns'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface interface WorkflowListProps {} export function WorkflowList(props: WorkflowListProps) { @@ -24,7 +23,7 @@ export function WorkflowList(props: WorkflowListProps) { }; return ( - + items={workflows} rowHeader="name" columns={columns} diff --git a/public/store/reducers/opensearch_reducer.ts b/public/store/reducers/opensearch_reducer.ts index 8bc8a53f..783ed400 100644 --- a/public/store/reducers/opensearch_reducer.ts +++ b/public/store/reducers/opensearch_reducer.ts @@ -5,12 +5,12 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { getRouteService } from '../../services'; -import { IIndex } from '../../../common'; +import { Index } from '../../../common'; const initialState = { loading: false, errorMessage: '', - indices: {} as { [key: string]: IIndex }, + indices: {} as { [key: string]: Index }, }; const OPENSEARCH_PREFIX = 'opensearch'; @@ -36,8 +36,8 @@ const opensearchSlice = createSlice({ state.loading = true; }) .addCase(fetchIndices.fulfilled, (state, action) => { - const indicesMap = new Map(); - action.payload.forEach((index: IIndex) => { + const indicesMap = new Map(); + action.payload.forEach((index: Index) => { indicesMap.set(index.name, index); }); state.indices = Object.fromEntries(indicesMap.entries()); diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts index 3d84bade..d181ebf8 100644 --- a/public/store/reducers/workflows_reducer.ts +++ b/public/store/reducers/workflows_reducer.ts @@ -4,7 +4,54 @@ */ import { createSlice } from '@reduxjs/toolkit'; -import { IWorkflow } from '../../../common'; +import { Workflow, ReactFlowComponent, ReactFlowEdge } from '../../../common'; + +// TODO: remove after fetching from server-side +const dummyNodes = [ + { + id: 'semantic-search', + position: { x: 40, y: 10 }, + data: { label: 'Semantic Search' }, + type: 'group', + style: { + height: 110, + width: 700, + }, + }, + { + id: 'model', + position: { x: 25, y: 25 }, + data: { label: 'Deployed Model ID' }, + type: 'default', + parentNode: 'semantic-search', + extent: 'parent', + }, + { + id: 'ingest-pipeline', + position: { x: 262, y: 25 }, + data: { label: 'Ingest Pipeline Name' }, + type: 'default', + parentNode: 'semantic-search', + extent: 'parent', + }, +] as ReactFlowComponent[]; + +const dummyEdges = [ + { + id: 'e1-2', + source: 'model', + target: 'ingest-pipeline', + style: { + strokeWidth: 2, + stroke: 'black', + }, + markerEnd: { + type: 'arrow', + strokeWidth: 1, + color: 'black', + }, + }, +] as ReactFlowEdge[]; const initialState = { // TODO: fetch from server-side later @@ -13,13 +60,23 @@ const initialState = { name: 'Workflow-1', id: 'workflow-1-id', description: 'description for workflow 1', + reactFlowState: { + nodes: dummyNodes, + edges: dummyEdges, + }, + template: {}, }, { name: 'Workflow-2', id: 'workflow-2-id', description: 'description for workflow 2', + reactFlowState: { + nodes: dummyNodes, + edges: dummyEdges, + }, + template: {}, }, - ] as IWorkflow[], + ] as Workflow[], loading: false, }; diff --git a/public/types.ts b/public/types.ts index c6b021cc..3695b033 100644 --- a/public/types.ts +++ b/public/types.ts @@ -5,9 +5,7 @@ import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AiFlowDashboardsPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AiFlowDashboardsPluginStart {} export interface AppPluginStartDependencies { diff --git a/server/routes/opensearch_routes.ts b/server/routes/opensearch_routes.ts index 6e6b81f3..5454c4b7 100644 --- a/server/routes/opensearch_routes.ts +++ b/server/routes/opensearch_routes.ts @@ -9,7 +9,7 @@ import { IRouter, IOpenSearchDashboardsResponse, } from '../../../../src/core/server'; -import { SEARCH_INDICES_PATH, FETCH_INDICES_PATH, IIndex } from '../../common'; +import { SEARCH_INDICES_PATH, FETCH_INDICES_PATH, Index } from '../../common'; import { generateCustomError } from './helpers'; export function registerOpenSearchRoutes(router: IRouter): void { @@ -61,11 +61,11 @@ export function registerOpenSearchRoutes(router: IRouter): void { h: 'health,index', }); - // re-formatting the index results to match IIndex + // re-formatting the index results to match Index const cleanedIndices = response.body.map((index) => ({ name: index.index, health: index.health, - })) as IIndex[]; + })) as Index[]; return res.ok({ body: cleanedIndices }); } catch (err: any) { diff --git a/server/types.ts b/server/types.ts index 1fd42a8a..35854daa 100644 --- a/server/types.ts +++ b/server/types.ts @@ -3,7 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AiFlowDashboardsPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AiFlowDashboardsPluginStart {}