From adc1fc316700608671a18bcdcba001eb45c0645b Mon Sep 17 00:00:00 2001 From: Philippe Perrin Date: Wed, 28 Jul 2021 10:49:06 +0200 Subject: [PATCH] SONAR-15138 Add report download and subscription buttons for projects and applications --- .../js/api/{report.ts => component-report.ts} | 42 +- .../apps/overview/branches/MeasuresPanel.tsx | 6 +- .../__snapshots__/MeasuresPanel-test.tsx.snap | 399 ++++++++++++++++-- .../main/js/apps/portfolio/components/App.tsx | 5 +- .../js/apps/portfolio/components/Report.tsx | 112 ----- .../portfolio/components/Subscription.tsx | 89 ---- .../components/__tests__/Report-test.tsx | 55 --- .../__tests__/Subscription-test.tsx | 108 ----- .../__tests__/__snapshots__/App-test.tsx.snap | 2 +- .../__snapshots__/Report-test.tsx.snap | 49 --- .../__snapshots__/Subscription-test.tsx.snap | 27 -- .../controls/ComponentReportActions.tsx | 140 ++++++ .../ComponentReportActionsRenderer.tsx | 100 +++++ .../__tests__/ComponentReportActions-test.tsx | 127 ++++++ .../ComponentReportActionsRenderer-test.tsx | 56 +++ ...mponentReportActionsRenderer-test.tsx.snap | 136 ++++++ .../main/js/helpers/mocks/component-report.ts | 36 ++ .../src/main/js/types/component-report.ts | 30 ++ .../resources/org/sonar/l10n/core.properties | 15 +- 19 files changed, 1036 insertions(+), 498 deletions(-) rename server/sonar-web/src/main/js/api/{report.ts => component-report.ts} (63%) delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/ComponentReportActionsRenderer.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActionsRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ComponentReportActionsRenderer-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/helpers/mocks/component-report.ts create mode 100644 server/sonar-web/src/main/js/types/component-report.ts diff --git a/server/sonar-web/src/main/js/api/report.ts b/server/sonar-web/src/main/js/api/component-report.ts similarity index 63% rename from server/sonar-web/src/main/js/api/report.ts rename to server/sonar-web/src/main/js/api/component-report.ts index 513f6d16a0d..08f6e7563cc 100644 --- a/server/sonar-web/src/main/js/api/report.ts +++ b/server/sonar-web/src/main/js/api/component-report.ts @@ -20,35 +20,43 @@ import { getJSON, post } from 'sonar-ui-common/helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; import { getBaseUrl } from '../helpers/system'; +import { ComponentReportStatus } from '../types/component-report'; -export interface ReportStatus { - canDownload?: boolean; - canSubscribe: boolean; - componentFrequency?: string; - globalFrequency: string; - subscribed?: boolean; -} - -export function getReportStatus(component: string): Promise { - return getJSON('/api/governance_reports/status', { componentKey: component }).catch( +export function getReportStatus( + componentKey: string, + branchKey?: string +): Promise { + return getJSON('/api/governance_reports/status', { componentKey, branchKey }).catch( throwGlobalError ); } -export function getReportUrl(component: string): string { - return `${getBaseUrl()}/api/governance_reports/download?componentKey=${encodeURIComponent( - component +export function getReportUrl(componentKey: string, branchKey?: string): string { + let url = `${getBaseUrl()}/api/governance_reports/download?componentKey=${encodeURIComponent( + componentKey )}`; + + if (branchKey) { + url += `&branchKey=${branchKey}`; + } + + return url; } -export function subscribe(component: string): Promise { - return post('/api/governance_reports/subscribe', { componentKey: component }).catch( +export function subscribeToEmailReport( + componentKey: string, + branchKey?: string +): Promise { + return post('/api/governance_reports/subscribe', { componentKey, branchKey }).catch( throwGlobalError ); } -export function unsubscribe(component: string): Promise { - return post('/api/governance_reports/unsubscribe', { componentKey: component }).catch( +export function unsubscribeFromEmailReport( + componentKey: string, + branchKey?: string +): Promise { + return post('/api/governance_reports/unsubscribe', { componentKey, branchKey }).catch( throwGlobalError ); } diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx index 5089095546f..2e1cbae10d9 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx @@ -23,6 +23,7 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { isDiffMetric } from 'sonar-ui-common/helpers/measures'; import { rawSizes } from '../../../app/theme'; +import ComponentReportActions from '../../../components/controls/ComponentReportActions'; import { findMeasure } from '../../../helpers/measures'; import { ApplicationPeriod } from '../../../types/application'; import { Branch } from '../../../types/branch-like'; @@ -95,7 +96,10 @@ export function MeasuresPanel(props: MeasuresPanelProps) { return (
-

