Skip to content

Commit

Permalink
Add new form interfaces (#151)
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
ohltyler authored May 13, 2024
1 parent c74260f commit 4a0ade3
Show file tree
Hide file tree
Showing 8 changed files with 822 additions and 741 deletions.
40 changes: 39 additions & 1 deletion common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,43 @@ export type Index = {
health: 'green' | 'yellow' | 'red';
};

/**
********** WORKFLOW TYPES/INTERFACES **********
TODO: over time these can become less generic as the form inputs & UX becomes finalized
*/

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

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

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

export type WorkflowConfig = {
ingest?: IngestConfig;
search?: SearchConfig;
};

export type WorkflowFormValues = {
ingest: FormikValues;
search: FormikValues;
};

export type WorkflowSchemaObj = {
[key: string]: ObjectSchema<any, any, any>;
};
export type WorkflowSchema = ObjectSchema<WorkflowSchemaObj>;

/**
********** WORKSPACE TYPES/INTERFACES **********
*/
Expand Down Expand Up @@ -111,7 +148,8 @@ type ReactFlowViewport = {
};

export type UIState = {
workspace_flow: WorkspaceFlowState;
config: WorkflowConfig;
workspace_flow?: WorkspaceFlowState;
};

export type WorkspaceFlowState = {
Expand Down
77 changes: 31 additions & 46 deletions public/pages/workflow_detail/resizable_workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useHistory } from 'react-router-dom';
import { useReactFlow } from 'reactflow';
import { Form, Formik, FormikProps } from 'formik';
import * as yup from 'yup';
import { cloneDeep } from 'lodash';
import {
EuiCallOut,
EuiFlexGroup,
Expand All @@ -21,18 +20,18 @@ import { getCore } from '../../services';
import {
Workflow,
WorkspaceFormValues,
WorkspaceSchema,
ReactFlowComponent,
WorkspaceSchemaObj,
WorkspaceFlowState,
WORKFLOW_STATE,
ReactFlowEdge,
WorkflowFormValues,
WorkflowSchema,
} from '../../../common';
import {
componentDataToFormik,
getComponentSchema,
processNodes,
APP_PATH,
uiConfigToFormik,
uiConfigToSchema,
} from '../../utils';
import { validateWorkspaceFlow, toTemplateFlows } from './utils';
import { AppState, setDirty, useAppDispatch } from '../../store';
Expand Down Expand Up @@ -69,8 +68,8 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
);

// Formik form state
const [formValues, setFormValues] = useState<WorkspaceFormValues>({});
const [formSchema, setFormSchema] = useState<WorkspaceSchema>(yup.object({}));
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
Expand All @@ -83,7 +82,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
(id: string, options: { direction: 'left' | 'right' }) => {}
);
const onToggleChange = () => {
collapseFn.current(COMPONENT_DETAILS_PANEL_ID, { direction: 'left' });
collapseFn.current(WORKFLOW_INPUTS_PANEL_ID, { direction: 'left' });
setisDetailsPanelOpen(!isDetailsPanelOpen);
};

Expand Down Expand Up @@ -158,7 +157,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
// In these cases, just render what is persisted, no action needed.
useEffect(() => {
const missingUiFlow =
props.workflow && !props.workflow?.ui_metadata?.workspace_flow;
props.workflow && !props.workflow?.ui_metadata?.config;
const missingCachedWorkflow = props.isNewWorkflow && !props.workflow;
if (missingUiFlow || missingCachedWorkflow) {
history.replace(APP_PATH.WORKFLOWS);
Expand Down Expand Up @@ -189,48 +188,34 @@ 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;
// 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);
setFormSchema(initFormSchema);
}
}, [workflow]);

