diff --git a/common/constants.ts b/common/constants.ts
index d036669b..b8c51b85 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -546,6 +546,7 @@ export const MAX_DOCS = 1000;
export const MAX_STRING_LENGTH = 100;
export const MAX_JSON_STRING_LENGTH = 10000;
export const MAX_TEMPLATE_STRING_LENGTH = 10000;
+export const MAX_BYTES = 1048576; // OSD REST request payload size limit
export const MAX_WORKFLOW_NAME_TO_DISPLAY = 40;
export const WORKFLOW_NAME_REGEXP = RegExp('^[a-zA-Z0-9_-]*$');
export const EMPTY_MAP_ENTRY = { key: '', value: '' } as MapEntry;
diff --git a/public/general_components/index.ts b/public/general_components/index.ts
index 60528d1e..ed0d4957 100644
--- a/public/general_components/index.ts
+++ b/public/general_components/index.ts
@@ -7,5 +7,6 @@ export { MultiSelectFilter } from './multi_select_filter';
export { ProcessorsTitle } from './processors_title';
export { ExperimentalBadge } from './experimental_badge';
export { QueryParamsList } from './query_params_list';
+export { JsonPathExamplesTable } from './jsonpath_examples_table';
export * from './results';
export * from './service_card';
diff --git a/public/general_components/jsonpath_examples_table.tsx b/public/general_components/jsonpath_examples_table.tsx
new file mode 100644
index 00000000..b05c9eb6
--- /dev/null
+++ b/public/general_components/jsonpath_examples_table.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { isEmpty } from 'lodash';
+import {
+ EuiInMemoryTable,
+ EuiCode,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+interface JsonPathExamplesTableProps {
+ headerText?: string;
+}
+
+type JSONPathExample = {
+ expression: string;
+ meaning: string;
+ example: string;
+};
+
+const examples = [
+ {
+ expression: '$.data',
+ meaning: 'The entire input',
+ example: '$.data',
+ },
+] as JSONPathExample[];
+
+const columns = [
+ {
+ field: 'expression',
+ name: 'Expression',
+ width: '25%',
+ sortable: false,
+ render: (expression: string) => {expression},
+ },
+ {
+ field: 'meaning',
+ name: 'Meaning',
+ width: '50%',
+ sortable: false,
+ render: (meaning: string) => {meaning},
+ },
+ {
+ field: 'example',
+ name: 'Example',
+ width: '25%',
+ sortable: false,
+ render: (example: string) => {example},
+ },
+];
+
+/**
+ * A stateless component containing JSONPath examples in a table. Optionally takes in
+ * a header text for some more contextual information.
+ */
+export function JsonPathExamplesTable(props: JsonPathExamplesTableProps) {
+ return (
+
+
+ {!isEmpty(props.headerText) && (
+
+ {props.headerText}
+
+ )}
+
+
+ items={examples}
+ columns={columns}
+ pagination={false}
+ sorting={false}
+ hasActions={false}
+ />
+
+
+
+ );
+}
diff --git a/public/pages/workflow_detail/tools/query/query.tsx b/public/pages/workflow_detail/tools/query/query.tsx
index 155751d6..4d4be165 100644
--- a/public/pages/workflow_detail/tools/query/query.tsx
+++ b/public/pages/workflow_detail/tools/query/query.tsx
@@ -4,6 +4,7 @@
*/
import React, { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
import { isEmpty } from 'lodash';
import { useFormikContext } from 'formik';
import {
@@ -13,7 +14,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSmallButton,
- EuiSwitch,
+ EuiSmallButtonEmpty,
EuiText,
} from '@elastic/eui';
import {
@@ -24,7 +25,7 @@ import {
SearchResponse,
WorkflowFormValues,
} from '../../../../../common';
-import { searchIndex, useAppDispatch } from '../../../../store';
+import { AppState, searchIndex, useAppDispatch } from '../../../../store';
import {
containsEmptyValues,
containsSameValues,
@@ -57,12 +58,11 @@ export function Query(props: QueryProps) {
const dispatch = useAppDispatch();
const dataSourceId = getDataSourceId();
+ const { loading } = useSelector((state: AppState) => state.opensearch);
+
// Form state
const { values } = useFormikContext();
- // use custom query state
- const [useCustomQuery, setUseCustomQuery] = useState(false);
-
// query response state
const [queryResponse, setQueryResponse] = useState<
SearchResponse | undefined
@@ -158,11 +158,41 @@ export function Query(props: QueryProps) {
- Search
+
+
+ Search
+
+
+ {
+ setIncludePipeline(!includePipeline);
+ }}
+ />
+
+
- {
- setIncludePipeline(!includePipeline);
- }}
- />
+
+
+ Query
+
+ {props.selectedStep === CONFIG_STEP.SEARCH &&
+ !isEmpty(values?.search?.request) &&
+ values?.search?.request !== tempRequest && (
+
+ {
+ setTempRequest(values?.search?.request);
+ }}
+ >
+ Revert to query definition
+
+
+ )}
+
-
- setUseCustomQuery(!useCustomQuery)}
+
+ {
+ setTempRequest(input);
+ }}
+ onBlur={() => {
+ try {
+ setTempRequest(customStringify(JSON.parse(tempRequest)));
+ } catch (error) {}
+ }}
+ readOnly={false}
+ setOptions={{
+ fontSize: '14px',
+ useWorker: true,
+ highlightActiveLine: true,
+ highlightSelectedWord: true,
+ highlightGutterLine: true,
+ wrap: true,
+ }}
+ aria-label="Code Editor"
+ tabSize={2}
/>
- {useCustomQuery && (
-
- {
- setTempRequest(input);
- }}
- onBlur={() => {
- try {
- setTempRequest(
- customStringify(JSON.parse(tempRequest))
- );
- } catch (error) {}
- }}
- readOnly={false}
- setOptions={{
- fontSize: '14px',
- useWorker: true,
- highlightActiveLine: true,
- highlightSelectedWord: true,
- highlightGutterLine: true,
- wrap: true,
- }}
- aria-label="Code Editor"
- tabSize={2}
- />
-
- )}
{/**
* This may return nothing if the list of params are empty
diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/override_query_modal.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/override_query_modal.tsx
index 7542912f..5d0e2ef7 100644
--- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/override_query_modal.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/override_query_modal.tsx
@@ -3,8 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useState } from 'react';
-import { useFormikContext, getIn } from 'formik';
+import React, { useEffect, useState } from 'react';
+import { useFormikContext, getIn, Formik } from 'formik';
+import * as yup from 'yup';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -23,8 +24,10 @@ import {
EuiCopy,
EuiButtonIcon,
EuiContextMenu,
+ EuiSmallButtonEmpty,
} from '@elastic/eui';
import {
+ IConfigField,
IMAGE_FIELD_PATTERN,
IProcessorConfig,
LABEL_FIELD_PATTERN,
@@ -35,6 +38,7 @@ import {
QUERY_PRESETS,
QUERY_TEXT_PATTERN,
QueryPreset,
+ RequestFormValues,
TEXT_FIELD_PATTERN,
VECTOR_FIELD_PATTERN,
VECTOR_PATTERN,
@@ -42,6 +46,7 @@ import {
} from '../../../../../../../common';
import { parseModelOutputs } from '../../../../../../utils/utils';
import { JsonField } from '../../../input_fields';
+import { getFieldSchema, getInitialValue } from '../../../../../../utils';
interface OverrideQueryModalProps {
config: IProcessorConfig;
@@ -59,6 +64,19 @@ export function OverrideQueryModal(props: OverrideQueryModalProps) {
WorkflowFormValues
>();
+ // sub-form values/schema
+ const requestFormValues = {
+ request: getInitialValue('json'),
+ } as RequestFormValues;
+ const requestFormSchema = yup.object({
+ request: getFieldSchema({
+ type: 'json',
+ } as IConfigField),
+ }) as yup.Schema;
+
+ // persist standalone values. update / initialize when it is first opened
+ const [tempRequest, setTempRequest] = useState('{}');
+
// get some current form values
const modelOutputs = parseModelOutputs(props.modelInterface);
const queryFieldPath = `${props.baseConfigPath}.${props.config.id}.query_template`;
@@ -82,145 +100,199 @@ export function OverrideQueryModal(props: OverrideQueryModalProps) {
const [presetsPopoverOpen, setPresetsPopoverOpen] = useState(false);
return (
- {}}
+ validate={(values) => {}}
>
-
-
- {`Override query`}
-
-
-
-
- Configure a custom query template to override the existing one.
- Optionally inject dynamic model outputs into the new query.
-
-
-
- <>
-
- setPresetsPopoverOpen(!presetsPopoverOpen)}
- iconSide="right"
- iconType="arrowDown"
- >
- Choose from a preset
-
- }
- isOpen={presetsPopoverOpen}
- closePopover={() => setPresetsPopoverOpen(false)}
- anchorPosition="downLeft"
- >
- ({
- name: preset.name,
- onClick: () => {
- setFieldValue(
- queryFieldPath,
- preset.query
- // sanitize the query preset string into valid template placeholder format, for
- // any placeholder values in the query.
- // for example, replacing `"{{vector}}"` with `${vector}`
- .replace(
- new RegExp(`"${VECTOR_FIELD_PATTERN}"`, 'g'),
- `\$\{vector_field\}`
- )
- .replace(
- new RegExp(`"${VECTOR_PATTERN}"`, 'g'),
- `\$\{vector\}`
- )
- .replace(
- new RegExp(`"${TEXT_FIELD_PATTERN}"`, 'g'),
- `\$\{text_field\}`
- )
- .replace(
- new RegExp(`"${IMAGE_FIELD_PATTERN}"`, 'g'),
- `\$\{image_field\}`
- )
- .replace(
- new RegExp(`"${LABEL_FIELD_PATTERN}"`, 'g'),
- `\$\{label_field\}`
- )
- .replace(
- new RegExp(`"${QUERY_TEXT_PATTERN}"`, 'g'),
- `\$\{query_text\}`
- )
- .replace(
- new RegExp(`"${QUERY_IMAGE_PATTERN}"`, 'g'),
- `\$\{query_image\}`
- )
- .replace(
- new RegExp(`"${MODEL_ID_PATTERN}"`, 'g'),
- `\$\{model_id\}`
- )
- );
- setFieldTouched(queryFieldPath, true);
- setPresetsPopoverOpen(false);
- },
- })),
- },
- ]}
- />
-
-
-
- {finalModelOutputs.length > 0 && (
- <>
-
-
- <>
-
- {
+ // override to parent form value when changes detected
+ useEffect(() => {
+ formikProps.setFieldValue('request', getIn(values, queryFieldPath));
+ }, [getIn(values, queryFieldPath)]);
+
+ // update tempRequest when form changes are detected
+ useEffect(() => {
+ setTempRequest(getIn(formikProps.values, 'request'));
+ }, [getIn(formikProps.values, 'request')]);
+
+ return (
+
+
+
+ {`Override query`}
+
+
+
+
+ Configure a custom query template to override the existing one.
+ Optionally inject dynamic model outputs into the new query.
+
+
+
+ <>
+
+
+ setPresetsPopoverOpen(!presetsPopoverOpen)
+ }
+ iconSide="right"
+ iconType="arrowDown"
+ >
+ Choose from a preset
+
+ }
+ isOpen={presetsPopoverOpen}
+ closePopover={() => setPresetsPopoverOpen(false)}
+ anchorPosition="downLeft"
+ >
+
- To use any model outputs in the query template, copy the
- placeholder string directly.
-
-
- ({
+ name: preset.name,
+ onClick: () => {
+ formikProps.setFieldValue(
+ 'request',
+ preset.query
+ // sanitize the query preset string into valid template placeholder format, for
+ // any placeholder values in the query.
+ // for example, replacing `"{{vector}}"` with `${vector}`
+ .replace(
+ new RegExp(
+ `"${VECTOR_FIELD_PATTERN}"`,
+ 'g'
+ ),
+ `\$\{vector_field\}`
+ )
+ .replace(
+ new RegExp(`"${VECTOR_PATTERN}"`, 'g'),
+ `\$\{vector\}`
+ )
+ .replace(
+ new RegExp(
+ `"${TEXT_FIELD_PATTERN}"`,
+ 'g'
+ ),
+ `\$\{text_field\}`
+ )
+ .replace(
+ new RegExp(
+ `"${IMAGE_FIELD_PATTERN}"`,
+ 'g'
+ ),
+ `\$\{image_field\}`
+ )
+ .replace(
+ new RegExp(
+ `"${LABEL_FIELD_PATTERN}"`,
+ 'g'
+ ),
+ `\$\{label_field\}`
+ )
+ .replace(
+ new RegExp(
+ `"${QUERY_TEXT_PATTERN}"`,
+ 'g'
+ ),
+ `\$\{query_text\}`
+ )
+ .replace(
+ new RegExp(
+ `"${QUERY_IMAGE_PATTERN}"`,
+ 'g'
+ ),
+ `\$\{query_image\}`
+ )
+ .replace(
+ new RegExp(`"${MODEL_ID_PATTERN}"`, 'g'),
+ `\$\{model_id\}`
+ )
+ );
+ formikProps.setFieldTouched('request', true);
+ setPresetsPopoverOpen(false);
+ },
+ })),
+ },
+ ]}
/>
- >
-
-
- >
- )}
- >
-
-
-
-
-
- Close
-
-
-
+
+
+
+ {finalModelOutputs.length > 0 && (
+ <>
+
+
+ <>
+
+
+ To use any model outputs in the query template,
+ copy the placeholder string directly.
+
+
+
+ >
+
+
+ >
+ )}
+ >
+
+
+
+
+
+ Cancel
+
+ {
+ setFieldValue(queryFieldPath, tempRequest);
+ setFieldTouched(queryFieldPath, true);
+ props.onClose();
+ }}
+ isDisabled={false} // users can always save. we can't easily validate the JSON, as it can contain placeholders that isn't valid JSON.
+ fill={true}
+ color="primary"
+ data-testid="updateOverrideQueryButton"
+ >
+ Save
+
+
+
+ );
+ }}
+
);
}
diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx
index 6ec1da14..1723dc43 100644
--- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx
@@ -190,16 +190,7 @@ export function ModelInputs(props: ModelInputsProps) {
// Adding a map entry to the end of the existing arr
function addMapEntry(curEntries: InputMapFormValue): void {
- const updatedEntries = [
- ...curEntries,
- {
- key: '',
- value: {
- transformType: '' as TRANSFORM_TYPE,
- value: '',
- },
- } as InputMapEntry,
- ];
+ const updatedEntries = [...curEntries, EMPTY_INPUT_MAP_ENTRY];
setFieldValue(inputMapFieldPath, updatedEntries);
setFieldTouched(inputMapFieldPath, true);
}
diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx
index 0ca94d8d..57383c5e 100644
--- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx
@@ -112,16 +112,7 @@ export function ModelOutputs(props: ModelOutputsProps) {
// Adding a map entry to the end of the existing arr
function addMapEntry(curEntries: OutputMapFormValue): void {
- const updatedEntries = [
- ...curEntries,
- {
- key: '',
- value: {
- transformType: '' as TRANSFORM_TYPE,
- value: '',
- },
- } as OutputMapEntry,
- ];
+ const updatedEntries = [...curEntries, EMPTY_OUTPUT_MAP_ENTRY];
setFieldValue(outputMapFieldPath, updatedEntries);
setFieldTouched(outputMapFieldPath, true);
}
@@ -427,6 +418,9 @@ export function ModelOutputs(props: ModelOutputsProps) {
}
showError={false}
/>
+ ) : transformType ===
+ NO_TRANSFORMATION ? (
+ -
) : undefined}
>
diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx
index 3823bd29..ce18a570 100644
--- a/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx
@@ -4,6 +4,7 @@
*/
import React, { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
import { Formik, getIn, useFormikContext } from 'formik';
import * as yup from 'yup';
import { isEmpty } from 'lodash';
@@ -42,7 +43,7 @@ import {
getPlaceholdersFromQuery,
injectParameters,
} from '../../../../utils';
-import { searchIndex, useAppDispatch } from '../../../../store';
+import { AppState, searchIndex, useAppDispatch } from '../../../../store';
import { QueryParamsList, Results } from '../../../../general_components';
interface EditQueryModalProps {
@@ -57,6 +58,7 @@ interface EditQueryModalProps {
export function EditQueryModal(props: EditQueryModalProps) {
const dispatch = useAppDispatch();
const dataSourceId = getDataSourceId();
+ const { loading } = useSelector((state: AppState) => state.opensearch);
// sub-form values/schema
const requestFormValues = {
@@ -257,6 +259,7 @@ export function EditQueryModal(props: EditQueryModalProps) {
{
dispatch(
diff --git a/public/utils/config_to_schema_utils.ts b/public/utils/config_to_schema_utils.ts
index 3741fbad..3d3b3c98 100644
--- a/public/utils/config_to_schema_utils.ts
+++ b/public/utils/config_to_schema_utils.ts
@@ -21,6 +21,7 @@ import {
MAX_JSON_STRING_LENGTH,
MAX_TEMPLATE_STRING_LENGTH,
TRANSFORM_TYPE,
+ MAX_BYTES,
} from '../../common';
/*
@@ -173,6 +174,18 @@ export function getFieldSchema(
return false;
}
}
+ )
+ .test(
+ 'jsonArray',
+ `Too large. Exceeds OpenSearch Dashboards limit of ${MAX_BYTES} bytes.`,
+ (value) => {
+ try {
+ // @ts-ignore
+ return new TextEncoder().encode(value)?.length < MAX_BYTES;
+ } catch (error) {
+ return false;
+ }
+ }
);
break;
}