]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15143 Enable users to download audit logs - UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 12 Jul 2021 16:12:10 +0000 (18:12 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 27 Jul 2021 20:03:02 +0000 (20:03 +0000)
21 files changed:
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/DownloadButton-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/style.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/audit-logs/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx
server/sonar-web/src/main/js/components/controls/__tests__/DateRangeInput-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateRangeInput-test.tsx.snap
server/sonar-web/src/main/js/types/extension.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5e63bd066005b634e50c7d2fc63406a7b28cdd69..0d4f96b0b195f9d3d8da1110da7cfd0b6f6761be 100644 (file)
@@ -26,6 +26,7 @@ import ContextNavBar from 'sonar-ui-common/components/ui/ContextNavBar';
 import NavBarTabs from 'sonar-ui-common/components/ui/NavBarTabs';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { AdminPageExtension } from '../../../../types/extension';
 import { PendingPluginResult } from '../../../../types/plugins';
 import { rawSizes } from '../../../theme';
 import PendingPluginsActionNotif from './PendingPluginsActionNotif';
@@ -75,6 +76,11 @@ export default class SettingsNav extends React.PureComponent<Props> {
     return this.isSomethingActive(urls);
   }
 
+  isAudit() {
+    const urls = ['/admin/audit'];
+    return this.isSomethingActive(urls);
+  }
+
   renderExtension = ({ key, name }: T.Extension) => {
     return (
       <li key={key}>
@@ -124,7 +130,8 @@ export default class SettingsNav extends React.PureComponent<Props> {
                   !this.isProjectsActive() &&
                   !this.isSystemActive() &&
                   !this.isSomethingActive(['/admin/extension/license/support']) &&
-                  !this.isMarketplace())
+                  !this.isMarketplace() &&
+                  !this.isAudit())
             })}
             href="#"
             id="settings-navigation-configuration"
