Skip to content

Commit e6e6b71

Browse files
committed
Onboard deprovision API; add fine-grained loading state
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 93d2ffc commit e6e6b71

File tree

6 files changed

+144
-8
lines changed

6 files changed

+144
-8
lines changed

common/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const SEARCH_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/se
3838
export const GET_WORKFLOW_STATE_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/state`;
3939
export const CREATE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/create`;
4040
export const PROVISION_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/provision`;
41+
export const DEPROVISION_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/deprovision`;
4142
export const DELETE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/delete`;
4243
export const GET_PRESET_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/presets`;
4344

public/pages/workflow_detail/workspace/resizable_workspace.tsx

+56-5
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ import {
3333
DEFAULT_NEW_WORKFLOW_NAME,
3434
DEFAULT_NEW_WORKFLOW_DESCRIPTION,
3535
USE_CASE,
36+
WORKFLOW_STATE,
3637
} from '../../../../common';
3738
import {
3839
AppState,
3940
createWorkflow,
41+
deprovisionWorkflow,
4042
getWorkflowState,
4143
provisionWorkflow,
4244
removeDirty,
@@ -66,14 +68,22 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
6668
const history = useHistory();
6769

6870
// Overall workspace state
69-
const isDirty = useSelector((state: AppState) => state.workspace.isDirty);
71+
const { isDirty } = useSelector((state: AppState) => state.workspace);
72+
const { loading } = useSelector((state: AppState) => state.workflows);
7073
const [isFirstSave, setIsFirstSave] = useState<boolean>(props.isNewWorkflow);
7174

7275
// Workflow state
7376
const [workflow, setWorkflow] = useState<Workflow | undefined>(
7477
props.workflow
7578
);
7679

80+
// Loading state
81+
const [isProvisioning, setIsProvisioning] = useState<boolean>(false);
82+
const [isDeprovisioning, setIsDeprovisioning] = useState<boolean>(false);
83+
const [isSaving, setIsSaving] = useState<boolean>(false);
84+
const isLoadingGlobal =
85+
loading || isProvisioning || isDeprovisioning || isSaving;
86+
7787
// Formik form state
7888
const [formValues, setFormValues] = useState<WorkspaceFormValues>({});
7989
const [formSchema, setFormSchema] = useState<WorkspaceSchema>(yup.object({}));
@@ -102,7 +112,14 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
102112
// Save/provision button state
103113
const isSaveable = isFirstSave ? true : isDirty;
104114
const isProvisionable =
105-
!isDirty && !props.isNewWorkflow && formValidOnSubmit && flowValidOnSubmit;
115+
!isDirty &&
116+
!props.isNewWorkflow &&
117+
formValidOnSubmit &&
118+
flowValidOnSubmit &&
119+
props.workflow?.state === WORKFLOW_STATE.NOT_STARTED;
120+
const isDeprovisionable =
121+
!props.isNewWorkflow &&
122+
props.workflow?.state !== WORKFLOW_STATE.NOT_STARTED;
106123

107124
/**
108125
* Custom listener on when nodes are selected / de-selected. Passed to
@@ -234,6 +251,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
234251
// The workaround is to additionally execute validateForm() which will return any errors found.
235252
formikProps.submitForm();
236253
formikProps.validateForm().then((validationResults: {}) => {
254+
setIsSaving(false);
237255
if (Object.keys(validationResults).length > 0) {
238256
setFormValidOnSubmit(false);
239257
} else {
@@ -298,30 +316,63 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
298316
rightSideItems={[
299317
<EuiButton
300318
fill={false}
301-
disabled={!isProvisionable}
319+
disabled={!isDeprovisionable || isLoadingGlobal}
320+
isLoading={isDeprovisioning}
321+
onClick={() => {
322+
if (workflow?.id) {
323+
setIsDeprovisioning(true);
324+
dispatch(deprovisionWorkflow(workflow.id))
325+
.unwrap()
326+
.then(async (result) => {
327+
await new Promise((f) => setTimeout(f, 3000));
328+
dispatch(getWorkflowState(workflow.id as string));
329+
setIsDeprovisioning(false);
330+
})
331+
.catch((error: any) => {
332+
// TODO: process error (toast msg?)
333+
console.log('error: ', error);
334+
setIsDeprovisioning(false);
335+
});
336+
} else {
337+
// TODO: this case should not happen
338+
}
339+
}}
340+
>
341+
Deprovision
342+
</EuiButton>,
343+
<EuiButton
344+
fill={false}
345+
disabled={!isProvisionable || isLoadingGlobal}
346+
isLoading={isProvisioning}
302347
onClick={() => {
303348
if (workflow?.id) {
349+
setIsProvisioning(true);
304350
dispatch(provisionWorkflow(workflow.id))
305351
.unwrap()
306352
.then(async (result) => {
307353
await new Promise((f) => setTimeout(f, 3000));
308-
console.log('done waiting. fetching updated state...');
309354
dispatch(getWorkflowState(workflow.id as string));
355+
setIsProvisioning(false);
310356
})
311357
.catch((error: any) => {
312358
// TODO: process error (toast msg?)
313359
console.log('error: ', error);
360+
setIsProvisioning(false);
314361
});
362+
} else {
363+
// TODO: this case should not happen
315364
}
316365
}}
317366
>
318367
Provision
319368
</EuiButton>,
320369
<EuiButton
321370
fill={false}
322-
disabled={!isSaveable}
371+
disabled={!isSaveable || isLoadingGlobal}
372+
isLoading={isSaving}
323373
// TODO: if props.isNewWorkflow is true, clear the workflow cache if saving is successful.
324374
onClick={() => {
375+
setIsSaving(true);
325376
dispatch(removeDirty());
326377
if (isFirstSave) {
327378
setIsFirstSave(false);

public/route_service.ts

+12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
GET_PRESET_WORKFLOWS_NODE_API_PATH,
1515
SEARCH_MODELS_NODE_API_PATH,
1616
PROVISION_WORKFLOW_NODE_API_PATH,
17+
DEPROVISION_WORKFLOW_NODE_API_PATH,
1718
} from '../common';
1819

1920
/**
@@ -32,6 +33,7 @@ export interface RouteService {
3233
provision?: boolean
3334
) => Promise<any | HttpFetchError>;
3435
provisionWorkflow: (workflowId: string) => Promise<any | HttpFetchError>;
36+
deprovisionWorkflow: (workflowId: string) => Promise<any | HttpFetchError>;
3537
deleteWorkflow: (workflowId: string) => Promise<any | HttpFetchError>;
3638
getWorkflowPresets: () => Promise<any | HttpFetchError>;
3739
catIndices: (pattern: string) => Promise<any | HttpFetchError>;
@@ -96,6 +98,16 @@ export function configureRoutes(core: CoreStart): RouteService {
9698
return e as HttpFetchError;
9799
}
98100
},
101+
deprovisionWorkflow: async (workflowId: string) => {
102+
try {
103+
const response = await core.http.post<{ respString: string }>(
104+
`${DEPROVISION_WORKFLOW_NODE_API_PATH}/${workflowId}`
105+
);
106+
return response;
107+
} catch (e: any) {
108+
return e as HttpFetchError;
109+
}
110+
},
99111
deleteWorkflow: async (workflowId: string) => {
100112
try {
101113
const response = await core.http.delete<{ respString: string }>(

public/store/reducers/workflows_reducer.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const SEARCH_WORKFLOWS_ACTION = `${WORKFLOWS_ACTION_PREFIX}/search`;
2121
const GET_WORKFLOW_STATE_ACTION = `${WORKFLOWS_ACTION_PREFIX}/getState`;
2222
const CREATE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/create`;
2323
const PROVISION_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/provision`;
24+
const DEPROVISION_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/deprovision`;
2425
const DELETE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/delete`;
2526
const CACHE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/cache`;
2627
const CLEAR_CACHED_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/clearCache`;
@@ -105,6 +106,24 @@ export const provisionWorkflow = createAsyncThunk(
105106
}
106107
);
107108

109+
export const deprovisionWorkflow = createAsyncThunk(
110+
DEPROVISION_WORKFLOW_ACTION,
111+
async (workflowId: string, { rejectWithValue }) => {
112+
const response:
113+
| any
114+
| HttpFetchError = await getRouteService().deprovisionWorkflow(
115+
workflowId
116+
);
117+
if (response instanceof HttpFetchError) {
118+
return rejectWithValue(
119+
'Error deprovisioning workflow: ' + response.body.message
120+
);
121+
} else {
122+
return response;
123+
}
124+
}
125+
);
126+
108127
export const deleteWorkflow = createAsyncThunk(
109128
DELETE_WORKFLOW_ACTION,
110129
async (workflowId: string, { rejectWithValue }) => {
@@ -158,6 +177,10 @@ const workflowsSlice = createSlice({
158177
state.loading = true;
159178
state.errorMessage = '';
160179
})
180+
.addCase(deprovisionWorkflow.pending, (state, action) => {
181+
state.loading = true;
182+
state.errorMessage = '';
183+
})
161184
.addCase(deleteWorkflow.pending, (state, action) => {
162185
state.loading = true;
163186
state.errorMessage = '';
@@ -206,9 +229,10 @@ const workflowsSlice = createSlice({
206229
state.errorMessage = '';
207230
})
208231
.addCase(provisionWorkflow.fulfilled, (state, action) => {
209-
// Provision just kicks off an async task. No state update needed.
210-
// Frontend should re-query to fetch and populate any updated state
211-
// for the workflow
232+
state.loading = false;
233+
state.errorMessage = '';
234+
})
235+
.addCase(deprovisionWorkflow.fulfilled, (state, action) => {
212236
state.loading = false;
213237
state.errorMessage = '';
214238
})
@@ -246,6 +270,10 @@ const workflowsSlice = createSlice({
246270
state.errorMessage = action.payload as string;
247271
state.loading = false;
248272
})
273+
.addCase(deprovisionWorkflow.rejected, (state, action) => {
274+
state.errorMessage = action.payload as string;
275+
state.loading = false;
276+
})
249277
.addCase(deleteWorkflow.rejected, (state, action) => {
250278
state.errorMessage = action.payload as string;
251279
state.loading = false;

server/cluster/flow_framework_plugin.ts

+13
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ export function flowFrameworkPlugin(Client: any, config: any, components: any) {
8686
method: 'POST',
8787
});
8888

89+
flowFramework.deprovisionWorkflow = ca({
90+
url: {
91+
fmt: `${FLOW_FRAMEWORK_WORKFLOW_ROUTE_PREFIX}/<%=workflow_id%>/_deprovision`,
92+
req: {
93+
workflow_id: {
94+
type: 'string',
95+
required: true,
96+
},
97+
},
98+
},
99+
method: 'POST',
100+
});
101+
89102
flowFramework.deleteWorkflow = ca({
90103
url: {
91104
fmt: `${FLOW_FRAMEWORK_WORKFLOW_ROUTE_PREFIX}/<%=workflow_id%>`,

server/routes/flow_framework_routes_service.ts

+31
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import {
1717
CREATE_WORKFLOW_NODE_API_PATH,
1818
DELETE_WORKFLOW_NODE_API_PATH,
19+
DEPROVISION_WORKFLOW_NODE_API_PATH,
1920
GET_PRESET_WORKFLOWS_NODE_API_PATH,
2021
GET_WORKFLOW_NODE_API_PATH,
2122
GET_WORKFLOW_STATE_NODE_API_PATH,
@@ -96,6 +97,18 @@ export function registerFlowFrameworkRoutes(
9697
flowFrameworkRoutesService.provisionWorkflow
9798
);
9899

100+
router.post(
101+
{
102+
path: `${DEPROVISION_WORKFLOW_NODE_API_PATH}/{workflow_id}`,
103+
validate: {
104+
params: schema.object({
105+
workflow_id: schema.string(),
106+
}),
107+
},
108+
},
109+
flowFrameworkRoutesService.deprovisionWorkflow
110+
);
111+
99112
router.delete(
100113
{
101114
path: `${DELETE_WORKFLOW_NODE_API_PATH}/{workflow_id}`,
@@ -230,6 +243,24 @@ export class FlowFrameworkRoutesService {
230243
}
231244
};
232245

246+
deprovisionWorkflow = async (
247+
context: RequestHandlerContext,
248+
req: OpenSearchDashboardsRequest,
249+
res: OpenSearchDashboardsResponseFactory
250+
): Promise<IOpenSearchDashboardsResponse<any>> => {
251+
const { workflow_id } = req.params as { workflow_id: string };
252+
try {
253+
await this.client
254+
.asScoped(req)
255+
.callAsCurrentUser('flowFramework.deprovisionWorkflow', {
256+
workflow_id,
257+
});
258+
return res.ok();
259+
} catch (err: any) {
260+
return generateCustomError(res, err);
261+
}
262+
};
263+
233264
deleteWorkflow = async (
234265
context: RequestHandlerContext,
235266
req: OpenSearchDashboardsRequest,

0 commit comments

Comments
 (0)