From af7516fd2dddfcae6a081b8da4106160312fc30b Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 16 Aug 2024 15:36:14 -0700 Subject: [PATCH] Improve input constraints; general usability improvements (#294) Signed-off-by: Tyler Ohlsen --- common/constants.ts | 6 + common/utils.ts | 11 ++ .../delete_workflow_modal.tsx | 50 ------ public/general_components/index.ts | 1 - .../components/export_modal.tsx | 11 +- .../workflow_detail/components/header.tsx | 9 +- .../pages/workflow_detail/workflow_detail.tsx | 55 +++++-- .../ingest_inputs/source_data.tsx | 2 + .../input_fields/json_field.tsx | 4 + .../workflow_inputs/workflow_inputs.tsx | 7 +- .../import_workflow/import_workflow_modal.tsx | 10 +- .../pages/workflows/new_workflow/use_case.tsx | 17 +- public/pages/workflows/new_workflow/utils.ts | 11 +- .../pages/workflows/workflow_list/columns.tsx | 11 +- .../workflow_list/delete_workflow_modal.tsx | 146 ++++++++++++++++++ .../workflows/workflow_list/workflow_list.tsx | 53 ++----- public/pages/workflows/workflows.tsx | 5 +- public/utils/config_to_schema_utils.ts | 52 ++++--- public/utils/utils.ts | 5 +- 19 files changed, 322 insertions(+), 144 deletions(-) delete mode 100644 public/general_components/delete_workflow_modal.tsx create mode 100644 public/pages/workflows/workflow_list/delete_workflow_modal.tsx diff --git a/common/constants.ts b/common/constants.ts index 3ea554bb..ec0efdd3 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -158,6 +158,7 @@ export const FETCH_ALL_QUERY_BODY = { size: 1000, }; export const INDEX_NOT_FOUND_EXCEPTION = 'index_not_found_exception'; +export const ERROR_GETTING_WORKFLOW_MSG = 'Failed to retrieve template'; export const NO_MODIFICATIONS_FOUND_TEXT = 'Template does not contain any modifications'; export const JSONPATH_ROOT_SELECTOR = '$.'; @@ -165,6 +166,11 @@ export enum SORT_ORDER { ASC = 'asc', DESC = 'desc', } +export const MAX_DOCS = 1000; +export const MAX_STRING_LENGTH = 100; +export const MAX_JSON_STRING_LENGTH = 10000; +export const MAX_WORKFLOW_NAME_TO_DISPLAY = 40; +export const WORKFLOW_NAME_REGEXP = RegExp('^[a-zA-Z0-9_-]*$'); export enum PROCESSOR_CONTEXT { INGEST = 'ingest', diff --git a/common/utils.ts b/common/utils.ts index b6892191..2242b028 100644 --- a/common/utils.ts +++ b/common/utils.ts @@ -24,3 +24,14 @@ export const prettifyErrorMessage = (rawErrorMessage: string) => { return `User ${match[2]} has no permissions to [${match[1]}].`; } }; + +export function getCharacterLimitedString( + input: string | undefined, + limit: number +): string { + return input !== undefined + ? input.length > limit + ? input.substring(0, limit - 3) + '...' + : input + : ''; +} diff --git a/public/general_components/delete_workflow_modal.tsx b/public/general_components/delete_workflow_modal.tsx deleted file mode 100644 index 081ef4ee..00000000 --- a/public/general_components/delete_workflow_modal.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { - EuiSmallButton, - EuiSmallButtonEmpty, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiText, -} from '@elastic/eui'; -import { Workflow } from '../../common'; - -interface DeleteWorkflowModalProps { - workflow: Workflow; - onClose: () => void; - onConfirm: () => void; -} - -/** - * A general delete workflow modal. - */ -export function DeleteWorkflowModal(props: DeleteWorkflowModalProps) { - return ( - - - -

{`Delete ${props.workflow.name}?`}

-
-
- - - The workflow will be permanently deleted. This action cannot be - undone. Resources created by this workflow will be retained. - - - - Cancel - - Delete - - -
- ); -} diff --git a/public/general_components/index.ts b/public/general_components/index.ts index b1232790..728fc38d 100644 --- a/public/general_components/index.ts +++ b/public/general_components/index.ts @@ -4,6 +4,5 @@ */ export { MultiSelectFilter } from './multi_select_filter'; -export { DeleteWorkflowModal } from './delete_workflow_modal'; export { ProcessorsTitle } from './processors_title'; export { ResourceList } from './resource_list'; diff --git a/public/pages/workflow_detail/components/export_modal.tsx b/public/pages/workflow_detail/components/export_modal.tsx index ce31b3da..7c4e1b52 100644 --- a/public/pages/workflow_detail/components/export_modal.tsx +++ b/public/pages/workflow_detail/components/export_modal.tsx @@ -19,7 +19,11 @@ import { EuiModalFooter, EuiSmallButtonEmpty, } from '@elastic/eui'; -import { CREATE_WORKFLOW_LINK, Workflow } from '../../../../common'; +import { + CREATE_WORKFLOW_LINK, + Workflow, + getCharacterLimitedString, +} from '../../../../common'; import { reduceToTemplate } from '../../../utils'; interface ExportModalProps { @@ -69,7 +73,10 @@ export function ExportModal(props: ExportModalProps) { props.setIsExportModalOpen(false)}> -

{`Export ${props.workflow?.name}`}

+

{`Export ${getCharacterLimitedString( + props.workflow?.name || '', + 25 + )}`}

diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx index a69afb22..b49716f8 100644 --- a/public/pages/workflow_detail/components/header.tsx +++ b/public/pages/workflow_detail/components/header.tsx @@ -15,8 +15,10 @@ import { } from '@elastic/eui'; import { DEFAULT_NEW_WORKFLOW_STATE, + MAX_WORKFLOW_NAME_TO_DISPLAY, WORKFLOW_STATE, Workflow, + getCharacterLimitedString, toFormattedDate, } from '../../../../common'; import { APP_PATH } from '../../../utils'; @@ -38,7 +40,12 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { useEffect(() => { if (props.workflow) { - setWorkflowName(props.workflow.name); + setWorkflowName( + getCharacterLimitedString( + props.workflow.name, + MAX_WORKFLOW_NAME_TO_DISPLAY + ) + ); setWorkflowState(props.workflow.state || DEFAULT_NEW_WORKFLOW_STATE); try { const formattedDate = toFormattedDate( diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index d7e3a7a7..46651109 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -7,8 +7,16 @@ import React, { useEffect, ReactElement } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { ReactFlowProvider } from 'reactflow'; -import { EuiPage, EuiPageBody } from '@elastic/eui'; -import { BREADCRUMBS } from '../../utils'; +import { escape } from 'lodash'; +import { + EuiButton, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, +} from '@elastic/eui'; +import { APP_PATH, BREADCRUMBS } from '../../utils'; import { getCore } from '../../services'; import { WorkflowDetailHeader } from './components'; import { @@ -19,8 +27,10 @@ import { } from '../../store'; import { ResizableWorkspace } from './resizable_workspace'; import { - DEFAULT_NEW_WORKFLOW_NAME, + ERROR_GETTING_WORKFLOW_MSG, FETCH_ALL_QUERY_BODY, + MAX_WORKFLOW_NAME_TO_DISPLAY, + getCharacterLimitedString, } from '../../../common'; import { MountPoint } from '../../../../../src/core/public'; @@ -28,7 +38,10 @@ import { MountPoint } from '../../../../../src/core/public'; import './workflow-detail-styles.scss'; import '../../global-styles.scss'; -import { getDataSourceId } from '../../utils/utils'; +import { + constructHrefWithDataSourceId, + getDataSourceId, +} from '../../utils/utils'; import { getDataSourceManagementPlugin, @@ -57,12 +70,17 @@ export function WorkflowDetail(props: WorkflowDetailProps) { const dispatch = useAppDispatch(); const dataSourceEnabled = getDataSourceEnabled().enabled; const dataSourceId = getDataSourceId(); - const { workflows } = useSelector((state: AppState) => state.workflows); + const { workflows, errorMessage } = useSelector( + (state: AppState) => state.workflows + ); // selected workflow state - const workflowId = props.match?.params?.workflowId; + const workflowId = escape(props.match?.params?.workflowId); const workflow = workflows[workflowId]; - const workflowName = workflow ? workflow.name : DEFAULT_NEW_WORKFLOW_NAME; + const workflowName = getCharacterLimitedString( + workflow?.name || '', + MAX_WORKFLOW_NAME_TO_DISPLAY + ); useEffect(() => { if (dataSourceEnabled) { @@ -78,7 +96,7 @@ export function WorkflowDetail(props: WorkflowDetailProps) { { text: workflowName }, ]); } - }, []); + }, [workflowName]); // On initial load: // - fetch workflow @@ -107,7 +125,26 @@ export function WorkflowDetail(props: WorkflowDetailProps) { ); } - return ( + return errorMessage.includes(ERROR_GETTING_WORKFLOW_MSG) ? ( + + + Oops! We couldn't find that workflow} + titleSize="s" + /> + + + + Return to home + + + + ) : ( {dataSourceEnabled && renderDataSourceComponent} diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx index 10a1aef7..e078a408 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx @@ -84,6 +84,7 @@ export function SourceData(props: SourceDataProps) { /> { return ( setIsModalOpen(false)}> -

{`Delete resources for workflow ${props.workflow?.name}?`}

+

{`Delete resources for workflow ${getCharacterLimitedString( + props.workflow?.name || '', + MAX_WORKFLOW_NAME_TO_DISPLAY + )}?`}

diff --git a/public/pages/workflows/import_workflow/import_workflow_modal.tsx b/public/pages/workflows/import_workflow/import_workflow_modal.tsx index f713a7c6..1e1d4bf8 100644 --- a/public/pages/workflows/import_workflow/import_workflow_modal.tsx +++ b/public/pages/workflows/import_workflow/import_workflow_modal.tsx @@ -32,7 +32,7 @@ import { } from '../../../store'; import { FETCH_ALL_QUERY_BODY, Workflow } from '../../../../common'; import { WORKFLOWS_TAB } from '../workflows'; -import { getDataSourceId } from '../../../utils/utils'; +import { getDataSourceId } from '../../../utils/utils'; interface ImportWorkflowModalProps { isImportModalOpen: boolean; @@ -51,6 +51,9 @@ export function ImportWorkflowModal(props: ImportWorkflowModalProps) { const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); + // transient importing state for button state + const [isImporting, setIsImporting] = useState(false); + // file contents & file obj state const [fileContents, setFileContents] = useState( undefined @@ -133,8 +136,10 @@ export function ImportWorkflowModal(props: ImportWorkflowModalProps) { onModalClose()}>Cancel { + setIsImporting(true); dispatch( createWorkflow({ apiBody: fileObj as Workflow, @@ -159,6 +164,7 @@ export function ImportWorkflowModal(props: ImportWorkflowModalProps) { getCore().notifications.toasts.addDanger(error); }) .finally(() => { + setIsImporting(false); onModalClose(); }); }} diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx index adc46c60..02467cc2 100644 --- a/public/pages/workflows/new_workflow/use_case.tsx +++ b/public/pages/workflows/new_workflow/use_case.tsx @@ -22,7 +22,7 @@ import { EuiCompressedFieldText, EuiCompressedFormRow, } from '@elastic/eui'; -import { Workflow } from '../../../../common'; +import { WORKFLOW_NAME_REGEXP, Workflow } from '../../../../common'; import { APP_PATH } from '../../../utils'; import { processWorkflowName } from './utils'; import { createWorkflow, useAppDispatch } from '../../../store'; @@ -45,6 +45,15 @@ export function UseCase(props: UseCaseProps) { processWorkflowName(props.workflow.name) ); + // custom sanitization on workflow name + function isInvalid(name: string): boolean { + return ( + name === '' || + name.length > 100 || + WORKFLOW_NAME_REGEXP.test(name) === false + ); + } + return ( <> {isNameModalOpen && ( @@ -57,8 +66,8 @@ export function UseCase(props: UseCaseProps) { { const workflowToCreate = { ...props.workflow, diff --git a/public/pages/workflows/new_workflow/utils.ts b/public/pages/workflows/new_workflow/utils.ts index 43f6a145..bcbf309f 100644 --- a/public/pages/workflows/new_workflow/utils.ts +++ b/public/pages/workflows/new_workflow/utils.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { snakeCase } from 'lodash'; import { MLIngestProcessor } from '../../../configs'; import { WorkflowTemplate, @@ -117,13 +118,5 @@ function fetchSemanticSearchMetadata(): UIState { export function processWorkflowName(workflowName: string): string { return workflowName === START_FROM_SCRATCH_WORKFLOW_NAME ? DEFAULT_NEW_WORKFLOW_NAME - : toSnakeCase(workflowName); -} - -function toSnakeCase(text: string): string { - return text - .replace(/\W+/g, ' ') - .split(/ |\B(?=[A-Z])/) - .map((word) => word.toLowerCase()) - .join('_'); + : snakeCase(workflowName); } diff --git a/public/pages/workflows/workflow_list/columns.tsx b/public/pages/workflows/workflow_list/columns.tsx index 46956ba0..369d1017 100644 --- a/public/pages/workflows/workflow_list/columns.tsx +++ b/public/pages/workflows/workflow_list/columns.tsx @@ -7,7 +7,9 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; import { EMPTY_FIELD_STRING, + MAX_WORKFLOW_NAME_TO_DISPLAY, Workflow, + getCharacterLimitedString, toFormattedDate, } from '../../../../common'; import { @@ -22,7 +24,7 @@ export const columns = (actions: any[]) => { { field: 'name', name: 'Name', - width: '33%', + width: '35%', sortable: true, render: (name: string, workflow: Workflow) => ( { dataSourceId )} > - {name} + {getCharacterLimitedString(name, MAX_WORKFLOW_NAME_TO_DISPLAY)} ), }, { field: 'ui_metadata.type', name: 'Type', - width: '33%', + width: '20%', sortable: true, }, { field: 'lastUpdated', name: 'Last saved', - width: '33%', + width: '35%', sortable: true, render: (lastUpdated: number) => lastUpdated !== undefined @@ -53,6 +55,7 @@ export const columns = (actions: any[]) => { }, { name: 'Actions', + width: '10%', actions, }, ]; diff --git a/public/pages/workflows/workflow_list/delete_workflow_modal.tsx b/public/pages/workflows/workflow_list/delete_workflow_modal.tsx new file mode 100644 index 00000000..f0bf94f3 --- /dev/null +++ b/public/pages/workflows/workflow_list/delete_workflow_modal.tsx @@ -0,0 +1,146 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiSmallButton, + EuiSmallButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiCheckbox, +} from '@elastic/eui'; +import { + MAX_WORKFLOW_NAME_TO_DISPLAY, + Workflow, + getCharacterLimitedString, +} from '../../../../common'; +import { + deleteWorkflow, + deprovisionWorkflow, + useAppDispatch, +} from '../../../store'; +import { getDataSourceId, getResourcesToBeForceDeleted } from '../../../utils'; +import { getCore } from '../../../services'; + +interface DeleteWorkflowModalProps { + workflow: Workflow; + clearDeleteState(): void; +} + +/** + * A modal to delete workflow. Optionally deprovision/delete associated resources. + */ +export function DeleteWorkflowModal(props: DeleteWorkflowModalProps) { + const dispatch = useAppDispatch(); + const dataSourceId = getDataSourceId(); + + // isDeleting state used for button states + const [isDeleting, setIsDeleting] = useState(false); + + // deprovision state + const [deprovision, setDeprovision] = useState(true); + + // reusable delete workflow fn + async function handleDelete() { + await dispatch( + deleteWorkflow({ + workflowId: props.workflow.id as string, + dataSourceId, + }) + ) + .unwrap() + .then((result) => { + getCore().notifications.toasts.addSuccess( + `Successfully deleted ${props.workflow.name}` + ); + }) + .catch((err: any) => { + getCore().notifications.toasts.addDanger( + `Failed to delete ${props.workflow.name}` + ); + console.error(`Failed to delete ${props.workflow.name}: ${err}`); + }); + } + + return ( + props.clearDeleteState()}> + + +

{`Delete ${getCharacterLimitedString( + props.workflow.name, + MAX_WORKFLOW_NAME_TO_DISPLAY + )}?`}

+
+
+ + + + The workflow will be permanently deleted. + + + { + setDeprovision(e.target.checked); + }} + checked={deprovision} + label="Delete associated resources" + /> + + + + + props.clearDeleteState()}> + {' '} + Cancel + + { + setIsDeleting(true); + if (deprovision) { + await dispatch( + deprovisionWorkflow({ + apiBody: { + workflowId: props.workflow.id as string, + resourceIds: getResourcesToBeForceDeleted(props.workflow), + }, + dataSourceId, + }) + ) + .unwrap() + .then(async (result) => { + handleDelete(); + }) + .catch((err: any) => { + getCore().notifications.toasts.addDanger( + `Failed to delete resources for ${props.workflow.name}` + ); + console.error( + `Failed to delete resources for ${props.workflow.name}: ${err}` + ); + }); + } else { + handleDelete(); + } + setIsDeleting(false); + props.clearDeleteState(); + }} + fill={true} + color="danger" + > + Delete + + +
+ ); +} diff --git a/public/pages/workflows/workflow_list/workflow_list.tsx b/public/pages/workflows/workflow_list/workflow_list.tsx index ee944ac1..16f52b4d 100644 --- a/public/pages/workflows/workflow_list/workflow_list.tsx +++ b/public/pages/workflows/workflow_list/workflow_list.tsx @@ -20,17 +20,18 @@ import { EuiFlyoutBody, EuiText, } from '@elastic/eui'; -import { AppState, deleteWorkflow, useAppDispatch } from '../../../store'; -import { UIState, WORKFLOW_TYPE, Workflow } from '../../../../common'; -import { columns } from './columns'; +import { AppState } from '../../../store'; import { - DeleteWorkflowModal, - MultiSelectFilter, - ResourceList, -} from '../../../general_components'; + MAX_WORKFLOW_NAME_TO_DISPLAY, + UIState, + WORKFLOW_TYPE, + Workflow, + getCharacterLimitedString, +} from '../../../../common'; +import { columns } from './columns'; +import { MultiSelectFilter, ResourceList } from '../../../general_components'; import { WORKFLOWS_TAB } from '../workflows'; -import { getCore } from '../../../services'; -import { getDataSourceId } from '../../../utils/utils'; +import { DeleteWorkflowModal } from './delete_workflow_modal'; interface WorkflowListProps { setSelectedTabId: (tabId: WORKFLOWS_TAB) => void; @@ -65,8 +66,6 @@ const filterOptions = [ * The searchable list of created workflows. */ export function WorkflowList(props: WorkflowListProps) { - const dispatch = useAppDispatch(); - const dataSourceId = getDataSourceId(); const { workflows, loading } = useSelector( (state: AppState) => state.workflows ); @@ -141,32 +140,7 @@ export function WorkflowList(props: WorkflowListProps) { {isDeleteModalOpen && selectedWorkflow?.id !== undefined && ( { - clearDeleteState(); - }} - onConfirm={async () => { - clearDeleteState(); - await dispatch( - deleteWorkflow({ - workflowId: selectedWorkflow.id as string, - dataSourceId, - }) - ) - .unwrap() - .then((result) => { - getCore().notifications.toasts.addSuccess( - `Successfully deleted ${selectedWorkflow.name}` - ); - }) - .catch((err: any) => { - getCore().notifications.toasts.addDanger( - `Failed to delete ${selectedWorkflow.name}` - ); - console.error( - `Failed to delete ${selectedWorkflow.name}: ${err}` - ); - }); - }} + clearDeleteState={clearDeleteState} /> )} {isResourcesFlyoutOpen && selectedWorkflow && ( @@ -176,7 +150,10 @@ export function WorkflowList(props: WorkflowListProps) { > -

{`Active resources with ${selectedWorkflow.name}`}

+

{`Active resources with ${getCharacterLimitedString( + selectedWorkflow.name, + MAX_WORKFLOW_NAME_TO_DISPLAY + )}`}

diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 1a5ca628..623d5270 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -5,6 +5,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import { RouteComponentProps, useLocation } from 'react-router-dom'; +import { escape } from 'lodash'; import { EuiPageHeader, EuiTitle, @@ -91,7 +92,9 @@ export function Workflows(props: WorkflowsProps) { const tabFromUrl = queryString.parse(useLocation().search)[ ACTIVE_TAB_PARAM ] as WORKFLOWS_TAB; - const [selectedTabId, setSelectedTabId] = useState(tabFromUrl); + const [selectedTabId, setSelectedTabId] = useState( + escape(tabFromUrl) as WORKFLOWS_TAB + ); // If there is no selected tab or invalid tab, default to manage tab useEffect(() => { diff --git a/public/utils/config_to_schema_utils.ts b/public/utils/config_to_schema_utils.ts index f243a529..b441cab7 100644 --- a/public/utils/config_to_schema_utils.ts +++ b/public/utils/config_to_schema_utils.ts @@ -15,6 +15,9 @@ import { IndexConfig, IConfigField, SearchIndexConfig, + MAX_DOCS, + MAX_STRING_LENGTH, + MAX_JSON_STRING_LENGTH, } from '../../common'; /* @@ -105,27 +108,29 @@ function getFieldSchema( optional: boolean = false ): Schema { let baseSchema: Schema; + const defaultStringSchema = yup + .string() + .trim() + .min(1, 'Too short') + .max(MAX_STRING_LENGTH, 'Too long'); + switch (field.type) { case 'string': case 'select': { - baseSchema = yup.string().min(1, 'Too short').max(70, 'Too long'); + baseSchema = defaultStringSchema; break; } case 'model': { baseSchema = yup.object().shape({ - id: yup.string().min(1, 'Too short').max(70, 'Too long').required(), + id: defaultStringSchema.required(), }); break; } case 'map': { baseSchema = yup.array().of( yup.object().shape({ - key: yup.string().min(1, 'Too short').max(70, 'Too long').required(), - value: yup - .string() - .min(1, 'Too short') - .max(70, 'Too long') - .required(), + key: defaultStringSchema.required(), + value: defaultStringSchema.required(), }) ); break; @@ -153,28 +158,35 @@ function getFieldSchema( } catch (error) { return false; } - }); + }) + .test( + 'jsonArray', + `Array length cannot exceed ${MAX_DOCS}`, + (value) => { + try { + // @ts-ignore + return JSON.parse(value).length <= MAX_DOCS; + } catch (error) { + return false; + } + } + ); break; } case 'jsonString': { - baseSchema = yup.string().min(1, 'Too short'); + baseSchema = yup + .string() + .min(1, 'Too short') + .max(MAX_JSON_STRING_LENGTH, 'Too long'); break; } case 'mapArray': { baseSchema = yup.array().of( yup.array().of( yup.object().shape({ - key: yup - .string() - .min(1, 'Too short') - .max(70, 'Too long') - .required(), - value: yup - .string() - .min(1, 'Too short') - .max(70, 'Too long') - .required(), + key: defaultStringSchema.required(), + value: defaultStringSchema.required(), }) ) ); diff --git a/public/utils/utils.ts b/public/utils/utils.ts index 5b0e5882..bd554c3f 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -5,7 +5,7 @@ import yaml from 'js-yaml'; import jsonpath from 'jsonpath'; -import { get } from 'lodash'; +import { escape, get } from 'lodash'; import { JSONPATH_ROOT_SELECTOR, MapFormValue, @@ -238,7 +238,8 @@ export const getDataSourceFromURL = (location: { const queryParams = queryString.parse(location.search); const dataSourceId = queryParams.dataSourceId; return { - dataSourceId: typeof dataSourceId === 'string' ? dataSourceId : undefined, + dataSourceId: + typeof dataSourceId === 'string' ? escape(dataSourceId) : undefined, }; };