diff --git a/common/constants.ts b/common/constants.ts
index b23b4efc..d398a9e1 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -123,6 +123,13 @@ export const NEURAL_SPARSE_TOKENIZER_TRANSFORMER = {
* Various constants pertaining to Workflow configs
*/
+// frontend-specific workflow types, derived from the available preset templates
+export enum WORKFLOW_TYPE {
+ SEMANTIC_SEARCH = 'Semantic search',
+ CUSTOM = 'Custom',
+ UNKNOWN = 'Unknown',
+}
+
export enum PROCESSOR_TYPE {
ML = 'ml_processor',
}
diff --git a/common/interfaces.ts b/common/interfaces.ts
index 15375052..81c88d7c 100644
--- a/common/interfaces.ts
+++ b/common/interfaces.ts
@@ -6,7 +6,7 @@
import { Node, Edge } from 'reactflow';
import { FormikValues } from 'formik';
import { ObjectSchema } from 'yup';
-import { COMPONENT_CLASS, PROCESSOR_TYPE } from './constants';
+import { COMPONENT_CLASS, PROCESSOR_TYPE, WORKFLOW_TYPE } from './constants';
export type Index = {
name: string;
@@ -163,6 +163,7 @@ type ReactFlowViewport = {
export type UIState = {
config: WorkflowConfig;
+ type: WORKFLOW_TYPE;
workspace_flow?: WorkspaceFlowState;
};
@@ -298,11 +299,11 @@ export type TemplateFlows = {
export type WorkflowTemplate = {
name: string;
description: string;
- use_case: USE_CASE;
// TODO: finalize on version type when that is implemented
// https://github.com/opensearch-project/flow-framework/issues/526
version: any;
workflows: TemplateFlows;
+ use_case?: USE_CASE;
// UI state and any ReactFlow state may not exist if a workflow is created via API/backend-only.
ui_metadata?: UIState;
};
diff --git a/public/general_components/delete_workflow_modal.tsx b/public/general_components/delete_workflow_modal.tsx
index ce718090..b15e0a97 100644
--- a/public/general_components/delete_workflow_modal.tsx
+++ b/public/general_components/delete_workflow_modal.tsx
@@ -6,6 +6,7 @@
import React from 'react';
import {
EuiButton,
+ EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
@@ -33,11 +34,15 @@ export function DeleteWorkflowModal(props: DeleteWorkflowModalProps) {
- The workflow will be permanently deleted.
+
+ The workflow will be permanently deleted. This action cannot be
+ undone. Resources created by this workflow will be retained.
+
+ Cancel
- Confirm
+ Delete
diff --git a/public/general_components/general-component-styles.scss b/public/general_components/general-component-styles.scss
index 54e5c17b..68f9cebf 100644
--- a/public/general_components/general-component-styles.scss
+++ b/public/general_components/general-component-styles.scss
@@ -1,5 +1,5 @@
.multi-select-filter {
&--width {
- width: 150px;
+ width: 200px;
}
}
diff --git a/public/general_components/index.ts b/public/general_components/index.ts
index 40873509..b1232790 100644
--- a/public/general_components/index.ts
+++ b/public/general_components/index.ts
@@ -6,3 +6,4 @@
export { MultiSelectFilter } from './multi_select_filter';
export { DeleteWorkflowModal } from './delete_workflow_modal';
export { ProcessorsTitle } from './processors_title';
+export { ResourceList } from './resource_list';
diff --git a/public/pages/workflow_detail/tools/resources/resource_list.tsx b/public/general_components/resource_list.tsx
similarity index 91%
rename from public/pages/workflow_detail/tools/resources/resource_list.tsx
rename to public/general_components/resource_list.tsx
index 98083b64..3788f2ba 100644
--- a/public/pages/workflow_detail/tools/resources/resource_list.tsx
+++ b/public/general_components/resource_list.tsx
@@ -10,8 +10,8 @@ import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
-import { Workflow, WorkflowResource } from '../../../../../common';
-import { columns } from './columns';
+import { Workflow, WorkflowResource } from '../../common';
+import { columns } from '../pages/workflow_detail/tools/resources/columns';
interface ResourceListProps {
workflow?: Workflow;
diff --git a/public/pages/workflow_detail/launches/columns.tsx b/public/pages/workflow_detail/launches/columns.tsx
deleted file mode 100644
index dfc5eea0..00000000
--- a/public/pages/workflow_detail/launches/columns.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export const columns = [
- {
- field: 'id',
- name: 'Launch ID',
- sortable: true,
- },
- {
- field: 'state',
- name: 'Status',
- sortable: true,
- },
- {
- field: 'lastUpdatedTime',
- name: 'Last updated time',
- sortable: true,
- },
-];
diff --git a/public/pages/workflow_detail/launches/index.ts b/public/pages/workflow_detail/launches/index.ts
deleted file mode 100644
index 4e21436b..00000000
--- a/public/pages/workflow_detail/launches/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export { Launches } from './launches';
diff --git a/public/pages/workflow_detail/launches/launch_details.tsx b/public/pages/workflow_detail/launches/launch_details.tsx
deleted file mode 100644
index 82f62c90..00000000
--- a/public/pages/workflow_detail/launches/launch_details.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React from 'react';
-import { EuiText } from '@elastic/eui';
-
-interface LaunchDetailsProps {}
-
-export function LaunchDetails(props: LaunchDetailsProps) {
- return TODO: add selected launch details here;
-}
diff --git a/public/pages/workflow_detail/launches/launch_list.tsx b/public/pages/workflow_detail/launches/launch_list.tsx
deleted file mode 100644
index 3062887f..00000000
--- a/public/pages/workflow_detail/launches/launch_list.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React, { useState, useEffect } from 'react';
-import { debounce } from 'lodash';
-import {
- EuiInMemoryTable,
- Direction,
- EuiFlexGroup,
- EuiFlexItem,
- EuiFieldSearch,
- EuiFilterSelectItem,
-} from '@elastic/eui';
-import { WORKFLOW_STATE, WorkflowLaunch } from '../../../../common';
-import { columns } from './columns';
-import { MultiSelectFilter } from '../../../general_components';
-import { getStateOptions } from '../../../utils';
-
-interface LaunchListProps {}
-
-/**
- * The searchable list of launches for this particular workflow.
- */
-export function LaunchList(props: LaunchListProps) {
- // TODO: finalize how we persist launches for a particular workflow.
- // We may just add UI metadata tags to group workflows under a single, overall "workflow"
- // const { workflows } = useSelector((state: AppState) => state.workflows);
- const workflowLaunches = [
- {
- id: 'Launch_1',
- state: WORKFLOW_STATE.PROVISIONING,
- lastUpdated: 12345678,
- },
- {
- id: 'Launch_2',
- state: WORKFLOW_STATE.FAILED,
- lastUpdated: 12345677,
- },
- ] as WorkflowLaunch[];
-
- // search bar state
- const [searchQuery, setSearchQuery] = useState('');
- const debounceSearchQuery = debounce((query: string) => {
- setSearchQuery(query);
- }, 100);
-
- // filters state
- const [selectedStates, setSelectedStates] = useState(
- getStateOptions()
- );
- const [filteredLaunches, setFilteredLaunches] = useState(
- workflowLaunches
- );
-
- // When a filter selection or search query changes, update the filtered launches
- useEffect(() => {
- setFilteredLaunches(
- fetchFilteredLaunches(workflowLaunches, selectedStates, searchQuery)
- );
- }, [selectedStates, searchQuery]);
-
- const sorting = {
- sort: {
- field: 'id',
- direction: 'asc' as Direction,
- },
- };
-
- return (
-
-
-
-
- debounceSearchQuery(e.target.value)}
- />
-
-
-
-
-
-
- items={filteredLaunches}
- rowHeader="id"
- columns={columns}
- sorting={sorting}
- pagination={true}
- message={'No existing launches found'}
- />
-
-
- );
-}
-
-// Collect the final launch list after applying all filters
-function fetchFilteredLaunches(
- allLaunches: WorkflowLaunch[],
- stateFilters: EuiFilterSelectItem[],
- searchQuery: string
-): WorkflowLaunch[] {
- // @ts-ignore
- const stateFilterStrings = stateFilters.map((filter) => filter.name);
- const filteredLaunches = allLaunches.filter((launch) =>
- stateFilterStrings.includes(launch.state)
- );
- return searchQuery.length === 0
- ? filteredLaunches
- : filteredLaunches.filter((launch) =>
- launch.id.toLowerCase().includes(searchQuery.toLowerCase())
- );
-}
diff --git a/public/pages/workflow_detail/launches/launches.tsx b/public/pages/workflow_detail/launches/launches.tsx
deleted file mode 100644
index b24ab016..00000000
--- a/public/pages/workflow_detail/launches/launches.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React from 'react';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiPageContent,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
-import { Workflow } from '../../../../common';
-import { LaunchList } from './launch_list';
-import { LaunchDetails } from './launch_details';
-
-interface LaunchesProps {
- workflow?: Workflow;
-}
-
-/**
- * The launches page to browse launch history and view individual launch details.
- */
-export function Launches(props: LaunchesProps) {
- return (
-
-
- Launches
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/public/pages/workflow_detail/tools/resources/resources.tsx b/public/pages/workflow_detail/tools/resources/resources.tsx
index 399663c3..1575f1d5 100644
--- a/public/pages/workflow_detail/tools/resources/resources.tsx
+++ b/public/pages/workflow_detail/tools/resources/resources.tsx
@@ -11,7 +11,7 @@ import {
EuiText,
} from '@elastic/eui';
import { Workflow } from '../../../../../common';
-import { ResourceList } from './resource_list';
+import { ResourceList } from '../../../../general_components';
interface ResourcesProps {
workflow?: Workflow;
diff --git a/public/pages/workflows/new_workflow/utils.ts b/public/pages/workflows/new_workflow/utils.ts
index b467b3dd..5c5497b7 100644
--- a/public/pages/workflows/new_workflow/utils.ts
+++ b/public/pages/workflows/new_workflow/utils.ts
@@ -18,6 +18,7 @@ import {
ReactFlowEdge,
WorkspaceFlowState,
IProcessorConfig,
+ WORKFLOW_TYPE,
} from '../../../../common';
import { generateId, initComponentData } from '../../../utils';
import { MarkerType } from 'reactflow';
@@ -33,21 +34,12 @@ export function enrichPresetWorkflowWithUiMetadata(
presetWorkflow: Partial
): WorkflowTemplate {
let uiMetadata = {} as UIState;
- // TODO: for now we are defaulting to empty for all presets. As the form values become finalized,
- // provide preset values for the different preset use cases.
- switch (presetWorkflow.use_case) {
- case USE_CASE.SEMANTIC_SEARCH: {
+ switch (presetWorkflow.ui_metadata?.type || WORKFLOW_TYPE.CUSTOM) {
+ case WORKFLOW_TYPE.SEMANTIC_SEARCH: {
uiMetadata = fetchSemanticSearchMetadata();
break;
}
- case USE_CASE.NEURAL_SPARSE_SEARCH: {
- uiMetadata = fetchEmptyMetadata();
- break;
- }
- case USE_CASE.HYBRID_SEARCH: {
- uiMetadata = fetchEmptyMetadata();
- break;
- }
+ // TODO: add more presets
default: {
uiMetadata = fetchEmptyMetadata();
break;
@@ -65,6 +57,7 @@ export function enrichPresetWorkflowWithUiMetadata(
function fetchEmptyMetadata(): UIState {
return {
+ type: WORKFLOW_TYPE.CUSTOM,
config: {
ingest: {
enabled: true,
@@ -111,6 +104,7 @@ function fetchSemanticSearchMetadata(): UIState {
// We can reuse the base state. Only need to override a few things,
// such as preset ingest processors.
let baseState = fetchEmptyMetadata();
+ baseState.type = WORKFLOW_TYPE.SEMANTIC_SEARCH;
baseState.config.ingest.enrich.processors = [new MLIngestProcessor().toObj()];
return baseState;
}
diff --git a/public/pages/workflows/workflow_list/columns.tsx b/public/pages/workflows/workflow_list/columns.tsx
index eefda067..1637dff1 100644
--- a/public/pages/workflows/workflow_list/columns.tsx
+++ b/public/pages/workflows/workflow_list/columns.tsx
@@ -16,41 +16,28 @@ export const columns = (actions: any[]) => [
{
field: 'name',
name: 'Name',
- width: '20%',
+ width: '33%',
sortable: true,
render: (name: string, workflow: Workflow) => (
{name}
),
},
{
- field: 'state',
- name: 'Status',
- sortable: true,
- },
- {
- field: 'use_case',
+ field: 'ui_metadata.type',
name: 'Type',
- width: '30%',
+ width: '33%',
sortable: true,
},
{
field: 'lastUpdated',
- name: 'Last updated',
+ name: 'Last saved',
+ width: '33%',
sortable: true,
render: (lastUpdated: number) =>
lastUpdated !== undefined
? toFormattedDate(lastUpdated)
: EMPTY_FIELD_STRING,
},
- {
- field: 'lastLaunched',
- name: 'Last launched',
- sortable: true,
- render: (lastLaunched: number) =>
- lastLaunched !== undefined
- ? toFormattedDate(lastLaunched)
- : EMPTY_FIELD_STRING,
- },
{
name: 'Actions',
actions,
diff --git a/public/pages/workflows/workflow_list/workflow_list.tsx b/public/pages/workflows/workflow_list/workflow_list.tsx
index 813f638f..0fd58c0e 100644
--- a/public/pages/workflows/workflow_list/workflow_list.tsx
+++ b/public/pages/workflows/workflow_list/workflow_list.tsx
@@ -14,17 +14,27 @@ import {
EuiFilterSelectItem,
EuiFieldSearch,
EuiLoadingSpinner,
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiFlyoutBody,
+ EuiText,
+ EuiLink,
} from '@elastic/eui';
import { AppState, deleteWorkflow, useAppDispatch } from '../../../store';
-import { Workflow } from '../../../../common';
+import { UIState, WORKFLOW_TYPE, Workflow } from '../../../../common';
import { columns } from './columns';
import {
DeleteWorkflowModal,
MultiSelectFilter,
+ ResourceList,
} from '../../../general_components';
-import { getStateOptions } from '../../../utils';
+import { WORKFLOWS_TAB } from '../workflows';
+import { getCore } from '../../../services';
-interface WorkflowListProps {}
+interface WorkflowListProps {
+ setSelectedTabId: (tabId: WORKFLOWS_TAB) => void;
+}
const sorting = {
sort: {
@@ -33,6 +43,24 @@ const sorting = {
},
};
+const filterOptions = [
+ // @ts-ignore
+ {
+ name: WORKFLOW_TYPE.SEMANTIC_SEARCH,
+ checked: 'on',
+ } as EuiFilterSelectItem,
+ // @ts-ignore
+ {
+ name: WORKFLOW_TYPE.CUSTOM,
+ checked: 'on',
+ } as EuiFilterSelectItem,
+ // @ts-ignore
+ {
+ name: WORKFLOW_TYPE.UNKNOWN,
+ checked: 'on',
+ } as EuiFilterSelectItem,
+];
+
/**
* The searchable list of created workflows.
*/
@@ -42,16 +70,23 @@ export function WorkflowList(props: WorkflowListProps) {
(state: AppState) => state.workflows
);
- // delete workflow state
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
- const [workflowToDelete, setWorkflowToDelete] = useState<
+ // actions state
+ const [selectedWorkflow, setSelectedWorkflow] = useState<
Workflow | undefined
>(undefined);
+
+ // delete workflow state
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
function clearDeleteState() {
- setWorkflowToDelete(undefined);
+ setSelectedWorkflow(undefined);
setIsDeleteModalOpen(false);
}
+ // view workflow resources state
+ const [isResourcesFlyoutOpen, setIsResourcesFlyoutOpen] = useState(
+ false
+ );
+
// search bar state
const [searchQuery, setSearchQuery] = useState('');
const debounceSearchQuery = debounce((query: string) => {
@@ -59,8 +94,8 @@ export function WorkflowList(props: WorkflowListProps) {
}, 200);
// filters state
- const [selectedStates, setSelectedStates] = useState(
- getStateOptions()
+ const [selectedTypes, setSelectedTypes] = useState(
+ filterOptions
);
const [filteredWorkflows, setFilteredWorkflows] = useState([]);
@@ -69,41 +104,94 @@ export function WorkflowList(props: WorkflowListProps) {
setFilteredWorkflows(
fetchFilteredWorkflows(
Object.values(workflows),
- selectedStates,
+ selectedTypes,
searchQuery
)
);
- }, [selectedStates, searchQuery, workflows]);
+ }, [selectedTypes, searchQuery, workflows]);
const tableActions = [
{
name: 'Delete',
- description: 'Delete this workflow',
+ description: 'Delete',
type: 'icon',
icon: 'trash',
color: 'danger',
onClick: (item: Workflow) => {
- setWorkflowToDelete(item);
+ setSelectedWorkflow(item);
setIsDeleteModalOpen(true);
},
},
+ {
+ name: 'View resources',
+ description: 'View related resources',
+ type: 'icon',
+ icon: 'link',
+ color: 'primary',
+ onClick: (item: Workflow) => {
+ setSelectedWorkflow(item);
+ setIsResourcesFlyoutOpen(true);
+ },
+ },
];
return (
<>
- {isDeleteModalOpen && workflowToDelete?.id !== undefined && (
+ {isDeleteModalOpen && selectedWorkflow?.id !== undefined && (
{
clearDeleteState();
}}
- onConfirm={() => {
- dispatch(deleteWorkflow(workflowToDelete.id as string));
+ onConfirm={async () => {
clearDeleteState();
+ await dispatch(deleteWorkflow(selectedWorkflow.id as string))
+ .unwrap()
+ .then((result) => {
+ getCore().notifications.toasts.addSuccess(
+ `Successfully deleted ${selectedWorkflow.name}`
+ );
+ })
+ .catch((err: any) => {
+ getCore().notifications.toasts.addSuccess(
+ `Failed to delete ${selectedWorkflow.name}`
+ );
+ console.error(
+ `Failed to delete ${selectedWorkflow.name}: ${err}`
+ );
+ });
}}
/>
)}
+ {isResourcesFlyoutOpen && selectedWorkflow && (
+ setIsResourcesFlyoutOpen(false)}
+ >
+
+
+ {`Active resources with ${selectedWorkflow.name}`}
+
+
+
+
+
+
+ )}
+
+
+ {`Manage existing workflows or`}
+
+
+ props.setSelectedTabId(WORKFLOWS_TAB.CREATE)}
+ >
+ create a new workflow
+
+
+
+
@@ -114,9 +202,9 @@ export function WorkflowList(props: WorkflowListProps) {
/>
@@ -140,13 +228,21 @@ export function WorkflowList(props: WorkflowListProps) {
// Collect the final workflow list after applying all filters
function fetchFilteredWorkflows(
allWorkflows: Workflow[],
- stateFilters: EuiFilterSelectItem[],
+ typeFilters: EuiFilterSelectItem[],
searchQuery: string
): Workflow[] {
+ // If missing/invalid ui metadata, add defaults
+ const allWorkflowsWithDefaults = allWorkflows.map((workflow) => ({
+ ...workflow,
+ ui_metadata: {
+ ...workflow.ui_metadata,
+ type: workflow.ui_metadata?.type || WORKFLOW_TYPE.UNKNOWN,
+ } as UIState,
+ }));
// @ts-ignore
- const stateFilterStrings = stateFilters.map((filter) => filter.name);
- const filteredWorkflows = allWorkflows.filter((workflow) =>
- stateFilterStrings.includes(workflow.state)
+ const typeFilterStrings = typeFilters.map((filter) => filter.name);
+ const filteredWorkflows = allWorkflowsWithDefaults.filter((workflow) =>
+ typeFilterStrings.includes(workflow.ui_metadata?.type)
);
return searchQuery.length === 0
? filteredWorkflows
diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx
index d4c27e1e..fd2ba6a9 100644
--- a/public/pages/workflows/workflows.tsx
+++ b/public/pages/workflows/workflows.tsx
@@ -27,7 +27,7 @@ export interface WorkflowsRouterProps {}
interface WorkflowsProps extends RouteComponentProps {}
-enum WORKFLOWS_TAB {
+export enum WORKFLOWS_TAB {
MANAGE = 'manage',
CREATE = 'create',
}
@@ -134,7 +134,9 @@ export function Workflows(props: WorkflowsProps) {
- {selectedTabId === WORKFLOWS_TAB.MANAGE && }
+ {selectedTabId === WORKFLOWS_TAB.MANAGE && (
+
+ )}
{selectedTabId === WORKFLOWS_TAB.CREATE && }
{selectedTabId === WORKFLOWS_TAB.MANAGE &&
Object.keys(workflows || {}).length === 0 &&
diff --git a/public/utils/utils.ts b/public/utils/utils.ts
index 35aabf6c..b84d0230 100644
--- a/public/utils/utils.ts
+++ b/public/utils/utils.ts
@@ -3,8 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { EuiFilterSelectItem } from '@elastic/eui';
-import { WORKFLOW_STATE, WORKFLOW_STEP_TYPE, Workflow } from '../../common';
+import { WORKFLOW_STEP_TYPE, Workflow } from '../../common';
// Append 16 random characters
export function generateId(prefix: string): string {
@@ -15,31 +14,6 @@ export function generateId(prefix: string): string {
return `${prefix}_${uniqueChar()}${uniqueChar()}${uniqueChar()}${uniqueChar()}`;
}
-export function getStateOptions(): EuiFilterSelectItem[] {
- return [
- // @ts-ignore
- {
- name: WORKFLOW_STATE.NOT_STARTED,
- checked: 'on',
- } as EuiFilterSelectItem,
- // @ts-ignore
- {
- name: WORKFLOW_STATE.PROVISIONING,
- checked: 'on',
- } as EuiFilterSelectItem,
- // @ts-ignore
- {
- name: WORKFLOW_STATE.FAILED,
- checked: 'on',
- } as EuiFilterSelectItem,
- // @ts-ignore
- {
- name: WORKFLOW_STATE.COMPLETED,
- checked: 'on',
- } as EuiFilterSelectItem,
- ];
-}
-
export function hasProvisionedIngestResources(
workflow: Workflow | undefined
): boolean {
diff --git a/server/resources/templates/semantic_search.json b/server/resources/templates/semantic_search.json
index 6436c279..788a603b 100644
--- a/server/resources/templates/semantic_search.json
+++ b/server/resources/templates/semantic_search.json
@@ -1,12 +1,14 @@
{
"name": "Semantic Search",
"description": "A basic workflow containing the ingest pipeline and index configurations for performing semantic search",
- "use_case": "SEMANTIC_SEARCH",
"version": {
"template": "1.0.0",
"compatibility": [
"2.13.0",
"3.0.0"
]
+ },
+ "ui_metadata": {
+ "type": "Semantic search"
}
}
\ No newline at end of file