From: Ambroise C Date: Fri, 1 Sep 2023 12:18:58 +0000 (+0200) Subject: SONAR-20249 Hide NCD banner on save (#9186) X-Git-Tag: 10.3.0.82913~490 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a3e95e51612ae5151d654acfd6a27f74e53dbc26;p=sonarqube.git SONAR-20249 Hide NCD banner on save (#9186) --- diff --git a/server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts index 147246c309e..167ee04638b 100644 --- a/server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts @@ -60,7 +60,13 @@ export default class NewCodeDefinitionServiceMock { .mockImplementation(this.handleListBranchesNewCodePeriod); } - handleGetNewCodePeriod = () => { + handleGetNewCodePeriod = (data?: { branch?: string; project?: string }) => { + if (data?.branch !== undefined) { + return this.reply( + this.#listBranchesNewCode.find((b) => b.branchKey === data?.branch) as NewCodeDefinition + ); + } + return this.reply(this.#newCodePeriod); }; @@ -70,15 +76,14 @@ export default class NewCodeDefinitionServiceMock { type: NewCodeDefinitionType; value?: string; }) => { - const { type, value, branch } = data; - if (branch) { - const branchNewCode = this.#listBranchesNewCode.find( - (bNew) => bNew.branchKey === branch - ) as NewCodeDefinitionBranch; - branchNewCode.type = type; - branchNewCode.value = value; + const { project, type, value, branch } = data; + if (project !== undefined && branch !== undefined) { + this.#listBranchesNewCode = this.#listBranchesNewCode.filter((b) => b.branchKey !== branch); + this.#listBranchesNewCode.push( + mockNewCodePeriodBranch({ type, value, branchKey: branch, projectKey: project }) + ); } else { - this.#newCodePeriod = mockNewCodePeriod({ type, value }); + this.#newCodePeriod = mockNewCodePeriod({ projectKey: project, type, value }); } return this.reply(undefined); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 151f5c72ab0..48cc565af1d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -21,17 +21,24 @@ import { TopBar } from 'design-system'; import * as React from 'react'; import NCDAutoUpdateMessage from '../../../../components/new-code-definition/NCDAutoUpdateMessage'; import { translate } from '../../../../helpers/l10n'; +import { withBranchLikes } from '../../../../queries/branch'; import { ProjectAlmBindingConfigurationErrors } from '../../../../types/alm-settings'; +import { Branch } from '../../../../types/branch-like'; import { ComponentQualifier } from '../../../../types/component'; +import { Feature } from '../../../../types/features'; import { Task } from '../../../../types/tasks'; import { Component } from '../../../../types/types'; import RecentHistory from '../../RecentHistory'; +import withAvailableFeatures, { + WithAvailableFeaturesProps, +} from '../../available-features/withAvailableFeatures'; import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif'; import Header from './Header'; import HeaderMeta from './HeaderMeta'; import Menu from './Menu'; -export interface ComponentNavProps { +export interface ComponentNavProps extends WithAvailableFeaturesProps { + branchLike?: Branch; component: Component; currentTask?: Task; isInProgress?: boolean; @@ -39,8 +46,16 @@ export interface ComponentNavProps { projectBindingErrors?: ProjectAlmBindingConfigurationErrors; } -export default function ComponentNav(props: ComponentNavProps) { - const { component, currentTask, isInProgress, isPending, projectBindingErrors } = props; +function ComponentNav(props: ComponentNavProps) { + const { + branchLike, + component, + currentTask, + hasFeature, + isInProgress, + isPending, + projectBindingErrors, + } = props; React.useEffect(() => { const { breadcrumbs, key, name } = component; @@ -70,10 +85,15 @@ export default function ComponentNav(props: ComponentNavProps) { - + {projectBindingErrors !== undefined && ( )} ); } + +export default withAvailableFeatures(withBranchLikes(ComponentNav)); diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx index 2ece6c731e8..a6cb0f013fd 100644 --- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx @@ -272,7 +272,7 @@ export class CreateProjectPage extends React.PureComponent
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx index a0563c51d6a..874dc3cc1a1 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx @@ -24,7 +24,6 @@ import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock'; import { getNewCodeDefinition } from '../../../../api/newCodeDefinition'; import { mockProject } from '../../../../helpers/mocks/projects'; -import { mockAppState } from '../../../../helpers/testMocks'; import { renderApp } from '../../../../helpers/testReactTestingUtils'; import { byRole, byText } from '../../../../helpers/testSelector'; import { NewCodeDefinitionType } from '../../../../types/new-code-definition'; @@ -73,8 +72,6 @@ const ui = { name: /new_code_definition.number_days.specify_days/, }), ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'), - ncdWarningTextAdmin: byText('new_code_definition.compliance.warning.explanation.admin'), - ncdWarningText: byText('new_code_definition.compliance.warning.explanation'), projectDashboardText: byText('/dashboard?id=foo'), }; @@ -138,63 +135,6 @@ it('should select the global NCD when it is compliant', async () => { expect(ui.projectCreateButton.get()).toBeEnabled(); }); -it('global NCD option should be disabled if not compliant', async () => { - jest - .mocked(getNewCodeDefinition) - .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' }); - const user = userEvent.setup(); - renderCreateProject(); - await fillFormAndNext('test', user); - - expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument(); - expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument(); - expect(ui.inheritGlobalNcdRadio.get()).toBeDisabled(); - expect(ui.projectCreateButton.get()).toBeDisabled(); -}); - -it.each([ - { canAdmin: true, message: ui.ncdWarningTextAdmin }, - { canAdmin: false, message: ui.ncdWarningText }, -])( - 'should show warning message when global NCD is not compliant', - async ({ canAdmin, message }) => { - jest - .mocked(getNewCodeDefinition) - .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' }); - const user = userEvent.setup(); - renderCreateProject({ appState: mockAppState({ canAdmin }) }); - await fillFormAndNext('test', user); - - expect(message.get()).toBeInTheDocument(); - } -); - -it.each([ui.ncdOptionRefBranchRadio, ui.ncdOptionPreviousVersionRadio])( - 'should override the global NCD and pick a compliant NCD', - async (option) => { - jest - .mocked(getNewCodeDefinition) - .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' }); - const user = userEvent.setup(); - renderCreateProject(); - await fillFormAndNext('test', user); - - expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument(); - expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument(); - expect(ui.inheritGlobalNcdRadio.get()).toBeDisabled(); - expect(ui.projectCreateButton.get()).toBeDisabled(); - expect(ui.overrideNcdRadio.get()).toBeEnabled(); - expect(option.get()).toHaveClass('disabled'); - - await user.click(ui.overrideNcdRadio.get()); - expect(option.get()).not.toHaveClass('disabled'); - - await user.click(option.get()); - - expect(ui.projectCreateButton.get()).toBeEnabled(); - } -); - it('number of days ignores non-numeric inputs', async () => { jest .mocked(getNewCodeDefinition) diff --git a/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx index 003be840a13..76f1c49061d 100644 --- a/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx @@ -30,13 +30,12 @@ import { NewCodeDefinitiondWithCompliance } from '../../../../types/new-code-def import { CreateProjectApiCallback } from '../types'; interface Props { - canAdmin: boolean; createProjectFnRef: CreateProjectApiCallback | null; router: Router; } export default function NewCodeDefinitionSelection(props: Props) { - const { canAdmin, createProjectFnRef, router } = props; + const { createProjectFnRef, router } = props; const [submitting, setSubmitting] = React.useState(false); const [selectedDefinition, selectDefinition] = React.useState(); @@ -75,7 +74,7 @@ export default function NewCodeDefinitionSelection(props: Props) { />

- +

{translate('baseline.new_code_period_for_branch_x.question')}

- + {currentSetting === NewCodeDefinitionType.SpecificAnalysis && ( + + )}
{ - mounted = false; - state: State = { - branchList: [], - numberOfDays: getNumberOfDaysDefaultValue(), - isChanged: false, - loading: true, - saving: false, - }; - - // We use debounce as we could have multiple save in less that 3sec. - resetSuccess = debounce(() => this.setState({ success: undefined }), 3000); - - componentDidMount() { - this.mounted = true; - this.fetchLeakPeriodSetting(); - this.sortAndFilterBranches(this.props.branchLikes); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.branchLikes !== this.props.branchLikes) { - this.sortAndFilterBranches(this.props.branchLikes); +function ProjectNewCodeDefinitionApp(props: ProjectNewCodeDefinitionAppProps) { + const { appState, component, branchLike, branchLikes, hasFeature } = props; + + const [isSpecificNewCodeDefinition, setIsSpecificNewCodeDefinition] = useState(); + const [numberOfDays, setNumberOfDays] = useState(getNumberOfDaysDefaultValue()); + const [referenceBranch, setReferenceBranch] = useState(undefined); + const [specificAnalysis, setSpecificAnalysis] = useState(undefined); + const [selectedNewCodeDefinitionType, setSelectedNewCodeDefinitionType] = + useState(DEFAULT_NEW_CODE_DEFINITION_TYPE); + + const { + data: globalNewCodeDefinition = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE }, + isLoading: isGlobalNCDLoading, + } = useNewCodeDefinitionQuery(); + const { data: projectNewCodeDefinition, isLoading: isProjectNCDLoading } = + useNewCodeDefinitionQuery({ + branchName: hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name, + projectKey: component.key, + }); + const { isLoading: isSaving, mutate: postNewCodeDefinition } = useNewCodeDefinitionMutation(); + + const branchList = useMemo(() => { + return sortBranches(branchLikes.filter(isBranch)); + }, [branchLikes]); + const isFormTouched = useMemo(() => { + if (isSpecificNewCodeDefinition === undefined) { + return false; + } + if (isSpecificNewCodeDefinition !== !projectNewCodeDefinition?.inherited) { + return true; } - } - - componentWillUnmount() { - this.mounted = false; - } - - getUpdatedState(params: { - newCodeDefinitionType?: NewCodeDefinitionType; - newCodeDefinitionValue?: string; - globalNewCodeDefinition: NewCodeDefinition; - previousNonCompliantValue?: string; - projectNcdUpdatedAt?: number; - }) { - const { - newCodeDefinitionType, - newCodeDefinitionValue, - globalNewCodeDefinition, - previousNonCompliantValue, - projectNcdUpdatedAt, - } = params; - const { referenceBranch } = this.state; - - const defaultDays = getNumberOfDaysDefaultValue(globalNewCodeDefinition); - - return { - loading: false, - newCodeDefinitionType, - newCodeDefinitionValue, - previousNonCompliantValue, - projectNcdUpdatedAt, - globalNewCodeDefinition, - isChanged: false, - selectedNewCodeDefinitionType: newCodeDefinitionType ?? globalNewCodeDefinition.type, - overrideGlobalNewCodeDefinition: Boolean(newCodeDefinitionType), - numberOfDays: - (newCodeDefinitionType === NewCodeDefinitionType.NumberOfDays && newCodeDefinitionValue) || - defaultDays, - analysis: - (newCodeDefinitionType === NewCodeDefinitionType.SpecificAnalysis && - newCodeDefinitionValue) || - '', - referenceBranch: - (newCodeDefinitionType === NewCodeDefinitionType.ReferenceBranch && - newCodeDefinitionValue) || - referenceBranch, - }; - } - - sortAndFilterBranches(branchLikes: BranchLike[] = []) { - const branchList = sortBranches(branchLikes.filter(isBranch)); - this.setState({ branchList, referenceBranch: branchList[0]?.name }); - } - - fetchLeakPeriodSetting() { - const { branchLike, component } = this.props; - this.setState({ loading: true }); + if (!isSpecificNewCodeDefinition) { + return false; + } - Promise.all([ - getNewCodeDefinition(), - getNewCodeDefinition({ - branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name, - project: component.key, - }), - ]).then( - ([globalNewCodeDefinition, setting]) => { - if (this.mounted) { - if (!globalNewCodeDefinition.type) { - globalNewCodeDefinition = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE }; - } - const newCodeDefinitionValue = setting.value; - const newCodeDefinitionType = setting.inherited - ? undefined - : setting.type || DEFAULT_NEW_CODE_DEFINITION_TYPE; + if (selectedNewCodeDefinitionType !== projectNewCodeDefinition?.type) { + return true; + } - this.setState( - this.getUpdatedState({ - globalNewCodeDefinition, - newCodeDefinitionType, - newCodeDefinitionValue, - previousNonCompliantValue: setting.previousNonCompliantValue, - projectNcdUpdatedAt: setting.updatedAt, - }) - ); - } - }, - () => { - this.setState({ loading: false }); - } + switch (selectedNewCodeDefinitionType) { + case NewCodeDefinitionType.NumberOfDays: + return numberOfDays !== String(projectNewCodeDefinition?.value); + case NewCodeDefinitionType.ReferenceBranch: + return referenceBranch !== projectNewCodeDefinition?.value; + case NewCodeDefinitionType.SpecificAnalysis: + return specificAnalysis !== projectNewCodeDefinition?.value; + default: + return false; + } + }, [ + isSpecificNewCodeDefinition, + numberOfDays, + projectNewCodeDefinition, + referenceBranch, + selectedNewCodeDefinitionType, + specificAnalysis, + ]); + + const defaultReferenceBranch = branchList[0]?.name; + const isLoading = isGlobalNCDLoading || isProjectNCDLoading; + const branchSupportEnabled = hasFeature(Feature.BranchSupport); + + const resetStatesFromProjectNewCodeDefinition = useCallback(() => { + setIsSpecificNewCodeDefinition( + projectNewCodeDefinition === undefined ? undefined : !projectNewCodeDefinition.inherited ); - } - - resetSetting = () => { - this.setState({ saving: true }); - resetNewCodeDefinition({ project: this.props.component.key }).then( - () => { - this.setState({ - saving: false, - newCodeDefinitionType: undefined, - isChanged: false, - selectedNewCodeDefinitionType: undefined, - success: true, - }); - this.resetSuccess(); - }, - () => { - this.setState({ saving: false }); - } + setSelectedNewCodeDefinitionType( + projectNewCodeDefinition?.type ?? DEFAULT_NEW_CODE_DEFINITION_TYPE ); - }; - - handleSelectDays = (days: string) => this.setState({ numberOfDays: days, isChanged: true }); - - handleSelectReferenceBranch = (referenceBranch: string) => { - this.setState({ referenceBranch, isChanged: true }); - }; - - handleCancel = () => - this.setState( - ({ - globalNewCodeDefinition = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE }, - newCodeDefinitionType, - newCodeDefinitionValue, - }) => - this.getUpdatedState({ - globalNewCodeDefinition, - newCodeDefinitionType, - newCodeDefinitionValue, - }) + setNumberOfDays(getNumberOfDaysDefaultValue(globalNewCodeDefinition, projectNewCodeDefinition)); + setReferenceBranch( + projectNewCodeDefinition?.type === NewCodeDefinitionType.ReferenceBranch + ? projectNewCodeDefinition.value + : defaultReferenceBranch ); - - handleSelectSetting = (selectedNewCodeDefinitionType?: NewCodeDefinitionType) => { - this.setState((currentState) => ({ - selectedNewCodeDefinitionType, - isChanged: selectedNewCodeDefinitionType !== currentState.selectedNewCodeDefinitionType, - })); + setSpecificAnalysis( + projectNewCodeDefinition?.type === NewCodeDefinitionType.SpecificAnalysis + ? projectNewCodeDefinition.value + : undefined + ); + }, [defaultReferenceBranch, globalNewCodeDefinition, projectNewCodeDefinition]); + + const onResetNewCodeDefinition = () => { + postNewCodeDefinition({ + branch: hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name, + project: component.key, + type: undefined, + }); }; - handleToggleSpecificSetting = (overrideGlobalNewCodeDefinition: boolean) => - this.setState((currentState) => ({ - overrideGlobalNewCodeDefinition, - isChanged: currentState.overrideGlobalNewCodeDefinition !== overrideGlobalNewCodeDefinition, - })); - - handleSubmit = (e: React.SyntheticEvent) => { + const onSubmit = (e: React.SyntheticEvent) => { e.preventDefault(); - const { component } = this.props; - const { - numberOfDays, - selectedNewCodeDefinitionType: type, - referenceBranch, - overrideGlobalNewCodeDefinition, - } = this.state; - - if (!overrideGlobalNewCodeDefinition) { - this.resetSetting(); + if (!isSpecificNewCodeDefinition) { + onResetNewCodeDefinition(); return; } - const value = getSettingValue({ type, numberOfDays, referenceBranch }); + const value = getSettingValue({ + type: selectedNewCodeDefinitionType, + numberOfDays, + referenceBranch, + }); - if (type) { - this.setState({ saving: true }); - setNewCodeDefinition({ + if (selectedNewCodeDefinitionType) { + postNewCodeDefinition({ + branch: hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name, project: component.key, - type, + type: selectedNewCodeDefinitionType, value, - }).then( - () => { - this.setState({ - saving: false, - newCodeDefinitionType: type, - newCodeDefinitionValue: value || undefined, - previousNonCompliantValue: undefined, - projectNcdUpdatedAt: Date.now(), - isChanged: false, - success: true, - }); - this.resetSuccess(); - }, - () => { - this.setState({ saving: false }); - } - ); + }); } }; - render() { - const { appState, component, branchLike } = this.props; - const { - analysis, - branchList, - newCodeDefinitionType, - numberOfDays, - previousNonCompliantValue, - projectNcdUpdatedAt, - globalNewCodeDefinition, - isChanged, - loading, - newCodeDefinitionValue, - overrideGlobalNewCodeDefinition, - referenceBranch, - saving, - selectedNewCodeDefinitionType, - success, - } = this.state; - const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport); - - return ( - <> - - -
- - - - {!loading && ( -
- {branchSupportEnabled &&

{translate('project_baseline.default_setting')}

} - - {globalNewCodeDefinition && overrideGlobalNewCodeDefinition !== undefined && ( - { + setReferenceBranch(defaultReferenceBranch); + }, [defaultReferenceBranch]); + + useEffect(() => { + resetStatesFromProjectNewCodeDefinition(); + }, [resetStatesFromProjectNewCodeDefinition]); + + return ( + <> + + +
+ + + + {!isLoading && ( +
+ {branchSupportEnabled &&

{translate('project_baseline.default_setting')}

} + + {globalNewCodeDefinition && isSpecificNewCodeDefinition !== undefined && ( + + )} + + {globalNewCodeDefinition && branchSupportEnabled && ( +
+
+

{translate('project_baseline.configure_branches')}

+ - )} - -
- - - {translate('settings.state.saved')} -
- {globalNewCodeDefinition && branchSupportEnabled && ( -
-
-

{translate('project_baseline.configure_branches')}

- -
- )} -
- )} -
- - ); - } + )} +
+ )} +
+ + ); } export default withComponentContext( diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx index 0aac108102e..c1f72916441 100644 --- a/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx @@ -21,17 +21,15 @@ import classNames from 'classnames'; import { RadioButton } from 'design-system'; import { noop } from 'lodash'; import * as React from 'react'; -import Tooltip from '../../../components/controls/Tooltip'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import GlobalNewCodeDefinitionDescription from '../../../components/new-code-definition/GlobalNewCodeDefinitionDescription'; +import NewCodeDefinitionAnalysisWarning from '../../../components/new-code-definition/NewCodeDefinitionAnalysisWarning'; import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption'; import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption'; -import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning'; import { NewCodeDefinitionLevels } from '../../../components/new-code-definition/utils'; import { Alert } from '../../../components/ui/Alert'; import Spinner from '../../../components/ui/Spinner'; import { translate } from '../../../helpers/l10n'; -import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition'; import { Branch } from '../../../types/branch-like'; import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition'; import { validateSetting } from '../utils'; @@ -44,7 +42,6 @@ export interface ProjectBaselineSelectorProps { branch?: Branch; branchList: Branch[]; branchesEnabled?: boolean; - canAdmin: boolean | undefined; component: string; newCodeDefinitionType?: NewCodeDefinitionType; newCodeDefinitionValue?: string; @@ -56,7 +53,7 @@ export interface ProjectBaselineSelectorProps { onCancel: () => void; onSelectDays: (value: string) => void; onSelectReferenceBranch: (value: string) => void; - onSelectSetting: (value?: NewCodeDefinitionType) => void; + onSelectSetting: (value: NewCodeDefinitionType) => void; onSubmit: (e: React.SyntheticEvent) => void; onToggleSpecificSetting: (selection: boolean) => void; referenceBranch?: string; @@ -75,7 +72,6 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS branch, branchList, branchesEnabled, - canAdmin, component, newCodeDefinitionType, newCodeDefinitionValue, @@ -90,8 +86,6 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS selectedNewCodeDefinitionType, } = props; - const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(globalNewCodeDefinition); - const isValid = validateSetting({ numberOfDays: days, overrideGlobalNewCodeDefinition, @@ -109,27 +103,14 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS props.onToggleSpecificSetting(false)} value="general" > - - {translate('project_baseline.global_setting')} - + {translate('project_baseline.global_setting')}
- +
- + {newCodeDefinitionType === NewCodeDefinitionType.SpecificAnalysis && ( + + )}
)}
-
- +
+ {translate('baseline.next_analysis_notice')} - {translate('save')} - - {translate('cancel')} - + {!saving && ( + <> + {translate('save')} + + {translate('cancel')} + + + )}
); diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx index 084e134f393..8acca30f39e 100644 --- a/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx @@ -70,39 +70,6 @@ it('renders correctly without branch support feature', async () => { expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument(); }); -it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => { - newCodeDefinitionMock.setNewCodePeriod({ - type: NewCodeDefinitionType.NumberOfDays, - value: '99', - inherited: true, - }); - - const { ui } = getPageObjects(); - renderProjectNewCodeDefinitionApp(); - await ui.appIsLoaded(); - - expect(await ui.generalSettingRadio.find()).toBeChecked(); - expect(ui.generalSettingRadio.get()).toBeDisabled(); - expect(ui.complianceWarning.get()).toBeVisible(); -}); - -it('prevents selection of global setting if it is not compliant and warns admin about it', async () => { - newCodeDefinitionMock.setNewCodePeriod({ - type: NewCodeDefinitionType.NumberOfDays, - value: '99', - inherited: true, - }); - - const { ui } = getPageObjects(); - renderProjectNewCodeDefinitionApp({ appState: mockAppState({ canAdmin: true }) }); - await ui.appIsLoaded(); - - expect(await ui.generalSettingRadio.find()).toBeChecked(); - expect(ui.generalSettingRadio.get()).toBeDisabled(); - expect(ui.complianceWarningAdmin.get()).toBeVisible(); - expect(ui.complianceWarning.query()).not.toBeInTheDocument(); -}); - it('renders correctly with branch support feature', async () => { const { ui } = getPageObjects(); renderProjectNewCodeDefinitionApp({ @@ -134,13 +101,13 @@ it('can set previous version specific setting', async () => { // Save changes await user.click(ui.saveButton.get()); - expect(ui.saved.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeDisabled(); // Set general setting await user.click(ui.generalSettingRadio.get()); expect(ui.previousVersionRadio.get()).toHaveClass('disabled'); await user.click(ui.saveButton.get()); - expect(ui.saved.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeDisabled(); }); it('can set number of days specific setting', async () => { @@ -161,7 +128,7 @@ it('can set number of days specific setting', async () => { await ui.setNumberDaysSetting('10'); await user.click(ui.saveButton.get()); - expect(ui.saved.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeDisabled(); }); it('can set reference branch specific setting', async () => { @@ -178,15 +145,18 @@ it('can set reference branch specific setting', async () => { // Save changes await user.click(ui.saveButton.get()); - expect(ui.saved.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeDisabled(); }); it('cannot set specific analysis setting', async () => { const { ui } = getPageObjects(); - newCodeDefinitionMock.setNewCodePeriod({ - type: NewCodeDefinitionType.SpecificAnalysis, - value: 'analysis_id', - }); + newCodeDefinitionMock.setListBranchesNewCode([ + mockNewCodePeriodBranch({ + branchKey: 'main', + type: NewCodeDefinitionType.SpecificAnalysis, + value: 'analysis_id', + }), + ]); renderProjectNewCodeDefinitionApp(); await ui.appIsLoaded(); @@ -430,9 +400,6 @@ function getPageObjects() { byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }), editButton: byRole('button', { name: 'edit' }), resetToDefaultButton: byRole('button', { name: 'reset_to_default' }), - saved: byText('settings.state.saved'), - complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'), - complianceWarning: byText('new_code_definition.compliance.warning.explanation'), branchNCDsBanner: byText(/new_code_definition.auto_update.branch.message/), dismissButton: byLabelText('alert.dismiss'), }; diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx index a2144e436b7..527e0a5e58e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx @@ -17,15 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as React from 'react'; +import classNames from 'classnames'; +import React, { useCallback, useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { getNewCodeDefinition, setNewCodeDefinition } from '../../../api/newCodeDefinition'; import DocLink from '../../../components/common/DocLink'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; -import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption'; import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption'; -import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning'; import { NewCodeDefinitionLevels } from '../../../components/new-code-definition/utils'; import Spinner from '../../../components/ui/Spinner'; import { translate } from '../../../helpers/l10n'; @@ -33,245 +31,149 @@ import { getNumberOfDaysDefaultValue, isNewCodeDefinitionCompliant, } from '../../../helpers/new-code-definition'; +import { + useNewCodeDefinitionMutation, + useNewCodeDefinitionQuery, +} from '../../../queries/newCodeDefinition'; import { NewCodeDefinitionType } from '../../../types/new-code-definition'; -interface State { - currentSetting?: NewCodeDefinitionType; - days: string; - previousNonCompliantValue?: string; - ncdUpdatedAt?: number; - loading: boolean; - currentSettingValue?: string; - isChanged: boolean; - projectKey?: string; - saving: boolean; - selected?: NewCodeDefinitionType; - success: boolean; -} - -export default class NewCodeDefinition extends React.PureComponent<{}, State> { - mounted = false; - state: State = { - loading: true, - days: getNumberOfDaysDefaultValue(), - isChanged: false, - saving: false, - success: false, - }; - - componentDidMount() { - this.mounted = true; - this.fetchNewCodePeriodSetting(); - } +export default function NewCodeDefinition() { + const [numberOfDays, setNumberOfDays] = React.useState(getNumberOfDaysDefaultValue()); + const [selectedNewCodeDefinitionType, setSelectedNewCodeDefinitionType] = React.useState< + NewCodeDefinitionType | undefined + >(undefined); - componentWillUnmount() { - this.mounted = false; - } - - fetchNewCodePeriodSetting() { - getNewCodeDefinition() - .then(({ type, value, previousNonCompliantValue, projectKey, updatedAt }) => { - this.setState(({ days }) => ({ - currentSetting: type, - days: type === NewCodeDefinitionType.NumberOfDays ? String(value) : days, - loading: false, - currentSettingValue: value, - selected: type, - previousNonCompliantValue, - projectKey, - ncdUpdatedAt: updatedAt, - })); - }) - .catch(() => { - this.setState({ loading: false }); - }); - } - - onSelectDays = (days: string) => { - this.setState({ days, success: false, isChanged: true }); - }; - - onSelectSetting = (selected: NewCodeDefinitionType) => { - this.setState((currentState) => ({ - selected, - success: false, - isChanged: selected !== currentState.selected, - })); - }; + const { data: newCodeDefinition, isLoading } = useNewCodeDefinitionQuery(); + const { isLoading: isSaving, mutate: postNewCodeDefinition } = useNewCodeDefinitionMutation(); - onCancel = () => { - this.setState(({ currentSetting, currentSettingValue, days }) => ({ - isChanged: false, - selected: currentSetting, - days: - currentSetting === NewCodeDefinitionType.NumberOfDays ? String(currentSettingValue) : days, - })); - }; + const resetNewCodeDefinition = useCallback(() => { + setSelectedNewCodeDefinitionType(newCodeDefinition?.type); + setNumberOfDays(getNumberOfDaysDefaultValue(newCodeDefinition)); + }, [newCodeDefinition]); - onSubmit = (e: React.SyntheticEvent) => { + const onSubmit = (e: React.SyntheticEvent) => { e.preventDefault(); - const { days, selected } = this.state; - - const type = selected; - const value = type === NewCodeDefinitionType.NumberOfDays ? days : undefined; + const type = selectedNewCodeDefinitionType; + const value = type === NewCodeDefinitionType.NumberOfDays ? numberOfDays : undefined; - this.setState({ saving: true, success: false }); - setNewCodeDefinition({ - type: type as NewCodeDefinitionType, + postNewCodeDefinition({ + type, value, - }).then( - () => { - if (this.mounted) { - this.setState({ - saving: false, - currentSetting: type, - currentSettingValue: value || undefined, - previousNonCompliantValue: undefined, - ncdUpdatedAt: Date.now(), - isChanged: false, - success: true, - }); - } - }, - () => { - if (this.mounted) { - this.setState({ - saving: false, - }); - } - } - ); + }); }; - render() { - const { - currentSetting, - days, - previousNonCompliantValue, - ncdUpdatedAt, - loading, - isChanged, - currentSettingValue, - projectKey, - saving, - selected, - success, - } = this.state; - - const isValid = - selected !== NewCodeDefinitionType.NumberOfDays || - isNewCodeDefinitionCompliant({ type: NewCodeDefinitionType.NumberOfDays, value: days }); - - return ( - <> -

- {translate('settings.new_code_period.title')} -

- -
    -
  • -
      -
    • -
      -
      -
      -

      - {translate('settings.new_code_period.description0')} -

      -

      - {translate('settings.new_code_period.description1')} -

      -

      - {translate('settings.new_code_period.description2')} -

      - -

      - - {translate('settings.new_code_period.description3.link')} - - ), - }} - /> -

      - -

      - {translate('settings.new_code_period.question')} -

      -
      + useEffect(() => { + resetNewCodeDefinition(); + }, [resetNewCodeDefinition]); + + const isValid = + selectedNewCodeDefinitionType !== NewCodeDefinitionType.NumberOfDays || + isNewCodeDefinitionCompliant({ type: NewCodeDefinitionType.NumberOfDays, value: numberOfDays }); + + const isFormTouched = + selectedNewCodeDefinitionType === NewCodeDefinitionType.NumberOfDays + ? numberOfDays !== newCodeDefinition?.value + : selectedNewCodeDefinitionType !== newCodeDefinition?.type; + + return ( + <> +

      + {translate('settings.new_code_period.title')} +

      + +
        +
      • +
          +
        • +
          +
          +
          +

          {translate('settings.new_code_period.description0')}

          +

          {translate('settings.new_code_period.description1')}

          +

          {translate('settings.new_code_period.description2')}

          + +

          + + {translate('settings.new_code_period.description3.link')} + + ), + }} + /> +

          + +

          + {translate('settings.new_code_period.question')} +

          +
          -
          - -
          - - - - {isChanged && ( -
          -

          - {translate('baseline.next_analysis_notice')} -

          - - +
          + + + + +
          +

          + {translate('baseline.next_analysis_notice')} +

          + + {!isSaving && ( + <> + {translate('save')} - + {translate('cancel')} -
          + )} - {!saving && !loading && success && ( -
          - - - {translate('settings.state.saved')} - -
          - )} - -
          -
          +
          + +
          -
        • -
        -
      • -
      - - ); - } +
      +
    • +
    +
  • +
+ + ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx index def197bba02..2d4621dadc0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { MessageTypes } from '../../../../api/messages'; @@ -60,7 +59,7 @@ it('renders and behaves as expected', async () => { expect(await ui.newCodeTitle.find()).toBeInTheDocument(); // Previous version should be checked by default - expect(ui.prevVersionRadio.get()).toBeChecked(); + expect(await ui.prevVersionRadio.find()).toBeChecked(); // Can select number of days await user.click(ui.daysNumberRadio.get()); @@ -91,31 +90,13 @@ it('renders and behaves as expected', async () => { await user.clear(ui.daysInput.get()); await user.type(ui.daysInput.get(), '10'); await user.click(ui.saveButton.get()); - expect(ui.savedMsg.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeDisabled(); await user.click(ui.prevVersionRadio.get()); await user.click(ui.cancelButton.get()); await user.click(ui.prevVersionRadio.get()); await user.click(ui.saveButton.get()); - expect(ui.savedMsg.get()).toBeInTheDocument(); -}); - -it('renders and behaves properly when the current value is not compliant', async () => { - const user = userEvent.setup(); - newCodeMock.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays, value: '91' }); - renderNewCodePeriod(); - - expect(await ui.newCodeTitle.find()).toBeInTheDocument(); - expect(ui.daysNumberRadio.get()).toBeChecked(); - expect(ui.daysInput.get()).toHaveValue(91); - - // Should warn about non compliant value - expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument(); - - await user.clear(ui.daysInput.get()); - await user.type(ui.daysInput.get(), '92'); - - expect(ui.daysNumberErrorMessage.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeDisabled(); }); it('displays information message when NCD is automatically updated', async () => { diff --git a/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx b/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx index 133e3e0b711..22958df2264 100644 --- a/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx +++ b/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx @@ -17,23 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { FlagMessage, Link } from 'design-system'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; import { translate, translateWithParameters } from '../../helpers/l10n'; import { NewCodeDefinition, NewCodeDefinitionType } from '../../types/new-code-definition'; interface Props { globalNcd: NewCodeDefinition; - isGlobalNcdCompliant: boolean; - canAdmin?: boolean; } -export default function GlobalNewCodeDefinitionDescription({ - globalNcd, - isGlobalNcdCompliant, - canAdmin, -}: Props) { +export default function GlobalNewCodeDefinitionDescription({ globalNcd }: Props) { let setting: string; let description: string; let useCase: string; @@ -51,46 +43,10 @@ export default function GlobalNewCodeDefinitionDescription({ } return ( - <> -
- {setting} - {isGlobalNcdCompliant && ( - <> - {description} - {useCase} - - )} -
- {!isGlobalNcdCompliant && ( - - -

- {translate('new_code_definition.compliance.warning.title.global')} -

-

- {canAdmin ? ( - - {translate( - 'new_code_definition.compliance.warning.explanation.action.admin.link' - )} - - ), - }} - /> - ) : ( - translate('new_code_definition.compliance.warning.explanation') - )} -

-
-
- )} - +
+ {setting} + {description} + {useCase} +
); } diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx index ecb71e82893..f4cd199ae0a 100644 --- a/server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx +++ b/server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx @@ -20,11 +20,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages'; -import { getNewCodeDefinition } from '../../api/newCodeDefinition'; import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; import { NEW_CODE_PERIOD_CATEGORY } from '../../apps/settings/constants'; import { queryToSearch } from '../../helpers/urls'; +import { useNewCodeDefinitionQuery } from '../../queries/newCodeDefinition'; import { Component } from '../../types/types'; import Link from '../common/Link'; import DismissableAlertComponent from '../ui/DismissableAlertComponent'; @@ -35,11 +35,12 @@ import { } from './utils'; interface NCDAutoUpdateMessageProps extends Pick { + branchName?: string; component?: Component; } function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) { - const { component, currentUser } = props; + const { branchName, component, currentUser } = props; const isGlobalBanner = component === undefined; const intl = useIntl(); @@ -47,10 +48,14 @@ function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) { const [previouslyNonCompliantNewCodeDefinition, setPreviouslyNonCompliantNewCodeDefinition] = useState(undefined); - const isAdmin = useMemo( - () => isGlobalOrProjectAdmin(currentUser, component), - [component, currentUser] - ); + const isAdmin = isGlobalOrProjectAdmin(currentUser, component); + + const { data: newCodeDefinition } = useNewCodeDefinitionQuery({ + branchName, + enabled: isAdmin, + projectKey: component?.key, + }); + const ncdReviewLinkTo = useMemo( () => isGlobalBanner @@ -79,37 +84,30 @@ function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) { }, [component, isGlobalBanner]); useEffect(() => { - async function fetchNewCodeDefinition() { - const newCodeDefinition = await getNewCodeDefinition( - component && { - project: component.key, - } + async function updateMessageStatus() { + const messageStatus = await checkMessageDismissed( + isGlobalBanner + ? { + messageType: MessageTypes.GlobalNcd90, + } + : { + messageType: MessageTypes.ProjectNcd90, + projectKey: component.key, + } ); - if (isPreviouslyNonCompliantDaysNCD(newCodeDefinition)) { - setPreviouslyNonCompliantNewCodeDefinition(newCodeDefinition); - - const messageStatus = await checkMessageDismissed( - isGlobalBanner - ? { - messageType: MessageTypes.GlobalNcd90, - } - : { - messageType: MessageTypes.ProjectNcd90, - projectKey: component.key, - } - ); - - setDismissed(messageStatus.dismissed); - } + setDismissed(messageStatus.dismissed); } - if (isAdmin) { - fetchNewCodeDefinition(); + if (newCodeDefinition && isPreviouslyNonCompliantDaysNCD(newCodeDefinition)) { + setPreviouslyNonCompliantNewCodeDefinition(newCodeDefinition); + updateMessageStatus(); + } else { + setPreviouslyNonCompliantNewCodeDefinition(undefined); } - }, [isAdmin, component, isGlobalBanner]); + }, [component?.key, isGlobalBanner, newCodeDefinition]); - if (!isAdmin || dismissed || !previouslyNonCompliantNewCodeDefinition) { + if (dismissed || !previouslyNonCompliantNewCodeDefinition) { return null; } diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx new file mode 100644 index 00000000000..44b830290dd --- /dev/null +++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 * as React from 'react'; +import { translate } from '../../helpers/l10n'; +import DocLink from '../common/DocLink'; +import { Alert } from '../ui/Alert'; + +export default function NewCodeDefinitionAnalysisWarning() { + return ( + +

