Fix(UI): Update the file upload option input for service config (#21490)

* Update the file upload option input for service config

* Separate out the field rendering for 'uiFieldType' as fileOrInput and file.

---------

Co-authored-by: Sriharsha Chintalapani <harshach@users.noreply.github.com>
This commit is contained in:
Aniket Katkar 2025-06-09 16:19:28 +05:30 committed by GitHub
parent 0fb88ea0ce
commit 9d458cd8bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 211 additions and 37 deletions

View File

@ -13,7 +13,7 @@
"type": "string",
"format": "password",
"accept": [".pem", ".crt", ".cer", ".der", ".p12"],
"uiFieldType": "file"
"uiFieldType": "fileOrInput"
},
"sslCertificate": {
"title": "SSL Certificate",
@ -21,7 +21,7 @@
"type": "string",
"format": "password",
"accept": [".pem", ".crt", ".cer", ".der", ".p12"],
"uiFieldType": "file"
"uiFieldType": "fileOrInput"
},
"sslKey": {
"title": "SSL Key",
@ -29,7 +29,7 @@
"type": "string",
"format": "password",
"accept": [".pem", ".crt", ".cer", ".der", ".p12"],
"uiFieldType": "file"
"uiFieldType": "fileOrInput"
}
}
}

View File

@ -44,6 +44,7 @@ const ConfigureService = ({
],
props: {
'data-testid': 'service-name',
autoFocus: true,
},
placeholder: t('label.service-name'),
formItemProps: {

View File

@ -20,7 +20,12 @@ import { useTranslation } from 'react-i18next';
import { FileUploadEnum } from '../../../../../enums/File.enum';
import { showErrorToast } from '../../../../../utils/ToastUtils';
const FileUploadWidget: FC<WidgetProps> = ({ onChange, onFocus, ...rest }) => {
const FileUploadWidget: FC<WidgetProps> = ({
onChange,
onFocus,
disabled,
...rest
}) => {
const { t } = useTranslation();
const defaultValue = useMemo((): UploadFile[] => {
@ -79,7 +84,9 @@ const FileUploadWidget: FC<WidgetProps> = ({ onChange, onFocus, ...rest }) => {
onChange={handleChange}>
<Button
data-testid="upload-file-widget-content"
disabled={disabled}
icon={<UploadOutlined />}
size="small"
onFocus={() => onFocus(rest.id, rest.value)}>
{t('message.upload-file')}
</Button>

View File

@ -21,7 +21,11 @@ import {
import PasswordWidget from './PasswordWidget';
jest.mock('./FileUploadWidget', () =>
jest.fn().mockReturnValue(<p>FileUploadWidget</p>)
jest
.fn()
.mockImplementation(({ disabled }) => (
<button disabled={disabled}>FileUploadWidget</button>
))
);
const mockOnFocus = jest.fn();
@ -48,7 +52,9 @@ describe('Test PasswordWidget Component', () => {
it('Should render select component', async () => {
render(<PasswordWidget {...mockProps} />);
const passwordInput = screen.getByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/password'
);
const FileUploadWidget = screen.queryByText('FileUploadWidget');
expect(passwordInput).toBeInTheDocument();
@ -58,7 +64,9 @@ describe('Test PasswordWidget Component', () => {
it('Should be disabled', async () => {
render(<PasswordWidget {...mockProps} disabled />);
const passwordInput = screen.getByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/password'
);
expect(passwordInput).toBeDisabled();
});
@ -66,7 +74,9 @@ describe('Test PasswordWidget Component', () => {
it('Should call onFocus', async () => {
render(<PasswordWidget {...mockProps} />);
const passwordInput = screen.getByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/password'
);
fireEvent.focus(passwordInput);
@ -76,7 +86,9 @@ describe('Test PasswordWidget Component', () => {
it('Should call onBlur', async () => {
render(<PasswordWidget {...mockProps} />);
const passwordInput = screen.getByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/password'
);
fireEvent.blur(passwordInput);
@ -86,7 +98,9 @@ describe('Test PasswordWidget Component', () => {
it('Should call onChange', async () => {
render(<PasswordWidget {...mockProps} />);
const passwordInput = screen.getByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/password'
);
fireEvent.change(passwordInput, { target: { value: 'password' } });
@ -96,7 +110,9 @@ describe('Test PasswordWidget Component', () => {
it('Should call onChange with asterisk', async () => {
render(<PasswordWidget {...mockProps} />);
const passwordInput = screen.getByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/password'
);
fireEvent.change(passwordInput, { target: { value: '*******' } });
@ -106,18 +122,52 @@ describe('Test PasswordWidget Component', () => {
it('Should not show password if the value is masked', async () => {
render(<PasswordWidget {...mockProps} />);
const passwordInput = screen.getByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/password'
);
expect(passwordInput).toHaveValue('');
});
it('Should render FileWidget component if uiFieldType is file', async () => {
it('Should render FileWidget and password input if uiFieldType is fileOrInput', async () => {
render(<PasswordWidget {...mockProps2} />);
const passwordInput = screen.queryByTestId('password-input-widget');
const passwordInput = screen.getByTestId(
'password-input-widget-root/sslConfig/caCertificate'
);
const fileUploadWidget = screen.getByText('FileUploadWidget');
expect(fileUploadWidget).toBeInTheDocument();
expect(passwordInput).not.toBeInTheDocument();
expect(passwordInput).toBeInTheDocument();
// Check if the password input is disabled
expect(passwordInput).toBeDisabled();
// Click on the Enter file content radio button
const enterFileContentRadioButton = screen.getByTestId('radio-file-path');
fireEvent.click(enterFileContentRadioButton);
// Check if the password input is enabled
expect(passwordInput).toBeEnabled();
// Check if the file upload widget is disabled
expect(fileUploadWidget).toBeDisabled();
});
it('Should render only FileWidget uiFieldType is file', async () => {
render(
<PasswordWidget
{...mockProps2}
schema={{ ...mockProps2.schema, uiFieldType: 'file' }}
/>
);
const passwordInput = screen.queryByTestId(
'password-input-widget-root/sslConfig/caCertificate'
);
const fileUploadWidget = screen.getByText('FileUploadWidget');
expect(fileUploadWidget).toBeInTheDocument();
expect(passwordInput).toBeNull();
});
});

View File

@ -11,12 +11,25 @@
* limitations under the License.
*/
import { WidgetProps } from '@rjsf/utils';
import { Input } from 'antd';
import React, { FC, useMemo } from 'react';
import { Col, Input, Radio, RadioChangeEvent, Row, Typography } from 'antd';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ALL_ASTERISKS_REGEX } from '../../../../../constants/regex.constants';
import { CertificationInputType } from '../../../../../enums/PasswordWidget.enum';
import FileUploadWidget from './FileUploadWidget';
import './password-widget.less';
const PasswordWidget: FC<WidgetProps> = (props) => {
const { t } = useTranslation();
const [inputType, setInputType] = useState<CertificationInputType>(
props.schema.uiFieldType === 'fileOrInput'
? CertificationInputType.FILE_UPLOAD
: CertificationInputType.FILE_PATH
);
const isInputTypeFile = props.schema.uiFieldType === 'file';
const isInputTypeFileOrInput = props.schema.uiFieldType === 'fileOrInput';
const passwordWidgetValue = useMemo(() => {
if (ALL_ASTERISKS_REGEX.test(props.value)) {
return undefined; // Do not show the password if it is masked
@ -25,29 +38,72 @@ const PasswordWidget: FC<WidgetProps> = (props) => {
}
}, [props.value]);
if (props.schema.uiFieldType === 'file') {
const getPasswordInput = useCallback(
(disabled?: boolean) => (
<Input.Password
autoComplete="off"
autoFocus={props.autofocus}
data-testid={`password-input-widget-${props.id}`}
disabled={disabled || props.disabled}
id={props.id}
name={props.name}
placeholder={props.placeholder}
readOnly={props.readonly}
required={props.required}
value={passwordWidgetValue}
onBlur={() => props.onBlur(props.id, props.value)}
onChange={(e) => props.onChange(e.target.value)}
onFocus={() => props.onFocus(props.id, props.value)}
/>
),
[props]
);
const onRadioChange = (e: RadioChangeEvent) => {
setInputType(e.target.value);
};
if (isInputTypeFile) {
return <FileUploadWidget {...props} />;
}
const { onFocus, onBlur, onChange, ...rest } = props;
if (isInputTypeFileOrInput) {
return (
<Radio.Group
className="password-widget"
data-testid={`password-input-radio-group-${props.id}`}
value={inputType}
onChange={onRadioChange}>
<Row>
<Col span={8}>
<Radio
className="widget-radio-option"
data-testid={`radio-${CertificationInputType.FILE_UPLOAD}`}
value={CertificationInputType.FILE_UPLOAD}>
<Typography.Text>{t('message.upload-file')}</Typography.Text>
<FileUploadWidget
{...props}
disabled={inputType === CertificationInputType.FILE_PATH}
/>
</Radio>
</Col>
<Col span={16}>
<Radio
className="widget-radio-option"
data-testid={`radio-${CertificationInputType.FILE_PATH}`}
value={CertificationInputType.FILE_PATH}>
<Typography.Text>{t('label.enter-file-content')}</Typography.Text>
{getPasswordInput(
inputType === CertificationInputType.FILE_UPLOAD
)}
</Radio>
</Col>
</Row>
</Radio.Group>
);
}
return (
<Input.Password
autoComplete="off"
autoFocus={rest.autofocus}
data-testid="password-input-widget"
disabled={rest.disabled}
id={rest.id}
name={rest.name}
placeholder={rest.placeholder}
readOnly={rest.readonly}
required={rest.required}
value={passwordWidgetValue}
onBlur={() => onBlur(rest.id, rest.value)}
onChange={(e) => onChange(e.target.value)}
onFocus={() => onFocus(rest.id, rest.value)}
/>
);
return getPasswordInput();
};
export default PasswordWidget;

View File

@ -0,0 +1,26 @@
/*
* Copyright 2025 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.
*/
.password-widget {
display: block;
.widget-radio-option {
width: 100%;
& > span:nth-child(2) {
display: inline-flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2025 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.
*/
export enum CertificationInputType {
FILE_UPLOAD = 'file-upload',
FILE_PATH = 'file-path',
}

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Geben Sie einen Namen für {{entity}} ein",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Geben Sie eine Beschreibung für {{field}} ein",
"enter-file-content": "Dateiinhalt eingeben",
"enter-property-value": "Geben Sie einen Wert für die Eigenschaft ein",
"enter-type-password": "{{type}}-Passwort eingeben",
"entity": "Entität",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Enter {{entity}} name",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Enter {{field}} description",
"enter-file-content": "Enter file content",
"enter-property-value": "Enter Property Value",
"enter-type-password": "Enter {{type}} Password",
"entity": "Entity",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Ingrese el nombre de {{entity}}",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Ingrese la descripción de {{field}}",
"enter-file-content": "Contenido del archivo",
"enter-property-value": "Ingrese el valor de la propiedad",
"enter-type-password": "Ingrese la contraseña de {{type}}",
"entity": "Entidad",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Entrer un nom pour {{entity}}",
"enter-entity-value": "Entrer une valeur pour {{entity}}",
"enter-field-description": "Entrer une description pour {{field}}",
"enter-file-content": "Entrer le contenu du fichier",
"enter-property-value": "Entrer une valeur pour la propriété",
"enter-type-password": "Entrer un mot de passe {{type}}",
"entity": "Entity",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Introducir o nome de {{entity}}",
"enter-entity-value": "Introducir o valor de {{entity}}",
"enter-field-description": "Introducir a descrición de {{field}}",
"enter-file-content": "Introducir o contido do ficheiro",
"enter-property-value": "Introducir o valor da propiedade",
"enter-type-password": "Introducir contrasinal de {{type}}",
"entity": "Entidade",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "הזן שם {{entity}}",
"enter-entity-value": "הזן ערך {{entity}}",
"enter-field-description": "הזן תיאור של {{field}}",
"enter-file-content": "הזן תוכן קובץ",
"enter-property-value": "הזן ערך נכס",
"enter-type-password": "הזן סיסמה עבור {{type}}",
"entity": "ישות",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "{{entity}}の名前を入力",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "{{field}}の説明を入力",
"enter-file-content": "ファイルの内容を入力",
"enter-property-value": "プロパティの値を入力",
"enter-type-password": "{{type}} のパスワードを入力",
"entity": "Entity",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "{{entity}} 이름 입력",
"enter-entity-value": "{{entity}} 값 입력",
"enter-field-description": "{{field}} 설명 입력",
"enter-file-content": "파일 내용 입력",
"enter-property-value": "속성 값 입력",
"enter-type-password": "{{type}} 비밀번호 입력",
"entity": "엔티티",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "{{entity}} नाव प्रविष्ट करा",
"enter-entity-value": "{{entity}} मूल्य प्रविष्ट करा",
"enter-field-description": "{{field}} वर्णन प्रविष्ट करा",
"enter-file-content": "फाइल मजकूर प्रविष्ट करा",
"enter-property-value": "गुणधर्म मूल्य प्रविष्ट करा",
"enter-type-password": "{{type}} पासवर्ड प्रविष्ट करा",
"entity": "घटक",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "{{entity}}-naam invoeren",
"enter-entity-value": "{{entity}}-waarde invoeren",
"enter-field-description": "{{field}}-beschrijving invoeren",
"enter-file-content": "Bestand inhoud invoeren",
"enter-property-value": "Eigenschapswaarde invoeren",
"enter-type-password": "{{type}}-wachtwoord invoeren",
"entity": "Entiteit",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "نام {{entity}} را وارد کنید",
"enter-entity-value": "مقدار {{entity}} را وارد کنید",
"enter-field-description": "توضیحات {{field}} را وارد کنید",
"enter-file-content": "فایل محتوا را وارد کنید",
"enter-property-value": "مقدار ویژگی را وارد کنید",
"enter-type-password": "رمز عبور {{type}} را وارد کنید",
"entity": "نهاد",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Inserir nome de {{entity}}",
"enter-entity-value": "Inserir Valor de {{entity}}",
"enter-field-description": "Inserir descrição de {{field}}",
"enter-file-content": "Insira o conteúdo do arquivo",
"enter-property-value": "Inserir Valor da Propriedade",
"enter-type-password": "Inserir Senha de {{type}}",
"entity": "Entidade",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Inserir nome de {{entity}}",
"enter-entity-value": "Inserir Valor de {{entity}}",
"enter-field-description": "Inserir descrição de {{field}}",
"enter-file-content": "Insira o conteúdo do arquivo",
"enter-property-value": "Inserir Valor da Propriedade",
"enter-type-password": "Inserir Senha de {{type}}",
"entity": "Entidade",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "Введите имя {{entity}}",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Введите описание {{field}}",
"enter-file-content": "Введите содержимое файла",
"enter-property-value": "Введите значение свойства",
"enter-type-password": "Введите {{type}} пароль",
"entity": "Entity",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "ป้อนชื่อ {{entity}}",
"enter-entity-value": "ป้อนค่าของ {{entity}}",
"enter-field-description": "ป้อนคำอธิบาย {{field}}",
"enter-file-content": "ป้อนเนื้อหาของไฟล์",
"enter-property-value": "ป้อนค่าของคุณสมบัติ",
"enter-type-password": "ป้อนรหัสผ่าน {{type}}",
"entity": "เอนทิตี",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "{{entity}} adı girin",
"enter-entity-value": "{{entity}} Değeri Girin",
"enter-field-description": "{{field}} açıklaması girin",
"enter-file-content": "Dosya içeriğini girin",
"enter-property-value": "Özellik Değeri Girin",
"enter-type-password": "{{type}} Şifresini Girin",
"entity": "Varlık",

View File

@ -487,6 +487,7 @@
"enter-entity-name": "输入{{entity}}的名称",
"enter-entity-value": "输入{{entity}}的值",
"enter-field-description": "输入{{field}}的描述",
"enter-file-content": "输入文件内容",
"enter-property-value": "输入属性值",
"enter-type-password": "输入{{type}}密码",
"entity": "实体",

View File

@ -58,7 +58,7 @@ export const MOCK_FILE_SELECT_WIDGET = {
description: 'The CA certificate used for SSL validation.',
format: 'password',
accept: ['.pem'],
uiFieldType: 'file',
uiFieldType: 'fileOrInput',
},
uiSchema: {},
value: MOCK_SSL_FILE_CONTENT,