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

Persist created resources; clean up unused tabs #135

Merged
merged 3 commits into from
Apr 17, 2024
Merged
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
16 changes: 16 additions & 0 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
@@ -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;
};
15 changes: 5 additions & 10 deletions public/app.tsx
Original file line number Diff line number Diff line change
@@ -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) => {
<Workflows {...routeProps} />
)}
/>
{/* Defaulting to Overview page */}
{/* Defaulting to Workflows page */}
<Route
path={`${APP_PATH.HOME}`}
render={(routeProps: RouteComponentProps) => <Overview />}
render={(routeProps: RouteComponentProps<WorkflowsRouterProps>) => (
<Workflows {...routeProps} />
)}
/>
</Switch>
</EuiPageTemplate>
1 change: 0 additions & 1 deletion public/pages/index.ts
Original file line number Diff line number Diff line change
@@ -4,5 +4,4 @@
*/

export * from './workflows';
export * from './overview';
export * from './workflow_detail';
44 changes: 0 additions & 44 deletions public/pages/overview/overview.tsx

This file was deleted.

6 changes: 0 additions & 6 deletions public/pages/workflow_detail/prototype/index.ts

This file was deleted.

17 changes: 17 additions & 0 deletions public/pages/workflow_detail/resources/columns.tsx
Original file line number Diff line number Diff line change
@@ -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,
},
];
Original file line number Diff line number Diff line change
@@ -3,4 +3,4 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { Overview } from './overview';
export { Resources } from './resources';
54 changes: 54 additions & 0 deletions public/pages/workflow_detail/resources/resource_list.tsx
Original file line number Diff line number Diff line change
@@ -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<WorkflowResource[]>([]);

// 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 (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiInMemoryTable<WorkflowResource>
items={allResources}
rowHeader="id"
columns={columns}
sorting={sorting}
pagination={true}
message={'No existing resources found'}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<EuiPageContent>
<EuiTitle>
<h2>Prototype</h2>
<h2>Resources</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexItem>
<EuiText>TODO: add prototype page</EuiText>
</EuiFlexItem>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<ResourceList workflow={props.workflow} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContent>
);
}
33 changes: 12 additions & 21 deletions public/pages/workflow_detail/workflow_detail.tsx
Original file line number Diff line number Diff line change
@@ -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) {
/>
</ReactFlowProvider>
)}
{selectedTabId === WORKFLOW_DETAILS_TAB.LAUNCHES && <Launches />}
{selectedTabId === WORKFLOW_DETAILS_TAB.PROTOTYPE && (
<Prototype workflow={workflow} />
{selectedTabId === WORKFLOW_DETAILS_TAB.RESOURCES && (
<Resources workflow={workflow} />
)}
</EuiPageBody>
</EuiPage>
3 changes: 2 additions & 1 deletion public/store/reducers/workflows_reducer.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 0 additions & 3 deletions public/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -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}` },
});

23 changes: 19 additions & 4 deletions server/routes/flow_framework_routes_service.ts
Original file line number Diff line number Diff line change
@@ -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);
35 changes: 32 additions & 3 deletions server/routes/helpers.ts
Original file line number Diff line number Diff line change
@@ -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,10 +96,32 @@ 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 {
const finalState = state || DEFAULT_NEW_WORKFLOW_STATE_TYPE;
// @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;
}