Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate initial workflow configs with Formik & yup #153

Merged
merged 6 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading