Skip to content

Commit 74c8559

Browse files
authored
UX fit-n-finish updates VII (opensearch-project#573)
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 735f760 commit 74c8559

File tree

12 files changed

+101
-27
lines changed

12 files changed

+101
-27
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The OpenSearch Flow plugin on OpenSearch Dashboards (OSD) gives users the ability to iteratively build out search and ingest pipelines, initially focusing on ease-of-use for AI/ML-enhanced use cases via [ML inference processors](https://opensearch.org/docs/latest/ingest-pipelines/processors/ml-inference/). Behind the scenes, the plugin uses the [Flow Framework OpenSearch plugin](https://opensearch.org/docs/latest/automating-configurations/index/) for resource management for each use case / workflow a user creates. For example, most use cases involve configuring and creating indices, ingest pipelines, and search pipelines. All of these resources are created, updated, deleted, and maintained by the Flow Framework plugin. When users are satisfied with a use case they have built out, they can export the produced [Workflow Template](https://opensearch.org/docs/latest/automating-configurations/workflow-templates/) to re-create resources for their use cases across different clusters / data sources.
44

5-
For tutorials on how to leverage this plugin to build out different AI/ML use cases, see [here](./documentation/.tutorial-11-18-2024.md).
5+
For tutorials on how to leverage this plugin to build out different AI/ML use cases, see [here](./documentation/.tutorial.md).
66

77
For how to set up this plugin in a development environment, see [DEVELOPER_GUIDE](./DEVELOPER_GUIDE.md).
88

common/constants.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ export const FETCH_ALL_QUERY = {
292292
},
293293
size: DEFAULT_FETCH_SIZE,
294294
};
295+
export const FETCH_ALL_QUERY_LARGE = {
296+
query: {
297+
match_all: {},
298+
},
299+
size: 1000,
300+
};
295301
export const TERM_QUERY_TEXT = {
296302
query: {
297303
term: {
@@ -600,6 +606,7 @@ export const MAX_TEMPLATE_STRING_LENGTH = 10000;
600606
export const MAX_BYTES = 1048576; // OSD REST request payload size limit
601607
export const MAX_WORKFLOW_NAME_TO_DISPLAY = 40;
602608
export const WORKFLOW_NAME_REGEXP = RegExp('^[a-zA-Z0-9_-]*$');
609+
export const INDEX_NAME_REGEXP = WORKFLOW_NAME_REGEXP;
603610
export const EMPTY_MAP_ENTRY = { key: '', value: '' } as MapEntry;
604611
export const EMPTY_INPUT_MAP_ENTRY = {
605612
key: '',
@@ -643,7 +650,7 @@ export const INSPECTOR_TABS = [
643650
},
644651
{
645652
id: INSPECTOR_TAB_ID.QUERY,
646-
name: 'Search response',
653+
name: 'Search tool',
647654
disabled: false,
648655
},
649656
{

public/pages/workflow_detail/components/edit_workflow_metadata_modal.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
EuiSmallButton,
2020
} from '@elastic/eui';
2121
import {
22-
FETCH_ALL_QUERY,
22+
FETCH_ALL_QUERY_LARGE,
2323
MAX_STRING_LENGTH,
2424
Workflow,
2525
WORKFLOW_NAME_REGEXP,
@@ -107,7 +107,7 @@ export function EditWorkflowMetadataModal(
107107
useEffect(() => {
108108
dispatch(
109109
searchWorkflows({
110-
apiBody: FETCH_ALL_QUERY,
110+
apiBody: FETCH_ALL_QUERY_LARGE,
111111
dataSourceId,
112112
})
113113
);

public/pages/workflow_detail/workflow_detail.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe('WorkflowDetail Page with create ingestion option', () => {
9696
expect(getByText('Visual')).toBeInTheDocument();
9797
expect(getByText('JSON')).toBeInTheDocument();
9898
expect(getByRole('tab', { name: 'Ingest response' })).toBeInTheDocument();
99-
expect(getByRole('tab', { name: 'Search response' })).toBeInTheDocument();
99+
expect(getByRole('tab', { name: 'Search tool' })).toBeInTheDocument();
100100
expect(getByRole('tab', { name: 'Errors' })).toBeInTheDocument();
101101
expect(getByRole('tab', { name: 'Resources' })).toBeInTheDocument();
102102

public/pages/workflow_detail/workflow_detail.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { ResizableWorkspace } from './resizable_workspace';
3939
import {
4040
CONFIG_STEP,
4141
ERROR_GETTING_WORKFLOW_MSG,
42-
FETCH_ALL_QUERY,
42+
FETCH_ALL_QUERY_LARGE,
4343
MAX_WORKFLOW_NAME_TO_DISPLAY,
4444
NO_TEMPLATES_FOUND_MSG,
4545
OMIT_SYSTEM_INDEX_PATTERN,
@@ -83,8 +83,10 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
8383
// - fetch all indices
8484
useEffect(() => {
8585
dispatch(getWorkflow({ workflowId, dataSourceId }));
86-
dispatch(searchModels({ apiBody: FETCH_ALL_QUERY, dataSourceId }));
87-
dispatch(searchConnectors({ apiBody: FETCH_ALL_QUERY, dataSourceId }));
86+
dispatch(searchModels({ apiBody: FETCH_ALL_QUERY_LARGE, dataSourceId }));
87+
dispatch(
88+
searchConnectors({ apiBody: FETCH_ALL_QUERY_LARGE, dataSourceId })
89+
);
8890
dispatch(catIndices({ pattern: OMIT_SYSTEM_INDEX_PATTERN, dataSourceId }));
8991
}, []);
9092

@@ -95,6 +97,8 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
9597
(state: AppState) => state.workflows
9698
);
9799

100+
const { indices } = useSelector((state: AppState) => state.opensearch);
101+
98102
// selected workflow state
99103
const workflowId = escape(props.match?.params?.workflowId);
100104
const workflow = workflows[workflowId];
@@ -165,7 +169,7 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
165169
useEffect(() => {
166170
if (uiConfig) {
167171
const initFormValues = uiConfigToFormik(uiConfig, ingestDocs);
168-
const initFormSchema = uiConfigToSchema(uiConfig);
172+
const initFormSchema = uiConfigToSchema(uiConfig, indices);
169173
setFormValues(initFormValues);
170174
setFormSchema(initFormSchema);
171175
}

public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ export function IngestData(props: IngestDataProps) {
4747
/>
4848
)}
4949
<EuiFlexItem>
50-
<TextField label="Index name" fieldPath={'ingest.index.name'} />
50+
<TextField
51+
label="Index name"
52+
fieldPath={'ingest.index.name'}
53+
showError={true}
54+
/>
5155
</EuiFlexItem>
5256
<EuiFlexItem>
5357
<AdvancedSettings setHasInvalidDimensions={setHasInvalidDimensions} />

public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data_modal.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import { JsonField } from '../input_fields';
2626
import {
2727
customStringify,
28-
FETCH_ALL_QUERY,
28+
FETCH_ALL_QUERY_LARGE,
2929
IConfigField,
3030
IndexMappings,
3131
IngestDocsFormValues,
@@ -167,7 +167,7 @@ export function SourceDataModal(props: SourceDataProps) {
167167
searchIndex({
168168
apiBody: {
169169
index: selectedIndex,
170-
body: FETCH_ALL_QUERY,
170+
body: FETCH_ALL_QUERY_LARGE,
171171
searchPipeline: '_none',
172172
},
173173
dataSourceId,

public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,33 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
540540
props.setIngestResponse(customStringify(resp));
541541
props.setIsRunningIngest(false);
542542
setLastIngested(Date.now());
543+
getCore().notifications.toasts.add({
544+
iconType: 'check',
545+
color: 'success',
546+
title: 'Ingest flow has been updated',
547+
// @ts-ignore
548+
text: (
549+
<EuiFlexGroup direction="column">
550+
<EuiFlexItem grow={false}>
551+
<EuiText size="s">
552+
Validate your ingest flow with the search tool
553+
</EuiText>
554+
</EuiFlexItem>
555+
<EuiFlexItem>
556+
<EuiFlexGroup direction="row" justifyContent="flexEnd">
557+
<EuiFlexItem grow={false}>
558+
<EuiSmallButton
559+
fill={false}
560+
onClick={() => props.displaySearchPanel()}
561+
>
562+
Open Search tool
563+
</EuiSmallButton>
564+
</EuiFlexItem>
565+
</EuiFlexGroup>
566+
</EuiFlexItem>
567+
</EuiFlexGroup>
568+
),
569+
});
543570
})
544571
.catch((error: any) => {
545572
props.setIngestResponse('');

public/pages/workflows/import_workflow/import_workflow_modal.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
searchWorkflows,
3131
useAppDispatch,
3232
} from '../../../store';
33-
import { FETCH_ALL_QUERY, Workflow } from '../../../../common';
33+
import { FETCH_ALL_QUERY_LARGE, Workflow } from '../../../../common';
3434
import { WORKFLOWS_TAB } from '../workflows';
3535
import { getDataSourceId } from '../../../utils/utils';
3636

@@ -156,7 +156,7 @@ export function ImportWorkflowModal(props: ImportWorkflowModalProps) {
156156
const { workflow } = result;
157157
dispatch(
158158
searchWorkflows({
159-
apiBody: FETCH_ALL_QUERY,
159+
apiBody: FETCH_ALL_QUERY_LARGE,
160160
dataSourceId,
161161
})
162162
);

public/pages/workflows/new_workflow/new_workflow.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { useSelector } from 'react-redux';
1616
import { UseCase } from './use_case';
1717
import {
18-
FETCH_ALL_QUERY,
18+
FETCH_ALL_QUERY_LARGE,
1919
Workflow,
2020
WorkflowTemplate,
2121
} from '../../../../common';
@@ -132,8 +132,10 @@ export function NewWorkflow(props: NewWorkflowProps) {
132132
useEffect(() => {
133133
dispatch(getWorkflowPresets());
134134
if (isDataSourceReady(dataSourceId)) {
135-
dispatch(searchModels({ apiBody: FETCH_ALL_QUERY, dataSourceId }));
136-
dispatch(searchConnectors({ apiBody: FETCH_ALL_QUERY, dataSourceId }));
135+
dispatch(searchModels({ apiBody: FETCH_ALL_QUERY_LARGE, dataSourceId }));
136+
dispatch(
137+
searchConnectors({ apiBody: FETCH_ALL_QUERY_LARGE, dataSourceId })
138+
);
137139
}
138140
}, [dataSourceId, dataSourceEnabled]);
139141

public/pages/workflows/workflows.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { WorkflowList } from './workflow_list';
2525
import { NewWorkflow } from './new_workflow';
2626
import { AppState, searchWorkflows, useAppDispatch } from '../../store';
2727
import { EmptyListMessage } from './empty_list_message';
28-
import { FETCH_ALL_QUERY, PLUGIN_NAME } from '../../../common';
28+
import { FETCH_ALL_QUERY_LARGE, PLUGIN_NAME } from '../../../common';
2929
import { ImportWorkflowModal } from './import_workflow';
3030
import { MountPoint } from '../../../../../src/core/public';
3131
import { DataSourceSelectableConfig } from '../../../../../src/plugins/data_source_management/public';
@@ -122,7 +122,7 @@ export function Workflows(props: WorkflowsProps) {
122122
if (isDataSourceReady(dataSourceId)) {
123123
dispatch(
124124
searchWorkflows({
125-
apiBody: FETCH_ALL_QUERY,
125+
apiBody: FETCH_ALL_QUERY_LARGE,
126126
dataSourceId,
127127
})
128128
);
@@ -147,7 +147,7 @@ export function Workflows(props: WorkflowsProps) {
147147
if (isDataSourceReady(dataSourceId)) {
148148
dispatch(
149149
searchWorkflows({
150-
apiBody: FETCH_ALL_QUERY,
150+
apiBody: FETCH_ALL_QUERY_LARGE,
151151
dataSourceId,
152152
})
153153
);
@@ -170,7 +170,7 @@ export function Workflows(props: WorkflowsProps) {
170170
if (isDataSourceReady(dataSourceId)) {
171171
dispatch(
172172
searchWorkflows({
173-
apiBody: FETCH_ALL_QUERY,
173+
apiBody: FETCH_ALL_QUERY_LARGE,
174174
dataSourceId,
175175
})
176176
);

public/utils/config_to_schema_utils.ts

+36-6
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,27 @@ import {
2222
MAX_TEMPLATE_STRING_LENGTH,
2323
TRANSFORM_TYPE,
2424
MAX_BYTES,
25+
INDEX_NAME_REGEXP,
26+
Index,
2527
} from '../../common';
2628

2729
/*
2830
**************** Schema / validation utils **********************
2931
*/
3032

31-
export function uiConfigToSchema(config: WorkflowConfig): WorkflowSchema {
33+
export function uiConfigToSchema(
34+
config: WorkflowConfig,
35+
indices: { [key: string]: Index }
36+
) {
3237
const schemaObj = {} as WorkflowSchemaObj;
33-
schemaObj['ingest'] = ingestConfigToSchema(config.ingest);
38+
schemaObj['ingest'] = ingestConfigToSchema(config.ingest, indices);
3439
schemaObj['search'] = searchConfigToSchema(config.search);
3540
return yup.object(schemaObj) as WorkflowSchema;
3641
}
3742

3843
function ingestConfigToSchema(
39-
ingestConfig: IngestConfig | undefined
44+
ingestConfig: IngestConfig | undefined,
45+
indices: { [key: string]: Index }
4046
): ObjectSchema<any> {
4147
const ingestSchemaObj = {} as { [key: string]: Schema };
4248
if (ingestConfig?.enabled) {
@@ -45,14 +51,38 @@ function ingestConfigToSchema(
4551
} as IConfigField);
4652
ingestSchemaObj['pipelineName'] = getFieldSchema(ingestConfig.pipelineName);
4753
ingestSchemaObj['enrich'] = processorsConfigToSchema(ingestConfig.enrich);
48-
ingestSchemaObj['index'] = indexConfigToSchema(ingestConfig.index);
54+
ingestSchemaObj['index'] = indexConfigToSchema(ingestConfig.index, indices);
4955
}
5056
return yup.object(ingestSchemaObj);
5157
}
5258

53-
function indexConfigToSchema(indexConfig: IndexConfig): Schema {
59+
function indexConfigToSchema(
60+
indexConfig: IndexConfig,
61+
indices: { [key: string]: Index }
62+
): Schema {
5463
const indexSchemaObj = {} as { [key: string]: Schema };
55-
indexSchemaObj['name'] = getFieldSchema(indexConfig.name);
64+
// Do a deeper check on the index name. Ensure the name is of valid format,
65+
// and that it doesn't name clash with an existing index.
66+
indexSchemaObj['name'] = yup
67+
.string()
68+
.test('name', 'Invalid index name', (name) => {
69+
return !(
70+
name === undefined ||
71+
name === '' ||
72+
name.length > 100 ||
73+
INDEX_NAME_REGEXP.test(name) === false
74+
);
75+
})
76+
.test(
77+
'name',
78+
'This index name is already in use. Use a different name',
79+
(name) => {
80+
return !Object.values(indices)
81+
.map((index) => index.name)
82+
.includes(name || '');
83+
}
84+
)
85+
.required('Required') as yup.Schema;
5686
indexSchemaObj['mappings'] = getFieldSchema(indexConfig.mappings);
5787
indexSchemaObj['settings'] = getFieldSchema(indexConfig.settings);
5888
return yup.object(indexSchemaObj);

0 commit comments

Comments
 (0)