]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21420 Migrate audit logs page to new UI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Wed, 17 Jan 2024 15:33:08 +0000 (16:33 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 19 Jan 2024 20:02:55 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/buttons/Button.tsx
server/sonar-web/design-system/src/components/buttons/__tests__/ButtonPrimary-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx
server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx

index 80e58555a0bc1b7fd27fc5085fe5576aad161266..6d17cd784f21978d0244f40acc0998ec84bc9b83 100644 (file)
@@ -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 (file)
index 0000000..af81f1d
--- /dev/null
@@ -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();
+});
index 9e7b58e11f7525973e3fc12c70ff57c6b9b3e7c6..f08f3ccb9b1e991b826b3edf2ba307fac4b61899 100644 (file)
@@ -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() {
index 84c58aa20f8feb7f3a4e177bd95f6e6548190795..7242e023fd0c9b288a0d7ddde824c59f343053ce 100644 (file)
  * 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>
   );
 }
index 132960224bc034ec9de28232d2335441aded44dc..718bc6ee8530bfe478a5e4dbdefa2c31de7e7f26 100644 (file)
@@ -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 />
index 83ad423c2ce857bf831a8efbd3bd621798c8fa0f..31bd0f534ac4a8bc6e082d62472355bd240894c9 100644 (file)
  * 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);