// Update the form values and validation schema when a node is added
// or removed from the workspace.
// For the schema, we do a deep clone of the underlying object, and later re-create the schema.
// For the form values, we update directly to prevent the form from being reinitialized.
function onNodesChange(nodes: ReactFlowComponent[]): void {
const updatedComponentIds = nodes.map((node) => node.id);
const existingComponentIds = Object.keys(formValues);
const updatedSchemaObj = cloneDeep(formSchema.fields) as WorkspaceSchemaObj;

if (updatedComponentIds.length > existingComponentIds.length) {
// TODO: implement for when a node is added
} else if (updatedComponentIds.length < existingComponentIds.length) {
existingComponentIds.forEach((existingId) => {
if (!updatedComponentIds.includes(existingId)) {
// Remove the mapping for the removed component in the form values
// and schema.
delete formValues[`${existingId}`];
delete updatedSchemaObj[`${existingId}`];
}
});
} else {
// if it is somehow triggered without node changes, be sure
// to prevent updating the form or schema
return;
}

const updatedSchema = yup.object(updatedSchemaObj) as WorkspaceSchema;
setFormSchema(updatedSchema);
}
// 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 <Form> components as a listener to propagate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,41 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import React, { useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiRadioGroup,
EuiTitle,
} from '@elastic/eui';
import { Workflow } from '../../../../../common';

interface IngestDataProps {}
interface IngestDataProps {
workflow: Workflow;
}

enum OPTION {
NEW = 'new',
EXISTING = 'existing',
}

const options = [
{
id: OPTION.NEW,
label: 'Create a new index',
},
{
id: OPTION.EXISTING,
label: 'Choose existing index',
},
];

/**
* Input component for configuring the data ingest (the OpenSearch index)
*/
export function IngestData(props: IngestDataProps) {
const [selectedOption, setSelectedOption] = useState<OPTION>(OPTION.NEW);

return (
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
Expand All @@ -20,7 +46,11 @@ export function IngestData(props: IngestDataProps) {
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText grow={false}>TODO</EuiText>
<EuiRadioGroup
options={options}
idSelected={selectedOption}
onChange={(optionId) => setSelectedOption(optionId as OPTION)}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { SourceData } from './source_data';
import { EnrichData } from './enrich_data';
import { IngestData } from './ingest_data';
import { Workflow } from '../../../../../common';

interface IngestInputsProps {}
interface IngestInputsProps {
workflow: Workflow;
}

/**
* The base component containing all of the ingest-related inputs
Expand All @@ -30,7 +33,7 @@ export function IngestInputs(props: IngestInputsProps) {
<EuiHorizontalRule margin="none" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<IngestData />
<IngestData workflow={props.workflow} />
</EuiFlexItem>
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ 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';
import { Workflow } from '../../../../../common';

interface SearchInputsProps {}
interface SearchInputsProps {
workflow: Workflow;
}

/**
* The base component containing all of the search-related inputs
Expand Down
60 changes: 35 additions & 25 deletions public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
*/

import React, { useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
import { Workflow } from '../../../../common';
import { Footer } from './footer';
import { IngestInputs } from './ingest_inputs';
Expand Down Expand Up @@ -33,30 +39,34 @@ export function WorkflowInputs(props: WorkflowInputsProps) {

return (
<EuiPanel paddingSize="m">
<EuiFlexGroup
direction="column"
justifyContent="spaceBetween"
style={{ height: '100%', paddingBottom: '48px' }}
>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h4>{selectedStep}</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
{selectedStep === CREATE_STEP.INGEST ? (
<IngestInputs />
) : (
<SearchInputs />
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Footer
selectedStep={selectedStep}
setSelectedStep={setSelectedStep}
/>
</EuiFlexItem>
</EuiFlexGroup>
{props.workflow === undefined ? (
<EuiLoadingSpinner size="xl" />
) : (
<EuiFlexGroup
direction="column"
justifyContent="spaceBetween"
style={{ height: '100%', paddingBottom: '48px' }}
>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h4>{selectedStep}</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
{selectedStep === CREATE_STEP.INGEST ? (
<IngestInputs workflow={props.workflow} />
) : (
<SearchInputs workflow={props.workflow} />
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Footer
selectedStep={selectedStep}
setSelectedStep={setSelectedStep}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiPanel>
);
}
Loading

0 comments on commit 4a0ade3

Please sign in to comment.