diff --git a/common/constants.ts b/common/constants.ts index 2bc45505..13b4abb9 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -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'; diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx index 4a5ee2a6..6fe7b244 100644 --- a/public/pages/workflow_detail/components/header.tsx +++ b/public/pages/workflow_detail/components/header.tsx @@ -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 ( diff --git a/public/pages/workflow_detail/resizable_workspace.tsx b/public/pages/workflow_detail/resizable_workspace.tsx index 9a759952..51037344 100644 --- a/public/pages/workflow_detail/resizable_workspace.tsx +++ b/public/pages/workflow_detail/resizable_workspace.tsx @@ -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 @@ -48,7 +28,6 @@ import '../../global-styles.scss'; import { Tools } from './tools'; interface ResizableWorkspaceProps { - isNewWorkflow: boolean; workflow?: Workflow; } @@ -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(props.isNewWorkflow); // Workflow state const [workflow, setWorkflow] = useState( @@ -77,9 +55,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { const [formValues, setFormValues] = useState({}); const [formSchema, setFormSchema] = useState(yup.object({})); - // Validation states - const [formValidOnSubmit, setFormValidOnSubmit] = useState(true); - // Workflow inputs side panel state const [isWorkflowInputsPanelOpen, setIsWorkflowInputsPanelOpen] = useState< boolean @@ -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(false); - const [isDeprovisioning, setIsDeprovisioning] = useState(false); - const [isSaving, setIsSaving] = useState(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); } @@ -179,63 +118,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { } } - // Utility validation fn used before executing any API calls (save, provision) - function validateAndSubmit( - formikProps: FormikProps - ): 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 ( diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index 16863cfb..6d240ca7 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -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 @@ -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([ @@ -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)); }, []); @@ -88,15 +80,9 @@ export function WorkflowDetail(props: WorkflowDetailProps) { - + - + diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx index 7e8ba2ca..fa55a595 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx @@ -13,6 +13,8 @@ import { Workflow } from '../../../../../common'; interface IngestInputsProps { workflow: Workflow; onFormChange: () => void; + ingestDocs: {}[]; + setIngestDocs: (docs: {}[]) => void; } /** @@ -22,7 +24,10 @@ export function IngestInputs(props: IngestInputsProps) { return ( - + diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx index 35d6af9a..ba590621 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx @@ -3,15 +3,32 @@ * 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('{}'); + + useEffect(() => { + try { + const json = JSON.parse(jsonStr); + props.setIngestDocs([json]); + } catch (e) {} + }, [jsonStr]); + return ( @@ -19,8 +36,23 @@ export function SourceData(props: SourceDataProps) {

Source data

