diff --git a/common/interfaces.ts b/common/interfaces.ts index 4025adbc..d9c6bc43 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -119,6 +119,8 @@ export type Workflow = WorkflowTemplate & { lastLaunched?: number; // won't exist until launched/provisioned in backend state?: WORKFLOW_STATE; + // won't exist until launched/provisioned in backend + resourcesCreated?: WorkflowResource[]; }; export enum USE_CASE { @@ -152,6 +154,20 @@ export enum WORKFLOW_STATE { COMPLETED = 'Completed', } +export type WorkflowResource = { + id: string; + type: WORKFLOW_RESOURCE_TYPE; +}; + +// Based off of https://github.com/opensearch-project/flow-framework/blob/main/src/main/java/org/opensearch/flowframework/common/WorkflowResources.java +export enum WORKFLOW_RESOURCE_TYPE { + PIPELINE_ID = 'Ingest pipeline', + INDEX_NAME = 'Index', + MODEL_ID = 'Model', + MODEL_GROUP_ID = 'Model group', + CONNECTOR_ID = 'Connector', +} + export type WorkflowDict = { [workflowId: string]: Workflow; }; diff --git a/public/app.tsx b/public/app.tsx index a95902b3..2e14044d 100644 --- a/public/app.tsx +++ b/public/app.tsx @@ -8,7 +8,6 @@ import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { EuiPageSideBar, EuiSideNav, EuiPageTemplate } from '@elastic/eui'; import { Navigation, APP_PATH } from './utils'; import { - Overview, Workflows, WorkflowDetail, WorkflowDetailRouterProps, @@ -27,15 +26,9 @@ export const FlowFrameworkDashboardsApp = (props: Props) => { name: Navigation.FlowFramework, id: 0, items: [ - { - name: Navigation.Overview, - id: 1, - href: `#${APP_PATH.OVERVIEW}`, - isSelected: props.location.pathname === APP_PATH.OVERVIEW, - }, { name: Navigation.Workflows, - id: 2, + id: 1, href: `#${APP_PATH.WORKFLOWS}`, isSelected: props.location.pathname === APP_PATH.WORKFLOWS, }, @@ -69,10 +62,12 @@ export const FlowFrameworkDashboardsApp = (props: Props) => { )} /> - {/* Defaulting to Overview page */} + {/* Defaulting to Workflows page */} } + render={(routeProps: RouteComponentProps) => ( + + )} /> diff --git a/public/pages/index.ts b/public/pages/index.ts index ad722b21..1db9a64a 100644 --- a/public/pages/index.ts +++ b/public/pages/index.ts @@ -4,5 +4,4 @@ */ export * from './workflows'; -export * from './overview'; export * from './workflow_detail'; diff --git a/public/pages/overview/overview.tsx b/public/pages/overview/overview.tsx deleted file mode 100644 index aa4c2ba0..00000000 --- a/public/pages/overview/overview.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from 'react'; -import { - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageContent, - EuiText, -} from '@elastic/eui'; -import { BREADCRUMBS } from '../../utils'; -import { getCore } from '../../services'; - -/** - * The overview page. This contains a detailed description on what - * this plugin offers, and links to different resources (blogs, demos, - * documentation, etc.) - * - * This may be hidden for the initial release until we have sufficient content - * such that this page adds enough utility & user value. - */ -export function Overview() { - useEffect(() => { - getCore().chrome.setBreadcrumbs([ - BREADCRUMBS.FLOW_FRAMEWORK, - BREADCRUMBS.OVERVIEW, - ]); - }); - - return ( - - - - - - TODO: Put overview details here... - - - - ); -} diff --git a/public/pages/workflow_detail/prototype/index.ts b/public/pages/workflow_detail/prototype/index.ts deleted file mode 100644 index 6b2a3ec5..00000000 --- a/public/pages/workflow_detail/prototype/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export { Prototype } from './prototype'; diff --git a/public/pages/workflow_detail/resources/columns.tsx b/public/pages/workflow_detail/resources/columns.tsx new file mode 100644 index 00000000..7e13b49c --- /dev/null +++ b/public/pages/workflow_detail/resources/columns.tsx @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const columns = [ + { + field: 'id', + name: 'ID', + sortable: true, + }, + { + field: 'type', + name: 'Type', + sortable: true, + }, +]; diff --git a/public/pages/overview/index.ts b/public/pages/workflow_detail/resources/index.ts similarity index 67% rename from public/pages/overview/index.ts rename to public/pages/workflow_detail/resources/index.ts index 5c460c3d..2132669a 100644 --- a/public/pages/overview/index.ts +++ b/public/pages/workflow_detail/resources/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { Overview } from './overview'; +export { Resources } from './resources'; diff --git a/public/pages/workflow_detail/resources/resource_list.tsx b/public/pages/workflow_detail/resources/resource_list.tsx new file mode 100644 index 00000000..0acb0f9f --- /dev/null +++ b/public/pages/workflow_detail/resources/resource_list.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { + EuiInMemoryTable, + Direction, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { Workflow, WorkflowResource } from '../../../../common'; +import { columns } from './columns'; + +interface ResourceListProps { + workflow?: Workflow; +} + +/** + * The searchable list of resources for a particular workflow. + */ +export function ResourceList(props: ResourceListProps) { + const [allResources, setAllResources] = useState([]); + + // Hook to initialize all resources + useEffect(() => { + if (props.workflow?.resourcesCreated) { + setAllResources(props.workflow.resourcesCreated); + } + }, [props.workflow?.resourcesCreated]); + + const sorting = { + sort: { + field: 'id', + direction: 'asc' as Direction, + }, + }; + + return ( + + + + items={allResources} + rowHeader="id" + columns={columns} + sorting={sorting} + pagination={true} + message={'No existing resources found'} + /> + + + ); +} diff --git a/public/pages/workflow_detail/prototype/prototype.tsx b/public/pages/workflow_detail/resources/resources.tsx similarity index 50% rename from public/pages/workflow_detail/prototype/prototype.tsx rename to public/pages/workflow_detail/resources/resources.tsx index ede48acd..cee54acd 100644 --- a/public/pages/workflow_detail/prototype/prototype.tsx +++ b/public/pages/workflow_detail/resources/resources.tsx @@ -5,33 +5,34 @@ import React from 'react'; import { + EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer, - EuiText, EuiTitle, } from '@elastic/eui'; import { Workflow } from '../../../../common'; +import { ResourceList } from './resource_list'; -interface PrototypeProps { +interface ResourcesProps { workflow?: Workflow; } /** - * The prototype page. Dedicated for testing out a launched workflow. - * Will have default simple interfaces for common application types, such as - * conversational chatbots. + * A simple resources page to browse created resources for a given Workflow. */ -export function Prototype(props: PrototypeProps) { +export function Resources(props: ResourcesProps) { return ( -

Prototype

+

Resources

- - TODO: add prototype page - + + + + +
); } diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index ff32777c..8c397b22 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -19,13 +19,12 @@ import { useAppDispatch, } from '../../store'; import { ResizableWorkspace } from './workspace'; -import { Launches } from './launches'; -import { Prototype } from './prototype'; import { DEFAULT_NEW_WORKFLOW_NAME, FETCH_ALL_QUERY_BODY, NEW_WORKFLOW_ID_URL, } from '../../../common'; +import { Resources } from './resources'; // styling import './workflow-detail-styles.scss'; @@ -39,8 +38,10 @@ interface WorkflowDetailProps enum WORKFLOW_DETAILS_TAB { EDITOR = 'editor', - LAUNCHES = 'launches', - PROTOTYPE = 'prototype', + // TODO: temporarily adding a resources tab until UX is finalized. + // This gives clarity into what has been done on the cluster on behalf + // of the frontend provisioning workflows. + RESOURCES = 'resources', } const ACTIVE_TAB_PARAM = 'tab'; @@ -133,21 +134,12 @@ export function WorkflowDetail(props: WorkflowDetailProps) { }, }, { - id: WORKFLOW_DETAILS_TAB.LAUNCHES, - label: 'Launches', - isSelected: selectedTabId === WORKFLOW_DETAILS_TAB.LAUNCHES, + id: WORKFLOW_DETAILS_TAB.RESOURCES, + label: 'Resources', + isSelected: selectedTabId === WORKFLOW_DETAILS_TAB.RESOURCES, onClick: () => { - setSelectedTabId(WORKFLOW_DETAILS_TAB.LAUNCHES); - replaceActiveTab(WORKFLOW_DETAILS_TAB.LAUNCHES, props); - }, - }, - { - id: WORKFLOW_DETAILS_TAB.PROTOTYPE, - label: 'Prototype', - isSelected: selectedTabId === WORKFLOW_DETAILS_TAB.PROTOTYPE, - onClick: () => { - setSelectedTabId(WORKFLOW_DETAILS_TAB.PROTOTYPE); - replaceActiveTab(WORKFLOW_DETAILS_TAB.PROTOTYPE, props); + setSelectedTabId(WORKFLOW_DETAILS_TAB.RESOURCES); + replaceActiveTab(WORKFLOW_DETAILS_TAB.RESOURCES, props); }, }, ]; @@ -169,9 +161,8 @@ export function WorkflowDetail(props: WorkflowDetailProps) { /> )} - {selectedTabId === WORKFLOW_DETAILS_TAB.LAUNCHES && } - {selectedTabId === WORKFLOW_DETAILS_TAB.PROTOTYPE && ( - + {selectedTabId === WORKFLOW_DETAILS_TAB.RESOURCES && ( + )} diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts index 31640fd3..2a04cb81 100644 --- a/public/store/reducers/workflows_reducer.ts +++ b/public/store/reducers/workflows_reducer.ts @@ -235,12 +235,13 @@ const workflowsSlice = createSlice({ state.errorMessage = ''; }) .addCase(getWorkflowState.fulfilled, (state, action) => { - const { workflowId, workflowState } = action.payload; + const { workflowId, workflowState, resourcesCreated } = action.payload; state.workflows = { ...state.workflows, [workflowId]: { ...state.workflows[workflowId], state: workflowState, + resourcesCreated, }, }; state.loading = false; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 1c0c4617..0f3b75f6 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -5,20 +5,17 @@ export enum Navigation { FlowFramework = 'Flow Framework', - Overview = 'Overview', Workflows = 'Workflows', } export enum APP_PATH { HOME = '/', - OVERVIEW = '/overview', WORKFLOWS = '/workflows', WORKFLOW_DETAIL = '/workflows/:workflowId', } export const BREADCRUMBS = Object.freeze({ FLOW_FRAMEWORK: { text: 'Flow Framework', href: '#/' }, - OVERVIEW: { text: 'Overview', href: `#${APP_PATH.OVERVIEW}` }, WORKFLOWS: { text: 'Workflows', href: `#${APP_PATH.WORKFLOWS}` }, }); diff --git a/server/routes/flow_framework_routes_service.ts b/server/routes/flow_framework_routes_service.ts index 0c7a3af0..7c60e631 100644 --- a/server/routes/flow_framework_routes_service.ts +++ b/server/routes/flow_framework_routes_service.ts @@ -26,10 +26,12 @@ import { WORKFLOW_STATE, Workflow, WorkflowDict, + WorkflowResource, WorkflowTemplate, } from '../../common'; import { generateCustomError, + getResourcesCreatedFromResponse, getWorkflowStateFromResponse, getWorkflowsFromResponses, isIgnorableError, @@ -174,10 +176,14 @@ export class FlowFrameworkRoutesService { const state = getWorkflowStateFromResponse( stateResponse.state as typeof WORKFLOW_STATE ); + const resourcesCreated = getResourcesCreatedFromResponse( + stateResponse.resources_created as WorkflowResource[] + ); const workflowWithState = { ...workflow, state, - }; + resourcesCreated, + } as Workflow; return res.ok({ body: { workflow: workflowWithState } }); } catch (err: any) { return generateCustomError(res, err); @@ -226,12 +232,21 @@ export class FlowFrameworkRoutesService { try { const response = await this.client .asScoped(req) - .callAsCurrentUser('flowFramework.getWorkflowState', { workflow_id }); + .callAsCurrentUser('flowFramework.getWorkflowState', { + workflow_id, + }); const state = getWorkflowStateFromResponse( - response.state as typeof WORKFLOW_STATE + response.state as typeof WORKFLOW_STATE | undefined + ); + const resourcesCreated = getResourcesCreatedFromResponse( + response.resources_created as WorkflowResource[] | undefined ); return res.ok({ - body: { workflowId: workflow_id, workflowState: state }, + body: { + workflowId: workflow_id, + workflowState: state, + resourcesCreated, + }, }); } catch (err: any) { return generateCustomError(res, err); diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts index 72385b86..b4d39991 100644 --- a/server/routes/helpers.ts +++ b/server/routes/helpers.ts @@ -8,9 +8,11 @@ import { INDEX_NOT_FOUND_EXCEPTION, Model, ModelDict, + WORKFLOW_RESOURCE_TYPE, WORKFLOW_STATE, Workflow, WorkflowDict, + WorkflowResource, } from '../../common'; // OSD does not provide an interface for this response, but this is following the suggested @@ -64,12 +66,17 @@ export function getWorkflowsFromResponses( const workflowStateHit = workflowStateHits.find( (workflowStateHit) => workflowStateHit._id === workflowHit._id ); - const workflowState = (workflowStateHit?._source?.state || - DEFAULT_NEW_WORKFLOW_STATE_TYPE) as typeof WORKFLOW_STATE; + const workflowState = getWorkflowStateFromResponse( + workflowStateHit?._source?.state + ); + const workflowResourcesCreated = getResourcesCreatedFromResponse( + workflowStateHit?._source?.resources_created + ); workflowDict[workflowHit._id] = { ...workflowDict[workflowHit._id], // @ts-ignore - state: WORKFLOW_STATE[workflowState], + state: workflowState, + resourcesCreated: workflowResourcesCreated, }; }); return workflowDict; @@ -89,6 +96,7 @@ export function getModelsFromResponses(modelHits: any[]): ModelDict { return modelDict; } +// Convert the workflow state into a readable/presentable state on frontend export function getWorkflowStateFromResponse( state: typeof WORKFLOW_STATE | undefined ): WORKFLOW_STATE { @@ -96,3 +104,24 @@ export function getWorkflowStateFromResponse( // @ts-ignore return WORKFLOW_STATE[finalState]; } + +// Convert the workflow resources into a readable/presentable state on frontend +export function getResourcesCreatedFromResponse( + resourcesCreated: any[] | undefined +): WorkflowResource[] { + const finalResources = [] as WorkflowResource[]; + if (resourcesCreated) { + resourcesCreated.forEach((backendResource) => { + finalResources.push({ + id: backendResource.resource_id, + type: + // @ts-ignore + WORKFLOW_RESOURCE_TYPE[ + // the backend persists the types in lowercase. e.g., "pipeline_id" + (backendResource.resource_type as string).toUpperCase() + ], + } as WorkflowResource); + }); + } + return finalResources; +}