@@ -218,6 +225,9 @@ export default class SettingsNav extends React.PureComponent<Props> {
   render() {
     const { extensions, pendingPlugins } = this.props;
     const hasSupportExtension = extensions.find(extension => extension.key === 'license/support');
+    const hasGovernanceExtension = extensions.find(
+      e => e.key === AdminPageExtension.GovernanceConsole
+    );
     const totalPendingPlugins =
       pendingPlugins.installing.length +
       pendingPlugins.removing.length +
@@ -263,6 +273,14 @@ export default class SettingsNav extends React.PureComponent<Props> {
             </IndexLink>
           </li>
 
+          {hasGovernanceExtension && (
+            <li>
+              <IndexLink activeClassName="active" to="/admin/audit">
+                {translate('audit_logs.page')}
+              </IndexLink>
+            </li>
+          )}
+
           {hasSupportExtension && (
             <li>
               <IndexLink activeClassName="active" to="/admin/extension/license/support">
index 5a32d90ce176c9c841f03fb5920e597cb018abde..a02adfd399403255958067abe2ad234076ca9398 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { AdminPageExtension } from '../../../../../types/extension';
 import SettingsNav from '../SettingsNav';
 
 it('should work with extensions', () => {
@@ -50,6 +51,14 @@ it('should display restart notif', () => {
   expect(wrapper.find('ContextNavBar').prop('notif')).toMatchSnapshot();
 });
 
+it('should render correctly when governance is active', () => {
+  expect(
+    shallowRender({
+      extensions: [{ key: AdminPageExtension.GovernanceConsole, name: 'governance' }]
+    })
+  ).toMatchSnapshot();
+});
+
 function shallowRender(props: Partial<SettingsNav['props']> = {}) {
   return shallow(
     <SettingsNav
index 54b7d0ac6241204c7e95cc8ed74cbe47a7f1f14e..a93612af5d076407ad40ce07a04beea49415407f 100644 (file)
@@ -24,6 +24,162 @@ exports[`should display a pending plugin notif 1`] = `
 
 exports[`should display restart notif 1`] = `<SystemRestartNotif />`;
 
+exports[`should render correctly when governance is active 1`] = `
+<ContextNavBar
+  height={72}
+  id="context-navigation"
+>
+  <header
+    className="navbar-context-header"
+  >
+    <h1>
+      layout.settings
+    </h1>
+  </header>
+  <NavBarTabs>
+    <Dropdown
+      overlay={
+        <ul
+          className="menu"
+        >
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/settings"
+            >
+              settings.page
+            </IndexLink>
+          </li>
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/settings/encryption"
+            >
+              property.category.security.encryption
+            </IndexLink>
+          </li>
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/webhooks"
+            >
+              webhooks.page
+            </IndexLink>
+          </li>
+          <li>
+            <Link
+              activeClassName="active"
+              onlyActiveOnIndex={false}
+              style={Object {}}
+              to="/admin/extension/governance/views_console"
+            >
+              governance
+            </Link>
+          </li>
+        </ul>
+      }
+      tagName="li"
+    >
+      <Component />
+    </Dropdown>
+    <Dropdown
+      overlay={
+        <ul
+          className="menu"
+        >
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/users"
+            >
+              users.page
+            </IndexLink>
+          </li>
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/groups"
+            >
+              user_groups.page
+            </IndexLink>
+          </li>
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/permissions"
+            >
+              global_permissions.page
+            </IndexLink>
+          </li>
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/permission_templates"
+            >
+              permission_templates
+            </IndexLink>
+          </li>
+        </ul>
+      }
+      tagName="li"
+    >
+      <Component />
+    </Dropdown>
+    <Dropdown
+      overlay={
+        <ul
+          className="menu"
+        >
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/projects_management"
+            >
+              management
+            </IndexLink>
+          </li>
+          <li>
+            <IndexLink
+              activeClassName="active"
+              to="/admin/background_tasks"
+            >
+              background_tasks.page
+            </IndexLink>
+          </li>
+        </ul>
+      }
+      tagName="li"
+    >
+      <Component />
+    </Dropdown>
+    <li>
+      <IndexLink
+        activeClassName="active"
+        to="/admin/system"
+      >
+        sidebar.system
+      </IndexLink>
+    </li>
+    <li>
+      <IndexLink
+        activeClassName="active"
+        to="/admin/marketplace"
+      >
+        marketplace.page
+      </IndexLink>
+    </li>
+    <li>
+      <IndexLink
+        activeClassName="active"
+        to="/admin/audit"
+      >
+        audit_logs.page
+      </IndexLink>
+    </li>
+  </NavBarTabs>
+</ContextNavBar>
+`;
+
 exports[`should work with extensions 1`] = `
 <ContextNavBar
   height={72}
index 5a026f3eae01318c499d7e6dc9f90d836ecbee8c..f3d5ed8709b6ec7580783a90812cdb121820dd12 100644 (file)
@@ -32,6 +32,7 @@ import getHistory from 'sonar-ui-common/helpers/getHistory';
 import aboutRoutes from '../../apps/about/routes';
 import accountRoutes from '../../apps/account/routes';
 import applicationConsoleRoutes from '../../apps/application-console/routes';
+import auditLogsRoutes from '../../apps/audit-logs/routes';
 import backgroundTasksRoutes from '../../apps/background-tasks/routes';
 import codeRoutes from '../../apps/code/routes';
 import codingRulesRoutes from '../../apps/coding-rules/routes';
@@ -229,6 +230,7 @@ function renderAdminRoutes() {
           import('../components/extensions/GlobalAdminPageExtension')
         )}
       />
+      <RouteWithChildRoutes path="audit" childRoutes={auditLogsRoutes} />
       <RouteWithChildRoutes path="background_tasks" childRoutes={backgroundTasksRoutes} />
       <RouteWithChildRoutes path="groups" childRoutes={groupsRoutes} />
       <RouteWithChildRoutes path="permission_templates" childRoutes={permissionTemplatesRoutes} />
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx
new file mode 100644 (file)
index 0000000..b33011d
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { connect } from 'react-redux';
+import { getAppState, getGlobalSettingValue, Store } from '../../../store/rootReducer';
+import { AdminPageExtension } from '../../../types/extension';
+import { fetchValues } from '../../settings/store/actions';
+import '../style.css';
+import { HousekeepingPolicy, RangeOption } from '../utils';
+import AuditAppRenderer from './AuditAppRenderer';
+
+interface Props {
+  auditHousekeepingPolicy: HousekeepingPolicy;
+  fetchValues: typeof fetchValues;
+  hasGovernanceExtension?: boolean;
+}
+
+interface State {
+  dateRange?: { from?: Date; to?: Date };
+  downloadStarted: boolean;
+  selection: RangeOption;
+}
+
+export class AuditApp extends React.PureComponent<Props, State> {
+  state: State = {
+    downloadStarted: false,
+    selection: RangeOption.Today
+  };
+
+  componentDidMount() {
+    const { hasGovernanceExtension } = this.props;
+    if (hasGovernanceExtension) {
+      this.props.fetchValues('sonar.dbcleaner.auditHousekeeping');
+    }
+  }
+
+  handleDateSelection = (dateRange: { from?: Date; to?: Date }) =>
+    this.setState({ dateRange, downloadStarted: false, selection: RangeOption.Custom });
+
+  handleOptionSelection = (selection: RangeOption) =>
+    this.setState({ dateRange: undefined, downloadStarted: false, selection });
+
+  handleStartDownload = () => {
+    setTimeout(() => {
+      this.setState({ downloadStarted: true });
+    }, 0);
+  };
+
+  render() {
+    const { hasGovernanceExtension, auditHousekeepingPolicy } = this.props;
+
+    return hasGovernanceExtension ? (
+      <AuditAppRenderer
+        handleDateSelection={this.handleDateSelection}
+        handleOptionSelection={this.handleOptionSelection}
+        handleStartDownload={this.handleStartDownload}
+        housekeepingPolicy={auditHousekeepingPolicy || HousekeepingPolicy.Monthly}
+        {...this.state}
+      />
+    ) : null;
+  }
+}
+
+const mapDispatchToProps = { fetchValues };
+
+const mapStateToProps = (state: Store) => {
+  const settingValue = getGlobalSettingValue(state, 'sonar.dbcleaner.auditHousekeeping');
+  const { adminPages } = getAppState(state);
+  const hasGovernanceExtension = Boolean(
+    adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole)
+  );
+  return {
+    auditHousekeepingPolicy: settingValue?.value as HousekeepingPolicy,
+    hasGovernanceExtension
+  };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(AuditApp);
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
new file mode 100644 (file)
index 0000000..7596356
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { subDays } from 'date-fns';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import DateRangeInput from '../../../components/controls/DateRangeInput';
+import '../style.css';
+import { HousekeepingPolicy, now, RangeOption } from '../utils';
+import DownloadButton from './DownloadButton';
+
+export interface AuditAppRendererProps {
+  dateRange?: { from?: Date; to?: Date };
+  downloadStarted: boolean;
+  handleOptionSelection: (option: RangeOption) => void;
+  handleDateSelection: (dateRange: { from?: Date; to?: Date }) => void;
+  handleStartDownload: () => void;
+  housekeepingPolicy: HousekeepingPolicy;
+  selection: RangeOption;
+}
+
+const HOUSEKEEPING_MONTH_THRESHOLD = 30;
+const HOUSEKEEPING_TRIMESTER_THRESHOLD = 90;
+
+const HOUSEKEEPING_POLICY_VALUES = {
+  [HousekeepingPolicy.Weekly]: 7,
+  [HousekeepingPolicy.Monthly]: 30,
+  [HousekeepingPolicy.Trimestrial]: 90,
+  [HousekeepingPolicy.Yearly]: 365
+};
+
+const getRangeOptions = (housekeepingPolicy: HousekeepingPolicy) => {
+  const rangeOptions = [RangeOption.Today, RangeOption.Week];
+
+  if (HOUSEKEEPING_POLICY_VALUES[housekeepingPolicy] >= HOUSEKEEPING_MONTH_THRESHOLD) {
+    rangeOptions.push(RangeOption.Month);
+  }
+
+  if (HOUSEKEEPING_POLICY_VALUES[housekeepingPolicy] >= HOUSEKEEPING_TRIMESTER_THRESHOLD) {
+    rangeOptions.push(RangeOption.Trimester);
+  }
+
+  rangeOptions.push(RangeOption.Custom);
+
+  return rangeOptions;
+};
+
+export default function AuditAppRenderer(props: AuditAppRendererProps) {
+  const { dateRange, downloadStarted, housekeepingPolicy, selection } = props;
+
+  return (
+    <div className="page page-limited" id="marketplace-page">
+      <Suggestions suggestions="audit-logs" />
+      <Helmet title={translate('audit_logs.page')} />
+
+      <h1 className="spacer-bottom">{translate('audit_logs.page')}</h1>
+      <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.houskeeping_policy', housekeepingPolicy),
+            link: (
+              <Link to={{ pathname: '/admin/settings', query: { category: 'housekeeping' } }}>
+                {translate('audit_logs.page.description.link')}
+              </Link>
+            )
+          }}
+        />
+      </p>
+
+      <div className="huge-spacer-bottom">
+        <h2 className="big-spacer-bottom">{translate('audit_logs.download')}</h2>
+
+        <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
+          className="big-spacer-left"
+          onChange={props.handleDateSelection}
+          minDate={subDays(now(), HOUSEKEEPING_POLICY_VALUES[housekeepingPolicy])}
+          maxDate={now()}
+          value={dateRange}
+        />
+      </div>
+
+      <DownloadButton
+        dateRange={dateRange}
+        downloadStarted={downloadStarted}
+        onStartDownload={props.handleStartDownload}
+        selection={selection}
+      />
+    </div>
+  );
+}
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
new file mode 100644 (file)
index 0000000..4fc60de
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 classNames from 'classnames';
+import { endOfDay, startOfDay, subDays } from 'date-fns';
+import * as React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from '../../../helpers/system';
+import '../style.css';
+import { now, RangeOption } from '../utils';
+
+export interface DownloadButtonProps {
+  dateRange?: { from?: Date; to?: Date };
+  downloadStarted: boolean;
+  onStartDownload: () => void;
+  selection: RangeOption;
+}
+
+const RANGE_OPTION_START = {
+  [RangeOption.Today]: () => now(),
+  [RangeOption.Week]: () => subDays(now(), 7),
+  [RangeOption.Month]: () => subDays(now(), 30),
+  [RangeOption.Trimester]: () => subDays(now(), 90)
+};
+
+const toISODateString = (date: Date) => date.toISOString();
+
+function getRangeParams(selection: RangeOption, dateRange?: { from?: Date; to?: Date }) {
+  if (selection === RangeOption.Custom) {
+    // dateRange should be complete if 'custom' is selected
+    if (!(dateRange?.to && dateRange?.from)) {
+      return '';
+    }
+
+    return new URLSearchParams({
+      from: toISODateString(startOfDay(dateRange.from)),
+      to: toISODateString(endOfDay(dateRange.to))
+    }).toString();
+  }
+
+  return new URLSearchParams({
+    from: toISODateString(startOfDay(RANGE_OPTION_START[selection]())),
+    to: toISODateString(now())
+  }).toString();
+}
+
+export default function DownloadButton(props: DownloadButtonProps) {
+  const { dateRange, downloadStarted, selection } = props;
+
+  const downloadDisabled =
+    downloadStarted ||
+    (selection === RangeOption.Custom &&
+      (dateRange?.from === undefined || dateRange?.to === undefined));
+
+  const downloadUrl = downloadDisabled
+    ? '#'
+    : `${getBaseUrl()}/api/audit_logs/download?${getRangeParams(selection, dateRange)}`;
+
+  return (
+    <>
+      <a
+        className={classNames('button button-primary', { disabled: downloadDisabled })}
+        download="audit-logs.json"
+        onClick={downloadDisabled ? undefined : props.onStartDownload}
+        href={downloadUrl}
+        rel="noopener noreferrer"
+        target="_blank">
+        {translate('download_verb')}
+      </a>
+
+      {downloadStarted && (
+        <div className="spacer-top">
+          <p>{translate('audit_logs.download_start.sentence.1')}</p>
+          <p>{translate('audit_logs.download_start.sentence.2')}</p>
+          <br />
+          <p>{translate('audit_logs.download_start.sentence.3')}</p>
+        </div>
+      )}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx
new file mode 100644 (file)
index 0000000..0c8e552
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { subDays } from 'date-fns';
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { HousekeepingPolicy, RangeOption } from '../../utils';
+import { AuditApp } from '../AuditApp';
+import AuditAppRenderer from '../AuditAppRenderer';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should do nothing if governance is not available', async () => {
+  const fetchValues = jest.fn();
+  const wrapper = shallowRender({ fetchValues, hasGovernanceExtension: false });
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.type()).toBeNull();
+  expect(fetchValues).not.toBeCalled();
+});
+
+it('should fetch houskeeping policy on mount', async () => {
+  const fetchValues = jest.fn();
+  const wrapper = shallowRender({ fetchValues });
+  await waitAndUpdate(wrapper);
+  expect(fetchValues).toBeCalled();
+});
+
+it('should handle date selection', () => {
+  const wrapper = shallowRender();
+  const range = { from: subDays(new Date(), 2), to: new Date() };
+
+  expect(wrapper.state().selection).toBe(RangeOption.Today);
+
+  wrapper
+    .find(AuditAppRenderer)
+    .props()
+    .handleDateSelection(range);
+
+  expect(wrapper.state().selection).toBe(RangeOption.Custom);
+  expect(wrapper.state().dateRange).toBe(range);
+});
+
+it('should handle predefined selection', () => {
+  const wrapper = shallowRender();
+  const dateRange = { from: subDays(new Date(), 2), to: new Date() };
+
+  wrapper.setState({ dateRange, selection: RangeOption.Custom });
+
+  wrapper
+    .find(AuditAppRenderer)
+    .props()
+    .handleOptionSelection(RangeOption.Week);
+
+  expect(wrapper.state().selection).toBe(RangeOption.Week);
+  expect(wrapper.state().dateRange).toBeUndefined();
+});
+
+function shallowRender(props: Partial<AuditApp['props']> = {}) {
+  return shallow<AuditApp>(
+    <AuditApp
+      auditHousekeepingPolicy={HousekeepingPolicy.Monthly}
+      fetchValues={jest.fn()}
+      hasGovernanceExtension={true}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx
new file mode 100644 (file)
index 0000000..e8c53c0
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { HousekeepingPolicy, RangeOption } from '../../utils';
+import AuditAppRenderer, { AuditAppRendererProps } from '../AuditAppRenderer';
+
+jest.mock('../../utils', () => {
+  const { HousekeepingPolicy, RangeOption } = jest.requireActual('../../utils');
+  const now = new Date('2020-07-21T12:00:00Z');
+
+  return {
+    HousekeepingPolicy,
+    now: jest.fn().mockReturnValue(now),
+    RangeOption
+  };
+});
+
+it.each([
+  [HousekeepingPolicy.Weekly],
+  [HousekeepingPolicy.Monthly],
+  [HousekeepingPolicy.Trimestrial],
+  [HousekeepingPolicy.Yearly]
+])('should render correctly for %s housekeeping policy', housekeepingPolicy => {
+  expect(shallowRender({ housekeepingPolicy })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<AuditAppRendererProps> = {}) {
+  return shallow(
+    <AuditAppRenderer
+      downloadStarted={false}
+      handleDateSelection={jest.fn()}
+      handleOptionSelection={jest.fn()}
+      handleStartDownload={jest.fn()}
+      housekeepingPolicy={HousekeepingPolicy.Yearly}
+      selection={RangeOption.Today}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx
new file mode 100644 (file)
index 0000000..1b7e4f7
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { subDays } from 'date-fns';
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { RangeOption } from '../../utils';
+import DownloadButton, { DownloadButtonProps } from '../DownloadButton';
+
+jest.mock('date-fns', () => {
+  const { subDays } = jest.requireActual('date-fns');
+  return {
+    endOfDay: jest.fn().mockImplementation(d => d),
+    startOfDay: jest.fn().mockImplementation(d => d),
+    subDays
+  };
+});
+
+jest.mock('../../utils', () => {
+  const { HousekeepingPolicy, RangeOption } = jest.requireActual('../../utils');
+  const now = new Date('2020-07-21T12:00:00Z');
+
+  return {
+    HousekeepingPolicy,
+    now: jest.fn().mockReturnValue(now),
+    RangeOption
+  };
+});
+
+it.each([[RangeOption.Today], [RangeOption.Week], [RangeOption.Month], [RangeOption.Trimester]])(
+  'should render correctly for %s',
+  selection => {
+    expect(shallowRender({ selection })).toMatchSnapshot('default');
+  }
+);
+
+it('should render correctly for custom range', () => {
+  const baseDate = new Date('2020-07-21T12:00:00Z');
+
+  expect(shallowRender({ selection: RangeOption.Custom })).toMatchSnapshot('no dates');
+  expect(
+    shallowRender({
+      dateRange: { from: subDays(baseDate, 2), to: baseDate },
+      selection: RangeOption.Custom
+    })
+  ).toMatchSnapshot('with dates');
+});
+
+it('should handle download', () => {
+  const onStartDownload = jest.fn();
+  const wrapper = shallowRender({ onStartDownload });
+
+  wrapper.find('a').simulate('click');
+  wrapper.setProps({ downloadStarted: true });
+  wrapper.find('a').simulate('click');
+
+  expect(onStartDownload).toBeCalledTimes(1);
+});
+
+function shallowRender(props: Partial<DownloadButtonProps> = {}) {
+  return shallow<DownloadButtonProps>(
+    <DownloadButton
+      downloadStarted={false}
+      selection={RangeOption.Today}
+      onStartDownload={jest.fn()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditApp-test.tsx.snap
new file mode 100644 (file)
index 0000000..6958c37
--- /dev/null
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<AuditAppRenderer
+  downloadStarted={false}
+  handleDateSelection={[Function]}
+  handleOptionSelection={[Function]}
+  handleStartDownload={[Function]}
+  housekeepingPolicy="monthly"
+  selection="today"
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..015eb52
--- /dev/null
@@ -0,0 +1,493 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for monthly housekeeping policy 1`] = `
+<div
+  className="page page-limited"
+  id="marketplace-page"
+>
+  <Suggestions
+    suggestions="audit-logs"
+  />
+  <Helmet
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="audit_logs.page"
+  />
+  <h1
+    className="spacer-bottom"
+  >
+    audit_logs.page
+  </h1>
+  <p
+    className="big-spacer-bottom"
+  >
+    audit_logs.page.description.1
+    <br />
+    <FormattedMessage
+      defaultMessage="audit_logs.page.description.2"
+      id="audit_logs.page.description.2"
+      values={
+        Object {
+          "housekeeping": "audit_logs.houskeeping_policy.monthly",
+          "link": <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/admin/settings",
+                "query": Object {
+                  "category": "housekeeping",
+                },
+              }
+            }
+          >
+            audit_logs.page.description.link
+          </Link>,
+        }
+      }
+    />
+  </p>
+  <div
+    className="huge-spacer-bottom"
+  >
+    <h2
+      className="big-spacer-bottom"
+    >
+      audit_logs.download
+    </h2>
+    <ul>
+      <li
+        className="spacer-bottom"
+        key="today"
+      >
+        <Radio
+          checked={true}
+          onCheck={[MockFunction]}
+          value="today"
+        >
+          audit_logs.range_option.today
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="7days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="7days"
+        >
+          audit_logs.range_option.7days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="30days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="30days"
+        >
+          audit_logs.range_option.30days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="custom"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="custom"
+        >
+          audit_logs.range_option.custom
+        </Radio>
+      </li>
+    </ul>
+    <DateRangeInput
+      className="big-spacer-left"
+      maxDate={2020-07-21T12:00:00.000Z}
+      minDate={2020-06-21T12:00:00.000Z}
+      onChange={[MockFunction]}
+    />
+  </div>
+  <DownloadButton
+    downloadStarted={false}
+    onStartDownload={[MockFunction]}
+    selection="today"
+  />
+</div>
+`;
+
+exports[`should render correctly for trimestrial housekeeping policy 1`] = `
+<div
+  className="page page-limited"
+  id="marketplace-page"
+>
+  <Suggestions
+    suggestions="audit-logs"
+  />
+  <Helmet
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="audit_logs.page"
+  />
+  <h1
+    className="spacer-bottom"
+  >
+    audit_logs.page
+  </h1>
+  <p
+    className="big-spacer-bottom"
+  >
+    audit_logs.page.description.1
+    <br />
+    <FormattedMessage
+      defaultMessage="audit_logs.page.description.2"
+      id="audit_logs.page.description.2"
+      values={
+        Object {
+          "housekeeping": "audit_logs.houskeeping_policy.trimestrial",
+          "link": <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/admin/settings",
+                "query": Object {
+                  "category": "housekeeping",
+                },
+              }
+            }
+          >
+            audit_logs.page.description.link
+          </Link>,
+        }
+      }
+    />
+  </p>
+  <div
+    className="huge-spacer-bottom"
+  >
+    <h2
+      className="big-spacer-bottom"
+    >
+      audit_logs.download
+    </h2>
+    <ul>
+      <li
+        className="spacer-bottom"
+        key="today"
+      >
+        <Radio
+          checked={true}
+          onCheck={[MockFunction]}
+          value="today"
+        >
+          audit_logs.range_option.today
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="7days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="7days"
+        >
+          audit_logs.range_option.7days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="30days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="30days"
+        >
+          audit_logs.range_option.30days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="90days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="90days"
+        >
+          audit_logs.range_option.90days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="custom"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="custom"
+        >
+          audit_logs.range_option.custom
+        </Radio>
+      </li>
+    </ul>
+    <DateRangeInput
+      className="big-spacer-left"
+      maxDate={2020-07-21T12:00:00.000Z}
+      minDate={2020-04-22T12:00:00.000Z}
+      onChange={[MockFunction]}
+    />
+  </div>
+  <DownloadButton
+    downloadStarted={false}
+    onStartDownload={[MockFunction]}
+    selection="today"
+  />
+</div>
+`;
+
+exports[`should render correctly for weekly housekeeping policy 1`] = `
+<div
+  className="page page-limited"
+  id="marketplace-page"
+>
+  <Suggestions
+    suggestions="audit-logs"
+  />
+  <Helmet
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="audit_logs.page"
+  />
+  <h1
+    className="spacer-bottom"
+  >
+    audit_logs.page
+  </h1>
+  <p
+    className="big-spacer-bottom"
+  >
+    audit_logs.page.description.1
+    <br />
+    <FormattedMessage
+      defaultMessage="audit_logs.page.description.2"
+      id="audit_logs.page.description.2"
+      values={
+        Object {
+          "housekeeping": "audit_logs.houskeeping_policy.weekly",
+          "link": <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/admin/settings",
+                "query": Object {
+                  "category": "housekeeping",
+                },
+              }
+            }
+          >
+            audit_logs.page.description.link
+          </Link>,
+        }
+      }
+    />
+  </p>
+  <div
+    className="huge-spacer-bottom"
+  >
+    <h2
+      className="big-spacer-bottom"
+    >
+      audit_logs.download
+    </h2>
+    <ul>
+      <li
+        className="spacer-bottom"
+        key="today"
+      >
+        <Radio
+          checked={true}
+          onCheck={[MockFunction]}
+          value="today"
+        >
+          audit_logs.range_option.today
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="7days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="7days"
+        >
+          audit_logs.range_option.7days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="custom"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="custom"
+        >
+          audit_logs.range_option.custom
+        </Radio>
+      </li>
+    </ul>
+    <DateRangeInput
+      className="big-spacer-left"
+      maxDate={2020-07-21T12:00:00.000Z}
+      minDate={2020-07-14T12:00:00.000Z}
+      onChange={[MockFunction]}
+    />
+  </div>
+  <DownloadButton
+    downloadStarted={false}
+    onStartDownload={[MockFunction]}
+    selection="today"
+  />
+</div>
+`;
+
+exports[`should render correctly for yearly housekeeping policy 1`] = `
+<div
+  className="page page-limited"
+  id="marketplace-page"
+>
+  <Suggestions
+    suggestions="audit-logs"
+  />
+  <Helmet
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="audit_logs.page"
+  />
+  <h1
+    className="spacer-bottom"
+  >
+    audit_logs.page
+  </h1>
+  <p
+    className="big-spacer-bottom"
+  >
+    audit_logs.page.description.1
+    <br />
+    <FormattedMessage
+      defaultMessage="audit_logs.page.description.2"
+      id="audit_logs.page.description.2"
+      values={
+        Object {
+          "housekeeping": "audit_logs.houskeeping_policy.yearly",
+          "link": <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/admin/settings",
+                "query": Object {
+                  "category": "housekeeping",
+                },
+              }
+            }
+          >
+            audit_logs.page.description.link
+          </Link>,
+        }
+      }
+    />
+  </p>
+  <div
+    className="huge-spacer-bottom"
+  >
+    <h2
+      className="big-spacer-bottom"
+    >
+      audit_logs.download
+    </h2>
+    <ul>
+      <li
+        className="spacer-bottom"
+        key="today"
+      >
+        <Radio
+          checked={true}
+          onCheck={[MockFunction]}
+          value="today"
+        >
+          audit_logs.range_option.today
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="7days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="7days"
+        >
+          audit_logs.range_option.7days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="30days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="30days"
+        >
+          audit_logs.range_option.30days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="90days"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="90days"
+        >
+          audit_logs.range_option.90days
+        </Radio>
+      </li>
+      <li
+        className="spacer-bottom"
+        key="custom"
+      >
+        <Radio
+          checked={false}
+          onCheck={[MockFunction]}
+          value="custom"
+        >
+          audit_logs.range_option.custom
+        </Radio>
+      </li>
+    </ul>
+    <DateRangeInput
+      className="big-spacer-left"
+      maxDate={2020-07-21T12:00:00.000Z}
+      minDate={2019-07-22T12:00:00.000Z}
+      onChange={[MockFunction]}
+    />
+  </div>
+  <DownloadButton
+    downloadStarted={false}
+    onStartDownload={[MockFunction]}
+    selection="today"
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/DownloadButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/DownloadButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..2bd20ad
--- /dev/null
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for 7days: default 1`] = `
+<Fragment>
+  <a
+    className="button button-primary"
+    download="audit_logs.json"
+    href="/api/audit_logs/download?from=2020-07-14T12%3A00%3A00.000Z&to=2020-07-21T12%3A00%3A00.000Z"
+    onClick={[MockFunction]}
+    rel="noopener noreferrer"
+    target="_blank"
+  >
+    download_verb
+  </a>
+</Fragment>
+`;
+
+exports[`should render correctly for 30days: default 1`] = `
+<Fragment>
+  <a
+    className="button button-primary"
+    download="audit_logs.json"
+    href="/api/audit_logs/download?from=2020-06-21T12%3A00%3A00.000Z&to=2020-07-21T12%3A00%3A00.000Z"
+    onClick={[MockFunction]}
+    rel="noopener noreferrer"
+    target="_blank"
+  >
+    download_verb
+  </a>
+</Fragment>
+`;
+
+exports[`should render correctly for 90days: default 1`] = `
+<Fragment>
+  <a
+    className="button button-primary"
+    download="audit_logs.json"
+    href="/api/audit_logs/download?from=2020-04-22T12%3A00%3A00.000Z&to=2020-07-21T12%3A00%3A00.000Z"
+    onClick={[MockFunction]}
+    rel="noopener noreferrer"
+    target="_blank"
+  >
+    download_verb
+  </a>
+</Fragment>
+`;
+
+exports[`should render correctly for custom range: no dates 1`] = `
+<Fragment>
+  <a
+    className="button button-primary disabled"
+    download="audit_logs.json"
+    href="#"
+    rel="noopener noreferrer"
+    target="_blank"
+  >
+    download_verb
+  </a>
+</Fragment>
+`;
+
+exports[`should render correctly for custom range: with dates 1`] = `
+<Fragment>
+  <a
+    className="button button-primary"
+    download="audit_logs.json"
+    href="/api/audit_logs/download?from=2020-07-19T12%3A00%3A00.000Z&to=2020-07-21T12%3A00%3A00.000Z"
+    onClick={[MockFunction]}
+    rel="noopener noreferrer"
+    target="_blank"
+  >
+    download_verb
+  </a>
+</Fragment>
+`;
+
+exports[`should render correctly for today: default 1`] = `
+<Fragment>
+  <a
+    className="button button-primary"
+    download="audit_logs.json"
+    href="/api/audit_logs/download?from=2020-07-21T12%3A00%3A00.000Z&to=2020-07-21T12%3A00%3A00.000Z"
+    onClick={[MockFunction]}
+    rel="noopener noreferrer"
+    target="_blank"
+  >
+    download_verb
+  </a>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/routes.ts b/server/sonar-web/src/main/js/apps/audit-logs/routes.ts
new file mode 100644 (file)
index 0000000..ac15e95
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';
+
+const routes = [
+  {
+    indexRoute: { component: lazyLoadComponent(() => import('./components/AuditApp')) }
+  }
+];
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/style.css b/server/sonar-web/src/main/js/apps/audit-logs/style.css
new file mode 100644 (file)
index 0000000..deac52a
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/utils.ts b/server/sonar-web/src/main/js/apps/audit-logs/utils.ts
new file mode 100644 (file)
index 0000000..f77590b
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+export enum HousekeepingPolicy {
+  Weekly = 'weekly',
+  Monthly = 'monthly',
+  Trimestrial = 'trimestrial',
+  Yearly = 'yearly'
+}
+
+export enum RangeOption {
+  Today = 'today',
+  Week = '7days',
+  Month = '30days',
+  Trimester = '90days',
+  Custom = 'custom'
+}
+
+export function now() {
+  return new Date();
+}
index 4fb7200eb11d9fe3a12c89a9de07127085c5142c..b258ca14313eba35194ecc32d3abed2efe78df49 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as classNames from 'classnames';
+import { max, min } from 'date-fns';
 import * as React from 'react';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import DateInput from './DateInput';
@@ -59,13 +60,16 @@ export default class DateRangeInput extends React.PureComponent<Props> {
   };
 
   render() {
+    const { minDate, maxDate } = this.props;
+
     return (
       <div className={classNames('display-inline-flex-center', this.props.className)}>
         <DateInput
           currentMonth={this.to}
           data-test="from"
           highlightTo={this.to}
-          maxDate={this.to}
+          minDate={minDate}
+          maxDate={maxDate && this.to ? min(maxDate, this.to) : maxDate || this.to}
           onChange={this.handleFromChange}
           placeholder={translate('start_date')}
           value={this.from}
@@ -75,7 +79,8 @@ export default class DateRangeInput extends React.PureComponent<Props> {
           currentMonth={this.from}
           data-test="to"
           highlightFrom={this.from}
-          minDate={this.from}
+          minDate={minDate && this.from ? max(minDate, this.from) : minDate || this.from}
+          maxDate={maxDate}
           onChange={this.handleToChange}
           placeholder={translate('end_date')}
           ref={element => (this.toDateInput = element)}
index ca0292978ac86ef7fce68efc10c14999571333d7..9e2a55482d05a8c94ae12945ca6a63e193c859a8 100644 (file)
@@ -29,6 +29,21 @@ it('should render', () => {
   expect(
     shallow(<DateRangeInput onChange={jest.fn()} value={{ from: dateA, to: dateB }} />)
   ).toMatchSnapshot();
+
+  expect(
+    shallow(<DateRangeInput onChange={jest.fn()} minDate={dateA} maxDate={dateB} />)
+  ).toMatchSnapshot('with min/max');
+
+  expect(
+    shallow(
+      <DateRangeInput
+        onChange={jest.fn()}
+        minDate={dateA}
+        maxDate={dateB}
+        value={{ from: dateA, to: dateB }}
+      />
+    )
+  ).toMatchSnapshot('with min/max and value');
 });
 
 it('should change', () => {
index 2faf7c8a67c1868756700a7bc65122bd2d6e98eb..dbc2f7cb5a2d2a7df71bc0edcf5ba743f3383883 100644 (file)
@@ -29,3 +29,61 @@ exports[`should render 1`] = `
   />
 </div>
 `;
+
+exports[`should render: with min/max 1`] = `
+<div
+  className="display-inline-flex-center"
+>
+  <DateInput
+    data-test="from"
+    maxDate={2018-02-05T00:00:00.000Z}
+    minDate={2018-01-17T00:00:00.000Z}
+    onChange={[Function]}
+    placeholder="start_date"
+  />
+  <span
+    className="note little-spacer-left little-spacer-right"
+  >
+    to_
+  </span>
+  <DateInput
+    data-test="to"
+    maxDate={2018-02-05T00:00:00.000Z}
+    minDate={2018-01-17T00:00:00.000Z}
+    onChange={[Function]}
+    placeholder="end_date"
+  />
+</div>
+`;
+
+exports[`should render: with min/max and value 1`] = `
+<div
+  className="display-inline-flex-center"
+>
+  <DateInput
+    currentMonth={2018-02-05T00:00:00.000Z}
+    data-test="from"
+    highlightTo={2018-02-05T00:00:00.000Z}
+    maxDate={2018-02-05T00:00:00.000Z}
+    minDate={2018-01-17T00:00:00.000Z}
+    onChange={[Function]}
+    placeholder="start_date"
+    value={2018-01-17T00:00:00.000Z}
+  />
+  <span
+    className="note little-spacer-left little-spacer-right"
+  >
+    to_
+  </span>
+  <DateInput
+    currentMonth={2018-01-17T00:00:00.000Z}
+    data-test="to"
+    highlightFrom={2018-01-17T00:00:00.000Z}
+    maxDate={2018-02-05T00:00:00.000Z}
+    minDate={2018-01-17T00:00:00.000Z}
+    onChange={[Function]}
+    placeholder="end_date"
+    value={2018-02-05T00:00:00.000Z}
+  />
+</div>
+`;
index b5c6cc1582be7132450e94fc90b6e2f1686194a5..7ad50afb69edf098fd77320f000f102ddb45bd39 100644 (file)
@@ -24,6 +24,10 @@ import { Location, Router } from '../components/hoc/withRouter';
 import { Store } from '../store/rootReducer';
 import { L10nBundle } from './l10n';
 
+export enum AdminPageExtension {
+  GovernanceConsole = 'governance/views_console'
+}
+
 export interface ExtensionStartMethod {
   (params: ExtensionStartMethodParameter | string): ExtensionStartMethodReturnType;
 }
index 130aa1ff065d96ec0354bcd2306cc04df5a6df60..39fd1247b93ceeb68a88df79a689c14e2a4f5614 100644 (file)
@@ -686,6 +686,34 @@ process.fail=Failed
 #------------------------------------------------------------------------------
 
 sessions.log_in=Log in
+#------------------------------------------------------------------------------
+#
+# Audit Logs
+#
+#------------------------------------------------------------------------------
+
+audit_logs.page=Audit Logs
+audit_logs.page.description.1=Audit logs help Administrators keep control and traceability of security related changes performed on the platform. 
+audit_logs.page.description.2=Your instance is set to keep audit logs for {housekeeping}. You can change the number of days by updating your {link}. 
+audit_logs.page.description.link=housekeeping policy
+
+audit_logs.houskeeping_policy.weekly=7 days
+audit_logs.houskeeping_policy.monthly=30 days
+audit_logs.houskeeping_policy.trimestrial=90 days
+audit_logs.houskeeping_policy.yearly=one year
+
+audit_logs.download=Download audit logs
+audit_logs.download_start.try_again=Try Again
+audit_logs.download_start.sentence.1=Your download should start shortly. For longer periods this might take some time.
+audit_logs.download_start.sentence.2=If the download doesn’t start after a few seconds, contact your administrator.
+audit_logs.download_start.sentence.3=Change your selection above to download additional audit logs.
+
+audit_logs.range_option.today=Today
+audit_logs.range_option.7days=7 days
+audit_logs.range_option.30days=30 days
+audit_logs.range_option.90days=90 days
+audit_logs.range_option.custom=Custom
+
 #------------------------------------------------------------------------------
 #
 # HOTSPOTS