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

Update create workflow, update workflow, and form validation logic #164

Merged
merged 3 commits into from
Jun 5, 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
1 change: 0 additions & 1 deletion common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ export enum COMPONENT_CLASS {
/**
* MISCELLANEOUS
*/
export const NEW_WORKFLOW_ID_URL = 'new';
export const START_FROM_SCRATCH_WORKFLOW_NAME = 'Start From Scratch';
export const DEFAULT_NEW_WORKFLOW_NAME = 'new_workflow';
export const DEFAULT_NEW_WORKFLOW_DESCRIPTION = 'My new workflow';
Expand Down
13 changes: 2 additions & 11 deletions public/pages/workflow_detail/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,16 @@ import {
} from '../../../../common';

interface WorkflowDetailHeaderProps {
isNewWorkflow: boolean;
workflow?: Workflow;
}

export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
function getTitle() {
return props.workflow
? props.workflow.name
: props.isNewWorkflow && !props.workflow
? DEFAULT_NEW_WORKFLOW_NAME
: '';
return props.workflow ? props.workflow.name : DEFAULT_NEW_WORKFLOW_NAME;
}

function getState() {
return props.workflow?.state
? props.workflow.state
: props.isNewWorkflow
? DEFAULT_NEW_WORKFLOW_STATE
: null;
return props.workflow ? props.workflow.state : DEFAULT_NEW_WORKFLOW_STATE;
}

return (
Expand Down
139 changes: 10 additions & 129 deletions public/pages/workflow_detail/resizable_workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,20 @@
import React, { useRef, useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { Form, Formik, FormikProps } from 'formik';
import { Form, Formik } from 'formik';
import * as yup from 'yup';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiResizableContainer,
EuiTitle,
} from '@elastic/eui';
import { getCore } from '../../services';

import {
Workflow,
WORKFLOW_STATE,
WorkflowFormValues,
WorkflowSchema,
WorkflowConfig,
} from '../../../common';
import {
APP_PATH,
uiConfigToFormik,
uiConfigToSchema,
formikToUiConfig,
reduceToTemplate,
} from '../../utils';
import {
AppState,
createWorkflow,
setDirty,
updateWorkflow,
useAppDispatch,
} from '../../store';
import { Workflow, WorkflowFormValues, WorkflowSchema } from '../../../common';
import { APP_PATH, uiConfigToFormik, uiConfigToSchema } from '../../utils';
import { AppState, setDirty, useAppDispatch } from '../../store';
import { WorkflowInputs } from './workflow_inputs';
import { configToTemplateFlows } from './utils';
import { Workspace } from './workspace';

// styling
Expand All @@ -48,7 +28,6 @@ import '../../global-styles.scss';
import { Tools } from './tools';

interface ResizableWorkspaceProps {
isNewWorkflow: boolean;
workflow?: Workflow;
}

Expand All @@ -66,7 +45,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
// Overall workspace state
const { isDirty } = useSelector((state: AppState) => state.workspace);
const { loading } = useSelector((state: AppState) => state.workflows);
const [isFirstSave, setIsFirstSave] = useState<boolean>(props.isNewWorkflow);

// Workflow state
const [workflow, setWorkflow] = useState<Workflow | undefined>(
Expand All @@ -77,9 +55,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
const [formValues, setFormValues] = useState<WorkflowFormValues>({});
const [formSchema, setFormSchema] = useState<WorkflowSchema>(yup.object({}));

// Validation states
const [formValidOnSubmit, setFormValidOnSubmit] = useState<boolean>(true);

// Workflow inputs side panel state
const [isWorkflowInputsPanelOpen, setIsWorkflowInputsPanelOpen] = useState<
boolean
Expand All @@ -104,56 +79,20 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
setIsToolsPanelOpen(!isToolsPanelOpen);
};

// Save/provision/deprovision button state
const isSaveable =
props.workflow !== undefined && (isFirstSave ? true : isDirty);
const isProvisionable =
props.workflow !== undefined &&
!isDirty &&
!props.isNewWorkflow &&
formValidOnSubmit &&
props.workflow?.state === WORKFLOW_STATE.NOT_STARTED;
const isDeprovisionable =
props.workflow !== undefined &&
!props.isNewWorkflow &&
props.workflow?.state !== WORKFLOW_STATE.NOT_STARTED;
// TODO: maybe remove this field. It depends on final UX if we want the
// workspace to be readonly once provisioned or not.
const readonly = props.workflow === undefined || isDeprovisionable;

// Loading state
const [isProvisioning, setIsProvisioning] = useState<boolean>(false);
const [isDeprovisioning, setIsDeprovisioning] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
const isCreating = isSaving && props.isNewWorkflow;
const isLoadingGlobal =
loading || isProvisioning || isDeprovisioning || isSaving || isCreating;

// 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.
// 1. Rendering backend-only-created workflow / an existing workflow with no ui_metadata.
// In this case, we revert to the home page with a warn toast that we don't support it, for now.
// This is because we initially have guardrails and a static set of readonly nodes/edges that we handle.
// 2. Rendering empty/null workflow, if refreshing the editor page where there is no cached workflow and
// no workflow ID in the URL.
// In this case, revert to home page and a warn toast that we don't support it for now.
// This is because we initially don't support building / drag-and-drop components.
// 3. Rendering a cached workflow via navigation from create workflow tab
// 4. Rendering a created workflow with ui_metadata.
// 2. Rendering a created workflow with ui_metadata.
// In these cases, just render what is persisted, no action needed.
useEffect(() => {
const missingUiFlow =
props.workflow && !props.workflow?.ui_metadata?.config;
const missingCachedWorkflow = props.isNewWorkflow && !props.workflow;
if (missingUiFlow || missingCachedWorkflow) {
if (missingUiFlow) {
history.replace(APP_PATH.WORKFLOWS);
if (missingCachedWorkflow) {
getCore().notifications.toasts.addWarning('No workflow found');
} else {
getCore().notifications.toasts.addWarning(
`There is no ui_metadata for workflow: ${props.workflow?.name}`
);
}
getCore().notifications.toasts.addWarning(
`There is no ui_metadata for workflow: ${props.workflow?.name}`
);
} else {
setWorkflow(props.workflow);
}
Expand All @@ -179,63 +118,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
}
}

// Utility validation fn used before executing any API calls (save, provision)
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
// open issues for that - see https://github.com/jaredpalmer/formik/issues/2057
// The workaround is to additionally execute validateForm() which will return any errors found.
formikProps.submitForm();
formikProps.validateForm().then((validationResults: {}) => {
if (Object.keys(validationResults).length > 0) {
setFormValidOnSubmit(false);
setIsSaving(false);
} else {
setFormValidOnSubmit(true);
const updatedConfig = formikToUiConfig(
formikProps.values,
workflow?.ui_metadata?.config as WorkflowConfig
);
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 {
dispatch(createWorkflow(updatedWorkflow))
.unwrap()
.then((result) => {
const { workflow } = result;
history.replace(`${APP_PATH.WORKFLOWS}/${workflow.id}`);
history.go(0);
})
.catch((error: any) => {
setIsSaving(false);
});
}
}
});
}

return (
<Formik
enableReinitialize={true}
Expand Down Expand Up @@ -278,7 +160,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
workflow={props.workflow}
formikProps={formikProps}
onFormChange={onFormChange}
validateAndSubmit={validateAndSubmit}
/>
</EuiResizablePanel>
<EuiResizableButton />
Expand Down
30 changes: 8 additions & 22 deletions public/pages/workflow_detail/workflow_detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { ResizableWorkspace } from './resizable_workspace';
import {
DEFAULT_NEW_WORKFLOW_NAME,
FETCH_ALL_QUERY_BODY,
NEW_WORKFLOW_ID_URL,
} from '../../../common';

// styling
Expand All @@ -39,24 +38,18 @@ interface WorkflowDetailProps
* The workflow details page. This is where users will configure, create, and
* test their created workflows. Additionally, can be used to load existing workflows
* to view details and/or make changes to them.
* New, unsaved workflows are cached in the redux store and displayed here.
*/

export function WorkflowDetail(props: WorkflowDetailProps) {
const dispatch = useAppDispatch();
const { workflows, cachedWorkflow, errorMessage } = useSelector(
const { workflows, errorMessage } = useSelector(
(state: AppState) => state.workflows
);

// selected workflow state
const workflowId = props.match?.params?.workflowId;
const isNewWorkflow = workflowId === NEW_WORKFLOW_ID_URL;
const workflow = isNewWorkflow ? cachedWorkflow : workflows[workflowId];
const workflowName = workflow
? workflow.name
: isNewWorkflow && !workflow
? DEFAULT_NEW_WORKFLOW_NAME
: '';
const workflow = workflows[workflowId];
const workflowName = workflow ? workflow.name : DEFAULT_NEW_WORKFLOW_NAME;

useEffect(() => {
getCore().chrome.setBreadcrumbs([
Expand All @@ -67,12 +60,11 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
});

// On initial load:
// - fetch workflow, if there is an existing workflow ID
// - fetch workflow
// - fetch available models as their IDs may be used when building flows
useEffect(() => {
if (!isNewWorkflow) {
dispatch(getWorkflow(workflowId));
}
dispatch(getWorkflow(workflowId));

dispatch(searchModels(FETCH_ALL_QUERY_BODY));
}, []);

Expand All @@ -88,15 +80,9 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
<ReactFlowProvider>
<EuiPage>
<EuiPageBody className="workflow-detail stretch-relative">
<WorkflowDetailHeader
workflow={workflow}
isNewWorkflow={isNewWorkflow}
/>
<WorkflowDetailHeader workflow={workflow} />
<ReactFlowProvider>
<ResizableWorkspace
isNewWorkflow={isNewWorkflow}
workflow={workflow}
/>
<ResizableWorkspace workflow={workflow} />
</ReactFlowProvider>
</EuiPageBody>
</EuiPage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Workflow } from '../../../../../common';
interface IngestInputsProps {
workflow: Workflow;
onFormChange: () => void;
ingestDocs: {}[];
setIngestDocs: (docs: {}[]) => void;
}

/**
Expand All @@ -22,7 +24,10 @@ export function IngestInputs(props: IngestInputsProps) {
return (
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<SourceData />
<SourceData
ingestDocs={props.ingestDocs}
setIngestDocs={props.setIngestDocs}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiHorizontalRule margin="none" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,56 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import {
EuiCodeEditor,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
} from '@elastic/eui';

interface SourceDataProps {}
interface SourceDataProps {
ingestDocs: {}[];
setIngestDocs: (docs: {}[]) => void;
}

/**
* Input component for configuring the source data for ingest.
*/
export function SourceData(props: SourceDataProps) {
const [jsonStr, setJsonStr] = useState<string>('{}');

useEffect(() => {
try {
const json = JSON.parse(jsonStr);
props.setIngestDocs([json]);
} catch (e) {}
}, [jsonStr]);

return (
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h4>Source data</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText grow={false}>TODO</EuiText>
<EuiFlexItem grow={false}>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="25vh"
value={jsonStr}
onChange={(input) => {
setJsonStr(input);
}}
readOnly={false}
setOptions={{
fontSize: '14px',
}}
aria-label="Code Editor"
tabSize={2}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
Expand Down
Loading
Loading