From 6d3100b556eacfb5d2a4ee10e010417e0bfdfb82 Mon Sep 17 00:00:00 2001 From: Shane Findley Date: Fri, 30 Aug 2024 16:04:21 +0200 Subject: [PATCH] SONAR-22666 Adding oauth email support (#11653) --- .../src/components/SelectionCard.tsx | 2 + server/sonar-web/package.json | 4 +- .../main/js/api/mocks/SystemServiceMock.ts | 55 ++- server/sonar-web/src/main/js/api/system.ts | 31 +- .../components/AdditionalCategories.tsx | 6 +- .../AuthenticationSelector.tsx | 132 +++++++ .../email-notification/CommonSMTP.tsx | 63 ++++ .../email-notification/EmailNotification.tsx | 29 +- .../EmailNotificationConfiguration.tsx | 147 ++++++++ .../EmailNotificationFormField.tsx | 163 +++++++++ .../email-notification/SenderInformation.tsx | 60 ++++ .../__tests__/EmailNotification-it.tsx | 334 ++++++++++++++++++ .../components/email-notification/utils.ts | 83 +++++ .../src/main/js/helpers/mocks/system.ts | 58 +++ .../sonar-web/src/main/js/queries/system.ts | 58 ++- server/sonar-web/src/main/js/types/system.ts | 34 ++ server/sonar-web/yarn.lock | 16 + .../resources/org/sonar/l10n/core.properties | 47 ++- 18 files changed, 1291 insertions(+), 31 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/CommonSMTP.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/SenderInformation.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/__tests__/EmailNotification-it.tsx create mode 100644 server/sonar-web/src/main/js/apps/settings/components/email-notification/utils.ts create mode 100644 server/sonar-web/src/main/js/helpers/mocks/system.ts diff --git a/server/sonar-web/design-system/src/components/SelectionCard.tsx b/server/sonar-web/design-system/src/components/SelectionCard.tsx index 6c39d0d9200..82317246792 100644 --- a/server/sonar-web/design-system/src/components/SelectionCard.tsx +++ b/server/sonar-web/design-system/src/components/SelectionCard.tsx @@ -157,6 +157,8 @@ const StyledRecommended = styled.div` ${tw`sw-py-2 sw-px-4`} ${tw`sw-box-border`} ${tw`sw-rounded-b-2`} + ${tw`sw-w-full`} + ${tw`sw-text-left`} color: ${themeContrast('infoBackground')}; background-color: ${themeColor('infoBackground')}; diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index c0d1ac4c33a..5dfb4282fc8 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -15,6 +15,7 @@ "@react-spring/web": "9.7.3", "@sonarsource/echoes-react": "0.6.0", "@tanstack/react-query": "5.18.1", + "@types/validator": "13.12.0", "axios": "1.7.2", "classnames": "2.5.1", "clipboard": "2.0.11", @@ -46,7 +47,8 @@ "react-virtualized": "9.22.5", "regenerator-runtime": "0.14.1", "shared-store-hook": "0.0.4", - "valid-url": "1.0.9" + "valid-url": "1.0.9", + "validator": "13.12.0" }, "devDependencies": { "@emotion/jest": "11.11.0", diff --git a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts index 34e832b5b3f..23da678af86 100644 --- a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts @@ -17,12 +17,26 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, uniqueId } from 'lodash'; import { Provider, SysInfoCluster, SysInfoLogging, SysInfoStandalone } from '../../types/types'; import { LogsLevels } from '../../apps/system/utils'; -import { mockClusterSysInfo, mockLogs, mockStandaloneSysInfo } from '../../helpers/testMocks'; -import { getSystemInfo, getSystemUpgrades, setLogLevel } from '../system'; +import { mockEmailConfiguration } from '../../helpers/mocks/system'; +import { + mockClusterSysInfo, + mockLogs, + mockPaging, + mockStandaloneSysInfo, +} from '../../helpers/testMocks'; +import { EmailConfiguration } from '../../types/system'; +import { + getEmailConfigurations, + getSystemInfo, + getSystemUpgrades, + patchEmailConfiguration, + postEmailConfiguration, + setLogLevel, +} from '../system'; jest.mock('../system'); @@ -44,11 +58,15 @@ export default class SystemServiceMock { installedVersionActive: true, }; + emailConfigurations: EmailConfiguration[] = []; + constructor() { this.updateSystemInfo(); jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); jest.mocked(setLogLevel).mockImplementation(this.handleSetLogLevel); jest.mocked(getSystemUpgrades).mockImplementation(this.handleGetSystemUpgrades); + jest.mocked(getEmailConfigurations).mockImplementation(this.handleGetEmailConfigurations); + jest.mocked(postEmailConfiguration).mockImplementation(this.handlePostEmailConfiguration); } handleGetSystemInfo = () => { @@ -97,10 +115,41 @@ export default class SystemServiceMock { this.updateSystemInfo(); }; + handleGetEmailConfigurations: typeof getEmailConfigurations = () => { + return this.reply({ + emailConfigurations: this.emailConfigurations, + page: mockPaging({ total: this.emailConfigurations.length }), + }); + }; + + handlePostEmailConfiguration: typeof postEmailConfiguration = (configuration) => { + const returnVal = mockEmailConfiguration(configuration.authMethod, { + ...configuration, + id: uniqueId('email-configuration-'), + }); + + this.emailConfigurations.push(returnVal); + return this.reply(returnVal); + }; + + handlePatchEmailConfiguration: typeof patchEmailConfiguration = (id, configuration) => { + const index = this.emailConfigurations.findIndex((c) => c.id === id); + this.emailConfigurations[index] = mockEmailConfiguration(configuration.authMethod, { + ...this.emailConfigurations[index], + ...configuration, + }); + return this.reply(this.emailConfigurations[index]); + }; + + addEmailConfiguration = (configuration: EmailConfiguration) => { + this.emailConfigurations.push(configuration); + }; + reset = () => { this.logging = mockLogs(); this.setIsCluster(false); this.updateSystemInfo(); + this.emailConfigurations = []; }; reply(response: T): Promise { diff --git a/server/sonar-web/src/main/js/api/system.ts b/server/sonar-web/src/main/js/api/system.ts index 6640209dc60..40cfd9a7de3 100644 --- a/server/sonar-web/src/main/js/api/system.ts +++ b/server/sonar-web/src/main/js/api/system.ts @@ -21,10 +21,16 @@ import axios from 'axios'; import { throwGlobalError } from '~sonar-aligned/helpers/error'; import { getJSON } from '~sonar-aligned/helpers/request'; import { post, postJSON, requestTryAndRepeatUntil } from '../helpers/request'; -import { MigrationStatus, MigrationsStatusResponse, SystemUpgrade } from '../types/system'; -import { SysInfoCluster, SysInfoStandalone, SysStatus } from '../types/types'; +import { + EmailConfiguration, + MigrationStatus, + MigrationsStatusResponse, + SystemUpgrade, +} from '../types/system'; +import { Paging, SysInfoCluster, SysInfoStandalone, SysStatus } from '../types/types'; const MIGRATIONS_STATUS_ENDPOINT = '/api/v2/system/migrations-status'; +const EMAIL_NOTIFICATION_PATH = '/api/v2/system/email-configurations'; export function setLogLevel(level: string): Promise { return post('/api/system/change_log_level', { level }).catch(throwGlobalError); @@ -74,3 +80,24 @@ export function waitSystemUPStatus(): Promise<{ ({ status }) => status === 'UP', ); } + +export function getEmailConfigurations(): Promise<{ + emailConfigurations: EmailConfiguration[]; + page: Paging; +}> { + return axios.get(EMAIL_NOTIFICATION_PATH); +} + +export function postEmailConfiguration(data: EmailConfiguration): Promise { + return axios.post(EMAIL_NOTIFICATION_PATH, data); +} + +export function patchEmailConfiguration( + id: string, + emailConfiguration: EmailConfiguration, +): Promise { + return axios.patch( + `${EMAIL_NOTIFICATION_PATH}/${id}`, + emailConfiguration, + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx index dc913bcdcee..0411fd4065d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx @@ -107,7 +107,7 @@ export const ADDITIONAL_CATEGORIES: AdditionalCategory[] = [ }, { key: EMAIL_NOTIFICATION_CATEGORY, - name: translate('settings.email_notification.category'), + name: translate('email_notification.category'), renderComponent: getEmailNotificationComponent, availableGlobally: true, availableForProject: false, @@ -139,6 +139,6 @@ function getPullRequestDecorationBindingComponent(props: AdditionalCategoryCompo return props.component && ; } -function getEmailNotificationComponent(props: AdditionalCategoryComponentProps) { - return ; +function getEmailNotificationComponent() { + return ; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx new file mode 100644 index 00000000000..05b65957c40 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/AuthenticationSelector.tsx @@ -0,0 +1,132 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { BasicSeparator, Note, SelectionCard } from 'design-system/lib'; +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { AuthMethod } from '../../../../types/system'; +import { EmailNotificationFormField } from './EmailNotificationFormField'; +import { + BASIC_PASSWORD, + EmailNotificationGroupProps, + OAUTH_AUTHENTICATION_HOST, + OAUTH_CLIENT_ID, + OAUTH_CLIENT_SECRET, + OAUTH_TENANT, + USERNAME, +} from './utils'; + +export function AuthenticationSelector(props: Readonly) { + const { configuration, onChange } = props; + + const isOAuth = configuration?.authMethod === AuthMethod.OAuth; + + return ( +
+
+ onChange({ authMethod: AuthMethod.Basic })} + title={translate('email_notification.form.basic_auth.title')} + > + {translate('email_notification.form.basic_auth.description')} + + onChange({ authMethod: AuthMethod.OAuth })} + recommended + recommendedReason={translate('email_notification.form.oauth_auth.recommended_reason')} + title={translate('email_notification.form.oauth_auth.title')} + > + {translate('email_notification.form.oauth_auth.description')} + +
+ + onChange({ username: value })} + name={translate('email_notification.form.username')} + required + value={configuration.username} + /> + + {isOAuth ? ( + <> + onChange({ oauthAuthenticationHost: value })} + name={translate('email_notification.form.oauth_authentication_host')} + required + value={ + configuration.authMethod === AuthMethod.OAuth + ? configuration.oauthAuthenticationHost + : '' + } + /> + + onChange({ oauthClientId: value })} + name={translate('email_notification.form.oauth_client_id')} + required + type="password" + value={configuration.authMethod === AuthMethod.OAuth ? configuration.oauthClientId : ''} + /> + + onChange({ oauthClientSecret: value })} + name={translate('email_notification.form.oauth_client_secret')} + required + type="password" + value={ + configuration.authMethod === AuthMethod.OAuth ? configuration.oauthClientSecret : '' + } + /> + + onChange({ oauthTenant: value })} + name={translate('email_notification.form.oauth_tenant')} + required + value={configuration.authMethod === AuthMethod.OAuth ? configuration.oauthTenant : ''} + /> + + ) : ( + onChange({ basicPassword: value })} + name={translate('email_notification.form.basic_password')} + required + type="password" + value={configuration.authMethod === AuthMethod.Basic ? configuration.basicPassword : ''} + /> + )} + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/CommonSMTP.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/CommonSMTP.tsx new file mode 100644 index 00000000000..31266135e01 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/CommonSMTP.tsx @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { BasicSeparator } from 'design-system/lib'; +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { EmailNotificationFormField } from './EmailNotificationFormField'; +import { EmailNotificationGroupProps, HOST, PORT, SECURITY_PROTOCOL } from './utils'; + +export function CommonSMTP(props: Readonly) { + const { configuration, onChange } = props; + + return ( +
+ onChange({ host: value })} + name={translate('email_notification.form.host')} + required + value={configuration.host} + /> + + onChange({ port: value })} + name={translate('email_notification.form.port')} + required + type="number" + value={configuration.port} + /> + + onChange({ securityProtocol: value })} + name={translate('email_notification.form.security_protocol')} + options={['NONE', 'STARTTLS', 'SSLTLS']} + required + type="select" + value={configuration.securityProtocol} + /> + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx index b4bd95e965d..2895abd0f8f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotification.tsx @@ -17,30 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { SubTitle } from 'design-system'; +import { Spinner } from '@sonarsource/echoes-react'; +import { SubTitle } from 'design-system/lib'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { AdditionalCategoryComponentProps } from '../AdditionalCategories'; -import CategoryDefinitionsList from '../CategoryDefinitionsList'; -import EmailForm from './EmailForm'; +import { useGetEmailConfiguration } from '../../../../queries/system'; +import EmailNotificationConfiguration from './EmailNotificationConfiguration'; -export default function EmailNotification(props: Readonly) { - const { component, definitions } = props; +export default function EmailNotification() { + const { data: configuration, isLoading } = useGetEmailConfiguration(); return ( -
+
- + - - + + + +
); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx new file mode 100644 index 00000000000..48f98ff7085 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationConfiguration.tsx @@ -0,0 +1,147 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { Button, ButtonVariety } from '@sonarsource/echoes-react'; +import { NumberedList, NumberedListItem } from 'design-system/lib'; +import React, { useCallback, useEffect } from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { + useSaveEmailConfigurationMutation, + useUpdateEmailConfigurationMutation, +} from '../../../../queries/system'; +import { AuthMethod, EmailConfiguration } from '../../../../types/system'; +import { AuthenticationSelector } from './AuthenticationSelector'; +import { CommonSMTP } from './CommonSMTP'; +import { SenderInformation } from './SenderInformation'; +import { checkEmailConfigurationValidity } from './utils'; + +interface Props { + emailConfiguration: EmailConfiguration | null; +} + +const FORM_ID = 'email-notifications'; +const EMAIL_CONFIGURATION_DEFAULT: EmailConfiguration = { + authMethod: AuthMethod.Basic, + basicPassword: '', + fromAddress: '', + fromName: 'SonarQube', + host: '', + port: '587', + securityProtocol: '', + subjectPrefix: '[SonarQube]', + username: '', +}; + +export default function EmailNotificationConfiguration(props: Readonly) { + const { emailConfiguration } = props; + const [canSave, setCanSave] = React.useState(false); + + const [newConfiguration, setNewConfiguration] = React.useState( + EMAIL_CONFIGURATION_DEFAULT, + ); + + const { mutateAsync: saveEmailConfiguration } = useSaveEmailConfigurationMutation(); + const { mutateAsync: updateEmailConfiguration } = useUpdateEmailConfigurationMutation(); + + const onChange = useCallback( + (newValue: Partial) => { + const newConfig = { + ...newConfiguration, + ...newValue, + }; + setCanSave(checkEmailConfigurationValidity(newConfig as EmailConfiguration)); + setNewConfiguration(newConfig as EmailConfiguration); + }, + [newConfiguration, setNewConfiguration], + ); + + const onSubmit = useCallback( + (event: React.SyntheticEvent) => { + event.preventDefault(); + if (canSave && newConfiguration) { + const authConfiguration = + newConfiguration.authMethod === AuthMethod.OAuth + ? { + oauthAuthenticationHost: newConfiguration.oauthAuthenticationHost, + oauthClientId: newConfiguration.oauthClientId, + oauthClientSecret: newConfiguration.oauthClientSecret, + oauthTenant: newConfiguration.oauthTenant, + } + : { + basicPassword: newConfiguration.basicPassword, + }; + + const newEmailConfiguration = { + ...newConfiguration, + ...authConfiguration, + }; + + if (newConfiguration?.id === undefined) { + saveEmailConfiguration(newEmailConfiguration); + } else { + newEmailConfiguration.id = newConfiguration.id; + updateEmailConfiguration({ + emailConfiguration: newEmailConfiguration, + id: newEmailConfiguration.id, + }); + } + } + }, + [canSave, newConfiguration, saveEmailConfiguration, updateEmailConfiguration], + ); + + useEffect(() => { + if (emailConfiguration !== null) { + setNewConfiguration(emailConfiguration); + } + }, [emailConfiguration]); + + return ( +
+ + + + {translate('email_notification.subheading.1')} + + + + + + {translate('email_notification.subheading.2')} + + + + + + {translate('email_notification.subheading.3')} + + + + + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx new file mode 100644 index 00000000000..d881be0d2d8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/email-notification/EmailNotificationFormField.tsx @@ -0,0 +1,163 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { InputSize, Select } from '@sonarsource/echoes-react'; +import { FormField, InputField, TextError } from 'design-system/lib'; +import { isEmpty, isUndefined } from 'lodash'; +import React from 'react'; +import isEmail from 'validator/lib/isEmail'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; + +type InputType = 'email' | 'number' | 'password' | 'select' | 'text'; + +interface Props { + children?: (props: { onChange: (value: string) => void }) => React.ReactNode; + description: string; + id: string; + name: string; + onChange: (value: string) => void; + options?: string[]; + required?: boolean; + type?: InputType; + value: string | undefined; +} + +export function EmailNotificationFormField(props: Readonly) { + const { description, id, name, options, required, type = 'text', value } = props; + + const [validationMessage, setValidationMessage] = React.useState(); + + const handleCheck = (changedValue?: string) => { + if (isEmpty(changedValue) && required) { + setValidationMessage(translate('settings.state.value_cant_be_empty_no_default')); + return false; + } + + if (type === 'email' && !isEmail(changedValue ?? '')) { + setValidationMessage(translate('email_notification.state.value_should_be_valid_email')); + return false; + } + + setValidationMessage(undefined); + return true; + }; + + const onChange = (newValue: string) => { + handleCheck(newValue); + props.onChange(newValue); + }; + + const hasValidationMessage = !isUndefined(validationMessage); + + return ( + +
+ {type === 'select' ? ( + + ) : ( + + )} + + {hasValidationMessage && ( + + )} +
+ +
+ {!isUndefined(description) &&
{description}
} +
+
+ ); +} + +function BasicInput( + props: Readonly<{ + id: string; + name: string; + onChange: (value: string) => void; + required?: boolean; + type: InputType; + value: string | undefined; + }>, +) { + const { id, onChange, name, required, type, value } = props; + + return ( + onChange(event.target.value)} + required={required} + size="large" + step={type === 'number' ? 1 : undefined} + type={type} + value={value ?? ''} + /> + ); +} + +function SelectInput( + props: Readonly<{ + id: string; + name: string; + onChange: (value: string) => void; + options: string[]; + required?: boolean; + value: string | undefined; + }>, +) { + const { id, name, onChange, options, required, value } = props; + + return ( +