Skip to content

Commit 1793726

Browse files
committed
Add navigation; handle new workflow state on details page
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 4e80e73 commit 1793726

File tree

6 files changed

+83
-12
lines changed

6 files changed

+83
-12
lines changed

common/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ export const SEARCH_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/se
2929
export const GET_WORKFLOW_STATE_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/state`;
3030
export const CREATE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/create`;
3131
export const DELETE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/delete`;
32+
33+
/**
34+
* MISCELLANEOUS
35+
*/
36+
export const NEW_WORKFLOW_ID_URL = 'new';

public/pages/workflow_detail/components/header.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { rfContext, AppState, removeDirty } from '../../../store';
1212

1313
interface WorkflowDetailHeaderProps {
1414
tabs: any[];
15+
formattedWorkflowName: string;
1516
workflow?: Workflow;
1617
}
1718

@@ -22,7 +23,7 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
2223

2324
return (
2425
<EuiPageHeader
25-
pageTitle={props.workflow ? props.workflow.name : ''}
26+
pageTitle={props.formattedWorkflowName}
2627
rightSideItems={[
2728
<EuiButton fill={false} onClick={() => {}}>
2829
Prototype

public/pages/workflow_detail/workflow_detail.tsx

+23-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import React, { useEffect, useState } from 'react';
77
import { RouteComponentProps, useLocation } from 'react-router-dom';
8-
import { useSelector } from 'react-redux';
8+
import { useDispatch, useSelector } from 'react-redux';
99
import { ReactFlowProvider } from 'reactflow';
1010
import queryString from 'query-string';
1111
import { EuiPage, EuiPageBody } from '@elastic/eui';
@@ -16,6 +16,7 @@ import { AppState } from '../../store';
1616
import { ResizableWorkspace } from './workspace';
1717
import { Launches } from './launches';
1818
import { Prototype } from './prototype';
19+
import { NEW_WORKFLOW_ID_URL } from '../../../common';
1920

2021
export interface WorkflowDetailRouterProps {
2122
workflowId: string;
@@ -45,13 +46,26 @@ function replaceActiveTab(activeTab: string, props: WorkflowDetailProps) {
4546
* The workflow details page. This is where users will configure, create, and
4647
* test their created workflows. Additionally, can be used to load existing workflows
4748
* to view details and/or make changes to them.
49+
* New, unsaved workflows are cached in the redux store and displayed here.
4850
*/
51+
52+
// TODO: if exiting the page, or if saving, clear the cached workflow. Can use redux clearCachedWorkflow()
4953
export function WorkflowDetail(props: WorkflowDetailProps) {
50-
const { workflows } = useSelector((state: AppState) => state.workflows);
54+
const { workflows, cachedWorkflow } = useSelector(
55+
(state: AppState) => state.workflows
56+
);
5157

52-
const workflow = workflows[props.match?.params?.workflowId];
53-
const workflowName = workflow ? workflow.name : '';
58+
const isNewWorkflow = props.match?.params?.workflowId === NEW_WORKFLOW_ID_URL;
59+
const workflow = isNewWorkflow
60+
? cachedWorkflow
61+
: workflows[props.match?.params?.workflowId];
62+
const workflowName = workflow
63+
? workflow.name
64+
: isNewWorkflow && !cachedWorkflow
65+
? 'new_workflow'
66+
: '';
5467

68+
// tab state
5569
const tabFromUrl = queryString.parse(useLocation().search)[
5670
ACTIVE_TAB_PARAM
5771
] as WORKFLOW_DETAILS_TAB;
@@ -112,7 +126,11 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
112126
<ReactFlowProvider>
113127
<EuiPage>
114128
<EuiPageBody>
115-
<WorkflowDetailHeader workflow={workflow} tabs={tabs} />
129+
<WorkflowDetailHeader
130+
workflow={workflow}
131+
formattedWorkflowName={workflowName}
132+
tabs={tabs}
133+
/>
116134
{selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR && (
117135
<ResizableWorkspace workflow={workflow} />
118136
)}

public/pages/workflows/new_workflow/new_workflow.tsx

+24-3
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import {
1111
EuiFlexGroup,
1212
EuiFieldSearch,
1313
} from '@elastic/eui';
14-
14+
import { useDispatch } from 'react-redux';
1515
import { UseCase } from './use_case';
1616
import { getPresetWorkflows } from './presets';
1717
import { Workflow } from '../../../../common';
18+
import { cacheWorkflow } from '../../../store';
1819

1920
interface NewWorkflowProps {}
2021

@@ -26,6 +27,7 @@ interface NewWorkflowProps {}
2627
* workflow for users to start with.
2728
*/
2829
export function NewWorkflow(props: NewWorkflowProps) {
30+
const dispatch = useDispatch();
2931
// preset workflow state
3032
const presetWorkflows = getPresetWorkflows();
3133
const [filteredWorkflows, setFilteredWorkflows] = useState<Workflow[]>(
@@ -54,12 +56,20 @@ export function NewWorkflow(props: NewWorkflowProps) {
5456
</EuiFlexItem>
5557
<EuiFlexItem>
5658
<EuiFlexGrid columns={3} gutterSize="l">
57-
{filteredWorkflows.map((workflow: Workflow) => {
59+
{filteredWorkflows.map((workflow: Workflow, index) => {
5860
return (
59-
<EuiFlexItem>
61+
<EuiFlexItem key={index}>
6062
<UseCase
6163
title={workflow.name}
6264
description={workflow.description || ''}
65+
onClick={() =>
66+
dispatch(
67+
cacheWorkflow({
68+
...workflow,
69+
name: toSnakeCase(workflow.name),
70+
})
71+
)
72+
}
6373
/>
6474
</EuiFlexItem>
6575
);
@@ -81,3 +91,14 @@ function fetchFilteredWorkflows(
8191
workflow.name.toLowerCase().includes(searchQuery.toLowerCase())
8292
);
8393
}
94+
95+
// Utility fn to convert to snakecase. Used when caching the workflow
96+
// to make a valid name and cause less friction if users decide
97+
// to save it later on.
98+
function toSnakeCase(text: string): string {
99+
return text
100+
.replace(/\W+/g, ' ')
101+
.split(/ |\B(?=[A-Z])/)
102+
.map((word) => word.toLowerCase())
103+
.join('_');
104+
}

public/pages/workflows/new_workflow/use_case.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ import {
1313
EuiHorizontalRule,
1414
EuiButton,
1515
} from '@elastic/eui';
16+
import { NEW_WORKFLOW_ID_URL, PLUGIN_ID } from '../../../../common';
17+
1618
interface UseCaseProps {
1719
title: string;
1820
description: string;
21+
onClick: () => {};
1922
}
2023

2124
export function UseCase(props: UseCaseProps) {
@@ -40,9 +43,8 @@ export function UseCase(props: UseCaseProps) {
4043
<EuiButton
4144
disabled={false}
4245
isLoading={false}
43-
onClick={() => {
44-
// TODO: possibly link to the workflow details with a pre-configured flow
45-
}}
46+
onClick={props.onClick}
47+
href={`${PLUGIN_ID}#/workflows/${NEW_WORKFLOW_ID_URL}`}
4648
>
4749
Go
4850
</EuiButton>

