Skip to content

Commit 73e80ae

Browse files
Make New Workflow page functional (#102) (#107)
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> (cherry picked from commit 2d176f0) Co-authored-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 8373270 commit 73e80ae

File tree

10 files changed

+259
-39
lines changed

10 files changed

+259
-39
lines changed

.github/workflows/build-and-test.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
product: opensearch-dashboards
1717

1818
build-and-test-linux:
19+
if: ${{ github.event.label.name != 'rapid' }}
1920
needs: Get-CI-Image-Tag
2021
name: Build & test
2122
strategy:
@@ -49,7 +50,8 @@ jobs:
4950

5051
# TODO: once github actions supports windows and macos docker containers, we can
5152
# merge these in to the above step's matrix, including adding windows support
52-
build-and-test-windows-macos:
53+
build-and-test-macos:
54+
if: ${{ github.event.label.name != 'rapid' }}
5355
name: Build & test
5456
strategy:
5557
matrix:

common/constants.ts

+7
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ 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';
37+
export const START_FROM_SCRATCH_WORKFLOW_NAME = 'Start From Scratch';
38+
export const DEFAULT_NEW_WORKFLOW_NAME = 'new_workflow';

common/interfaces.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,20 @@ export type UseCaseTemplate = {
6969
};
7070

7171
export type Workflow = {
72-
id: string;
72+
// won't exist until created in backend
73+
id?: string;
7374
name: string;
7475
useCase: string;
76+
template: UseCaseTemplate;
7577
description?: string;
7678
// ReactFlow state may not exist if a workflow is created via API/backend-only.
7779
workspaceFlowState?: WorkspaceFlowState;
78-
template: UseCaseTemplate;
79-
lastUpdated: number;
80-
lastLaunched: number;
81-
state: WORKFLOW_STATE;
80+
// won't exist until created in backend
81+
lastUpdated?: number;
82+
// won't exist until launched/provisioned in backend
83+
lastLaunched?: number;
84+
// won't exist until launched/provisioned in backend
85+
state?: WORKFLOW_STATE;
8286
};
8387

8488
export enum USE_CASE {

public/pages/workflow_detail/components/header.tsx

+15-4
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
import React, { useContext } from 'react';
77
import { useDispatch, useSelector } from 'react-redux';
8-
import { EuiPageHeader, EuiButton } from '@elastic/eui';
9-
import { Workflow } from '../../../../common';
8+
import { EuiPageHeader, EuiButton, EuiLoadingSpinner } from '@elastic/eui';
9+
import { DEFAULT_NEW_WORKFLOW_NAME, Workflow } from '../../../../common';
1010
import { saveWorkflow } from '../utils';
1111
import { rfContext, AppState, removeDirty } from '../../../store';
1212

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

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

2324
return (
2425
<EuiPageHeader
25-
pageTitle={props.workflow ? props.workflow.name : ''}
26+
pageTitle={
27+
props.workflow ? (
28+
props.workflow.name
29+
) : props.isNewWorkflow && !props.workflow ? (
30+
DEFAULT_NEW_WORKFLOW_NAME
31+
) : (
32+
<EuiLoadingSpinner size="xl" />
33+
)
34+
}
2635
rightSideItems={[
36+
// TODO: add launch logic
2737
<EuiButton fill={false} onClick={() => {}}>
28-
Prototype
38+
Launch
2939
</EuiButton>,
3040
<EuiButton
3141
fill={false}
3242
disabled={!props.workflow || !isDirty}
43+
// TODO: if isNewWorkflow is true, clear the workflow cache if saving is successful.
3344
onClick={() => {
3445
// @ts-ignore
3546
saveWorkflow(props.workflow, reactFlowInstance);

public/pages/workflow_detail/workflow_detail.tsx

+41-6
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@
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';
1212
import { BREADCRUMBS } from '../../utils';
1313
import { getCore } from '../../services';
1414
import { WorkflowDetailHeader } from './components';
15-
import { AppState } from '../../store';
15+
import { AppState, searchWorkflows } from '../../store';
1616
import { ResizableWorkspace } from './workspace';
1717
import { Launches } from './launches';
1818
import { Prototype } from './prototype';
19+
import {
20+
DEFAULT_NEW_WORKFLOW_NAME,
21+
NEW_WORKFLOW_ID_URL,
22+
} from '../../../common';
1923

2024
export interface WorkflowDetailRouterProps {
2125
workflowId: string;
@@ -45,13 +49,27 @@ function replaceActiveTab(activeTab: string, props: WorkflowDetailProps) {
4549
* The workflow details page. This is where users will configure, create, and
4650
* test their created workflows. Additionally, can be used to load existing workflows
4751
* to view details and/or make changes to them.
52+
* New, unsaved workflows are cached in the redux store and displayed here.
4853
*/
54+
4955
export function WorkflowDetail(props: WorkflowDetailProps) {
50-
const { workflows } = useSelector((state: AppState) => state.workflows);
56+
const dispatch = useDispatch();
57+
const { workflows, cachedWorkflow } = useSelector(
58+
(state: AppState) => state.workflows
59+
);
60+
const { isDirty } = useSelector((state: AppState) => state.workspace);
5161

52-
const workflow = workflows[props.match?.params?.workflowId];
53-
const workflowName = workflow ? workflow.name : '';
62+
// selected workflow state
63+
const workflowId = props.match?.params?.workflowId;
64+
const isNewWorkflow = workflowId === NEW_WORKFLOW_ID_URL;
65+
const workflow = isNewWorkflow ? cachedWorkflow : workflows[workflowId];
66+
const workflowName = workflow
67+
? workflow.name
68+
: isNewWorkflow && !workflow
69+
? DEFAULT_NEW_WORKFLOW_NAME
70+
: '';
5471

72+
// tab state
5573
const tabFromUrl = queryString.parse(useLocation().search)[
5674
ACTIVE_TAB_PARAM
5775
] as WORKFLOW_DETAILS_TAB;
@@ -78,6 +96,19 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
7896
]);
7997
});
8098

99+
// On initial load:
100+
// - fetch workflow, if there is an existing workflow ID
101+
// - add a window listener to warn users if they exit/refresh
102+
// without saving latest changes
103+
useEffect(() => {
104+
if (!isNewWorkflow) {
105+
// TODO: can optimize to only fetch a single workflow
106+
dispatch(searchWorkflows({ query: { match_all: {} } }));
107+
}
108+
window.onbeforeunload = (e) =>
109+
isDirty || isNewWorkflow ? true : undefined;
110+
}, []);
111+
81112
const tabs = [
82113
{
83114
id: WORKFLOW_DETAILS_TAB.EDITOR,
@@ -112,7 +143,11 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
112143
<ReactFlowProvider>
113144
<EuiPage>
114145
<EuiPageBody>
115-
<WorkflowDetailHeader workflow={workflow} tabs={tabs} />
146+
<WorkflowDetailHeader
147+
workflow={workflow}
148+
isNewWorkflow={isNewWorkflow}
149+
tabs={tabs}
150+
/>
116151
{selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR && (
117152
<ResizableWorkspace workflow={workflow} />
118153
)}

public/pages/workflows/new_workflow/new_workflow.tsx

+90-19
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,23 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React from 'react';
7-
import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui';
8-
6+
import React, { useState, useEffect } from 'react';
7+
import { debounce } from 'lodash';
8+
import {
9+
EuiFlexItem,
10+
EuiFlexGrid,
11+
EuiFlexGroup,
12+
EuiFieldSearch,
13+
} from '@elastic/eui';
14+
import { useDispatch } from 'react-redux';
915
import { UseCase } from './use_case';
16+
import { getPresetWorkflows } from './presets';
17+
import {
18+
DEFAULT_NEW_WORKFLOW_NAME,
19+
START_FROM_SCRATCH_WORKFLOW_NAME,
20+
Workflow,
21+
} from '../../../../common';
22+
import { cacheWorkflow } from '../../../store';
1023

1124
interface NewWorkflowProps {}
1225

@@ -18,26 +31,84 @@ interface NewWorkflowProps {}
1831
* workflow for users to start with.
1932
*/
2033
export function NewWorkflow(props: NewWorkflowProps) {
34+
const dispatch = useDispatch();
35+
// preset workflow state
36+
const presetWorkflows = getPresetWorkflows();
37+
const [filteredWorkflows, setFilteredWorkflows] = useState<Workflow[]>(
38+
getPresetWorkflows()
39+
);
40+
41+
// search bar state
42+
const [searchQuery, setSearchQuery] = useState<string>('');
43+
const debounceSearchQuery = debounce((query: string) => {
44+
setSearchQuery(query);
45+
}, 200);
46+
47+
// When search query updated, re-filter preset list
48+
useEffect(() => {
49+
setFilteredWorkflows(fetchFilteredWorkflows(presetWorkflows, searchQuery));
50+
}, [searchQuery]);
51+
2152
return (
22-
<EuiFlexGrid columns={3} gutterSize="l">
23-
<EuiFlexItem>
24-
<UseCase
25-
title="Semantic Search"
26-
description="Semantic search description..."
27-
/>
28-
</EuiFlexItem>
29-
<EuiFlexItem>
30-
<UseCase
31-
title="Multi-modal Search"
32-
description="Multi-modal search description..."
53+
<EuiFlexGroup direction="column">
54+
<EuiFlexItem grow={true}>
55+
<EuiFieldSearch
56+
fullWidth={true}
57+
placeholder="Search"
58+
onChange={(e) => debounceSearchQuery(e.target.value)}
3359
/>
3460
</EuiFlexItem>
3561
<EuiFlexItem>
36-
<UseCase
37-
title="Search Summarization"
38-
description="Search summarization description..."
39-
/>
62+
<EuiFlexGrid columns={3} gutterSize="l">
63+
{filteredWorkflows.map((workflow: Workflow, index) => {
64+
return (
65+
<EuiFlexItem key={index}>
66+
<UseCase
67+
title={workflow.name}
68+
description={workflow.description || ''}
69+
onClick={() =>
70+
dispatch(
71+
cacheWorkflow({
72+
...workflow,
73+
name: processWorkflowName(workflow.name),
74+
})
75+
)
76+
}
77+
/>
78+
</EuiFlexItem>
79+
);
80+
})}
81+
</EuiFlexGrid>
4082
</EuiFlexItem>
41-
</EuiFlexGrid>
83+
</EuiFlexGroup>
4284
);
4385
}
86+
87+
// Collect the final preset workflow list after applying all filters
88+
function fetchFilteredWorkflows(
89+
allWorkflows: Workflow[],
90+
searchQuery: string
91+
): Workflow[] {
92+
return searchQuery.length === 0
93+
? allWorkflows
94+
: allWorkflows.filter((workflow) =>
95+
workflow.name.toLowerCase().includes(searchQuery.toLowerCase())
96+
);
97+
}
98+
99+
// Utility fn to process workflow names from their presentable/readable titles
100+
// on the UI, to a valid name format.
101+
// This leads to less friction if users decide to save the name later on.
102+
function processWorkflowName(workflowName: string): string {
103+
return workflowName === START_FROM_SCRATCH_WORKFLOW_NAME
104+
? DEFAULT_NEW_WORKFLOW_NAME
105+
: toSnakeCase(workflowName);
106+
}
107+
108+
function toSnakeCase(text: string): string {
109+
return text
110+
.replace(/\W+/g, ' ')
111+
.split(/ |\B(?=[A-Z])/)
112+
.map((word) => word.toLowerCase())
113+
.join('_');
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import {
7+
START_FROM_SCRATCH_WORKFLOW_NAME,
8+
Workflow,
9+
WorkspaceFlowState,
10+
} from '../../../../common';
11+
12+
// TODO: fetch from the backend when the workflow library is complete.
13+
/**
14+
* Used to fetch the library of preset workflows to provide to users.
15+
*/
16+
export function getPresetWorkflows(): Workflow[] {
17+
return [
18+
{
19+
name: 'Semantic Search',
20+
description:
21+
'This semantic search workflow includes the essential ingestion and search pipelines that covers the most common search use cases.',
22+
useCase: 'SEMANTIC_SEARCH',
23+
template: {},
24+
workspaceFlowState: {
25+
nodes: [],
26+
edges: [],
27+
} as WorkspaceFlowState,
28+
},
29+
{
30+
name: 'Semantic Search with Reranking',
31+
description:
32+
'This semantic search workflow variation includes an ML processor to rerank fetched results.',
33+
useCase: 'SEMANTIC_SEARCH_WITH_RERANK',
34+
template: {},
35+
workspaceFlowState: {
36+
nodes: [],
37+
edges: [],
38+
} as WorkspaceFlowState,
39+
},
40+
{
41+
name: START_FROM_SCRATCH_WORKFLOW_NAME,
42+
description:
43+
'Build your workflow from scratch according to your specific use cases. Start by adding components for your ingest or query needs.',
44+
useCase: '',
45+
template: {},
46+
workspaceFlowState: {
47+
nodes: [],
48+
edges: [],
49+
} as WorkspaceFlowState,
50+
},
51+
{
52+
name: 'Visual Search',
53+
description:
54+
'Build an application that will return results based on images.',
55+
useCase: 'SEMANTIC_SEARCH',
56+
template: {},
57+
workspaceFlowState: {
58+
nodes: [],
59+
edges: [],
60+
} as WorkspaceFlowState,
61+
},
62+
] as Workflow[];
63+
}

0 commit comments

Comments
 (0)