Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Make New Workflow page functional #107

Merged
merged 1 commit into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
product: opensearch-dashboards

build-and-test-linux:
if: ${{ github.event.label.name != 'rapid' }}
needs: Get-CI-Image-Tag
name: Build & test
strategy:
Expand Down Expand Up @@ -49,7 +50,8 @@ jobs:

# TODO: once github actions supports windows and macos docker containers, we can
# merge these in to the above step's matrix, including adding windows support
build-and-test-windows-macos:
build-and-test-macos:
if: ${{ github.event.label.name != 'rapid' }}
name: Build & test
strategy:
matrix:
Expand Down
7 changes: 7 additions & 0 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ export const SEARCH_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/se
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 DELETE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/delete`;

/**
* 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';
14 changes: 9 additions & 5 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,20 @@ export type UseCaseTemplate = {
};

export type Workflow = {
id: string;
// won't exist until created in backend
id?: string;
name: string;
useCase: string;
template: UseCaseTemplate;
description?: string;
// ReactFlow state may not exist if a workflow is created via API/backend-only.
workspaceFlowState?: WorkspaceFlowState;
template: UseCaseTemplate;
lastUpdated: number;
lastLaunched: number;
state: WORKFLOW_STATE;
// won't exist until created in backend
lastUpdated?: number;
// won't exist until launched/provisioned in backend
lastLaunched?: number;
// won't exist until launched/provisioned in backend
state?: WORKFLOW_STATE;
};

export enum USE_CASE {
Expand Down
19 changes: 15 additions & 4 deletions public/pages/workflow_detail/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import React, { useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiPageHeader, EuiButton } from '@elastic/eui';
import { Workflow } from '../../../../common';
import { EuiPageHeader, EuiButton, EuiLoadingSpinner } from '@elastic/eui';
import { DEFAULT_NEW_WORKFLOW_NAME, Workflow } from '../../../../common';
import { saveWorkflow } from '../utils';
import { rfContext, AppState, removeDirty } from '../../../store';

interface WorkflowDetailHeaderProps {
tabs: any[];
isNewWorkflow: boolean;
workflow?: Workflow;
}

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

return (
<EuiPageHeader
pageTitle={props.workflow ? props.workflow.name : ''}
pageTitle={
props.workflow ? (
props.workflow.name
) : props.isNewWorkflow && !props.workflow ? (
DEFAULT_NEW_WORKFLOW_NAME
) : (
<EuiLoadingSpinner size="xl" />
)
}
rightSideItems={[
// TODO: add launch logic
<EuiButton fill={false} onClick={() => {}}>
Prototype
Launch
</EuiButton>,
<EuiButton
fill={false}
disabled={!props.workflow || !isDirty}
// TODO: if isNewWorkflow is true, clear the workflow cache if saving is successful.
onClick={() => {
// @ts-ignore
saveWorkflow(props.workflow, reactFlowInstance);
Expand Down
47 changes: 41 additions & 6 deletions public/pages/workflow_detail/workflow_detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@

import React, { useEffect, useState } from 'react';
import { RouteComponentProps, useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useDispatch, 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 } from '../../store';
import { AppState, searchWorkflows } from '../../store';
import { ResizableWorkspace } from './workspace';
import { Launches } from './launches';
import { Prototype } from './prototype';
import {
DEFAULT_NEW_WORKFLOW_NAME,
NEW_WORKFLOW_ID_URL,
} from '../../../common';

export interface WorkflowDetailRouterProps {
workflowId: string;
Expand Down Expand Up @@ -45,13 +49,27 @@ function replaceActiveTab(activeTab: string, props: 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 { workflows } = useSelector((state: AppState) => state.workflows);
const dispatch = useDispatch();
const { workflows, cachedWorkflow } = useSelector(
(state: AppState) => state.workflows
);
const { isDirty } = useSelector((state: AppState) => state.workspace);

const workflow = workflows[props.match?.params?.workflowId];
const workflowName = workflow ? workflow.name : '';
// 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
: '';

// tab state
const tabFromUrl = queryString.parse(useLocation().search)[
ACTIVE_TAB_PARAM
] as WORKFLOW_DETAILS_TAB;
Expand All @@ -78,6 +96,19 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
]);
});

// On initial load:
// - fetch workflow, if there is an existing workflow ID
// - add a window listener to warn users if they exit/refresh
// without saving latest changes
useEffect(() => {
if (!isNewWorkflow) {
// TODO: can optimize to only fetch a single workflow
dispatch(searchWorkflows({ query: { match_all: {} } }));
}
window.onbeforeunload = (e) =>
isDirty || isNewWorkflow ? true : undefined;
}, []);

const tabs = [
{
id: WORKFLOW_DETAILS_TAB.EDITOR,
Expand Down Expand Up @@ -112,7 +143,11 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
<ReactFlowProvider>
<EuiPage>
<EuiPageBody>
<WorkflowDetailHeader workflow={workflow} tabs={tabs} />
<WorkflowDetailHeader
workflow={workflow}
isNewWorkflow={isNewWorkflow}
tabs={tabs}
/>
{selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR && (
<ResizableWorkspace workflow={workflow} />
)}
Expand Down
109 changes: 90 additions & 19 deletions public/pages/workflows/new_workflow/new_workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,23 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui';

import React, { useState, useEffect } from 'react';
import { debounce } from 'lodash';
import {
EuiFlexItem,
EuiFlexGrid,
EuiFlexGroup,
EuiFieldSearch,
} from '@elastic/eui';
import { useDispatch } from 'react-redux';
import { UseCase } from './use_case';
import { getPresetWorkflows } from './presets';
import {
DEFAULT_NEW_WORKFLOW_NAME,
START_FROM_SCRATCH_WORKFLOW_NAME,
Workflow,
} from '../../../../common';
import { cacheWorkflow } from '../../../store';

interface NewWorkflowProps {}

Expand All @@ -18,26 +31,84 @@ interface NewWorkflowProps {}
* workflow for users to start with.
*/
export function NewWorkflow(props: NewWorkflowProps) {
const dispatch = useDispatch();
// preset workflow state
const presetWorkflows = getPresetWorkflows();
const [filteredWorkflows, setFilteredWorkflows] = useState<Workflow[]>(
getPresetWorkflows()
);

// search bar state
const [searchQuery, setSearchQuery] = useState<string>('');
const debounceSearchQuery = debounce((query: string) => {
setSearchQuery(query);
}, 200);

// When search query updated, re-filter preset list
useEffect(() => {
setFilteredWorkflows(fetchFilteredWorkflows(presetWorkflows, searchQuery));
}, [searchQuery]);

return (
<EuiFlexGrid columns={3} gutterSize="l">
<EuiFlexItem>
<UseCase
title="Semantic Search"
description="Semantic search description..."
/>
</EuiFlexItem>
<EuiFlexItem>
<UseCase
title="Multi-modal Search"
description="Multi-modal search description..."
<EuiFlexGroup direction="column">
<EuiFlexItem grow={true}>
<EuiFieldSearch
fullWidth={true}
placeholder="Search"
onChange={(e) => debounceSearchQuery(e.target.value)}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseCase
title="Search Summarization"
description="Search summarization description..."
/>
<EuiFlexGrid columns={3} gutterSize="l">
{filteredWorkflows.map((workflow: Workflow, index) => {
return (
<EuiFlexItem key={index}>
<UseCase
title={workflow.name}
description={workflow.description || ''}
onClick={() =>
dispatch(
cacheWorkflow({
...workflow,
name: processWorkflowName(workflow.name),
})
)
}
/>
</EuiFlexItem>
);
})}
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFlexGroup>
);
}

// Collect the final preset workflow list after applying all filters
function fetchFilteredWorkflows(
allWorkflows: Workflow[],
searchQuery: string
): Workflow[] {
return searchQuery.length === 0
? allWorkflows
: allWorkflows.filter((workflow) =>
workflow.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}

// Utility fn to process workflow names from their presentable/readable titles
// on the UI, to a valid name format.
// This leads to less friction if users decide to save the name later on.
function processWorkflowName(workflowName: string): string {
return workflowName === START_FROM_SCRATCH_WORKFLOW_NAME
? DEFAULT_NEW_WORKFLOW_NAME
: toSnakeCase(workflowName);
}

function toSnakeCase(text: string): string {
return text
.replace(/\W+/g, ' ')
.split(/ |\B(?=[A-Z])/)
.map((word) => word.toLowerCase())
.join('_');
}
63 changes: 63 additions & 0 deletions public/pages/workflows/new_workflow/presets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
START_FROM_SCRATCH_WORKFLOW_NAME,
Workflow,
WorkspaceFlowState,
} from '../../../../common';

// TODO: fetch from the backend when the workflow library is complete.
/**
* Used to fetch the library of preset workflows to provide to users.
*/
export function getPresetWorkflows(): Workflow[] {
return [
{
name: 'Semantic Search',
description:
'This semantic search workflow includes the essential ingestion and search pipelines that covers the most common search use cases.',
useCase: 'SEMANTIC_SEARCH',
template: {},
workspaceFlowState: {
nodes: [],
edges: [],
} as WorkspaceFlowState,
},
{
name: 'Semantic Search with Reranking',
description:
'This semantic search workflow variation includes an ML processor to rerank fetched results.',
useCase: 'SEMANTIC_SEARCH_WITH_RERANK',
template: {},
workspaceFlowState: {
nodes: [],
edges: [],
} as WorkspaceFlowState,
},
{
name: START_FROM_SCRATCH_WORKFLOW_NAME,
description:
'Build your workflow from scratch according to your specific use cases. Start by adding components for your ingest or query needs.',
useCase: '',
template: {},
workspaceFlowState: {
nodes: [],
edges: [],
} as WorkspaceFlowState,
},
{
name: 'Visual Search',
description:
'Build an application that will return results based on images.',
useCase: 'SEMANTIC_SEARCH',
template: {},
workspaceFlowState: {
nodes: [],
edges: [],
} as WorkspaceFlowState,
},
] as Workflow[];
}
Loading
Loading