diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2024-01-17 16:33:08 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-01-19 20:02:55 +0000 |
commit | 792921dfe50178050aa591ebcd4d7f62325684f0 (patch) | |
tree | 29aa2970a4d5d93729fbe5eb75542462747abee6 | |
parent | e608a535ede1786dffff2b1771b5ff7bb13ee15b (diff) | |
download | sonarqube-792921dfe50178050aa591ebcd4d7f62325684f0.tar.gz sonarqube-792921dfe50178050aa591ebcd4d7f62325684f0.zip |
SONAR-21420 Migrate audit logs page to new UI
6 files changed, 171 insertions, 84 deletions
diff --git a/server/sonar-web/design-system/src/components/buttons/Button.tsx b/server/sonar-web/design-system/src/components/buttons/Button.tsx index 80e58555a0b..6d17cd784f2 100644 --- a/server/sonar-web/design-system/src/components/buttons/Button.tsx +++ b/server/sonar-web/design-system/src/components/buttons/Button.tsx @@ -149,6 +149,21 @@ export const buttonStyle = (props: ThemedProps) => css` const BaseButtonLink = styled(BaseLink)` ${buttonStyle} + + /* + Workaround to apply disable style to button-link + as link does not have disabled attribute, using props instead + */ + + ${({ disabled, theme }) => + disabled + ? `&, &:hover, &:focus, &:active { + color: ${themeContrast('buttonDisabled')({ theme })}; + background-color: ${themeColor('buttonDisabled')({ theme })}; + border: ${themeBorder('default', 'buttonDisabledBorder')({ theme })}; + cursor: not-allowed; + }` + : undefined}; `; const BaseButton = styled.button` diff --git a/server/sonar-web/design-system/src/components/buttons/__tests__/ButtonPrimary-test.tsx b/server/sonar-web/design-system/src/components/buttons/__tests__/ButtonPrimary-test.tsx new file mode 100644 index 00000000000..af81f1dd7d1 --- /dev/null +++ b/server/sonar-web/design-system/src/components/buttons/__tests__/ButtonPrimary-test.tsx @@ -0,0 +1,36 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { ButtonPrimary } from '../ButtonPrimary'; + +it('renders ButtonPrimary correctly', () => { + render(<ButtonPrimary>Hello</ButtonPrimary>); + expect(screen.getByRole('button', { name: 'Hello' })).toBeInTheDocument(); +}); + +it('renders ButtonPrimary correctly when to is defined', () => { + render( + <ButtonPrimary download="http://link.com" to="http://link.com"> + Hello + </ButtonPrimary>, + ); + expect(screen.queryByRole('button', { name: 'Hello' })).not.toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Hello' })).toBeInTheDocument(); +}); diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 9e7b58e11f7..f08f3ccb9b1 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -83,6 +83,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [ '/admin/users', '/admin/settings/encryption', '/admin/extension/license/support', + '/admin/audit', ]; export default function GlobalContainer() { diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx index 84c58aa20f8..7242e023fd0 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx @@ -17,13 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { subDays } from 'date-fns'; +import { + DateRangePicker, + LargeCenteredLayout, + Link, + PageContentFontWrapper, + PopupZLevel, + RadioButton, + Title, +} from 'design-system'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import Link from '../../../components/common/Link'; -import DateRangeInput from '../../../components/controls/DateRangeInput'; -import Radio from '../../../components/controls/Radio'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { now } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; @@ -72,68 +79,74 @@ export default function AuditAppRenderer(props: AuditAppRendererProps) { const { dateRange, downloadStarted, housekeepingPolicy, selection } = props; return ( - <main className="page page-limited" id="marketplace-page"> - <Suggestions suggestions="audit-logs" /> - <Helmet title={translate('audit_logs.page')} /> - - <header className="page-header"> - <h2 className="page-title">{translate('audit_logs.page')}</h2> - </header> - - <p className="big-spacer-bottom"> - {translate('audit_logs.page.description.1')} - <br /> - <FormattedMessage - id="audit_logs.page.description.2" - defaultMessage={translate('audit_logs.page.description.2')} - values={{ - housekeeping: translate('audit_logs.housekeeping_policy', housekeepingPolicy), - link: ( - <Link - to={{ - pathname: '/admin/settings', - search: queryToSearch({ category: 'housekeeping' }), - hash: '#auditLogs', - }} - > - {translate('audit_logs.page.description.link')} - </Link> - ), - }} - /> - </p> - - <div className="huge-spacer-bottom"> - <h3 className="big-spacer-bottom">{translate('audit_logs.download')}</h3> - - <ul> - {getRangeOptions(housekeepingPolicy).map((option) => ( - <li key={option} className="spacer-bottom"> - <Radio - checked={selection === option} - onCheck={props.handleOptionSelection} - value={option} - > - {translate('audit_logs.range_option', option)} - </Radio> - </li> - ))} - </ul> - - <DateRangeInput - onChange={props.handleDateSelection} - minDate={subDays(now(), HOUSEKEEPING_POLICY_VALUES[housekeepingPolicy])} - maxDate={now()} - value={dateRange} + <LargeCenteredLayout as="main" id="audit-logs-page"> + <PageContentFontWrapper className="sw-body-sm sw-my-8"> + <Suggestions suggestions="audit-logs" /> + <Helmet title={translate('audit_logs.page')} /> + + <Title>{translate('audit_logs.page')}</Title> + + <p className="sw-mb-4"> + {translate('audit_logs.page.description.1')} + <br /> + <FormattedMessage + id="audit_logs.page.description.2" + defaultMessage={translate('audit_logs.page.description.2')} + values={{ + housekeeping: translate('audit_logs.housekeeping_policy', housekeepingPolicy), + link: ( + <Link + to={{ + pathname: '/admin/settings', + search: queryToSearch({ category: 'housekeeping' }), + hash: '#auditLogs', + }} + > + {translate('audit_logs.page.description.link')} + </Link> + ), + }} + /> + </p> + + <div className="sw-mb-6"> + <h3 className="sw-mb-4">{translate('audit_logs.download')}</h3> + + <ul> + {getRangeOptions(housekeepingPolicy).map((option) => ( + <li key={option} className="sw-mb-2"> + <RadioButton + checked={selection === option} + onCheck={props.handleOptionSelection} + value={option} + > + {translate('audit_logs.range_option', option)} + </RadioButton> + </li> + ))} + </ul> + + <DateRangePicker + className="sw-w-abs-350 sw-mt-4" + clearButtonLabel={translate('clear')} + fromLabel={translate('start_date')} + onChange={props.handleDateSelection} + separatorText={translate('to_')} + toLabel={translate('end_date')} + value={dateRange} + minDate={subDays(now(), HOUSEKEEPING_POLICY_VALUES[housekeepingPolicy])} + maxDate={now()} + zLevel={PopupZLevel.Content} + /> + </div> + + <DownloadButton + dateRange={dateRange} + downloadStarted={downloadStarted} + onStartDownload={props.handleStartDownload} + selection={selection} /> - </div> - - <DownloadButton - dateRange={dateRange} - downloadStarted={downloadStarted} - onStartDownload={props.handleStartDownload} - selection={selection} - /> - </main> + </PageContentFontWrapper> + </LargeCenteredLayout> ); } diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx index 132960224bc..718bc6ee853 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.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 classNames from 'classnames'; import { endOfDay, startOfDay, subDays } from 'date-fns'; +import { ButtonPrimary } from 'design-system/lib'; import * as React from 'react'; import { now } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; @@ -63,7 +63,7 @@ function getRangeParams(selection: RangeOption, dateRange?: { from?: Date; to?: }).toString(); } -export default function DownloadButton(props: DownloadButtonProps) { +export default function DownloadButton(props: Readonly<DownloadButtonProps>) { const { dateRange, downloadStarted, selection } = props; const downloadDisabled = @@ -77,20 +77,19 @@ export default function DownloadButton(props: DownloadButtonProps) { return ( <> - <a - className={classNames('button button-primary', { disabled: downloadDisabled })} + <ButtonPrimary download="audit_logs.json" + disabled={downloadDisabled} aria-disabled={downloadDisabled} onClick={downloadDisabled ? undefined : props.onStartDownload} - href={downloadUrl} - rel="noopener noreferrer" + to={downloadUrl} target="_blank" > {translate('download_verb')} - </a> + </ButtonPrimary> {downloadStarted && ( - <div className="spacer-top"> + <div className="sw-mt-2"> <p>{translate('audit_logs.download_start.sentence.1')}</p> <p>{translate('audit_logs.download_start.sentence.2')}</p> <br /> diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx index 83ad423c2ce..31bd0f534ac 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx @@ -17,14 +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 { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { getDate, getMonth, getYear, subDays } from 'date-fns'; import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock'; import { now } from '../../../../helpers/dates'; -import { getMonthName } from '../../../../helpers/l10n'; +import { getShortMonthName } from '../../../../helpers/l10n'; import { renderAppWithAdminContext } from '../../../../helpers/testReactTestingUtils'; -import { byPlaceholderText, byRole, byText } from '../../../../helpers/testSelector'; +import { byPlaceholderText, byRole, byTestId, byText } from '../../../../helpers/testSelector'; import { AdminPageExtension } from '../../../../types/extension'; import { SettingsKey } from '../../../../types/settings'; import routes from '../../routes'; @@ -64,8 +63,8 @@ const ui = { downloadSentenceStart: byText('audit_logs.download_start.sentence.1'), startDateInput: byPlaceholderText('start_date'), endDateInput: byPlaceholderText('end_date'), - dateInputMonthSelect: byRole('combobox', { name: 'Month:' }), - dateInputYearSelect: byRole('combobox', { name: 'Year:' }), + dateInputMonthSelect: byTestId('month-select'), + dateInputYearSelect: byTestId('year-select'), }; let handler: SettingsServiceMock; @@ -113,15 +112,39 @@ it('should handle download button click', async () => { await user.click(ui.customRadio.get()); expect(ui.downloadButton.get()).toHaveAttribute('aria-disabled', 'true'); await user.click(ui.startDateInput.get()); + const monthSelector = ui.dateInputMonthSelect.byRole('combobox').get(); + await user.click(monthSelector); + await user.click( + ui.dateInputMonthSelect + .byText(getShortMonthName(getMonth(startDay))) + .getAll() + .slice(-1)[0], + ); + + const yearSelector = ui.dateInputYearSelect.byRole('combobox').get(); + await user.click(yearSelector); + await user.click( + ui.dateInputYearSelect.byText(getYear(startDay).toString()).getAll().slice(-1)[0], + ); + + await user.click(byText(getDate(startDay), { selector: 'button' }).get()); - await user.selectOptions(ui.dateInputMonthSelect.get(), getMonthName(getMonth(startDay))); - await user.selectOptions(ui.dateInputYearSelect.get(), getYear(startDay).toString()); - await user.click(screen.getByText(getDate(startDay))); await user.click(ui.endDateInput.get()); - await user.selectOptions(ui.dateInputMonthSelect.get(), getMonthName(getMonth(endDate))); - await user.selectOptions(ui.dateInputYearSelect.get(), getYear(endDate).toString()); - await user.click(screen.getByText(getDate(endDate))); + await user.click(monthSelector); + await user.click( + ui.dateInputMonthSelect + .byText(getShortMonthName(getMonth(endDate))) + .getAll() + .slice(-1)[0], + ); + + await user.click(yearSelector); + await user.click( + ui.dateInputYearSelect.byText(getYear(endDate).toString()).getAll().slice(-1)[0], + ); + + await user.click(byText(getDate(endDate), { selector: 'button' }).get()); expect(await ui.downloadButton.find()).toHaveAttribute('aria-disabled', 'false'); await user.click(downloadButton); |