+ {translate('baseline.specific_analysis.compliance_warning.title')} +

+

+ {translate('baseline.specific_analysis.compliance_warning.explanation')} +

+

+ {translate('learn_more')}:  + + {translate('baseline.specific_analysis.compliance_warning.link')} + +

+
+ ); +} diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx index de6016a23b9..fad3f8e7282 100644 --- a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx +++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx @@ -125,11 +125,13 @@ export default function NewCodeDefinitionDaysOption(props: Props) {
onChangeDays(e.currentTarget.value)} + required + type="number" value={days} /> {!isValid && } diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx index 59f0d4eee6c..329a84c2d53 100644 --- a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx +++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx @@ -38,30 +38,23 @@ import { NewCodeDefinitionType, NewCodeDefinitiondWithCompliance, } from '../../types/new-code-definition'; -import Tooltip from '../controls/Tooltip'; import GlobalNewCodeDefinitionDescription from './GlobalNewCodeDefinitionDescription'; import NewCodeDefinitionDaysOption from './NewCodeDefinitionDaysOption'; import NewCodeDefinitionPreviousVersionOption from './NewCodeDefinitionPreviousVersionOption'; import { NewCodeDefinitionLevels } from './utils'; interface Props { - canAdmin: boolean | undefined; onNcdChanged: (ncd: NewCodeDefinitiondWithCompliance) => void; } export default function NewCodeDefinitionSelector(props: Props) { - const { canAdmin, onNcdChanged } = props; + const { onNcdChanged } = props; const [globalNcd, setGlobalNcd] = React.useState(null); const [selectedNcdType, setSelectedNcdType] = React.useState(null); const [days, setDays] = React.useState(''); const [isChanged, setIsChanged] = React.useState(false); - const isGlobalNcdCompliant = React.useMemo( - () => Boolean(globalNcd && isNewCodeDefinitionCompliant(globalNcd)), - [globalNcd] - ); - React.useEffect(() => { const numberOfDays = getNumberOfDaysDefaultValue(globalNcd); setDays(numberOfDays); @@ -115,34 +108,19 @@ export default function NewCodeDefinitionSelector(props: Props) { handleNcdChanged(NewCodeDefinitionType.Inherited)} value="general" > - - - {translate('new_code_definition.global_setting')} - - + + {translate('new_code_definition.global_setting')} + - {globalNcd && ( - - )} + {globalNcd && } ; -} - -export default function NewCodeDefinitionWarning({ - newCodeDefinitionType, - newCodeDefinitionValue, - isBranchSupportEnabled, - level, -}: NewCodeDefinitionWarningProps) { - if ( - newCodeDefinitionType === undefined || - isNewCodeDefinitionCompliant({ type: newCodeDefinitionType, value: newCodeDefinitionValue }) - ) { - return null; - } - - if (newCodeDefinitionType === NewCodeDefinitionType.SpecificAnalysis) { - return ( - -

- {translate('baseline.specific_analysis.compliance_warning.title')} -

-

- {translate('baseline.specific_analysis.compliance_warning.explanation')} -

-

- {translate('learn_more')}:  - - {translate('baseline.specific_analysis.compliance_warning.link')} - -

-
- ); - } - - if (newCodeDefinitionType === NewCodeDefinitionType.NumberOfDays) { - return ( - -

- {translate('baseline.number_days.compliance_warning.title')} -

-

- {translate( - `baseline.number_days.compliance_warning.content.${level}${ - isBranchSupportEnabled && level === NewCodeDefinitionLevels.Project - ? '.with_branch_support' - : '' - }` - )} -

-

- {translate('learn_more')}:  - - {translate('baseline.number_days.compliance_warning.link')} - -

-
- ); - } - - return null; -} diff --git a/server/sonar-web/src/main/js/helpers/new-code-definition.ts b/server/sonar-web/src/main/js/helpers/new-code-definition.ts index d02b8742c62..9f724a3f9d4 100644 --- a/server/sonar-web/src/main/js/helpers/new-code-definition.ts +++ b/server/sonar-web/src/main/js/helpers/new-code-definition.ts @@ -32,8 +32,7 @@ export function isNewCodeDefinitionCompliant(newCodePeriod: NewCodeDefinition) { return false; } return ( - !/\D/.test(newCodePeriod.value) && - Number.isInteger(+newCodePeriod.value) && + /^\d+$/.test(newCodePeriod.value) && NUMBER_OF_DAYS_MIN_VALUE <= +newCodePeriod.value && +newCodePeriod.value <= NUMBER_OF_DAYS_MAX_VALUE ); diff --git a/server/sonar-web/src/main/js/queries/newCodeDefinition.ts b/server/sonar-web/src/main/js/queries/newCodeDefinition.ts new file mode 100644 index 00000000000..9dcdea5e40a --- /dev/null +++ b/server/sonar-web/src/main/js/queries/newCodeDefinition.ts @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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. + */ +// React-query component for new code definition + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + getNewCodeDefinition, + resetNewCodeDefinition, + setNewCodeDefinition, +} from '../api/newCodeDefinition'; +import { NewCodeDefinitionType } from '../types/new-code-definition'; + +function getNewCodeDefinitionQueryKey(projectKey?: string, branchName?: string) { + return ['new-code-definition', { projectKey, branchName }]; +} + +export function useNewCodeDefinitionQuery(params?: { + branchName?: string; + enabled?: boolean; + projectKey?: string; +}) { + return useQuery( + getNewCodeDefinitionQueryKey(params?.projectKey, params?.branchName), + () => getNewCodeDefinition({ branch: params?.branchName, project: params?.projectKey }), + { enabled: params?.enabled ?? true, refetchOnWindowFocus: false } + ); +} + +export function useNewCodeDefinitionMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (newCodeDefinition: { + project?: string; + branch?: string; + type?: NewCodeDefinitionType; + value?: string; + }) => { + const { branch, project, type, value } = newCodeDefinition; + + if (type === undefined) { + return resetNewCodeDefinition({ + branch, + project, + }); + } + + return setNewCodeDefinition({ branch, project, type, value }); + }, + onSuccess(_, { branch, project }) { + queryClient.invalidateQueries({ + queryKey: getNewCodeDefinitionQueryKey(project, branch), + }); + }, + }); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index e78def32875..e3daa4badec 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -655,17 +655,7 @@ project_baseline.specific_setting=Define a specific setting for this project project_baseline.configure_branches=Set a specific setting for a branch project_baseline.compliance.warning.title.project=Your project new code definition is not compliant with the Clean as You Code methodology -project_baseline.compliance.warning.title.global=Your global new code definition is not compliant with the Clean as You Code methodology -project_baseline.compliance.warning.explanation=Please ask an administrator to update the global new code definition before switching back to it. -project_baseline.compliance.warning.explanation.admin=Please update the global new code definition under {link} before switching back to it. -project_baseline.warning.explanation.action.admin.link=General Settings > New Code - -baseline.number_days.compliance_warning.title=Your new code definition is not compliant with the Clean as You Code methodology -baseline.number_days.compliance_warning.content.global=We recommend that you update this new code definition so that new projects and existing projects that do not use a specific New Code definition benefit from the Clean as You Code methodology by default. -baseline.number_days.compliance_warning.content.project=We recommend that you update this new code definition so that your project benefits from the Clean as You Code methodology. -baseline.number_days.compliance_warning.content.project.with_branch_support=We recommend that you update this new code definition so that new branches and existing branches that do not use a specific New Code definition benefit from the Clean as You Code methodology by default. -baseline.number_days.compliance_warning.content.branch=We recommend that you update this new code definition so that your branch benefits from the Clean as You Code methodology. -baseline.number_days.compliance_warning.link=Defining New Code + baseline.specific_analysis=Specific analysis baseline.specific_analysis.description=Choose an analysis as the baseline for the new code. baseline.specific_analysis.compliance_warning.title=Choosing the "Specific analysis" option from the SonarQube UI is not compliant with the Clean as You Code methodology @@ -3955,11 +3945,6 @@ new_code_definition.question=Choose the baseline for new code for this project new_code_definition.global_setting=Use the global setting new_code_definition.specific_setting=Define a specific setting for this project -new_code_definition.compliance.warning.title.global=Your global new code definition is not compliant with the Clean as You Code methodology -new_code_definition.compliance.warning.explanation=Please ask an administrator to update the global new code definition before you can use it for your project. -new_code_definition.compliance.warning.explanation.admin=Please update the global new code definition under {link} before you can use it for your project. -new_code_definition.compliance.warning.explanation.action.admin.link=General Settings > New Code - new_code_definition.previous_version=Previous version new_code_definition.previous_version.usecase=Recommended for projects following regular versions or releases. new_code_definition.previous_version.description=Any code that has changed since the previous version is considered new code.