Skip to content

Commit

Permalink
Integrate initial workflow configs with Formik & yup (#153)
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
(cherry picked from commit dcef748)
  • Loading branch information
ohltyler authored and github-actions[bot] committed May 15, 2024
1 parent ba402d6 commit 314e71f
Show file tree
Hide file tree
Showing 23 changed files with 1,028 additions and 201 deletions.
43 changes: 35 additions & 8 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,48 @@ export type Index = {
TODO: over time these can become less generic as the form inputs & UX becomes finalized
*/

export type ConfigFieldType = 'string' | 'json' | 'select' | 'model';
export type ConfigSelectType = 'model';
export type ConfigFieldValue = string | {};
export interface IConfigField {
label: string;
type: ConfigFieldType;
id: string;
value?: ConfigFieldValue;
placeholder?: string;
helpText?: string;
helpLink?: string;
selectType?: ConfigSelectType;
}

export interface IConfigMetadata {
label?: string;
}

export interface IConfig {
id: string;
fields: IConfigField[];
metadata?: IConfigMetadata;
}

export type EnrichConfig = {
processors: IConfig[];
};

export type IndexConfig = {
isNew: boolean;
indexName: string;
name: IConfigField;
};

export type IngestConfig = {
source: FormikValues;
enrich: FormikValues;
ingest: IndexConfig;
source: IConfig;
enrich: EnrichConfig;
index: IndexConfig;
};

export type SearchConfig = {
request: FormikValues;
enrichRequest: FormikValues;
enrichResponse: FormikValues;
request: IConfig;
enrichRequest: IConfig;
enrichResponse: IConfig;
};

export type WorkflowConfig = {
Expand Down
26 changes: 26 additions & 0 deletions public/configs/base_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { IConfig, IConfigField } from '../../common';

/**
* A base UI config class.
*/
export abstract class BaseConfig implements IConfig {
id: string;
fields: IConfigField[];

// No-op constructor. If there are general / defaults for field values, add in here.
constructor() {
this.id = '';
this.fields = [];
}

// Persist a standard toObj() fn that all component classes can use. This is necessary
// so we have standard JS Object when serializing comoponent state in redux.
toObj() {
return Object.assign({}, this);
}
}
6 changes: 6 additions & 0 deletions public/configs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './ingest_processors';
19 changes: 19 additions & 0 deletions public/configs/ingest_processors/base_ingest_processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { BaseConfig } from '../base_config';

/**
* A base ingest processor config
*/
export abstract class BaseIngestProcessor extends BaseConfig {
name: string;
type: string;
constructor() {
super();
this.name = '';
this.type = '';
}
}
6 changes: 6 additions & 0 deletions public/configs/ingest_processors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './text_embedding_processor';
46 changes: 46 additions & 0 deletions public/configs/ingest_processors/text_embedding_processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { generateId } from '../../utils';
import { BaseIngestProcessor } from './base_ingest_processor';

/**
* A specialized text embedding processor config
*/
export class TextEmbeddingProcessor extends BaseIngestProcessor {
constructor() {
super();
this.id = generateId('text_embedding_processor');
this.name = 'Text embedding processor';
this.type = 'text_embedding';
this.fields = [
{
label: 'Text Embedding Model',
id: 'model',
type: 'model',
helpText: 'A text embedding model for embedding text.',
helpLink:
'https://opensearch.org/docs/latest/ml-commons-plugin/integrating-ml-models/#choosing-a-model',
},
{
label: 'Input Field',
id: 'inputField',
type: 'string',
helpText:
'The name of the document field from which to obtain text for generating text embeddings.',
helpLink:
'https://opensearch.org/docs/latest/ingest-pipelines/processors/text-embedding/',
},
{
label: 'Vector Field',
id: 'vectorField',
type: 'string',
helpText: `The name of the document's vector field in which to store the generated text embeddings.`,
helpLink:
'https://opensearch.org/docs/latest/ingest-pipelines/processors/text-embedding/',
},
];
}
}
125 changes: 58 additions & 67 deletions public/pages/workflow_detail/resizable_workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,38 @@ import { useHistory } from 'react-router-dom';
import { useReactFlow } from 'reactflow';
import { Form, Formik, FormikProps } from 'formik';
import * as yup from 'yup';
import {
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiResizableContainer,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiResizableContainer } from '@elastic/eui';
import { getCore } from '../../services';

import {
Workflow,
WorkspaceFormValues,
ReactFlowComponent,
WorkspaceFlowState,
WORKFLOW_STATE,
ReactFlowEdge,
WorkflowFormValues,
WorkflowSchema,
} from '../../../common';
import {
processNodes,
APP_PATH,
uiConfigToFormik,
uiConfigToSchema,
formikToUiConfig,
reduceToTemplate,
} from '../../utils';
import { validateWorkspaceFlow, toTemplateFlows } from './utils';
import { AppState, setDirty, useAppDispatch } from '../../store';
import {
AppState,
createWorkflow,
setDirty,
updateWorkflow,
useAppDispatch,
} from '../../store';
import { Workspace } from './workspace/workspace';

// styling
import './workspace/workspace-styles.scss';
import '../../global-styles.scss';
import { WorkflowInputs } from './workflow_inputs';
import { configToTemplateFlows } from './utils';

interface ResizableWorkspaceProps {
isNewWorkflow: boolean;
Expand Down Expand Up @@ -71,10 +71,8 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
const [formValues, setFormValues] = useState<WorkflowFormValues>({});
const [formSchema, setFormSchema] = useState<WorkflowSchema>(yup.object({}));

// Validation states. Maintain separate state for form vs. overall flow so
// we can have fine-grained errors and action items for users
// Validation states
const [formValidOnSubmit, setFormValidOnSubmit] = useState<boolean>(true);
const [flowValidOnSubmit, setFlowValidOnSubmit] = useState<boolean>(true);

// Component details side panel state
const [isDetailsPanelOpen, setisDetailsPanelOpen] = useState<boolean>(true);
Expand All @@ -100,7 +98,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
!isDirty &&
!props.isNewWorkflow &&
formValidOnSubmit &&
flowValidOnSubmit &&
props.workflow?.state === WORKFLOW_STATE.NOT_STARTED;
const isDeprovisionable =
props.workflow !== undefined &&
Expand Down Expand Up @@ -188,24 +185,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {

// Initialize the form state to an existing workflow, if applicable.
useEffect(() => {
// if (workflow?.ui_metadata?.workspace_flow) {
// const initFormValues = {} as WorkspaceFormValues;
// const initSchemaObj = {} as WorkspaceSchemaObj;
// workflow.ui_metadata.workspace_flow.nodes.forEach((node) => {
// initFormValues[node.id] = componentDataToFormik(node.data);
// initSchemaObj[node.id] = getComponentSchema(node.data);
// });
// const initFormSchema = yup.object(initSchemaObj) as WorkspaceSchema;
// setFormValues(initFormValues);
// setFormSchema(initFormSchema);
// }
if (workflow?.ui_metadata?.config) {
// TODO: implement below fns to generate the final form and schema objs.
// Should generate the form and its values on-the-fly
// similar to what we do with ComponentData in above commented-out code.
// This gives us more flexibility and maintainability instead of having to update
// low-level form and schema when making config changes (e.g., if of type 'string',
// automatically generate the default form values, and the default validation schema)
const initFormValues = uiConfigToFormik(workflow.ui_metadata.config);
const initFormSchema = uiConfigToSchema(workflow.ui_metadata.config);
setFormValues(initFormValues);
Expand All @@ -228,9 +208,8 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
}

// Utility validation fn used before executing any API calls (save, provision)
function validateFormAndFlow(
formikProps: FormikProps<WorkspaceFormValues>,
processWorkflowFn: (workflow: Workflow) => void
function validateAndSubmit(
formikProps: FormikProps<WorkflowFormValues>
): void {
// Submit the form to bubble up any errors.
// Ideally we handle Promise accept/rejects with submitForm(), but there is
Expand All @@ -243,25 +222,40 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
setIsSaving(false);
} else {
setFormValidOnSubmit(true);
let curFlowState = reactFlowInstance.toObject() as WorkspaceFlowState;
curFlowState = {
...curFlowState,
nodes: processNodes(curFlowState.nodes, formikProps.values),
};
if (validateWorkspaceFlow(curFlowState)) {
setFlowValidOnSubmit(true);
const updatedWorkflow = {
...workflow,
ui_metadata: {
...workflow?.ui_metadata,
workspace_flow: curFlowState,
},
workflows: toTemplateFlows(curFlowState),
} as Workflow;
processWorkflowFn(updatedWorkflow);
const updatedConfig = formikToUiConfig(formikProps.values);
const updatedWorkflow = {
...workflow,
ui_metadata: {
...workflow?.ui_metadata,
config: updatedConfig,
},
workflows: configToTemplateFlows(updatedConfig),
} as Workflow;
if (updatedWorkflow.id) {
dispatch(
updateWorkflow({
workflowId: updatedWorkflow.id,
workflowTemplate: reduceToTemplate(updatedWorkflow),
})
)
.unwrap()
.then((result) => {
setIsSaving(false);
})
.catch((error: any) => {
setIsSaving(false);
});
} else {
setFlowValidOnSubmit(false);
setIsSaving(false);
dispatch(createWorkflow(updatedWorkflow))
.unwrap()
.then((result) => {
const { workflow } = result;
history.replace(`${APP_PATH.WORKFLOWS}/${workflow.id}`);
history.go(0);
})
.catch((error: any) => {
setIsSaving(false);
});
}
}
});
Expand All @@ -277,7 +271,10 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
>
{(formikProps) => (
<Form>
{!formValidOnSubmit && (
{/*
TODO: finalize where/how to show invalidations
*/}
{/* {!formValidOnSubmit && (
<EuiCallOut
title="There are empty or invalid fields"
color="danger"
Expand All @@ -287,17 +284,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
Please address the highlighted fields and try saving again.
</EuiCallOut>
)}
{!flowValidOnSubmit && (
<EuiCallOut
title="The configured flow is invalid"
color="danger"
iconType="alert"
style={{ marginBottom: '16px' }}
>
Please ensure there are no open connections between the
components.
</EuiCallOut>
)}
{isDeprovisionable && isDirty && (
<EuiCallOut
title="The configured flow has been provisioned"
Expand All @@ -308,7 +294,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
Changes cannot be saved until the workflow has first been
deprovisioned.
</EuiCallOut>
)}
)} */}
<EuiResizableContainer
direction="horizontal"
className="stretch-absolute"
Expand Down Expand Up @@ -338,7 +324,12 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
className="workspace-panel"
>
<EuiFlexItem>
<WorkflowInputs workflow={props.workflow} />
<WorkflowInputs
workflow={props.workflow}
formikProps={formikProps}
onFormChange={onFormChange}
validateAndSubmit={validateAndSubmit}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiResizablePanel>
Expand Down
Loading

0 comments on commit 314e71f

Please sign in to comment.