diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2021-07-12 18:12:10 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-07-27 20:03:02 +0000 |
commit | 87e140b0878e858911e9763c4b677c49334e1e3d (patch) | |
tree | 783224eb9fdb19ad9da4ec9ff7ab35907ba0e657 /server/sonar-web/src/main/js/apps/audit-logs | |
parent | 9519c05d2ca5d3df513ab322baa9a3f23e93d6b7 (diff) | |
download | sonarqube-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')
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(); +} |