From e672a064e2e3942e4336e97a50fb3f9bcbd84c6b Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Tue, 6 Jun 2023 21:19:33 +0530 Subject: [PATCH] chore(ui): add validation for custom options and arguments in json schema form (#11897) * chore(ui): add validation for custom options and arguments in json schema form * chore: update the regex * add unit test --- .../common/FormBuilder/FormBuilder.tsx | 3 +- .../ui/src/constants/regex.constants.ts | 2 + .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/locale/languages/es-es.json | 1 + .../ui/src/locale/languages/fr-fr.json | 1 + .../ui/src/locale/languages/ja-jp.json | 1 + .../ui/src/locale/languages/pt-br.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + .../resources/ui/src/utils/formUtils.test.tsx | 98 +++++++++++++++++++ .../main/resources/ui/src/utils/formUtils.tsx | 30 ++++++ 10 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/formUtils.test.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx index 32776835d45..88b53815a42 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx @@ -27,7 +27,7 @@ import { isEmpty, isUndefined } from 'lodash'; import { LoadingState } from 'Models'; import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; import { getPipelineServiceHostIp } from 'rest/ingestionPipelineAPI'; -import { transformErrors } from 'utils/formUtils'; +import { customValidate, transformErrors } from 'utils/formUtils'; import { ConfigData } from '../../../interface/service.interface'; import { formatFormDataForRender } from '../../../utils/JSONSchemaFormUtils'; import Loader from '../../Loader/Loader'; @@ -114,6 +114,7 @@ const FormBuilder: FunctionComponent = ({ className={classNames('rjsf', props.className, { 'no-header': !showFormHeader, })} + customValidate={customValidate} formContext={{ handleFocus: onFocus }} formData={localFormData} idSeparator="/" diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts index e711fd2e171..7f5a31c7e65 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts @@ -38,3 +38,5 @@ export const MARKDOWN_MATCH_ID = /\$\(id="(.*?)"\)/; export const CUSTOM_PROPERTY_NAME_REGEX = /^[a-z][a-zA-Z0-9]*$/; export const ENDS_WITH_NUMBER_REGEX = /\d+$/; + +export const VALID_OBJECT_KEY_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index e5206a20afd..57de1edeae0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -1167,6 +1167,7 @@ "ingestion-pipeline-name-message": "Name that identifies this pipeline instance uniquely.", "ingestion-pipeline-name-successfully-deployed-entity": "You are all set! The has been successfully deployed. The {{entity}} will run at a regular interval as per the schedule.", "instance-identifier": "A Name that uniquely identifies this configuration instance.", + "invalid-object-key": "Invalid object key. Must start with a letter, underscore, or dollar sign, followed by letters, underscores, dollar signs, or digits.", "invalid-property-name": "Invalid Property Name", "jwt-token": "Token you have generated that can be used to access the OpenMetadata API.", "kill-ingestion-warning": "Once you kill this Ingestion, all running and queued workflows will be stopped and marked as Failed.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index ab38f6e3a28..fdd26e93a98 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -1167,6 +1167,7 @@ "ingestion-pipeline-name-message": "Nombre que identifica esta instancia de pipeline de manera única.", "ingestion-pipeline-name-successfully-deployed-entity": "¡Listo! El se ha implementado correctamente. El {{entity}} se ejecutará a intervalos regulares según el horario.", "instance-identifier": "Un nombre que identifica de manera única esta instancia de configuración.", + "invalid-object-key": "Invalid object key. Must start with a letter, underscore, or dollar sign, followed by letters, underscores, dollar signs, or digits.", "invalid-property-name": "Nombre de propiedad no válido", "jwt-token": "Token que ha generado que se puede utilizar para acceder a la API de OpenMetadata.", "kill-ingestion-warning": "Una vez que se detenga esta ingestión, se detendrán y marcarán como fallidas todos los flujos de trabajo en ejecución y en cola.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 19f1f1a5c11..f8dd2823fb0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -1167,6 +1167,7 @@ "ingestion-pipeline-name-message": "Nom qui identifie de manière unique cette instance de pipeline.", "ingestion-pipeline-name-successfully-deployed-entity": "C'est bon! Le pipeline d'ingestion a été déployé avec succès. {{entity}} s'exécutera à intervalles réguliers selon le calendrier.", "instance-identifier": "Un nom qui identifie de manière unique cette instance de configuration.", + "invalid-object-key": "Invalid object key. Must start with a letter, underscore, or dollar sign, followed by letters, underscores, dollar signs, or digits.", "invalid-property-name": "Nom de propriété non valide", "jwt-token": "Le Jeton que vous avez généré peut être utilisé pour accéder à l’API OpenMetadata.", "kill-ingestion-warning": "Une fois que vous avez interrompu cette Ingestion, tous les workflows en cours d'éxécutions et en files d'attente seront arrêtés et marqués comme aillant échoué.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 2a2d4e9ca0b..83f8d6f4906 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -1167,6 +1167,7 @@ "ingestion-pipeline-name-message": "このパイプラインを一意に識別する名前", "ingestion-pipeline-name-successfully-deployed-entity": "You are all set! The has been successfully deployed. The {{entity}} will run at a regular interval as per the schedule.", "instance-identifier": "A Name that uniquely identifies this configuration instance.", + "invalid-object-key": "Invalid object key. Must start with a letter, underscore, or dollar sign, followed by letters, underscores, dollar signs, or digits.", "invalid-property-name": "無効なプロパティ名", "jwt-token": "Token you have generated that can be used to access the OpenMetadata API.", "kill-ingestion-warning": "Once you kill this Ingestion, all running and queued workflows will be stopped and marked as Failed.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index 8328e91b305..3c23b621664 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -1167,6 +1167,7 @@ "ingestion-pipeline-name-message": "Nome que identifica exclusivamente esta instância do pipeline.", "ingestion-pipeline-name-successfully-deployed-entity": "Tudo pronto! O foi implantado com sucesso. O {{entity}} será executado em intervalos regulares de acordo com o agendamento.", "instance-identifier": "Um nome que identifica exclusivamente esta instância de configuração.", + "invalid-object-key": "Invalid object key. Must start with a letter, underscore, or dollar sign, followed by letters, underscores, dollar signs, or digits.", "invalid-property-name": "Nome de propriedade inválido", "jwt-token": "Token que você gerou que pode ser usado para acessar a API do OpenMetadata.", "kill-ingestion-warning": "Uma vez que você interromper esta Ingestão, todos os fluxos de trabalho em execução e em fila serão interrompidos e marcados como Falha.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 34ff25908fc..a2bef8537e4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -1167,6 +1167,7 @@ "ingestion-pipeline-name-message": "唯一标识此工作流实例的名称", "ingestion-pipeline-name-successfully-deployed-entity": "您已准备就绪!提取工作流已成功部署,{{entity}}将按照计划定期运行。", "instance-identifier": "唯一标识此配置实例的名称", + "invalid-object-key": "Invalid object key. Must start with a letter, underscore, or dollar sign, followed by letters, underscores, dollar signs, or digits.", "invalid-property-name": "无效属性名称", "jwt-token": "您生成的,用于访问 OpenMetadata API 的令牌", "kill-ingestion-warning": "一旦您终止此提取工作流,所有正在运行和排队的工作流任务都将停止并标记为失败", diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.test.tsx new file mode 100644 index 00000000000..ec8b0783051 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { FormValidation } from '@rjsf/utils'; +import { customValidate } from './formUtils'; + +describe('customValidate', () => { + it('should add error message when connectionArguments have invalid object keys', () => { + const formData = { + connectionArguments: { + validKey: 'value', + '"invalidKey"': 'value', + }, + connectionOptions: { + option1: 'value', + }, + }; + const errors = { + addError: jest.fn(), + connectionOptions: { + addError: jest.fn(), + }, + connectionArguments: { + addError: jest.fn(), + }, + }; + + customValidate?.(formData, errors as unknown as FormValidation); + + expect(errors.connectionArguments.addError).toHaveBeenCalledWith( + 'message.invalid-object-key' + ); + expect(errors.connectionOptions.addError).not.toHaveBeenCalled(); + }); + + it('should add error message when connectionOptions have invalid object keys', () => { + const formData = { + connectionArguments: { + validKey: 'value', + }, + connectionOptions: { + option1: 'value', + 'invalid Key': 'value', + }, + }; + const errors = { + addError: jest.fn(), + connectionOptions: { + addError: jest.fn(), + }, + connectionArguments: { + addError: jest.fn(), + }, + }; + + customValidate?.(formData, errors as unknown as FormValidation); + + expect(errors.connectionArguments.addError).not.toHaveBeenCalled(); + expect(errors.connectionOptions.addError).toHaveBeenCalledWith( + 'message.invalid-object-key' + ); + }); + + it('should not add error message when all object keys are valid', () => { + const formData = { + connectionArguments: { + validKey1: 'value', + validKey2: 'value', + }, + connectionOptions: { + option1: 'value', + option2: 'value', + }, + }; + const errors = { + connectionArguments: { + addError: jest.fn(), + }, + connectionOptions: { + addError: jest.fn(), + }, + }; + + customValidate?.(formData, errors as unknown as FormValidation); + + expect(errors.connectionArguments.addError).not.toHaveBeenCalled(); + expect(errors.connectionOptions.addError).not.toHaveBeenCalled(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx index 3d5fc024264..682c44a1aef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { FormProps } from '@rjsf/core'; import { ErrorTransformer } from '@rjsf/utils'; import { Divider, @@ -31,6 +32,7 @@ import { UserTeamSelectableList } from 'components/common/UserTeamSelectableList import { UserSelectDropdownProps } from 'components/common/UserTeamSelectableList/UserTeamSelectableList.interface'; import SliderWithInput from 'components/SliderWithInput/SliderWithInput'; import { SliderWithInputProps } from 'components/SliderWithInput/SliderWithInput.interface'; +import { VALID_OBJECT_KEY_REGEX } from 'constants/regex.constants'; import { FieldProp, FieldTypes } from 'interface/FormUtils.interface'; import { compact, startCase } from 'lodash'; import TagSuggestion, { @@ -219,3 +221,31 @@ export const transformErrors: ErrorTransformer = (errors) => { return compact(errorRet); }; + +export const customValidate: FormProps['customValidate'] = ( + formData, + errors +) => { + const { connectionArguments = {}, connectionOptions = {} } = formData; + + const connectionArgumentsKeys = Object.keys(connectionArguments); + const connectionOptionsKeys = Object.keys(connectionOptions); + + const connectionArgumentsHasError = connectionArgumentsKeys.some( + (key) => !VALID_OBJECT_KEY_REGEX.test(key) + ); + + const connectionOptionsHasError = connectionOptionsKeys.some( + (key) => !VALID_OBJECT_KEY_REGEX.test(key) + ); + + if (connectionArgumentsHasError && errors?.connectionArguments) { + errors.connectionArguments?.addError(i18n.t('message.invalid-object-key')); + } + + if (connectionOptionsHasError && errors?.connectionOptions) { + errors.connectionOptions?.addError(i18n.t('message.invalid-object-key')); + } + + return errors; +};