diff options
author | Philippe Perrin <philippe.perrin@sonarsource.com> | 2021-07-28 10:49:06 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-08-09 20:03:18 +0000 |
commit | adc1fc316700608671a18bcdcba001eb45c0645b (patch) | |
tree | f400f41707d23b58ffb298b1317d7063480e7a16 /server/sonar-web/src/main | |
parent | 90198d576c83d7e63cfe9bdfd2bb1db8eed9fad6 (diff) | |
download | sonarqube-adc1fc316700608671a18bcdcba001eb45c0645b.tar.gz sonarqube-adc1fc316700608671a18bcdcba001eb45c0645b.zip |
SONAR-15138 Add report download and subscription buttons for projects and applications
Diffstat (limited to 'server/sonar-web/src/main')
18 files changed, 1022 insertions, 497 deletions
diff --git a/server/sonar-web/src/main/js/api/report.ts b/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<ReportStatus> { - return getJSON('/api/governance_reports/status', { componentKey: component }).catch( +export function getReportStatus( + componentKey: string, + branchKey?: string +): Promise<ComponentReportStatus> { + 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<void | Response> { - return post('/api/governance_reports/subscribe', { componentKey: component }).catch( +export function subscribeToEmailReport( + componentKey: string, + branchKey?: string +): Promise<void | Response> { + return post('/api/governance_reports/subscribe', { componentKey, branchKey }).catch( throwGlobalError ); } -export function unsubscribe(component: string): Promise<void | Response> { - return post('/api/governance_reports/unsubscribe', { componentKey: component }).catch( +export function unsubscribeFromEmailReport( + componentKey: string, + branchKey?: string +): Promise<void | Response> { + 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 ( <div className="overview-panel" data-test="overview__measures-panel"> - <h2 className="overview-panel-title">{translate('overview.measures')}</h2> + <div className="display-flex-space-between display-flex-start"> + <h2 className="overview-panel-title">{translate('overview.measures')}</h2> + <ComponentReportActions component={component} branch={branch} /> + </div> {loading ? ( <div className="overview-panel-content overview-panel-big-padded"> 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" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={0} @@ -691,11 +727,47 @@ exports[`should render correctly for applications 2`] = ` className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "APP", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={1} @@ -1587,11 +1659,47 @@ exports[`should render correctly for projects 1`] = ` className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={0} @@ -2273,11 +2381,47 @@ exports[`should render correctly for projects 2`] = ` className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={1} @@ -3169,11 +3313,47 @@ exports[`should render correctly if branch is misconfigured: hide settings 1`] = className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "own-reference", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={0} @@ -3274,11 +3454,50 @@ exports[`should render correctly if branch is misconfigured: show settings 1`] = className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "own-reference", + } + } + component={ + Object { + "breadcrumbs": Array [], + "configuration": Object { + "showSettings": true, + }, + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={0} @@ -3382,11 +3601,47 @@ exports[`should render correctly if the data is still loading 1`] = ` className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <div className="overview-panel-content overview-panel-big-padded" > @@ -3402,11 +3657,47 @@ exports[`should render correctly if there is no coverage 1`] = ` className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={0} @@ -3820,11 +4111,47 @@ exports[`should render correctly if there is no new code measures 1`] = ` className="overview-panel" data-test="overview__measures-panel" > - <h2 - className="overview-panel-title" + <div + className="display-flex-space-between display-flex-start" > - overview.measures - </h2> + <h2 + className="overview-panel-title" + > + overview.measures + </h2> + <Connect(withCurrentUser(Connect(ComponentReportActions))) + branch={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> + </div> <BoxedTabs onSelect={[Function]} selected={0} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx index e7c170aa7a7..6546a42ce8f 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx @@ -23,6 +23,7 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; import { getChildren } from '../../../api/components'; import { getMeasures } from '../../../api/measures'; import MeasuresLink from '../../../components/common/MeasuresLink'; +import ComponentReportActions from '../../../components/controls/ComponentReportActions'; import Measure from '../../../components/measure/Measure'; import { fetchMetrics } from '../../../store/rootActions'; import { getMetrics, Store } from '../../../store/rootReducer'; @@ -30,7 +31,6 @@ import '../styles.css'; import { SubComponent } from '../types'; import { convertMeasures, PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS } from '../utils'; import MetricBox from './MetricBox'; -import Report from './Report'; import WorstProjects from './WorstProjects'; interface OwnProps { @@ -159,8 +159,9 @@ export class App extends React.PureComponent<Props, State> { return ( <div className="page page-limited portfolio-overview"> <div className="page-actions"> - <Report component={component} /> + <ComponentReportActions component={component} /> </div> + {component.description && ( <div className="portfolio-description display-inline-block big-spacer-bottom"> {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<Props, State> { - 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 ? ( - <Dropdown - overlay={ - <ul className="menu"> - <li> - <a - download={component.name + ' - Executive Report.pdf'} - href={getReportUrl(component.key)} - target="_blank" - rel="noopener noreferrer"> - {translate('report.print')} - </a> - </li> - <li> - <Subscription - component={component.key} - onSubscribe={this.loadStatus} - status={status} - /> - </li> - </ul> - } - tagName="li"> - <Button className="dropdown-toggle"> - {translate('portfolio.pdf_report')} - <DropdownIcon className="spacer-left icon-half-transparent" /> - </Button> - </Dropdown> - ) : ( - <a - className="button" - download={component.name + ' - Executive Report.pdf'} - href={getReportUrl(component.key)} - target="_blank" - rel="noopener noreferrer"> - {translate('report.print')} - </a> - ); - } -} 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<Props> { - 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 <span className="text-muted-2">{translate('report.no_email_to_subscribe')}</span>; - } - - return status.subscribed ? ( - <a href="#" onClick={this.handleUnsubscribe}> - {translateWithParameters('report.unsubscribe_x', this.getFrequencyText())} - </a> - ) : ( - <a href="#" onClick={this.handleSubscribe}> - {translateWithParameters('report.subscribe_x', this.getFrequencyText())} - </a> - ); - } -} - -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<any>; - -const component = { key: 'foo', name: 'Foo' }; - -it('renders', () => { - const wrapper = shallow(<Report component={component} />); - 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(<Report component={component} />); - 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<any>; -const unsubscribe = require('../../../../api/report').unsubscribe as jest.Mock<any>; - -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( - <Subscription - component="foo" - currentUser={currentUser} - onSubscribe={jest.fn()} - status={status} - /> - ); - - 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<Subscription['props']> = {}, - statusOverrides: Partial<ReportStatus> = {} -) { - const status = { - canDownload: true, - canSubscribe: true, - componentFrequency: 'montly', - globalFrequency: 'weekly', - subscribed: true, - ...statusOverrides - }; - - const currentUser = { isLoggedIn: true, email: 'foo@example.com' }; - - return shallow<Subscription>( - <Subscription - component="foo" - currentUser={currentUser} - onSubscribe={jest.fn()} - status={status} - {...props} - /> - ); -} 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`] = ` <div className="page-actions" > - <Report + <Connect(withCurrentUser(Connect(ComponentReportActions))) component={ Object { "description": "accurate description", diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap deleted file mode 100644 index 3852cde2841..00000000000 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = `""`; - -exports[`renders 2`] = ` -<Dropdown - overlay={ - <ul - className="menu" - > - <li> - <a - download="Foo - Executive Report.pdf" - href="/api/governance_reports/download?componentKey=foo" - rel="noopener noreferrer" - target="_blank" - > - report.print - </a> - </li> - <li> - <Connect(Subscription) - component="foo" - onSubscribe={[Function]} - status={ - Object { - "canDownload": true, - "canSubscribe": true, - "componentFrequency": "montly", - "globalFrequency": "weekly", - "subscribed": true, - } - } - /> - </li> - </ul> - } - tagName="li" -> - <Button - className="dropdown-toggle" - > - portfolio.pdf_report - <DropdownIcon - className="spacer-left icon-half-transparent" - /> - </Button> -</Dropdown> -`; 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`] = ` -<span - className="text-muted-2" -> - report.no_email_to_subscribe -</span> -`; - -exports[`renders when not subscribed 1`] = ` -<a - href="#" - onClick={[Function]} -> - report.subscribe_x.report.frequency.montly -</a> -`; - -exports[`renders when subscribed 1`] = ` -<a - href="#" - onClick={[Function]} -> - report.unsubscribe_x.report.frequency.montly -</a> -`; 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<T.AppState, 'qualifiers'>; + component: T.Component; + branch?: Branch; + currentUser: T.CurrentUser; +} + +interface State { + loadingStatus?: boolean; + status?: ComponentReportStatus; +} + +export class ComponentReportActions extends React.PureComponent<Props, State> { + 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 ( + <ComponentReportActionsRenderer + branch={branch} + component={component} + frequency={status.componentFrequency || status.globalFrequency} + subscribed={status.subscribed} + canSubscribe={status.canSubscribe} + currentUserHasEmail={currentUserHasEmail} + handleSubscription={this.handleSubscribe} + handleUnsubscription={this.handleUnsubscribe} + /> + ); + } +} + +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 ( + <a + download={[component.name, branch?.name, 'PDF Report.pdf'].filter(s => !!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() + )} + </a> + ); + }; + + const renderSubscriptionButton = () => { + if (!currentUserHasEmail) { + return ( + <span className="text-muted-2">{translate('component_report.no_email_to_subscribe')}</span> + ); + } + + 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 ( + <a href="#" onClick={onClickHandler}> + {translateWithParameters(translationKey, frequencyTranslation)} + </a> + ); + }; + + return canSubscribe && isPortfolioLike(component.qualifier) ? ( + <Dropdown + overlay={ + <ul className="menu"> + <li>{renderDownloadButton(true)}</li> + <li>{renderSubscriptionButton()}</li> + </ul> + }> + <Button className="dropdown-toggle"> + {translateWithParameters( + 'component_report.report', + translate('qualifier', component.qualifier) + )} + <DropdownIcon className="spacer-left icon-half-transparent" /> + </Button> + </Dropdown> + ) : ( + 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<ComponentReportActions['props']> = {}) { + return shallow<ComponentReportActions>( + <ComponentReportActions + appState={{ qualifiers: [ComponentQualifier.Portfolio] }} + component={mockComponent()} + currentUser={mockCurrentUser()} + {...props} + /> + ); +} 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<ComponentReportActionsRendererProps> = {}) { + return shallow<ComponentReportActionsRendererProps>( + <ComponentReportActionsRenderer + component={mockComponent({ qualifier: ComponentQualifier.Portfolio })} + canSubscribe={true} + subscribed={false} + currentUserHasEmail={true} + frequency="weekly" + handleSubscription={jest.fn()} + handleUnsubscription={jest.fn()} + {...props} + /> + ); +} 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`] = ` +<Dropdown + overlay={ + <ul + className="menu" + > + <li> + <a + download="MyProject - PDF Report.pdf" + href="/api/governance_reports/download?componentKey=my-project" + rel="noopener noreferrer" + target="_blank" + > + download_verb + </a> + </li> + <li> + <a + href="#" + onClick={[MockFunction]} + > + component_report.subscribe_x.report.frequency.weekly + </a> + </li> + </ul> + } +> + <Button + className="dropdown-toggle" + > + component_report.report.qualifier.VW + <DropdownIcon + className="spacer-left icon-half-transparent" + /> + </Button> +</Dropdown> +`; + +exports[`should render correctly: can subscribe, subscribed 1`] = ` +<Dropdown + overlay={ + <ul + className="menu" + > + <li> + <a + download="MyProject - PDF Report.pdf" + href="/api/governance_reports/download?componentKey=my-project" + rel="noopener noreferrer" + target="_blank" + > + download_verb + </a> + </li> + <li> + <a + href="#" + onClick={[MockFunction]} + > + component_report.unsubscribe_x.report.frequency.weekly + </a> + </li> + </ul> + } +> + <Button + className="dropdown-toggle" + > + component_report.report.qualifier.VW + <DropdownIcon + className="spacer-left icon-half-transparent" + /> + </Button> +</Dropdown> +`; + +exports[`should render correctly: cannot subscribe 1`] = ` +<a + download="MyProject - PDF Report.pdf" + href="/api/governance_reports/download?componentKey=my-project" + rel="noopener noreferrer" + target="_blank" +> + component_report.download.qualifier.vw +</a> +`; + +exports[`should render correctly: current user without email 1`] = ` +<Dropdown + overlay={ + <ul + className="menu" + > + <li> + <a + download="MyProject - PDF Report.pdf" + href="/api/governance_reports/download?componentKey=my-project" + rel="noopener noreferrer" + target="_blank" + > + download_verb + </a> + </li> + <li> + <span + className="text-muted-2" + > + component_report.no_email_to_subscribe + </span> + </li> + </ul> + } +> + <Button + className="dropdown-toggle" + > + component_report.report.qualifier.VW + <DropdownIcon + className="spacer-left icon-half-transparent" + /> + </Button> +</Dropdown> +`; + +exports[`should render correctly: not a portfolio 1`] = ` +<a + download="MyProject - PDF Report.pdf" + href="/api/governance_reports/download?componentKey=my-project" + rel="noopener noreferrer" + target="_blank" +> + component_report.download.qualifier.trk +</a> +`; 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> +): 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<string>; + globalFrequency: string; + globalRecipients: Array<string>; + subscribed: boolean; +} |