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