{translate('overview.measures')}

+
+

{translate('overview.measures')}

+ +
{loading ? (
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap index 6870020f8aa..0ccb4ef60f0 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap @@ -5,11 +5,47 @@ exports[`should render correctly for applications 1`] = ` className="overview-panel" data-test="overview__measures-panel" > -

- overview.measures -

+

+ overview.measures +

+ +
-

- overview.measures -

+

+ overview.measures +

+ +
-

- overview.measures -

+

+ overview.measures +

+ + -

- overview.measures -

+

+ overview.measures +

+ + -

- overview.measures -

+

+ overview.measures +

+ + -

- overview.measures -

+

+ overview.measures +

+ + -

- overview.measures -

+

+ overview.measures +

+ +
@@ -3402,11 +3657,47 @@ exports[`should render correctly if there is no coverage 1`] = ` className="overview-panel" data-test="overview__measures-panel" > -

- overview.measures -

+

+ overview.measures +

+ +
-

- overview.measures -

+

+ overview.measures +

+ + { return (
- +
+ {component.description && (
{component.description} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx deleted file mode 100644 index 699b6ba73f9..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 { Button } from 'sonar-ui-common/components/controls/buttons'; -import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; -import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; -import { translate } from 'sonar-ui-common/helpers/l10n'; -import { getReportStatus, getReportUrl, ReportStatus } from '../../../api/report'; -import Subscription from './Subscription'; - -interface Props { - component: { key: string; name: string }; -} - -interface State { - loading: boolean; - status?: ReportStatus; -} - -export default class Report extends React.PureComponent { - mounted = false; - state: State = { loading: true }; - - componentDidMount() { - this.mounted = true; - this.loadStatus(); - } - - componentWillUnmount() { - this.mounted = false; - } - - loadStatus = () => { - getReportStatus(this.props.component.key).then( - status => { - if (this.mounted) { - this.setState({ status, loading: false }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - }; - - render() { - const { component } = this.props; - const { status, loading } = this.state; - - if (loading || !status) { - return null; - } - - return status.canSubscribe ? ( - -
  • - - {translate('report.print')} - -
  • -
  • - -
  • - - } - tagName="li"> - -
    - ) : ( - - {translate('report.print')} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx deleted file mode 100644 index 0ed99c67a64..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import { ReportStatus, subscribe, unsubscribe } from '../../../api/report'; -import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage'; -import throwGlobalError from '../../../app/utils/throwGlobalError'; -import { isLoggedIn } from '../../../helpers/users'; -import { getCurrentUser, Store } from '../../../store/rootReducer'; - -interface Props { - component: string; - currentUser: T.CurrentUser; - onSubscribe: () => void; - status: ReportStatus; -} - -export class Subscription extends React.PureComponent { - handleSubscription = (subscribed: boolean) => { - addGlobalSuccessMessage( - subscribed - ? translateWithParameters('report.subscribe_x_success', this.getFrequencyText()) - : translateWithParameters('report.unsubscribe_x_success', this.getFrequencyText()) - ); - this.props.onSubscribe(); - }; - - handleSubscribe = () => { - subscribe(this.props.component) - .then(() => this.handleSubscription(true)) - .catch(throwGlobalError); - }; - - handleUnsubscribe = () => { - unsubscribe(this.props.component) - .then(() => this.handleSubscription(false)) - .catch(throwGlobalError); - }; - - getFrequencyText = () => { - const effectiveFrequency = - this.props.status.componentFrequency || this.props.status.globalFrequency; - return translate('report.frequency', effectiveFrequency); - }; - - render() { - const hasEmail = isLoggedIn(this.props.currentUser) && !!this.props.currentUser.email; - - const { status } = this.props; - - if (!hasEmail) { - return {translate('report.no_email_to_subscribe')}; - } - - return status.subscribed ? ( - - {translateWithParameters('report.unsubscribe_x', this.getFrequencyText())} - - ) : ( - - {translateWithParameters('report.subscribe_x', this.getFrequencyText())} - - ); - } -} - -const mapStateToProps = (state: Store) => ({ - currentUser: getCurrentUser(state) -}); - -export default connect(mapStateToProps)(Subscription); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx deleted file mode 100644 index 26638a78f2c..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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. - */ -/* eslint-disable import/first */ -jest.mock('../../../../api/report', () => { - const report = jest.requireActual('../../../../api/report'); - report.getReportStatus = jest.fn(() => Promise.resolve({})); - return report; -}); - -import { mount, shallow } from 'enzyme'; -import * as React from 'react'; -import Report from '../Report'; - -const getReportStatus = require('../../../../api/report').getReportStatus as jest.Mock; - -const component = { key: 'foo', name: 'Foo' }; - -it('renders', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - wrapper.setState({ - loading: false, - status: { - canDownload: true, - canSubscribe: true, - componentFrequency: 'montly', - globalFrequency: 'weekly', - subscribed: true - } - }); - expect(wrapper).toMatchSnapshot(); -}); - -it('fetches status', () => { - getReportStatus.mockClear(); - mount(); - expect(getReportStatus).toBeCalledWith('foo'); -}); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx deleted file mode 100644 index 045226a7e85..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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. - */ -/* eslint-disable import/first */ -jest.mock('../../../../api/report', () => { - const report = jest.requireActual('../../../../api/report'); - report.subscribe = jest.fn(() => Promise.resolve()); - report.unsubscribe = jest.fn(() => Promise.resolve()); - return report; -}); - -import { mount, shallow } from 'enzyme'; -import * as React from 'react'; -import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { ReportStatus } from '../../../../api/report'; -import { Subscription } from '../Subscription'; - -const subscribe = require('../../../../api/report').subscribe as jest.Mock; -const unsubscribe = require('../../../../api/report').unsubscribe as jest.Mock; - -beforeEach(() => { - subscribe.mockClear(); - unsubscribe.mockClear(); -}); - -it('renders when subscribed', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('renders when not subscribed', () => { - expect(shallowRender({}, { subscribed: false })).toMatchSnapshot(); -}); - -it('renders when no email', () => { - expect(shallowRender({ currentUser: { isLoggedIn: false } })).toMatchSnapshot(); -}); - -it('changes subscription', async () => { - const status = { - canDownload: true, - canSubscribe: true, - componentFrequency: 'montly', - globalFrequency: 'weekly', - subscribed: true - }; - - const currentUser = { isLoggedIn: true, email: 'foo@example.com' }; - - const wrapper = mount( - - ); - - click(wrapper.find('a')); - expect(unsubscribe).toBeCalledWith('foo'); - - wrapper.setProps({ status: { ...status, subscribed: false } }); - await waitAndUpdate(wrapper); - - click(wrapper.find('a')); - expect(subscribe).toBeCalledWith('foo'); -}); - -function shallowRender( - props: Partial = {}, - statusOverrides: Partial = {} -) { - const status = { - canDownload: true, - canSubscribe: true, - componentFrequency: 'montly', - globalFrequency: 'weekly', - subscribed: true, - ...statusOverrides - }; - - const currentUser = { isLoggedIn: true, email: 'foo@example.com' }; - - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap index fb87db2a7d0..a80e83b3132 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap @@ -7,7 +7,7 @@ exports[`renders 1`] = `
    - -
  • - - report.print - -
  • -
  • - -
  • - - } - tagName="li" -> - - -`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap deleted file mode 100644 index a3df93ebe6e..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders when no email 1`] = ` - - report.no_email_to_subscribe - -`; - -exports[`renders when not subscribed 1`] = ` - - report.subscribe_x.report.frequency.montly - -`; - -exports[`renders when subscribed 1`] = ` - - report.unsubscribe_x.report.frequency.montly - -`; diff --git a/server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx b/server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx new file mode 100644 index 00000000000..84e7be44554 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/ComponentReportActions.tsx @@ -0,0 +1,140 @@ +/* + * 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 { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { + getReportStatus, + subscribeToEmailReport, + unsubscribeFromEmailReport +} from '../../api/component-report'; +import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; +import { isLoggedIn } from '../../helpers/users'; +import { Store } from '../../store/rootReducer'; +import { Branch } from '../../types/branch-like'; +import { ComponentQualifier } from '../../types/component'; +import { ComponentReportStatus } from '../../types/component-report'; +import { withCurrentUser } from '../hoc/withCurrentUser'; +import ComponentReportActionsRenderer from './ComponentReportActionsRenderer'; + +interface Props { + appState: Pick; + component: T.Component; + branch?: Branch; + currentUser: T.CurrentUser; +} + +interface State { + loadingStatus?: boolean; + status?: ComponentReportStatus; +} + +export class ComponentReportActions extends React.PureComponent { + mounted = false; + state: State = {}; + + componentDidMount() { + this.mounted = true; + const governanceEnabled = this.props.appState.qualifiers.includes(ComponentQualifier.Portfolio); + if (governanceEnabled) { + this.loadReportStatus(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + loadReportStatus = async () => { + const { component, branch } = this.props; + + const status = await getReportStatus(component.key, branch?.name).catch(() => undefined); + + if (this.mounted) { + this.setState({ status, loadingStatus: false }); + } + }; + + handleSubscription = (subscribed: boolean) => { + const { component } = this.props; + const { status } = this.state; + + const translationKey = subscribed + ? 'component_report.subscribe_x_success' + : 'component_report.unsubscribe_x_success'; + const frequencyTranslation = translate( + 'report.frequency', + status?.componentFrequency || status?.globalFrequency || '' + ).toLowerCase(); + const qualifierTranslation = translate('qualifier', component.qualifier).toLowerCase(); + + addGlobalSuccessMessage( + translateWithParameters(translationKey, frequencyTranslation, qualifierTranslation) + ); + + this.loadReportStatus(); + }; + + handleSubscribe = async () => { + const { component, branch } = this.props; + + await subscribeToEmailReport(component.key, branch?.name); + + this.handleSubscription(true); + }; + + handleUnsubscribe = async () => { + const { component, branch } = this.props; + + await unsubscribeFromEmailReport(component.key, branch?.name); + + this.handleSubscription(false); + }; + + render() { + const { currentUser, component, branch } = this.props; + const { status, loadingStatus } = this.state; + + if (loadingStatus || !status || (branch && !branch.excludedFromPurge)) { + return null; + } + + const currentUserHasEmail = isLoggedIn(currentUser) && !!currentUser.email; + + return ( + + ); + } +} + +const mapStateToProps = (state: Store) => ({ + appState: state.appState +}); + +export default withCurrentUser(connect(mapStateToProps)(ComponentReportActions)); diff --git a/server/sonar-web/src/main/js/components/controls/ComponentReportActionsRenderer.tsx b/server/sonar-web/src/main/js/components/controls/ComponentReportActionsRenderer.tsx new file mode 100644 index 00000000000..b08035d5325 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/ComponentReportActionsRenderer.tsx @@ -0,0 +1,100 @@ +/* + * 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 { Button } from 'sonar-ui-common/components/controls/buttons'; +import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; +import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { getReportUrl } from '../../api/component-report'; +import { Branch } from '../../types/branch-like'; +import { isPortfolioLike } from '../../types/component'; + +export interface ComponentReportActionsRendererProps { + component: T.Component; + branch?: Branch; + frequency: string; + subscribed: boolean; + canSubscribe: boolean; + currentUserHasEmail: boolean; + handleSubscription: () => void; + handleUnsubscription: () => void; +} + +export default function ComponentReportActionsRenderer(props: ComponentReportActionsRendererProps) { + const { branch, component, frequency, subscribed, canSubscribe, currentUserHasEmail } = props; + + const renderDownloadButton = (simple = false) => { + return ( + !!s).join(' - ')} + href={getReportUrl(component.key, branch?.name)} + target="_blank" + rel="noopener noreferrer"> + {simple + ? translate('download_verb') + : translateWithParameters( + 'component_report.download', + translate('qualifier', component.qualifier).toLowerCase() + )} + + ); + }; + + const renderSubscriptionButton = () => { + if (!currentUserHasEmail) { + return ( + {translate('component_report.no_email_to_subscribe')} + ); + } + + const translationKey = subscribed + ? 'component_report.unsubscribe_x' + : 'component_report.subscribe_x'; + const onClickHandler = subscribed ? props.handleUnsubscription : props.handleSubscription; + const frequencyTranslation = translate('report.frequency', frequency).toLowerCase(); + + return ( + + {translateWithParameters(translationKey, frequencyTranslation)} + + ); + }; + + return canSubscribe && isPortfolioLike(component.qualifier) ? ( + +
  • {renderDownloadButton(true)}
  • +
  • {renderSubscriptionButton()}
  • + + }> + +
    + ) : ( + renderDownloadButton() + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx new file mode 100644 index 00000000000..844b4d78cb9 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActions-test.tsx @@ -0,0 +1,127 @@ +/* + * 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { + getReportStatus, + subscribeToEmailReport, + unsubscribeFromEmailReport +} from '../../../api/component-report'; +import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage'; +import { mockBranch } from '../../../helpers/mocks/branch-like'; +import { mockComponentReportStatus } from '../../../helpers/mocks/component-report'; +import { mockComponent, mockCurrentUser } from '../../../helpers/testMocks'; +import { ComponentQualifier } from '../../../types/component'; +import { ComponentReportActions } from '../ComponentReportActions'; + +jest.mock('../../../api/component-report', () => ({ + ...jest.requireActual('../../../api/component-report'), + getReportStatus: jest + .fn() + .mockResolvedValue( + jest.requireActual('../../../helpers/mocks/component-report').mockComponentReportStatus() + ), + subscribeToEmailReport: jest.fn().mockResolvedValue(undefined), + unsubscribeFromEmailReport: jest.fn().mockResolvedValue(undefined) +})); + +jest.mock('../../../helpers/system', () => ({ + ...jest.requireActual('../../../helpers/system'), + getBaseUrl: jest.fn().mockReturnValue('baseUrl') +})); + +jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ default: jest.fn() })); + +beforeEach(jest.clearAllMocks); + +it('should not render anything', async () => { + // loading + expect(shallowRender().type()).toBeNull(); + + // No status + (getReportStatus as jest.Mock).mockResolvedValueOnce(undefined); + const w1 = shallowRender(); + await waitAndUpdate(w1); + expect(w1.type()).toBeNull(); + + // Branch purgeable + const w2 = shallowRender({ branch: mockBranch({ excludedFromPurge: false }) }); + await waitAndUpdate(w2); + expect(w2.type()).toBeNull(); + + // no governance + const w3 = shallowRender({ appState: { qualifiers: [] } }); + await waitAndUpdate(w3); + expect(w3.type()).toBeNull(); +}); + +it('should call for status properly', async () => { + const component = mockComponent(); + const branch = mockBranch(); + + const wrapper = shallowRender({ component, branch }); + + await waitAndUpdate(wrapper); + + expect(getReportStatus).toHaveBeenCalledWith(component.key, branch.name); +}); + +it('should handle subscription', async () => { + const component = mockComponent(); + const branch = mockBranch(); + const wrapper = shallowRender({ component, branch }); + + await wrapper.instance().handleSubscribe(); + + expect(subscribeToEmailReport).toHaveBeenCalledWith(component.key, branch.name); + expect(addGlobalSuccessMessage).toHaveBeenCalledWith( + 'component_report.subscribe_x_success.report.frequency..qualifier.trk' + ); +}); + +it('should handle unsubscription', async () => { + const component = mockComponent(); + const branch = mockBranch(); + const wrapper = shallowRender({ component, branch }); + + await waitAndUpdate(wrapper); + + wrapper.setState({ status: mockComponentReportStatus({ componentFrequency: 'compfreq' }) }); + + await wrapper.instance().handleUnsubscribe(); + + expect(unsubscribeFromEmailReport).toHaveBeenCalledWith(component.key, branch.name); + expect(addGlobalSuccessMessage).toHaveBeenCalledWith( + 'component_report.unsubscribe_x_success.report.frequency.compfreq.qualifier.trk' + ); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActionsRenderer-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActionsRenderer-test.tsx new file mode 100644 index 00000000000..76144927f72 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/ComponentReportActionsRenderer-test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { mockComponent } from '../../../helpers/testMocks'; +import { ComponentQualifier } from '../../../types/component'; +import ComponentReportActionsRenderer, { + ComponentReportActionsRendererProps +} from '../ComponentReportActionsRenderer'; + +it('should render correctly', () => { + expect(shallowRender({ canSubscribe: false })).toMatchSnapshot('cannot subscribe'); + expect(shallowRender({ canSubscribe: true, subscribed: false })).toMatchSnapshot( + 'can subscribe, not subscribed' + ); + expect(shallowRender({ canSubscribe: true, subscribed: true })).toMatchSnapshot( + 'can subscribe, subscribed' + ); + expect(shallowRender({ canSubscribe: true, currentUserHasEmail: false })).toMatchSnapshot( + 'current user without email' + ); + expect(shallowRender({ component: mockComponent() })).toMatchSnapshot('not a portfolio'); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ComponentReportActionsRenderer-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ComponentReportActionsRenderer-test.tsx.snap new file mode 100644 index 00000000000..df37dbbcc68 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ComponentReportActionsRenderer-test.tsx.snap @@ -0,0 +1,136 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: can subscribe, not subscribed 1`] = ` + +
  • + + download_verb + +
  • +
  • + + component_report.subscribe_x.report.frequency.weekly + +
  • + + } +> + +
    +`; + +exports[`should render correctly: can subscribe, subscribed 1`] = ` + +
  • + + download_verb + +
  • +
  • + + component_report.unsubscribe_x.report.frequency.weekly + +
  • + + } +> + +
    +`; + +exports[`should render correctly: cannot subscribe 1`] = ` + + component_report.download.qualifier.vw + +`; + +exports[`should render correctly: current user without email 1`] = ` + +
  • + + download_verb + +
  • +
  • + + component_report.no_email_to_subscribe + +
  • + + } +> + +
    +`; + +exports[`should render correctly: not a portfolio 1`] = ` + + component_report.download.qualifier.trk + +`; diff --git a/server/sonar-web/src/main/js/helpers/mocks/component-report.ts b/server/sonar-web/src/main/js/helpers/mocks/component-report.ts new file mode 100644 index 00000000000..0d1d987d084 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/mocks/component-report.ts @@ -0,0 +1,36 @@ +/* + * 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 { ComponentReportStatus } from '../../types/component-report'; + +export function mockComponentReportStatus( + props?: Partial +): ComponentReportStatus { + return { + canAdmin: true, + canDownload: true, + canSubscribe: true, + componentRecipients: [], + globalFrequency: '', + globalRecipients: [], + subscribed: false, + ...props + }; +} diff --git a/server/sonar-web/src/main/js/types/component-report.ts b/server/sonar-web/src/main/js/types/component-report.ts new file mode 100644 index 00000000000..24024a4f370 --- /dev/null +++ b/server/sonar-web/src/main/js/types/component-report.ts @@ -0,0 +1,30 @@ +/* + * 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 interface ComponentReportStatus { + canAdmin: boolean; + canDownload: boolean; + canSubscribe: boolean; + componentFrequency?: string; + componentRecipients: Array; + globalFrequency: string; + globalRecipients: Array; + subscribed: boolean; +} 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 8c92018ee34..fbb789fd998 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3937,7 +3937,6 @@ portfolio.activity_link=Activity portfolio.measures_link=Measures portfolio.language_breakdown_link=Language breakdown portfolio.breakdown=Portfolio breakdown -portfolio.pdf_report=Portfolio PDF Report portfolio.number_of_projects=Number of projects portfolio.number_of_lines=Number of lines of code @@ -4168,3 +4167,17 @@ webhooks.url.bad_format=Bad format of URL. webhooks.url.bad_protocol=URL must start with "http://" or "https://". webhooks.url.description=Server endpoint that will receive the webhook payload, for example: "http://my_server/foo". If HTTP Basic authentication is used, HTTPS is recommended to avoid man in the middle attacks. Example: "https://myLogin:myPassword@my_server/foo" webhooks.url.required=URL is required. + + +#------------------------------------------------------------------------------ +# +# COMPONENT REPORT +# +#------------------------------------------------------------------------------ +component_report.report={0} PDF report +component_report.download=Download {0} PDF report +component_report.no_email_to_subscribe=Email subscription requires an email address. +component_report.subscribe_x=Subscribe to {0} report +component_report.unsubscribe_x=Unsubscribe from {0} report +component_report.subscribe_x_success=Subscription successful. You will receive a {0} report for this {1} by email. +component_report.unsubscribe_x_success=Subscription successfully canceled. You won't receive a {0} report for this {1} by email. \ No newline at end of file -- 2.39.5