From 87e140b0878e858911e9763c4b677c49334e1e3d Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 12 Jul 2021 18:12:10 +0200 Subject: [PATCH] SONAR-15143 Enable users to download audit logs - UI --- .../components/nav/settings/SettingsNav.tsx | 20 +- .../settings/__tests__/SettingsNav-test.tsx | 9 + .../__snapshots__/SettingsNav-test.tsx.snap | 156 ++++++ .../src/main/js/app/utils/startReactApp.tsx | 2 + .../apps/audit-logs/components/AuditApp.tsx | 95 ++++ .../components/AuditAppRenderer.tsx | 128 +++++ .../audit-logs/components/DownloadButton.tsx | 97 ++++ .../components/__tests__/AuditApp-test.tsx | 87 ++++ .../__tests__/AuditAppRenderer-test.tsx | 57 ++ .../__tests__/DownloadButton-test.tsx | 85 +++ .../__snapshots__/AuditApp-test.tsx.snap | 12 + .../AuditAppRenderer-test.tsx.snap | 493 ++++++++++++++++++ .../DownloadButton-test.tsx.snap | 90 ++++ .../src/main/js/apps/audit-logs/routes.ts | 28 + .../src/main/js/apps/audit-logs/style.css | 19 + .../src/main/js/apps/audit-logs/utils.ts | 37 ++ .../js/components/controls/DateRangeInput.tsx | 9 +- .../__tests__/DateRangeInput-test.tsx | 15 + .../DateRangeInput-test.tsx.snap | 58 +++ .../sonar-web/src/main/js/types/extension.ts | 4 + .../resources/org/sonar/l10n/core.properties | 28 + 21 files changed, 1526 insertions(+), 3 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditApp-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/DownloadButton-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/routes.ts create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/style.css create mode 100644 server/sonar-web/src/main/js/apps/audit-logs/utils.ts diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index 5e63bd06600..0d4f96b0b19 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -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 { return this.isSomethingActive(urls); } + isAudit() { + const urls = ['/admin/audit']; + return this.isSomethingActive(urls); + } + renderExtension = ({ key, name }: T.Extension) => { return (
  • @@ -124,7 +130,8 @@ export default class SettingsNav extends React.PureComponent { !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 { 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 {
  • + {hasGovernanceExtension && ( +
  • + + {translate('audit_logs.page')} + +
  • + )} + {hasSupportExtension && (
  • diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx index 5a32d90ce17..a02adfd3994 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx @@ -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 = {}) { return shallow( `; +exports[`should render correctly when governance is active 1`] = ` + +
    +

    + layout.settings +

    +
    + + +
  • + + settings.page + +
  • +
  • + + property.category.security.encryption + +
  • +
  • + + webhooks.page + +
  • +
  • + + governance + +
  • + + } + tagName="li" + > + + + +
  • + + users.page + +
  • +
  • + + user_groups.page + +
  • +
  • + + global_permissions.page + +
  • +
  • + + permission_templates + +
  • + + } + tagName="li" + > + +
    + +
  • + + management + +
  • +
  • + + background_tasks.page + +
  • + + } + tagName="li" + > + +
    +
  • + + sidebar.system + +
  • +
  • + + marketplace.page + +
  • +
  • + + audit_logs.page + +
  • + + +`; + exports[`should work with extensions 1`] = ` + 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 { + 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 ? ( + + ) : 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 ( +
    + + + +

    {translate('audit_logs.page')}

    +

    + {translate('audit_logs.page.description.1')} +
    + + {translate('audit_logs.page.description.link')} + + ) + }} + /> +

    + +
    +

    {translate('audit_logs.download')}

    + +
      + {getRangeOptions(housekeepingPolicy).map(option => ( +
    • + + {translate('audit_logs.range_option', option)} + +
    • + ))} +
    + + +
    + + +
    + ); +} 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 ( + <> + + {translate('download_verb')} + + + {downloadStarted && ( +
    +

    {translate('audit_logs.download_start.sentence.1')}

    +

    {translate('audit_logs.download_start.sentence.2')}

    +
    +

    {translate('audit_logs.download_start.sentence.3')}

    +
    + )} + + ); +} 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 = {}) { + return shallow( + + ); +} 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 = {}) { + return shallow( + + ); +} 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 = {}) { + return shallow( + + ); +} 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`] = ` + +`; 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`] = ` +
    + + +

    + audit_logs.page +

    +

    + audit_logs.page.description.1 +
    + + audit_logs.page.description.link + , + } + } + /> +

    +
    +

    + audit_logs.download +

    +
      +
    • + + audit_logs.range_option.today + +
    • +
    • + + audit_logs.range_option.7days + +
    • +
    • + + audit_logs.range_option.30days + +
    • +
    • + + audit_logs.range_option.custom + +
    • +
    + +
    + +
    +`; + +exports[`should render correctly for trimestrial housekeeping policy 1`] = ` +
    + + +

    + audit_logs.page +

    +

    + audit_logs.page.description.1 +
    + + audit_logs.page.description.link + , + } + } + /> +

    +
    +

    + audit_logs.download +

    +
      +
    • + + audit_logs.range_option.today + +
    • +
    • + + audit_logs.range_option.7days + +
    • +
    • + + audit_logs.range_option.30days + +
    • +
    • + + audit_logs.range_option.90days + +
    • +
    • + + audit_logs.range_option.custom + +
    • +
    + +
    + +
    +`; + +exports[`should render correctly for weekly housekeeping policy 1`] = ` +
    + + +

    + audit_logs.page +

    +

    + audit_logs.page.description.1 +
    + + audit_logs.page.description.link + , + } + } + /> +

    +
    +

    + audit_logs.download +

    +
      +
    • + + audit_logs.range_option.today + +
    • +
    • + + audit_logs.range_option.7days + +
    • +
    • + + audit_logs.range_option.custom + +
    • +
    + +
    + +
    +`; + +exports[`should render correctly for yearly housekeeping policy 1`] = ` +
    + + +

    + audit_logs.page +

    +

    + audit_logs.page.description.1 +
    + + audit_logs.page.description.link + , + } + } + /> +

    +
    +

    + audit_logs.download +

    +
      +
    • + + audit_logs.range_option.today + +
    • +
    • + + audit_logs.range_option.7days + +
    • +
    • + + audit_logs.range_option.30days + +
    • +
    • + + audit_logs.range_option.90days + +
    • +
    • + + audit_logs.range_option.custom + +
    • +
    + +
    + +
    +`; 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`] = ` + + + download_verb + + +`; + +exports[`should render correctly for 30days: default 1`] = ` + + + download_verb + + +`; + +exports[`should render correctly for 90days: default 1`] = ` + + + download_verb + + +`; + +exports[`should render correctly for custom range: no dates 1`] = ` + + + download_verb + + +`; + +exports[`should render correctly for custom range: with dates 1`] = ` + + + download_verb + + +`; + +exports[`should render correctly for today: default 1`] = ` + + + download_verb + + +`; 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(); +} diff --git a/server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx b/server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx index 4fb7200eb11..b258ca14313 100644 --- a/server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/DateRangeInput.tsx @@ -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 { }; render() { + const { minDate, maxDate } = this.props; + return (
    { 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)} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/DateRangeInput-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/DateRangeInput-test.tsx index ca0292978ac..9e2a55482d0 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/DateRangeInput-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/DateRangeInput-test.tsx @@ -29,6 +29,21 @@ it('should render', () => { expect( shallow() ).toMatchSnapshot(); + + expect( + shallow() + ).toMatchSnapshot('with min/max'); + + expect( + shallow( + + ) + ).toMatchSnapshot('with min/max and value'); }); it('should change', () => { diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateRangeInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateRangeInput-test.tsx.snap index 2faf7c8a67c..dbc2f7cb5a2 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateRangeInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateRangeInput-test.tsx.snap @@ -29,3 +29,61 @@ exports[`should render 1`] = ` />
    `; + +exports[`should render: with min/max 1`] = ` +
    + + + to_ + + +
    +`; + +exports[`should render: with min/max and value 1`] = ` +
    + + + to_ + + +
    +`; diff --git a/server/sonar-web/src/main/js/types/extension.ts b/server/sonar-web/src/main/js/types/extension.ts index b5c6cc1582b..7ad50afb69e 100644 --- a/server/sonar-web/src/main/js/types/extension.ts +++ b/server/sonar-web/src/main/js/types/extension.ts @@ -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; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 130aa1ff065..39fd1247b93 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 -- 2.39.5