Skip to content

Commit

Permalink
Support importing local workflow templates (#208)
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
ohltyler committed Jul 10, 2024
1 parent ad03380 commit ae53f00
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 89 deletions.
7 changes: 4 additions & 3 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,13 @@ export type TemplateFlows = {

// A stateless template of a workflow
export type WorkflowTemplate = {
// Name is the only required field: see https://opensearch.org/docs/latest/automating-configurations/api/create-workflow/#request-fields
name: string;
description: string;
description?: string;
// TODO: finalize on version type when that is implemented
// https://github.com/opensearch-project/flow-framework/issues/526
version: any;
workflows: TemplateFlows;
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;
Expand Down
52 changes: 28 additions & 24 deletions public/pages/workflow_detail/resizable_workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
WorkflowSchema,
} from '../../../common';
import {
isValidUiWorkflow,
reduceToTemplate,
uiConfigToFormik,
uiConfigToSchema,
Expand Down Expand Up @@ -110,8 +111,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {

// Hook to check if the workflow is valid or not
useEffect(() => {
const missingUiFlow =
props.workflow && !props.workflow?.ui_metadata?.config;
const missingUiFlow = props.workflow && !isValidUiWorkflow(props.workflow);
if (missingUiFlow) {
setIsValidWorkflow(false);
} else {
Expand Down Expand Up @@ -286,27 +286,31 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
)}
</Formik>
) : (
<>
<EuiEmptyPrompt
iconType={'cross'}
title={<h2>Unable to view workflow details</h2>}
titleSize="s"
body={
<>
<EuiText>
Only valid workflows created from this OpenSearch Dashboards
application are editable and viewable.
</EuiText>
</>
}
/>
<EuiCodeBlock language="json" fontSize="m" isCopyable={false}>
{JSON.stringify(
reduceToTemplate(props.workflow as Workflow),
undefined,
2
)}
</EuiCodeBlock>
</>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={3}>
<EuiEmptyPrompt
iconType={'cross'}
title={<h2>Unable to view workflow details</h2>}
titleSize="s"
body={
<>
<EuiText>
Only valid workflows created from this OpenSearch Dashboards
application are editable and viewable.
</EuiText>
</>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiCodeBlock language="json" fontSize="m" isCopyable={false}>
{JSON.stringify(
reduceToTemplate(props.workflow as Workflow),
undefined,
2
)}
</EuiCodeBlock>
</EuiFlexItem>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { JsonField } from '../input_fields';
import { IConfigField, WorkspaceFormValues } from '../../../../../common';
import { WorkspaceFormValues } from '../../../../../common';

interface SourceDataProps {
setIngestDocs: (docs: string) => void;
Expand Down
163 changes: 163 additions & 0 deletions public/pages/workflows/import_workflow/import_workflow_modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect, useState } from 'react';
import {
EuiSpacer,
EuiFlexGroup,
EuiButtonEmpty,
EuiButton,
EuiText,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiFilePicker,
EuiCallOut,
EuiFlexItem,
} from '@elastic/eui';
import {
getObjFromJsonOrYamlString,
isValidUiWorkflow,
isValidWorkflow,
} from '../../../utils';
import { getCore } from '../../../services';
import {
createWorkflow,
searchWorkflows,
useAppDispatch,
} from '../../../store';
import { FETCH_ALL_QUERY_BODY, Workflow } from '../../../../common';
import { WORKFLOWS_TAB } from '../workflows';

interface ImportWorkflowModalProps {
isImportModalOpen: boolean;
setIsImportModalOpen(isOpen: boolean): void;
setSelectedTabId(tabId: WORKFLOWS_TAB): void;
}

/**
* The import workflow modal. Allows uploading local JSON or YAML files to be uploaded, parsed, and
* created as new workflows. Automatic validation is handled to:
* 1/ allow upload (valid workflow with UI data),
* 2/ warn and allow upload (valid workflow but missing/no UI data), and
* 3/ prevent upload (invalid workflow).
*/
export function ImportWorkflowModal(props: ImportWorkflowModalProps) {
const dispatch = useAppDispatch();

// file contents & file obj state
const [fileContents, setFileContents] = useState<string | undefined>(
undefined
);
const [fileObj, setFileObj] = useState<object | undefined>(undefined);
useEffect(() => {
setFileObj(getObjFromJsonOrYamlString(fileContents));
}, [fileContents]);

// file reader to read the file and set the fileContents var
const fileReader = new FileReader();
fileReader.onload = (e) => {
if (e.target) {
setFileContents(e.target.result as string);
}
};

function onModalClose(): void {
props.setIsImportModalOpen(false);
setFileContents(undefined);
setFileObj(undefined);
}

return (
<EuiModal onClose={() => onModalClose()} style={{ width: '40vw' }}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<p>{`Import a workflow (JSON/YAML)`}</p>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiFlexGroup
direction="column"
justifyContent="center"
alignItems="center"
>
{fileContents !== undefined && !isValidWorkflow(fileObj) && (
<>
<EuiFlexItem>
<EuiCallOut
title="The uploaded file is not a valid workflow, remove the file and upload a compatible workflow in JSON or YAML format."
iconType={'alert'}
color="danger"
/>
</EuiFlexItem>
<EuiSpacer size="m" />
</>
)}
{isValidWorkflow(fileObj) && !isValidUiWorkflow(fileObj) && (
<>
<EuiFlexItem>
<EuiCallOut
title="The uploaded file may not be compatible with Search Studio. You may not be able to edit or run this file with Search Studio."
iconType={'help'}
color="warning"
/>
</EuiFlexItem>
<EuiSpacer size="m" />
</>
)}
<EuiFlexItem grow={false}>
<EuiFilePicker
multiple={false}
initialPromptText="Select or drag and drop a file"
onChange={(files) => {
if (files && files.length > 0) {
fileReader.readAsText(files[0]);
}
}}
display="large"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color="subdued">
Must be in JSON or YAML format.
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={() => onModalClose()}>Cancel</EuiButtonEmpty>
<EuiButton
disabled={!isValidWorkflow(fileObj)}
onClick={() => {
dispatch(createWorkflow(fileObj as Workflow))
.unwrap()
.then((result) => {
const { workflow } = result;
dispatch(searchWorkflows(FETCH_ALL_QUERY_BODY));
props.setSelectedTabId(WORKFLOWS_TAB.MANAGE);
getCore().notifications.toasts.addSuccess(
`Successfully imported ${workflow.name}`
);
})
.catch((error: any) => {
getCore().notifications.toasts.addDanger(error);
})
.finally(() => {
onModalClose();
});
}}
fill={true}
color="primary"
>
{isValidWorkflow(fileObj) && !isValidUiWorkflow(fileObj)
? 'Import anyway'
: 'Import'}
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
}
6 changes: 6 additions & 0 deletions public/pages/workflows/import_workflow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { ImportWorkflowModal } from './import_workflow_modal';
11 changes: 1 addition & 10 deletions public/pages/workflows/workflow_list/workflow_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
EuiTitle,
EuiFlyoutBody,
EuiText,
EuiLink,
} from '@elastic/eui';
import { AppState, deleteWorkflow, useAppDispatch } from '../../../store';
import { UIState, WORKFLOW_TYPE, Workflow } from '../../../../common';
Expand Down Expand Up @@ -181,15 +180,7 @@ export function WorkflowList(props: WorkflowListProps) {
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row" style={{ marginLeft: '0px' }}>
<EuiText color="subdued">{`Manage existing workflows or`}</EuiText>
&nbsp;
<EuiText>
<EuiLink
onClick={() => props.setSelectedTabId(WORKFLOWS_TAB.CREATE)}
>
create a new workflow
</EuiLink>
</EuiText>
<EuiText color="subdued">{`Manage existing workflows`}</EuiText>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
Expand Down
Loading

0 comments on commit ae53f00

Please sign in to comment.