- - TODO + + { + setJsonStr(input); + }} + readOnly={false} + setOptions={{ + fontSize: '14px', + }} + aria-label="Code Editor" + tabSize={2} + />
); diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index f851651b..0eddf53f 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -3,9 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; +import { FormikProps } from 'formik'; import { EuiButton, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, @@ -13,10 +15,16 @@ import { EuiPanel, EuiTitle, } from '@elastic/eui'; -import { Workflow, WorkflowFormValues } from '../../../../common'; +import { + Workflow, + WorkflowConfig, + WorkflowFormValues, +} from '../../../../common'; import { IngestInputs } from './ingest_inputs'; import { SearchInputs } from './search_inputs'; -import { FormikProps } from 'formik'; +import { updateWorkflow, useAppDispatch } from '../../../store'; +import { formikToUiConfig, reduceToTemplate } from '../../../utils'; +import { configToTemplateFlows } from '../utils'; // styling import '../workspace/workspace-styles.scss'; @@ -25,7 +33,6 @@ interface WorkflowInputsProps { workflow: Workflow | undefined; formikProps: FormikProps; onFormChange: () => void; - validateAndSubmit: (formikProps: FormikProps) => void; } export enum CREATE_STEP { @@ -39,11 +46,90 @@ export enum CREATE_STEP { */ export function WorkflowInputs(props: WorkflowInputsProps) { + const dispatch = useAppDispatch(); + + // selected step state const [selectedStep, setSelectedStep] = useState( CREATE_STEP.INGEST ); - useEffect(() => {}, [selectedStep]); + // ingestion data state. We need to persist separately from the form, since + // we do not need/want to persist this in form state or in backend + const [ingestDocs, setIngestDocs] = useState<{}[]>([]); + + // Utility fn to update the workflow + function executeUpdate(updatedWorkflow: Workflow): void { + dispatch( + updateWorkflow({ + workflowId: updatedWorkflow.id as string, + workflowTemplate: reduceToTemplate(updatedWorkflow), + }) + ) + .unwrap() + .then((result) => {}) + .catch((error: any) => {}); + } + + // Utility fn to validate the form and update the workflow if valid + function validateAndUpdateWorkflow( + formikProps: FormikProps + ): 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) { + console.error('Form invalid'); + } else { + const updatedConfig = formikToUiConfig( + formikProps.values, + props.workflow?.ui_metadata?.config as WorkflowConfig + ); + const updatedWorkflow = { + ...props.workflow, + ui_metadata: { + ...props.workflow?.ui_metadata, + config: updatedConfig, + }, + workflows: configToTemplateFlows(updatedConfig), + } as Workflow; + executeUpdate(updatedWorkflow); + } + }); + } + + // TODO: running props.validateAndSubmit(props.formikProps) will need to be ran before every ingest and + // search, if the form is dirty / values have changed. This will update the workflow if needed. + // Note that the temporary data (the ingest docs and the search query) will not need to be persisted + // in the form (need to confirm if query-side / using search template, will need to persist something) + function validateAndRunIngestion(): void { + console.log('running ingestion...'); + try { + validateAndUpdateWorkflow(props.formikProps); + const indexName = props.formikProps.values.ingest.index.name; + const doc = ingestDocs[0]; + // dispatch(ingest({ index: indexName, doc: docObj })) + // .unwrap() + // .then(async (resp) => { + // //setResponse(result); + // console.log('response: ', resp); + // }) + // .catch((error: any) => { + // getCore().notifications.toasts.addDanger(error); + // // setResponse({}); + // console.log('error: ', error); + // }); + } catch (err) { + console.log('error: ', err); + } + } + + function validateAndRunQuery(): void { + console.log('running query...'); + validateAndUpdateWorkflow(props.formikProps); + } return ( @@ -73,6 +159,8 @@ export function WorkflowInputs(props: WorkflowInputsProps) { ) : ( @@ -86,30 +174,42 @@ export function WorkflowInputs(props: WorkflowInputsProps) { {selectedStep === CREATE_STEP.INGEST ? ( - - setSelectedStep(CREATE_STEP.SEARCH)} - > - Next - - - ) : ( <> + + setSelectedStep(CREATE_STEP.SEARCH)} + > + Skip + + { + validateAndRunIngestion(); + }} + > + Run ingestion + + + + ) : ( + <> + + setSelectedStep(CREATE_STEP.INGEST)} > Back - + { - props.validateAndSubmit(props.formikProps); + validateAndRunQuery(); }} > - Create + Run query diff --git a/public/pages/workflows/new_workflow/new_workflow.tsx b/public/pages/workflows/new_workflow/new_workflow.tsx index e0ca3036..108aa2be 100644 --- a/public/pages/workflows/new_workflow/new_workflow.tsx +++ b/public/pages/workflows/new_workflow/new_workflow.tsx @@ -15,16 +15,8 @@ import { import { useSelector } from 'react-redux'; import { UseCase } from './use_case'; import { Workflow, WorkflowTemplate } from '../../../../common'; -import { - AppState, - cacheWorkflow, - useAppDispatch, - getWorkflowPresets, -} from '../../../store'; -import { - enrichPresetWorkflowWithUiMetadata, - processWorkflowName, -} from './utils'; +import { AppState, useAppDispatch, getWorkflowPresets } from '../../../store'; +import { enrichPresetWorkflowWithUiMetadata } from './utils'; interface NewWorkflowProps {} @@ -95,18 +87,7 @@ export function NewWorkflow(props: NewWorkflowProps) { {filteredWorkflows.map((workflow: Workflow, index) => { return ( - - dispatch( - cacheWorkflow({ - ...workflow, - name: processWorkflowName(workflow.name), - }) - ) - } - /> + ); })} diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx index aafaadac..687cf9b8 100644 --- a/public/pages/workflows/new_workflow/use_case.tsx +++ b/public/pages/workflows/new_workflow/use_case.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; +import { useHistory } from 'react-router-dom'; import { EuiText, EuiFlexGroup, @@ -13,21 +14,25 @@ import { EuiHorizontalRule, EuiButton, } from '@elastic/eui'; -import { NEW_WORKFLOW_ID_URL, PLUGIN_ID } from '../../../../common'; +import { Workflow } from '../../../../common'; import { APP_PATH } from '../../../utils'; +import { processWorkflowName } from './utils'; +import { createWorkflow, useAppDispatch } from '../../../store'; interface UseCaseProps { - title: string; - description: string; - onClick: () => {}; + // onClick: () => {}; + workflow: Workflow; } export function UseCase(props: UseCaseProps) { + const dispatch = useAppDispatch(); + const history = useHistory(); + return ( -

{props.title}

+

{props.workflow.name}

} titleSize="s" @@ -37,17 +42,34 @@ export function UseCase(props: UseCaseProps) { - {props.description} + {props.workflow.description} { + const workflowToCreate = { + ...props.workflow, + // TODO: handle duplicate name gracefully. it won't slash or produce errors, but nice-to-have + // as long as not too expensive to fetch duplicate names + name: processWorkflowName(props.workflow.name), + }; + + dispatch(createWorkflow(workflowToCreate)) + .unwrap() + .then((result) => { + const { workflow } = result; + history.replace(`${APP_PATH.WORKFLOWS}/${workflow.id}`); + history.go(0); + }) + .catch((err: any) => { + console.error(err); + }); + }} > - Go + Create diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts index 2a04cb81..7f169ed1 100644 --- a/public/store/reducers/workflows_reducer.ts +++ b/public/store/reducers/workflows_reducer.ts @@ -12,7 +12,6 @@ const initialState = { loading: false, errorMessage: '', workflows: {} as WorkflowDict, - cachedWorkflow: undefined as Workflow | undefined, }; const WORKFLOWS_ACTION_PREFIX = 'workflows'; @@ -24,8 +23,6 @@ const UPDATE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/update`; const PROVISION_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/provision`; const DEPROVISION_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/deprovision`; const DELETE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/delete`; -const CACHE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/cache`; -const CLEAR_CACHED_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/clearCache`; export const getWorkflow = createAsyncThunk( GET_WORKFLOW_ACTION, @@ -164,20 +161,6 @@ export const deleteWorkflow = createAsyncThunk( } ); -export const cacheWorkflow = createAsyncThunk( - CACHE_WORKFLOW_ACTION, - async (workflow: Workflow) => { - return workflow; - } -); - -// A no-op function to trigger a reducer case. -// Will clear any stored workflow in the cachedWorkflow state -export const clearCachedWorkflow = createAsyncThunk( - CLEAR_CACHED_WORKFLOW_ACTION, - async () => {} -); - const workflowsSlice = createSlice({ name: 'workflows', initialState, @@ -286,13 +269,6 @@ const workflowsSlice = createSlice({ state.loading = false; state.errorMessage = ''; }) - .addCase(cacheWorkflow.fulfilled, (state, action) => { - const workflow = action.payload; - state.cachedWorkflow = workflow; - }) - .addCase(clearCachedWorkflow.fulfilled, (state, action) => { - state.cachedWorkflow = undefined; - }) // Rejected states: set state consistently across all actions .addCase(getWorkflow.rejected, (state, action) => { state.errorMessage = action.payload as string;