diff --git a/common/constants.ts b/common/constants.ts
index f82a1167..a7f442cc 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import { WORKFLOW_STATE } from './interfaces';
+
export const PLUGIN_ID = 'flow-framework';
/**
@@ -35,6 +37,8 @@ export const GET_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}`;
export const SEARCH_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/search`;
export const GET_WORKFLOW_STATE_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/state`;
export const CREATE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/create`;
+export const PROVISION_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/provision`;
+export const DEPROVISION_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/deprovision`;
export const DELETE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/delete`;
export const GET_PRESET_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/presets`;
@@ -49,5 +53,13 @@ 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';
+export const DEFAULT_NEW_WORKFLOW_STATE = WORKFLOW_STATE.NOT_STARTED;
+export const DEFAULT_NEW_WORKFLOW_STATE_TYPE = ('NOT_STARTED' as any) as typeof WORKFLOW_STATE;
export const DATE_FORMAT_PATTERN = 'MM/DD/YY hh:mm A';
export const EMPTY_FIELD_STRING = '--';
+export const FETCH_ALL_QUERY_BODY = {
+ query: {
+ match_all: {},
+ },
+ size: 1000,
+};
diff --git a/public/pages/workflow_detail/component_details/component_details.tsx b/public/pages/workflow_detail/component_details/component_details.tsx
index 7ecaed6e..242ae81d 100644
--- a/public/pages/workflow_detail/component_details/component_details.tsx
+++ b/public/pages/workflow_detail/component_details/component_details.tsx
@@ -4,7 +4,7 @@
*/
import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
+import { EuiPanel } from '@elastic/eui';
import { ReactFlowComponent } from '../../../../common';
import { ComponentInputs } from './component_inputs';
import { EmptyComponentInputs } from './empty_component_inputs';
@@ -24,23 +24,15 @@ interface ComponentDetailsProps {
*/
export function ComponentDetails(props: ComponentDetailsProps) {
return (
-
-
-
- {props.selectedComponent ? (
-
- ) : (
-
- )}
-
-
-
+
+ {props.selectedComponent ? (
+
+ ) : (
+
+ )}
+
);
}
diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx
index c3c2e967..b31dab11 100644
--- a/public/pages/workflow_detail/components/header.tsx
+++ b/public/pages/workflow_detail/components/header.tsx
@@ -4,8 +4,19 @@
*/
import React from 'react';
-import { EuiPageHeader, EuiButton, EuiLoadingSpinner } from '@elastic/eui';
-import { DEFAULT_NEW_WORKFLOW_NAME, Workflow } from '../../../../common';
+import {
+ EuiPageHeader,
+ EuiButton,
+ EuiLoadingSpinner,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+} from '@elastic/eui';
+import {
+ DEFAULT_NEW_WORKFLOW_NAME,
+ DEFAULT_NEW_WORKFLOW_STATE,
+ Workflow,
+} from '../../../../common';
interface WorkflowDetailHeaderProps {
tabs: any[];
@@ -14,20 +25,42 @@ interface WorkflowDetailHeaderProps {
}
export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
+ function getTitle() {
+ return props.workflow ? (
+ props.workflow.name
+ ) : props.isNewWorkflow && !props.workflow ? (
+ DEFAULT_NEW_WORKFLOW_NAME
+ ) : (
+
+ );
+ }
+
+ function getState() {
+ return props.workflow?.state
+ ? props.workflow.state
+ : props.isNewWorkflow
+ ? DEFAULT_NEW_WORKFLOW_STATE
+ : null;
+ }
+
return (
- )
+
+ {getTitle()}
+
+ {getState()}
+
+
}
rightSideItems={[
// TODO: finalize if this is needed
- {}}>
+ {}}
+ >
Delete
,
]}
diff --git a/public/pages/workflow_detail/workflow-detail-styles.scss b/public/pages/workflow_detail/workflow-detail-styles.scss
new file mode 100644
index 00000000..1d1194d8
--- /dev/null
+++ b/public/pages/workflow_detail/workflow-detail-styles.scss
@@ -0,0 +1,3 @@
+.workflow-detail {
+ overflow: hidden;
+}
diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx
index 43664bf5..a2f15dd8 100644
--- a/public/pages/workflow_detail/workflow_detail.tsx
+++ b/public/pages/workflow_detail/workflow_detail.tsx
@@ -5,22 +5,31 @@
import React, { useEffect, useState } from 'react';
import { RouteComponentProps, useLocation } from 'react-router-dom';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import { ReactFlowProvider } from 'reactflow';
import queryString from 'query-string';
import { EuiPage, EuiPageBody } from '@elastic/eui';
import { BREADCRUMBS } from '../../utils';
import { getCore } from '../../services';
import { WorkflowDetailHeader } from './components';
-import { AppState, searchModels, searchWorkflows } from '../../store';
+import {
+ AppState,
+ searchModels,
+ searchWorkflows,
+ useAppDispatch,
+} from '../../store';
import { ResizableWorkspace } from './workspace';
import { Launches } from './launches';
import { Prototype } from './prototype';
import {
DEFAULT_NEW_WORKFLOW_NAME,
+ FETCH_ALL_QUERY_BODY,
NEW_WORKFLOW_ID_URL,
} from '../../../common';
+// styling
+import './workflow-detail-styles.scss';
+
export interface WorkflowDetailRouterProps {
workflowId: string;
}
@@ -53,7 +62,7 @@ function replaceActiveTab(activeTab: string, props: WorkflowDetailProps) {
*/
export function WorkflowDetail(props: WorkflowDetailProps) {
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
const { workflows, cachedWorkflow } = useSelector(
(state: AppState) => state.workflows
);
@@ -101,9 +110,9 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
useEffect(() => {
if (!isNewWorkflow) {
// TODO: can optimize to only fetch a single workflow
- dispatch(searchWorkflows({ query: { match_all: {} } }));
+ dispatch(searchWorkflows(FETCH_ALL_QUERY_BODY));
}
- dispatch(searchModels({ query: { match_all: {} } }));
+ dispatch(searchModels(FETCH_ALL_QUERY_BODY));
}, []);
const tabs = [
@@ -139,7 +148,7 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
return (
-
+
state.workspace.isDirty);
+ const { isDirty } = useSelector((state: AppState) => state.workspace);
+ const { loading } = useSelector((state: AppState) => state.workflows);
const [isFirstSave, setIsFirstSave] = useState(props.isNewWorkflow);
- const isSaveable = isFirstSave ? true : isDirty;
// Workflow state
const [workflow, setWorkflow] = useState(
@@ -95,6 +102,26 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
ReactFlowComponent
>();
+ // Save/provision/deprovision button state
+ const isSaveable = isFirstSave ? true : isDirty;
+ const isProvisionable =
+ !isDirty &&
+ !props.isNewWorkflow &&
+ formValidOnSubmit &&
+ flowValidOnSubmit &&
+ props.workflow?.state === WORKFLOW_STATE.NOT_STARTED;
+ const isDeprovisionable =
+ !props.isNewWorkflow &&
+ props.workflow?.state !== WORKFLOW_STATE.NOT_STARTED;
+
+ // 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;
+
/**
* Custom listener on when nodes are selected / de-selected. Passed to
* downstream ReactFlow components you can listen using
@@ -214,6 +241,47 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
}
}
+ // Utility validation fn used before executing any API calls (save, provision)
+ function validateFormAndFlow(
+ formikProps: FormikProps,
+ processWorkflowFn: (workflow: Workflow) => void
+ ): 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);
+ let curFlowState = reactFlowInstance.toObject() as WorkspaceFlowState;
+ curFlowState = {
+ ...curFlowState,
+ nodes: processNodes(curFlowState.nodes),
+ };
+ if (validateWorkspaceFlow(curFlowState)) {
+ setFlowValidOnSubmit(true);
+ const updatedWorkflow = {
+ ...workflow,
+ ui_metadata: {
+ ...workflow?.ui_metadata,
+ workspaceFlow: curFlowState,
+ },
+ workflows: toTemplateFlows(curFlowState, formikProps.values),
+ } as Workflow;
+ processWorkflowFn(updatedWorkflow);
+ } else {
+ // TODO: bubble up flow error?
+ setFlowValidOnSubmit(false);
+ setIsSaving(false);
+ }
+ }
+ });
+ }
+
return (
{}}>
- Launch
+ {
+ if (workflow?.id) {
+ setIsDeprovisioning(true);
+ dispatch(deprovisionWorkflow(workflow.id))
+ .unwrap()
+ .then(async (result) => {
+ await new Promise((f) => setTimeout(f, 3000));
+ dispatch(getWorkflowState(workflow.id as string));
+ setIsDeprovisioning(false);
+ })
+ .catch((error: any) => {
+ // TODO: process error (toast msg?)
+ console.log('error: ', error);
+ setIsDeprovisioning(false);
+ });
+ } else {
+ // TODO: this case should not happen
+ }
+ }}
+ >
+ Deprovision
,
{
+ if (workflow?.id) {
+ setIsProvisioning(true);
+ dispatch(provisionWorkflow(workflow.id))
+ .unwrap()
+ .then(async (result) => {
+ await new Promise((f) => setTimeout(f, 3000));
+ dispatch(getWorkflowState(workflow.id as string));
+ setIsProvisioning(false);
+ })
+ .catch((error: any) => {
+ // TODO: process error (toast msg?)
+ console.log('error: ', error);
+ setIsProvisioning(false);
+ });
+ } else {
+ // TODO: this case should not happen
+ }
+ }}
+ >
+ Provision
+ ,
+ {
+ setIsSaving(true);
dispatch(removeDirty());
if (isFirstSave) {
setIsFirstSave(false);
}
- // 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);
- } else {
- setFormValidOnSubmit(true);
- let curFlowState = reactFlowInstance.toObject() as WorkspaceFlowState;
- curFlowState = {
- ...curFlowState,
- nodes: processNodes(curFlowState.nodes),
- };
- if (validateWorkspaceFlow(curFlowState)) {
- setFlowValidOnSubmit(true);
- const updatedWorkflow = {
- ...workflow,
- ui_metadata: {
- ...workflow?.ui_metadata,
- workspaceFlow: curFlowState,
- },
- workflows: toTemplateFlows(
- curFlowState,
- formikProps.values
- ),
- } as Workflow;
- if (updatedWorkflow.id) {
- // TODO: add update workflow API
- } else {
- dispatch(createWorkflow(updatedWorkflow));
- }
+ validateFormAndFlow(
+ formikProps,
+ // The callback fn to run if everything is valid.
+ (updatedWorkflow) => {
+ if (updatedWorkflow.id) {
+ // TODO: add update workflow API
+ // make sure to set isSaving to false in catch block
} else {
- setFlowValidOnSubmit(false);
+ dispatch(createWorkflow(updatedWorkflow))
+ .unwrap()
+ .then((result) => {
+ const { workflow } = result;
+ history.replace(`/workflows/${workflow.id}`);
+ history.go(0);
+ })
+ .catch((error: any) => {
+ // TODO: process error (toast msg?)
+ console.log('error: ', error);
+ setIsSaving(false);
+ });
}
}
- });
+ );
}}
>
- Save
+ {props.isNewWorkflow || isCreating ? 'Create' : 'Save'}
,
]}
bottomBorder={false}
@@ -343,7 +446,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
onToggleChange()}
>
-
+
+
+
+
+
>
);
diff --git a/public/pages/workflow_detail/workspace/workspace.tsx b/public/pages/workflow_detail/workspace/workspace.tsx
index b5af1b0c..234e3518 100644
--- a/public/pages/workflow_detail/workspace/workspace.tsx
+++ b/public/pages/workflow_detail/workspace/workspace.tsx
@@ -4,7 +4,6 @@
*/
import React, { useRef, useCallback, useEffect } from 'react';
-import { useDispatch } from 'react-redux';
import ReactFlow, {
Controls,
Background,
@@ -18,7 +17,7 @@ import ReactFlow, {
MarkerType,
} from 'reactflow';
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
-import { setDirty } from '../../../store';
+import { setDirty, useAppDispatch } from '../../../store';
import {
IComponentData,
ReactFlowComponent,
@@ -53,7 +52,7 @@ const nodeTypes = {
const edgeTypes = { customEdge: DeletableEdge };
export function Workspace(props: WorkspaceProps) {
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
// ReactFlow state
const reactFlowWrapper = useRef(null);
diff --git a/public/pages/workflow_detail/workspace/workspace_components/new_or_existing_tabs.tsx b/public/pages/workflow_detail/workspace/workspace_components/new_or_existing_tabs.tsx
index f5a936fc..777e36b4 100644
--- a/public/pages/workflow_detail/workspace/workspace_components/new_or_existing_tabs.tsx
+++ b/public/pages/workflow_detail/workspace/workspace_components/new_or_existing_tabs.tsx
@@ -30,7 +30,7 @@ const inputTabs = [
export function NewOrExistingTabs(props: NewOrExistingTabsProps) {
return (
-
+
{inputTabs.map((tab, idx) => {
return (
state.presets
);
diff --git a/public/pages/workflows/workflow_list/workflow_list.tsx b/public/pages/workflows/workflow_list/workflow_list.tsx
index f122a2e0..813f638f 100644
--- a/public/pages/workflows/workflow_list/workflow_list.tsx
+++ b/public/pages/workflows/workflow_list/workflow_list.tsx
@@ -4,7 +4,7 @@
*/
import React, { useState, useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import { debounce } from 'lodash';
import {
EuiInMemoryTable,
@@ -15,7 +15,7 @@ import {
EuiFieldSearch,
EuiLoadingSpinner,
} from '@elastic/eui';
-import { AppState, deleteWorkflow } from '../../../store';
+import { AppState, deleteWorkflow, useAppDispatch } from '../../../store';
import { Workflow } from '../../../../common';
import { columns } from './columns';
import {
@@ -37,7 +37,7 @@ const sorting = {
* The searchable list of created workflows.
*/
export function WorkflowList(props: WorkflowListProps) {
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
const { workflows, loading } = useSelector(
(state: AppState) => state.workflows
);
@@ -91,14 +91,14 @@ export function WorkflowList(props: WorkflowListProps) {
return (
<>
- {isDeleteModalOpen && workflowToDelete !== undefined && (
+ {isDeleteModalOpen && workflowToDelete?.id !== undefined && (
{
clearDeleteState();
}}
onConfirm={() => {
- dispatch(deleteWorkflow(workflowToDelete.id));
+ dispatch(deleteWorkflow(workflowToDelete.id as string));
clearDeleteState();
}}
/>
diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx
index b745d888..fdc5acdb 100644
--- a/public/pages/workflows/workflows.tsx
+++ b/public/pages/workflows/workflows.tsx
@@ -14,13 +14,14 @@ import {
EuiSpacer,
} from '@elastic/eui';
import queryString from 'query-string';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import { BREADCRUMBS } from '../../utils';
import { getCore } from '../../services';
import { WorkflowList } from './workflow_list';
import { NewWorkflow } from './new_workflow';
-import { AppState, searchWorkflows } from '../../store';
+import { AppState, searchWorkflows, useAppDispatch } from '../../store';
import { EmptyListMessage } from './empty_list_message';
+import { FETCH_ALL_QUERY_BODY } from '../../../common';
export interface WorkflowsRouterProps {}
@@ -48,7 +49,7 @@ function replaceActiveTab(activeTab: string, props: WorkflowsProps) {
* to get started on a new workflow.
*/
export function Workflows(props: WorkflowsProps) {
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
const { workflows, loading } = useSelector(
(state: AppState) => state.workflows
);
@@ -72,7 +73,7 @@ export function Workflows(props: WorkflowsProps) {
// If the user navigates back to the manage tab, re-fetch workflows
useEffect(() => {
if (selectedTabId === WORKFLOWS_TAB.MANAGE) {
- dispatch(searchWorkflows({ query: { match_all: {} } }));
+ dispatch(searchWorkflows(FETCH_ALL_QUERY_BODY));
}
}, [selectedTabId]);
@@ -85,7 +86,7 @@ export function Workflows(props: WorkflowsProps) {
// On initial render: fetch all workflows
useEffect(() => {
- dispatch(searchWorkflows({ query: { match_all: {} } }));
+ dispatch(searchWorkflows(FETCH_ALL_QUERY_BODY));
}, []);
return (
diff --git a/public/route_service.ts b/public/route_service.ts
index 78faddd9..20aaf202 100644
--- a/public/route_service.ts
+++ b/public/route_service.ts
@@ -13,6 +13,8 @@ import {
SEARCH_WORKFLOWS_NODE_API_PATH,
GET_PRESET_WORKFLOWS_NODE_API_PATH,
SEARCH_MODELS_NODE_API_PATH,
+ PROVISION_WORKFLOW_NODE_API_PATH,
+ DEPROVISION_WORKFLOW_NODE_API_PATH,
} from '../common';
/**
@@ -30,6 +32,8 @@ export interface RouteService {
body: {},
provision?: boolean
) => Promise;
+ provisionWorkflow: (workflowId: string) => Promise;
+ deprovisionWorkflow: (workflowId: string) => Promise;
deleteWorkflow: (workflowId: string) => Promise;
getWorkflowPresets: () => Promise;
catIndices: (pattern: string) => Promise;
@@ -71,10 +75,10 @@ export function configureRoutes(core: CoreStart): RouteService {
return e as HttpFetchError;
}
},
- createWorkflow: async (body: {}, provision: boolean = false) => {
+ createWorkflow: async (body: {}) => {
try {
const response = await core.http.post<{ respString: string }>(
- `${CREATE_WORKFLOW_NODE_API_PATH}/${provision}`,
+ CREATE_WORKFLOW_NODE_API_PATH,
{
body: JSON.stringify(body),
}
@@ -84,6 +88,26 @@ export function configureRoutes(core: CoreStart): RouteService {
return e as HttpFetchError;
}
},
+ provisionWorkflow: async (workflowId: string) => {
+ try {
+ const response = await core.http.post<{ respString: string }>(
+ `${PROVISION_WORKFLOW_NODE_API_PATH}/${workflowId}`
+ );
+ return response;
+ } catch (e: any) {
+ return e as HttpFetchError;
+ }
+ },
+ deprovisionWorkflow: async (workflowId: string) => {
+ try {
+ const response = await core.http.post<{ respString: string }>(
+ `${DEPROVISION_WORKFLOW_NODE_API_PATH}/${workflowId}`
+ );
+ return response;
+ } catch (e: any) {
+ return e as HttpFetchError;
+ }
+ },
deleteWorkflow: async (workflowId: string) => {
try {
const response = await core.http.delete<{ respString: string }>(
diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts
index f748f08b..f8ac6b9d 100644
--- a/public/store/reducers/workflows_reducer.ts
+++ b/public/store/reducers/workflows_reducer.ts
@@ -16,13 +16,15 @@ const initialState = {
};
const WORKFLOWS_ACTION_PREFIX = 'workflows';
-const GET_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/getWorkflow`;
-const SEARCH_WORKFLOWS_ACTION = `${WORKFLOWS_ACTION_PREFIX}/searchWorkflows`;
-const GET_WORKFLOW_STATE_ACTION = `${WORKFLOWS_ACTION_PREFIX}/getWorkflowState`;
-const CREATE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/createWorkflow`;
-const DELETE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/deleteWorkflow`;
-const CACHE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/cacheWorkflow`;
-const CLEAR_CACHED_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/clearCachedWorkflow`;
+const GET_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/get`;
+const SEARCH_WORKFLOWS_ACTION = `${WORKFLOWS_ACTION_PREFIX}/search`;
+const GET_WORKFLOW_STATE_ACTION = `${WORKFLOWS_ACTION_PREFIX}/getState`;
+const CREATE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/create`;
+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,
@@ -77,10 +79,7 @@ export const createWorkflow = createAsyncThunk(
async (workflowBody: {}, { rejectWithValue }) => {
const response:
| any
- | HttpFetchError = await getRouteService().createWorkflow(
- workflowBody,
- false
- );
+ | HttpFetchError = await getRouteService().createWorkflow(workflowBody);
if (response instanceof HttpFetchError) {
return rejectWithValue(
'Error creating workflow: ' + response.body.message
@@ -91,6 +90,40 @@ export const createWorkflow = createAsyncThunk(
}
);
+export const provisionWorkflow = createAsyncThunk(
+ PROVISION_WORKFLOW_ACTION,
+ async (workflowId: string, { rejectWithValue }) => {
+ const response:
+ | any
+ | HttpFetchError = await getRouteService().provisionWorkflow(workflowId);
+ if (response instanceof HttpFetchError) {
+ return rejectWithValue(
+ 'Error provisioning workflow: ' + response.body.message
+ );
+ } else {
+ return response;
+ }
+ }
+);
+
+export const deprovisionWorkflow = createAsyncThunk(
+ DEPROVISION_WORKFLOW_ACTION,
+ async (workflowId: string, { rejectWithValue }) => {
+ const response:
+ | any
+ | HttpFetchError = await getRouteService().deprovisionWorkflow(
+ workflowId
+ );
+ if (response instanceof HttpFetchError) {
+ return rejectWithValue(
+ 'Error deprovisioning workflow: ' + response.body.message
+ );
+ } else {
+ return response;
+ }
+ }
+);
+
export const deleteWorkflow = createAsyncThunk(
DELETE_WORKFLOW_ACTION,
async (workflowId: string, { rejectWithValue }) => {
@@ -140,6 +173,14 @@ const workflowsSlice = createSlice({
state.loading = true;
state.errorMessage = '';
})
+ .addCase(provisionWorkflow.pending, (state, action) => {
+ state.loading = true;
+ state.errorMessage = '';
+ })
+ .addCase(deprovisionWorkflow.pending, (state, action) => {
+ state.loading = true;
+ state.errorMessage = '';
+ })
.addCase(deleteWorkflow.pending, (state, action) => {
state.loading = true;
state.errorMessage = '';
@@ -167,22 +208,31 @@ const workflowsSlice = createSlice({
state.errorMessage = '';
})
.addCase(getWorkflowState.fulfilled, (state, action) => {
- // TODO: add logic to mutate state
- // const workflow = action.payload;
- // state.workflows = {
- // ...state.workflows,
- // [workflow.id]: workflow,
- // };
+ const { workflowId, workflowState } = action.payload;
+ state.workflows = {
+ ...state.workflows,
+ [workflowId]: {
+ ...state.workflows[workflowId],
+ state: workflowState,
+ },
+ };
state.loading = false;
state.errorMessage = '';
})
.addCase(createWorkflow.fulfilled, (state, action) => {
- // TODO: add logic to mutate state
- // const workflow = action.payload;
- // state.workflows = {
- // ...state.workflows,
- // [workflow.id]: workflow,
- // };
+ const workflow = action.payload;
+ state.workflows = {
+ ...state.workflows,
+ [workflow.id]: workflow,
+ };
+ state.loading = false;
+ state.errorMessage = '';
+ })
+ .addCase(provisionWorkflow.fulfilled, (state, action) => {
+ state.loading = false;
+ state.errorMessage = '';
+ })
+ .addCase(deprovisionWorkflow.fulfilled, (state, action) => {
state.loading = false;
state.errorMessage = '';
})
@@ -216,6 +266,14 @@ const workflowsSlice = createSlice({
state.errorMessage = action.payload as string;
state.loading = false;
})
+ .addCase(provisionWorkflow.rejected, (state, action) => {
+ state.errorMessage = action.payload as string;
+ state.loading = false;
+ })
+ .addCase(deprovisionWorkflow.rejected, (state, action) => {
+ state.errorMessage = action.payload as string;
+ state.loading = false;
+ })
.addCase(deleteWorkflow.rejected, (state, action) => {
state.errorMessage = action.payload as string;
state.loading = false;
diff --git a/public/store/store.ts b/public/store/store.ts
index 06d7d2ea..da1aa929 100644
--- a/public/store/store.ts
+++ b/public/store/store.ts
@@ -3,8 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { configureStore } from '@reduxjs/toolkit';
-import { combineReducers } from 'redux';
+import { ThunkDispatch, configureStore } from '@reduxjs/toolkit';
+import { AnyAction, combineReducers } from 'redux';
+import { useDispatch } from 'react-redux';
import {
workspaceReducer,
opensearchReducer,
@@ -26,3 +27,5 @@ export const store = configureStore({
});
export type AppState = ReturnType;
+export type AppThunkDispatch = ThunkDispatch;
+export const useAppDispatch = () => useDispatch();
diff --git a/server/cluster/flow_framework_plugin.ts b/server/cluster/flow_framework_plugin.ts
index 2e649a4b..690047a2 100644
--- a/server/cluster/flow_framework_plugin.ts
+++ b/server/cluster/flow_framework_plugin.ts
@@ -67,15 +67,35 @@ export function flowFrameworkPlugin(Client: any, config: any, components: any) {
flowFramework.createWorkflow = ca({
url: {
- fmt: `${FLOW_FRAMEWORK_WORKFLOW_ROUTE_PREFIX}?provision=<%=provision%>`,
+ fmt: `${FLOW_FRAMEWORK_WORKFLOW_ROUTE_PREFIX}?provision=false`,
+ },
+ needBody: true,
+ method: 'POST',
+ });
+
+ flowFramework.provisionWorkflow = ca({
+ url: {
+ fmt: `${FLOW_FRAMEWORK_WORKFLOW_ROUTE_PREFIX}/<%=workflow_id%>/_provision`,
req: {
- provision: {
- type: 'boolean',
+ workflow_id: {
+ type: 'string',
+ required: true,
+ },
+ },
+ },
+ method: 'POST',
+ });
+
+ flowFramework.deprovisionWorkflow = ca({
+ url: {
+ fmt: `${FLOW_FRAMEWORK_WORKFLOW_ROUTE_PREFIX}/<%=workflow_id%>/_deprovision`,
+ req: {
+ workflow_id: {
+ type: 'string',
required: true,
},
},
},
- needBody: true,
method: 'POST',
});
diff --git a/server/routes/flow_framework_routes_service.ts b/server/routes/flow_framework_routes_service.ts
index 4eda47b5..83fe7580 100644
--- a/server/routes/flow_framework_routes_service.ts
+++ b/server/routes/flow_framework_routes_service.ts
@@ -16,14 +16,22 @@ import {
import {
CREATE_WORKFLOW_NODE_API_PATH,
DELETE_WORKFLOW_NODE_API_PATH,
+ DEPROVISION_WORKFLOW_NODE_API_PATH,
GET_PRESET_WORKFLOWS_NODE_API_PATH,
GET_WORKFLOW_NODE_API_PATH,
GET_WORKFLOW_STATE_NODE_API_PATH,
+ PROVISION_WORKFLOW_NODE_API_PATH,
SEARCH_WORKFLOWS_NODE_API_PATH,
+ WORKFLOW_STATE,
+ Workflow,
WorkflowTemplate,
validateWorkflowTemplate,
} from '../../common';
-import { generateCustomError, getWorkflowsFromResponses } from './helpers';
+import {
+ generateCustomError,
+ getWorkflowStateFromResponse,
+ getWorkflowsFromResponses,
+} from './helpers';
/**
* Server-side routes to process flow-framework-related node API calls and execute the
@@ -69,15 +77,36 @@ export function registerFlowFrameworkRoutes(
router.post(
{
- path: `${CREATE_WORKFLOW_NODE_API_PATH}/{provision}`,
+ path: CREATE_WORKFLOW_NODE_API_PATH,
validate: {
body: schema.any(),
+ },
+ },
+ flowFrameworkRoutesService.createWorkflow
+ );
+
+ router.post(
+ {
+ path: `${PROVISION_WORKFLOW_NODE_API_PATH}/{workflow_id}`,
+ validate: {
params: schema.object({
- provision: schema.boolean(),
+ workflow_id: schema.string(),
}),
},
},
- flowFrameworkRoutesService.createWorkflow
+ flowFrameworkRoutesService.provisionWorkflow
+ );
+
+ router.post(
+ {
+ path: `${DEPROVISION_WORKFLOW_NODE_API_PATH}/{workflow_id}`,
+ validate: {
+ params: schema.object({
+ workflow_id: schema.string(),
+ }),
+ },
+ },
+ flowFrameworkRoutesService.deprovisionWorkflow
);
router.delete(
@@ -157,7 +186,6 @@ export class FlowFrameworkRoutesService {
}
};
- // TODO: test e2e
getWorkflowState = async (
context: RequestHandlerContext,
req: OpenSearchDashboardsRequest,
@@ -168,27 +196,66 @@ export class FlowFrameworkRoutesService {
const response = await this.client
.asScoped(req)
.callAsCurrentUser('flowFramework.getWorkflowState', { workflow_id });
- console.log('response from get workflow state: ', response);
- // TODO: format response
- return res.ok({ body: response });
+ const state = getWorkflowStateFromResponse(
+ response.state as typeof WORKFLOW_STATE
+ );
+ return res.ok({
+ body: { workflowId: workflow_id, workflowState: state },
+ });
} catch (err: any) {
return generateCustomError(res, err);
}
};
- // TODO: test e2e
createWorkflow = async (
context: RequestHandlerContext,
req: OpenSearchDashboardsRequest,
res: OpenSearchDashboardsResponseFactory
): Promise> => {
- const body = req.body;
- const { provision } = req.params as { provision: boolean };
+ const body = req.body as Workflow;
try {
const response = await this.client
.asScoped(req)
- .callAsCurrentUser('flowFramework.createWorkflow', { body, provision });
- return res.ok({ body: { id: response._id } });
+ .callAsCurrentUser('flowFramework.createWorkflow', { body });
+ const workflowWithId = {
+ ...body,
+ id: response.workflow_id,
+ };
+ return res.ok({ body: { workflow: workflowWithId } });
+ } catch (err: any) {
+ return generateCustomError(res, err);
+ }
+ };
+
+ provisionWorkflow = async (
+ context: RequestHandlerContext,
+ req: OpenSearchDashboardsRequest,
+ res: OpenSearchDashboardsResponseFactory
+ ): Promise> => {
+ const { workflow_id } = req.params as { workflow_id: string };
+ try {
+ await this.client
+ .asScoped(req)
+ .callAsCurrentUser('flowFramework.provisionWorkflow', { workflow_id });
+ return res.ok();
+ } catch (err: any) {
+ return generateCustomError(res, err);
+ }
+ };
+
+ deprovisionWorkflow = async (
+ context: RequestHandlerContext,
+ req: OpenSearchDashboardsRequest,
+ res: OpenSearchDashboardsResponseFactory
+ ): Promise> => {
+ const { workflow_id } = req.params as { workflow_id: string };
+ try {
+ await this.client
+ .asScoped(req)
+ .callAsCurrentUser('flowFramework.deprovisionWorkflow', {
+ workflow_id,
+ });
+ return res.ok();
} catch (err: any) {
return generateCustomError(res, err);
}
diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts
index 5b912b49..c3c671be 100644
--- a/server/routes/helpers.ts
+++ b/server/routes/helpers.ts
@@ -4,6 +4,7 @@
*/
import {
+ DEFAULT_NEW_WORKFLOW_STATE_TYPE,
Model,
ModelDict,
WORKFLOW_STATE,
@@ -55,15 +56,13 @@ export function getWorkflowsFromResponses(
const workflowStateHit = workflowStateHits.find(
(workflowStateHit) => workflowStateHit._id === workflowHit._id
);
- if (workflowStateHit) {
- const workflowState = workflowStateHit._source
- .state as typeof WORKFLOW_STATE;
- workflowDict[workflowHit._id] = {
- ...workflowDict[workflowHit._id],
- // @ts-ignore
- state: WORKFLOW_STATE[workflowState],
- };
- }
+ const workflowState = (workflowStateHit?._source?.state ||
+ DEFAULT_NEW_WORKFLOW_STATE_TYPE) as typeof WORKFLOW_STATE;
+ workflowDict[workflowHit._id] = {
+ ...workflowDict[workflowHit._id],
+ // @ts-ignore
+ state: WORKFLOW_STATE[workflowState],
+ };
});
return workflowDict;
}
@@ -81,3 +80,11 @@ export function getModelsFromResponses(modelHits: any[]): ModelDict {
});
return modelDict;
}
+
+export function getWorkflowStateFromResponse(
+ state: typeof WORKFLOW_STATE | undefined
+): WORKFLOW_STATE {
+ const finalState = state || DEFAULT_NEW_WORKFLOW_STATE_TYPE;
+ // @ts-ignore
+ return WORKFLOW_STATE[finalState];
+}