public/store/reducers/workflows_reducer.ts

+24
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const initialState = {
7878
loading: false,
7979
errorMessage: '',
8080
workflows: {} as WorkflowDict,
81+
cachedWorkflow: undefined as Workflow | undefined,
8182
};
8283

8384
const WORKFLOWS_ACTION_PREFIX = 'workflows';
@@ -86,6 +87,8 @@ const SEARCH_WORKFLOWS_ACTION = `${WORKFLOWS_ACTION_PREFIX}/searchWorkflows`;
8687
const GET_WORKFLOW_STATE_ACTION = `${WORKFLOWS_ACTION_PREFIX}/getWorkflowState`;
8788
const CREATE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/createWorkflow`;
8889
const DELETE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/deleteWorkflow`;
90+
const CACHE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/cacheWorkflow`;
91+
const CLEAR_CACHED_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/clearCachedWorkflow`;
8992

9093
export const getWorkflow = createAsyncThunk(
9194
GET_WORKFLOW_ACTION,
@@ -167,6 +170,20 @@ export const deleteWorkflow = createAsyncThunk(
167170
}
168171
);
169172

173+
export const cacheWorkflow = createAsyncThunk(
174+
CACHE_WORKFLOW_ACTION,
175+
async (workflow: Workflow) => {
176+
return workflow;
177+
}
178+
);
179+
180+
// A no-op function to trigger a reducer case.
181+
// Will clear any stored workflow in the cachedWorkflow state
182+
export const clearCachedWorkflow = createAsyncThunk(
183+
CLEAR_CACHED_WORKFLOW_ACTION,
184+
async () => {}
185+
);
186+
170187
const workflowsSlice = createSlice({
171188
name: 'workflows',
172189
initialState,
@@ -238,6 +255,13 @@ const workflowsSlice = createSlice({
238255
state.loading = false;
239256
state.errorMessage = '';
240257
})
258+
.addCase(cacheWorkflow.fulfilled, (state, action) => {
259+
const workflow = action.payload;
260+
state.cachedWorkflow = workflow;
261+
})
262+
.addCase(clearCachedWorkflow.fulfilled, (state, action) => {
263+
state.cachedWorkflow = undefined;
264+
})
241265
// Rejected states: set state consistently across all actions
242266
.addCase(getWorkflow.rejected, (state, action) => {
243267
state.errorMessage = action.payload as string;

0 commit comments

Comments
 (0)