aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/audit-logs
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2021-07-12 18:12:10 +0200
committersonartech <sonartech@sonarsource.com>2021-07-27 20:03:02 +0000
commit87e140b0878e858911e9763c4b677c49334e1e3d (patch)
tree783224eb9fdb19ad9da4ec9ff7ab35907ba0e657 /server/sonar-web/src/main/js/apps/audit-logs
parent9519c05d2ca5d3df513ab322baa9a3f23e93d6b7 (diff)
downloadsonarqube-87e140b0878e858911e9763c4b677c49334e1e3d.tar.gz
sonarqube-87e140b0878e858911e9763c4b677c49334e1e3d.zip
SONAR-15143 Enable users to download audit logs - UI
Diffstat (limited to 'server/sonar-web/src/main/js/apps/audit-logs')
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx128
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx57
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx85
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditApp-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap493
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/DownloadButton-test.tsx.snap90
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/routes.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/style.css19
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/utils.ts37
12 files changed, 1228 insertions, 0 deletions
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
index 00000000000..b33011d4050
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx
@@ -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
index 00000000000..7596356c453
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx
@@ -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
index 00000000000..4fc60de4916
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx
@@ -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
index 00000000000..0c8e55241cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx
@@ -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
index 00000000000..e8c53c0585b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx
@@ -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
index 00000000000..1b7e4f795e6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx
@@ -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
index 00000000000..6958c37fd2a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditApp-test.tsx.snap
@@ -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
index 00000000000..015eb529785
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap
@@ -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
index 00000000000..2bd20adf4e5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/DownloadButton-test.tsx.snap
@@ -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
index 00000000000..ac15e95075b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/routes.ts
@@ -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
index 00000000000..deac52aa61c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/style.css
@@ -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
index 00000000000..f77590b1a69
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/audit-logs/utils.ts
@@ -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();
+}