diff options
9 files changed, 191 insertions, 172 deletions
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx index 244705fad3f..dc21f8ca35a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx @@ -17,16 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import styled from '@emotion/styled'; +import { FormField, Highlight, InputField, Note, RequiredIcon } from 'design-system'; import React from 'react'; +import { useIntl } from 'react-intl'; import ValidationInput, { ValidationInputErrorPlacement, } from '../../../../components/controls/ValidationInput'; import { DefinitionV2, ExtendedSettingDefinition, SettingType } from '../../../../types/settings'; import { getPropertyDescription, getPropertyName, isSecuredDefinition } from '../../utils'; -import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper'; import AuthenticationMultiValueField from './AuthenticationMultiValuesField'; import AuthenticationSecuredField from './AuthenticationSecuredField'; -import AuthenticationToggleField from './AuthenticationToggleField'; +import AuthenticationToggleFormField from './AuthenticationToggleField'; interface Props { settingValue?: string | boolean | string[]; @@ -40,15 +42,44 @@ interface Props { export default function AuthenticationFormField(props: Readonly<Props>) { const { mandatory = false, definition, settingValue, isNotSet, error } = props; + const intl = useIntl(); + const name = getPropertyName(definition); const description = getPropertyDescription(definition); + if (!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN) { + return ( + <> + <div className="sw-flex"> + <Highlight className="sw-mb-4 sw-mr-4 sw-flex sw-items-center sw-gap-2"> + <StyledLabel aria-label={name} htmlFor={definition.key}> + {name} + {mandatory && ( + <RequiredIcon + aria-label={intl.formatMessage({ id: 'required' })} + className="sw-ml-1" + /> + )} + </StyledLabel> + </Highlight> + <AuthenticationToggleFormField + definition={definition} + settingValue={settingValue as string | boolean} + onChange={(value) => props.onFieldChange(definition.key, value)} + /> + </div> + {description && <Note className="sw-mt-2">{description}</Note>} + </> + ); + } + return ( - <AuthenticationFormFieldWrapper - title={name} - defKey={definition.key} - mandatory={mandatory} + <FormField + htmlFor={definition.key} + ariaLabel={name} + label={name} description={description} + required={mandatory} > {definition.multiValues && ( <AuthenticationMultiValueField @@ -65,13 +96,6 @@ export default function AuthenticationFormField(props: Readonly<Props>) { isNotSet={isNotSet} /> )} - {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && ( - <AuthenticationToggleField - definition={definition} - settingValue={settingValue as string | boolean} - onChange={(value) => props.onFieldChange(definition.key, value)} - /> - )} {!isSecuredDefinition(definition) && definition.type === undefined && !definition.multiValues && ( @@ -81,8 +105,8 @@ export default function AuthenticationFormField(props: Readonly<Props>) { isValid={false} isInvalid={Boolean(error)} > - <input - className="width-100" + <InputField + size="full" id={definition.key} maxLength={4000} name={definition.key} @@ -92,6 +116,12 @@ export default function AuthenticationFormField(props: Readonly<Props>) { /> </ValidationInput> )} - </AuthenticationFormFieldWrapper> + </FormField> ); } + +// This is needed to prevent the target input/button from being focused +// when clicking/hovering on the label. More info https://stackoverflow.com/questions/9098581/why-is-hover-for-input-triggered-on-corresponding-label-in-css +const StyledLabel = styled.label` + pointer-events: none; +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx index e3bcc9a384b..d10f23ab05d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationMultiValuesField.tsx @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DestructiveIcon, InputField, TrashIcon } from 'design-system'; import * as React from 'react'; -import { DeleteButton } from '../../../../components/controls/buttons'; import { translateWithParameters } from '../../../../helpers/l10n'; import { DefinitionV2, ExtendedSettingDefinition } from '../../../../types/settings'; import { getPropertyName } from '../../utils'; @@ -52,10 +52,10 @@ export default function AuthenticationMultiValueField(props: Props) { {displayValue.map((value, index) => { const isNotLast = index !== displayValue.length - 1; return ( - <li className="spacer-bottom" key={index}> - <input - className="width-80" + <li className="sw-flex sw-mb-2" key={index}> + <InputField id={definition.key} + size="large" maxLength={4000} name={definition.key} onChange={(e) => handleSingleInputChange(index, e.currentTarget.value)} @@ -64,8 +64,9 @@ export default function AuthenticationMultiValueField(props: Props) { /> {isNotLast && ( - <div className="display-inline-block spacer-left"> - <DeleteButton + <div className="sw-ml-2"> + <DestructiveIcon + Icon={TrashIcon} className="js-remove-value" aria-label={translateWithParameters( 'settings.definition.delete_value', diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx index 5fe167ee2cb..7aea3852525 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationSecuredField.tsx @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ButtonSecondary, InputField, InputTextArea } from 'design-system'; import React, { useEffect } from 'react'; -import { ButtonLink } from '../../../../components/controls/buttons'; import { translate } from '../../../../helpers/l10n'; import { DefinitionV2, ExtendedSettingDefinition, SettingType } from '../../../../types/settings'; import { isSecuredDefinition } from '../../utils'; @@ -45,8 +45,8 @@ export default function AuthenticationSecuredField(props: SamlToggleFieldProps) <> {!showSecretField && (definition.type === SettingType.TEXT ? ( - <textarea - className="width-100" + <InputTextArea + size="full" id={definition.key} maxLength={4000} onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)} @@ -55,8 +55,8 @@ export default function AuthenticationSecuredField(props: SamlToggleFieldProps) value={settingValue ?? ''} /> ) : ( - <input - className="width-100" + <InputField + size="full" id={definition.key} maxLength={4000} name={definition.key} @@ -66,15 +66,15 @@ export default function AuthenticationSecuredField(props: SamlToggleFieldProps) /> ))} {showSecretField && ( - <div> - <p>{translate('settings.almintegration.form.secret.field')}</p> - <ButtonLink + <div className="sw-flex sw-items-center"> + <p className="sw-mr-2">{translate('settings.almintegration.form.secret.field')}</p> + <ButtonSecondary onClick={() => { setShowSecretField(false); }} > {translate('settings.almintegration.form.secret.update_field')} - </ButtonLink> + </ButtonSecondary> </div> )} </> diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx index af61c60f350..95f3f8cf671 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationToggleField.tsx @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Switch } from 'design-system'; import React from 'react'; -import Toggle from '../../../../components/controls/Toggle'; import { DefinitionV2, ExtendedSettingDefinition } from '../../../../types/settings'; +import { getPropertyName } from '../../utils'; interface SamlToggleFieldProps { onChange: (value: boolean) => void; @@ -30,10 +31,11 @@ interface SamlToggleFieldProps { export default function AuthenticationToggleField(props: SamlToggleFieldProps) { const { settingValue, definition } = props; + const label = getPropertyName(definition); + return ( - <Toggle - ariaLabel={definition.key} - name={definition.key} + <Switch + labels={{ on: label, off: label }} onChange={props.onChange} value={settingValue ?? ''} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx index 20dea07fa28..fdd9db6cf0e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx @@ -17,14 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ButtonPrimary, FlagMessage, Modal, Spinner } from 'design-system'; import { keyBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import DocLink from '../../../../components/common/DocLink'; -import Modal from '../../../../components/controls/Modal'; -import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons'; -import { Alert } from '../../../../components/ui/Alert'; -import Spinner from '../../../../components/ui/Spinner'; +import DocumentationLink from '../../../../components/common/DocumentationLink'; import { translate } from '../../../../helpers/l10n'; import { useSaveValuesMutation } from '../../../../queries/settings'; import { Dict } from '../../../../types/types'; @@ -64,7 +61,7 @@ export default function ConfigurationForm(props: Props) { const { mutateAsync: changeConfig } = useSaveValuesMutation(); - const headerLabel = translate('settings.authentication.form', create ? 'create' : 'edit', tab); + const header = translate('settings.authentication.form', create ? 'create' : 'edit', tab); const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => { event.preventDefault(); @@ -88,71 +85,68 @@ export default function ConfigurationForm(props: Props) { } }; - return ( - <Modal - contentLabel={headerLabel} - onRequestClose={props.onClose} - shouldCloseOnOverlayClick={false} - shouldCloseOnEsc - size="medium" - > - <form onSubmit={handleSubmit}> - <div className="modal-head"> - <h2>{headerLabel}</h2> - </div> - <div className="modal-body modal-container"> - <Spinner loading={loading} ariaLabel={translate('settings.authentication.form.loading')}> - <Alert variant={hasLegacyConfiguration ? 'warning' : 'info'}> - <FormattedMessage - id={`settings.authentication.${ - hasLegacyConfiguration ? `legacy_help.${tab}` : 'help' - }`} - defaultMessage={translate( - `settings.authentication.${ - hasLegacyConfiguration ? `legacy_help.${tab}` : 'help' - }`, - )} - values={{ - link: ( - <DocLink - to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[tab]}/`} - > - {translate('settings.authentication.help.link')} - </DocLink> - ), - }} - /> - </Alert> - {Object.values(values).map((val) => { - if (excludedField.includes(val.key)) { - return null; - } + const helpMessage = hasLegacyConfiguration ? `legacy_help.${tab}` : 'help'; + + const FORM_ID = 'configuration-form'; + + const formBody = ( + <form id={FORM_ID} onSubmit={handleSubmit}> + <Spinner loading={loading} ariaLabel={translate('settings.authentication.form.loading')}> + <FlagMessage + className="sw-w-full sw-mb-8" + variant={hasLegacyConfiguration ? 'warning' : 'info'} + > + <span> + <FormattedMessage + id={`settings.authentication.${helpMessage}`} + defaultMessage={translate(`settings.authentication.${helpMessage}`)} + values={{ + link: ( + <DocumentationLink + to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[tab]}/`} + > + {translate('settings.authentication.help.link')} + </DocumentationLink> + ), + }} + /> + </span> + </FlagMessage> + {Object.values(values).map((val) => { + if (excludedField.includes(val.key)) { + return null; + } - const isSet = hasLegacyConfiguration ? false : !val.isNotSet; - return ( - <div key={val.key}> - <AuthenticationFormField - settingValue={values[val.key]?.newValue ?? values[val.key]?.value} - definition={val.definition} - mandatory={val.mandatory} - onFieldChange={setNewValue} - isNotSet={!isSet} - error={errors[val.key]?.message} - /> - </div> - ); - })} - </Spinner> - </div> + const isSet = hasLegacyConfiguration ? false : !val.isNotSet; + return ( + <div key={val.key} className="sw-mb-8"> + <AuthenticationFormField + settingValue={values[val.key]?.newValue ?? values[val.key]?.value} + definition={val.definition} + mandatory={val.mandatory} + onFieldChange={setNewValue} + isNotSet={!isSet} + error={errors[val.key]?.message} + /> + </div> + ); + })} + </Spinner> + </form> + ); - <div className="modal-foot"> - <SubmitButton disabled={!canBeSave}> - {translate('settings.almintegration.form.save')} - <Spinner className="spacer-left" loading={loading} /> - </SubmitButton> - <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink> - </div> - </form> - </Modal> + return ( + <Modal + headerTitle={header} + isScrollable + onClose={props.onClose} + body={formBody} + primaryButton={ + <ButtonPrimary form={FORM_ID} type="submit" autoFocus disabled={!canBeSave}> + {translate('settings.almintegration.form.save')} + <Spinner className="sw-ml-2" loading={loading} /> + </ButtonPrimary> + } + /> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx index 3321b646714..8851567b3dc 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx @@ -17,14 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { ButtonPrimary, FlagMessage, Modal, Spinner } from 'design-system'; import { isArray, keyBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import DocLink from '../../../../components/common/DocLink'; -import Modal from '../../../../components/controls/Modal'; -import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons'; -import { Alert } from '../../../../components/ui/Alert'; -import Spinner from '../../../../components/ui/Spinner'; +import DocumentationLink from '../../../../components/common/DocumentationLink'; import { translate } from '../../../../helpers/l10n'; import { useCreateGitLabConfigurationMutation, @@ -117,10 +115,7 @@ export default function GitLabConfigurationForm(props: Readonly<Props>) { }, }); - const headerLabel = translate( - 'settings.authentication.gitlab.form', - isCreate ? 'create' : 'edit', - ); + const header = translate('settings.authentication.gitlab.form', isCreate ? 'create' : 'edit'); const canBeSaved = Object.values(formData).every(({ definition, required, value }) => { return ( @@ -159,62 +154,59 @@ export default function GitLabConfigurationForm(props: Readonly<Props>) { } }; - return ( - <Modal - contentLabel={headerLabel} - onRequestClose={props.onClose} - shouldCloseOnOverlayClick={false} - shouldCloseOnEsc - size="medium" - > - <form onSubmit={handleSubmit}> - <div className="modal-head"> - <h2>{headerLabel}</h2> - </div> - <div className="modal-body modal-container"> - <Alert variant="info"> - <FormattedMessage - id="settings.authentication.help" - values={{ - link: ( - <DocLink - to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES.gitlab}/`} - > - {translate('settings.authentication.help.link')} - </DocLink> - ), + const FORM_ID = 'gitlab-configuration-form'; + + const formBody = ( + <form id={FORM_ID} onSubmit={handleSubmit}> + <FlagMessage variant="info" className="sw-w-full sw-mb-8"> + <span> + <FormattedMessage + id="settings.authentication.help" + values={{ + link: ( + <DocumentationLink + to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES.gitlab}/`} + > + {translate('settings.authentication.help.link')} + </DocumentationLink> + ), + }} + /> + </span> + </FlagMessage> + {Object.entries(formData).map( + ([key, { value, required, definition }]: [ + key: keyof GitLabConfigurationCreateBody, + FormData, + ]) => ( + <div key={key} className="sw-mb-8"> + <AuthenticationFormField + settingValue={value} + definition={definition} + mandatory={required} + onFieldChange={(_, value) => { + setFormData((prev) => ({ ...prev, [key]: { ...prev[key], value } })); }} + isNotSet={isCreate} + error={errors[key]?.message} /> - </Alert> - {Object.entries(formData).map( - ([key, { value, required, definition }]: [ - key: keyof GitLabConfigurationCreateBody, - FormData, - ]) => ( - <div key={key}> - <AuthenticationFormField - settingValue={value} - definition={definition} - mandatory={required} - onFieldChange={(_, value) => { - setFormData((prev) => ({ ...prev, [key]: { ...prev[key], value } })); - }} - isNotSet={isCreate} - error={errors[key]?.message} - /> - </div> - ), - )} - </div> + </div> + ), + )} + </form> + ); - <div className="modal-foot"> - <SubmitButton disabled={!canBeSaved}> - {translate('settings.almintegration.form.save')} - <Spinner className="spacer-left" loading={createLoading || updateLoading} /> - </SubmitButton> - <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink> - </div> - </form> - </Modal> + return ( + <Modal + headerTitle={header} + onClose={props.onClose} + body={formBody} + primaryButton={ + <ButtonPrimary form={FORM_ID} type="submit" disabled={!canBeSaved}> + {translate('settings.almintegration.form.save')} + <Spinner className="sw-ml-2" loading={createLoading || updateLoading} /> + </ButtonPrimary> + } + /> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx index b0e03e7c541..d66da0f2a4c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx @@ -247,7 +247,7 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps return null; } return ( - <div key={val.key}> + <div key={val.key} className="sw-mb-8"> <AuthenticationFormField settingValue={ values[val.key]?.newValue ?? values[val.key]?.value diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx index 9a040e650da..df3e3652f1f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx @@ -81,7 +81,7 @@ const ui = { githubApiUrl: byRole('textbox', { name: 'property.sonar.auth.github.apiUrl.name' }), githubWebUrl: byRole('textbox', { name: 'property.sonar.auth.github.webUrl.name' }), allowUsersToSignUp: byRole('switch', { - name: 'sonar.auth.github.allowUsersToSignUp', + name: 'property.sonar.auth.github.allowUsersToSignUp.name', }), organizations: byRole('textbox', { name: 'property.sonar.auth.github.organizations.name', diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx index ceae3eb5aa5..3cc5ca6050f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx @@ -86,7 +86,7 @@ const ui = { name: 'property.secret.name', }), synchronizeGroups: byRole('switch', { - name: 'synchronizeGroups', + name: 'property.synchronizeGroups.name', }), saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }), jitProvisioningRadioButton: glContainer.byRole('radio', { @@ -95,7 +95,7 @@ const ui = { autoProvisioningRadioButton: glContainer.byRole('radio', { name: 'settings.authentication.gitlab.form.provisioning_with_gitlab', }), - jitAllowUsersToSignUpToggle: byRole('switch', { name: 'allowUsersToSignUp' }), + jitAllowUsersToSignUpToggle: byRole('switch', { name: 'property.allowUsersToSignUp.name' }), autoProvisioningToken: byRole('textbox', { name: 'property.provisioningToken.name', }), |