diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2021-03-24 16:22:53 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-04-15 20:03:44 +0000 |
commit | 83774f0672fe554b374128808b46c1a0abaca286 (patch) | |
tree | 3e6d2559418a817006162bb54fcebf70ff1d1412 /server/sonar-web/src/main | |
parent | 8b518c6bccb19f5416991dd05b0924ddd0838526 (diff) | |
download | sonarqube-83774f0672fe554b374128808b46c1a0abaca286.tar.gz sonarqube-83774f0672fe554b374128808b46c1a0abaca286.zip |
SONAR-14606 Plugins require risk acknowledgment
Diffstat (limited to 'server/sonar-web/src/main')
18 files changed, 427 insertions, 157 deletions
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx index 39a5b9b2d82..6111cb3bd0d 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx @@ -24,6 +24,7 @@ import withIndexationContext, { } from '../../../components/hoc/withIndexationContext'; import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; import { IndexationNotificationType } from '../../../types/indexation'; +import { Permissions } from '../../../types/permissions'; import './IndexationNotification.css'; import IndexationNotificationHelper from './IndexationNotificationHelper'; import IndexationNotificationRenderer from './IndexationNotificationRenderer'; @@ -44,7 +45,8 @@ export class IndexationNotification extends React.PureComponent<Props, State> { super(props); this.isSystemAdmin = - isLoggedIn(this.props.currentUser) && hasGlobalPermission(this.props.currentUser, 'admin'); + isLoggedIn(this.props.currentUser) && + hasGlobalPermission(this.props.currentUser, Permissions.Admin); } componentDidMount() { diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx index 9c0f15d14cc..6c837133eb2 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -20,6 +20,8 @@ import { sortBy, uniqBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { @@ -28,10 +30,13 @@ import { getInstalledPluginsWithUpdates, getPluginUpdates } from '../../api/plugins'; +import { getValues, setSimpleSettingValue } from '../../api/settings'; import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { EditionKey } from '../../types/editions'; -import { PendingPluginResult, Plugin } from '../../types/plugins'; +import { PendingPluginResult, Plugin, RiskConsent } from '../../types/plugins'; +import { SettingsKey } from '../../types/settings'; +import PluginRiskConsentBox from './components/PluginRiskConsentBox'; import EditionBoxes from './EditionBoxes'; import Footer from './Footer'; import Header from './Header'; @@ -53,6 +58,7 @@ interface Props { interface State { loadingPlugins: boolean; plugins: Plugin[]; + riskConsent?: RiskConsent; } export class App extends React.PureComponent<Props, State> { @@ -62,6 +68,7 @@ export class App extends React.PureComponent<Props, State> { componentDidMount() { this.mounted = true; this.fetchQueryPlugins(); + this.fetchRiskConsent(); } componentDidUpdate(prevProps: Props) { @@ -102,6 +109,27 @@ export class App extends React.PureComponent<Props, State> { ); }; + fetchRiskConsent = async () => { + const result = await getValues({ keys: SettingsKey.PluginRiskConsent }); + + if (!result || result.length < 1) { + return; + } + + const [consent] = result; + + this.setState({ riskConsent: consent.value as RiskConsent | undefined }); + }; + + acknowledgeRisk = async () => { + await setSimpleSettingValue({ + key: SettingsKey.PluginRiskConsent, + value: RiskConsent.Accepted + }); + + await this.fetchRiskConsent(); + }; + updateQuery = (newQuery: Partial<Query>) => { const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery }); this.props.router.push({ pathname: this.props.location.pathname, query }); @@ -115,7 +143,7 @@ export class App extends React.PureComponent<Props, State> { render() { const { currentEdition, standaloneMode, pendingPlugins } = this.props; - const { loadingPlugins, plugins } = this.state; + const { loadingPlugins, plugins, riskConsent } = this.state; const query = parseQuery(this.props.location.query); const filteredPlugins = filterPlugins(plugins, query.search); @@ -128,9 +156,33 @@ export class App extends React.PureComponent<Props, State> { <header className="page-header"> <h1 className="page-title">{translate('marketplace.page.plugins')}</h1> <div className="page-description"> - {translate('marketplace.page.plugins.description')} + <p>{translate('marketplace.page.plugins.description')}</p> + {currentEdition !== EditionKey.community && ( + <p className="spacer-top"> + <FormattedMessage + id="marketplace.page.plugins.description2" + defaultMessage={translate('marketplace.page.plugins.description2')} + values={{ + link: ( + <Link + to="/documentation/instance-administration/marketplace/" + target="_blank"> + {translate('marketplace.page.plugins.description2.link')} + </Link> + ) + }} + /> + </p> + )} </div> </header> + + <PluginRiskConsentBox + acknowledgeRisk={this.acknowledgeRisk} + currentEdition={currentEdition} + riskConsent={riskConsent} + /> + <Search query={query} updateCenterActive={this.props.updateCenterActive} @@ -144,7 +196,7 @@ export class App extends React.PureComponent<Props, State> { <PluginsList pending={pendingPlugins} plugins={filteredPlugins} - readOnly={!standaloneMode} + readOnly={!standaloneMode || riskConsent !== RiskConsent.Accepted} refreshPending={this.props.fetchPendingPlugins} /> <Footer total={filteredPlugins.length} /> diff --git a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx index 9a65f60dadb..e49721d363f 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx @@ -43,12 +43,14 @@ const mapStateToProps = (state: Store) => { }; }; -const WithAdminContext = (props: StateToProps & OwnProps) => ( - <AdminContext.Consumer> - {({ fetchPendingPlugins, pendingPlugins }) => ( - <App fetchPendingPlugins={fetchPendingPlugins} pendingPlugins={pendingPlugins} {...props} /> - )} - </AdminContext.Consumer> -); +function WithAdminContext(props: StateToProps & OwnProps) { + return ( + <AdminContext.Consumer> + {({ fetchPendingPlugins, pendingPlugins }) => ( + <App fetchPendingPlugins={fetchPendingPlugins} pendingPlugins={pendingPlugins} {...props} /> + )} + </AdminContext.Consumer> + ); +} export default connect(mapStateToProps)(WithAdminContext); diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/App-test.tsx index be40182dbb6..e3e9e93bfd0 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/App-test.tsx @@ -26,7 +26,10 @@ import { getInstalledPluginsWithUpdates, getPluginUpdates } from '../../../api/plugins'; +import { getValues, setSimpleSettingValue } from '../../../api/settings'; import { mockLocation, mockRouter } from '../../../helpers/testMocks'; +import { RiskConsent } from '../../../types/plugins'; +import { SettingsKey } from '../../../types/settings'; import { App } from '../App'; jest.mock('../../../api/plugins', () => { @@ -40,6 +43,11 @@ jest.mock('../../../api/plugins', () => { }; }); +jest.mock('../../../api/settings', () => ({ + getValues: jest.fn().mockResolvedValue([]), + setSimpleSettingValue: jest.fn().mockResolvedValue(true) +})); + beforeEach(jest.clearAllMocks); it('should render correctly', async () => { @@ -50,6 +58,25 @@ it('should render correctly', async () => { expect(wrapper).toMatchSnapshot('loaded'); }); +it('should handle accepting the risk', async () => { + (getValues as jest.Mock) + .mockResolvedValueOnce([{ value: RiskConsent.NotAccepted }]) + .mockResolvedValueOnce([{ value: RiskConsent.Accepted }]); + + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + expect(getValues).toBeCalledWith({ keys: SettingsKey.PluginRiskConsent }); + + wrapper.instance().acknowledgeRisk(); + + await new Promise(setImmediate); + + expect(setSimpleSettingValue).toBeCalled(); + expect(getValues).toBeCalledWith({ keys: SettingsKey.PluginRiskConsent }); + expect(wrapper.state().riskConsent).toBe(RiskConsent.Accepted); +}); + it('should fetch plugin info', async () => { const wrapper = shallowRender(); @@ -69,6 +96,7 @@ it('should fetch plugin info', async () => { function shallowRender(props: Partial<App['props']> = {}) { return shallow<App>( <App + currentEdition={EditionKey.developer} fetchPendingPlugins={jest.fn()} location={mockLocation()} pendingPlugins={{ diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx new file mode 100644 index 00000000000..595ba21f4dd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx @@ -0,0 +1,59 @@ +/* + * 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 { connect } from 'react-redux'; +import { mockStore } from '../../../helpers/testMocks'; +import { getAppState, getGlobalSettingValue } from '../../../store/rootReducer'; +import { EditionKey } from '../../../types/editions'; +import '../AppContainer'; + +jest.mock('react-redux', () => ({ + connect: jest.fn(() => (a: any) => a) +})); + +jest.mock('../../../store/rootReducer', () => { + return { + getAppState: jest.fn(), + getGlobalSettingValue: jest.fn() + }; +}); + +describe('redux', () => { + it('should correctly map state and dispatch props', () => { + const store = mockStore(); + const edition = EditionKey.developer; + const standalone = true; + const updateCenterActive = true; + (getAppState as jest.Mock).mockReturnValue({ edition, standalone }); + (getGlobalSettingValue as jest.Mock).mockReturnValueOnce({ + value: `${updateCenterActive}` + }); + + const [mapStateToProps] = (connect as jest.Mock).mock.calls[0]; + + const props = mapStateToProps(store); + expect(props).toEqual({ + currentEdition: edition, + standaloneMode: standalone, + updateCenterActive + }); + + expect(getGlobalSettingValue).toHaveBeenCalledWith(store, 'sonar.updatecenter.activate'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index 09a6bc145c1..00000000000 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: loaded 1`] = ` -<div - className="page page-limited" - id="marketplace-page" -> - <Suggestions - suggestions="marketplace" - /> - <Helmet - defer={true} - encodeSpecialCharacters={true} - title="marketplace.page" - /> - <Header /> - <EditionBoxes /> - <header - className="page-header" - > - <h1 - className="page-title" - > - marketplace.page.plugins - </h1> - <div - className="page-description" - > - marketplace.page.plugins.description - </div> - </header> - <Search - query={ - Object { - "filter": "all", - "search": "", - } - } - updateCenterActive={false} - updateQuery={[Function]} - /> - <DeferredSpinner - loading={false} - > - <PluginsList - pending={ - Object { - "installing": Array [], - "removing": Array [], - "updating": Array [], - } - } - plugins={ - Array [ - Object { - "key": "sonar-foo", - "name": "Sonar Foo", - }, - ] - } - readOnly={true} - refreshPending={[MockFunction]} - /> - <Footer - total={1} - /> - </DeferredSpinner> -</div> -`; - -exports[`should render correctly: loading 1`] = ` -<div - className="page page-limited" - id="marketplace-page" -> - <Suggestions - suggestions="marketplace" - /> - <Helmet - defer={true} - encodeSpecialCharacters={true} - title="marketplace.page" - /> - <Header /> - <EditionBoxes /> - <header - className="page-header" - > - <h1 - className="page-title" - > - marketplace.page.plugins - </h1> - <div - className="page-description" - > - marketplace.page.plugins.description - </div> - </header> - <Search - query={ - Object { - "filter": "all", - "search": "", - } - } - updateCenterActive={false} - updateQuery={[Function]} - /> - <DeferredSpinner - loading={true} - > - marketplace.plugin_list.no_plugins.all - </DeferredSpinner> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx new file mode 100644 index 00000000000..ed8cff315fd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx @@ -0,0 +1,55 @@ +/* + * 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 { translate } from 'sonar-ui-common/helpers/l10n'; +import { EditionKey } from '../../../types/editions'; +import { RiskConsent } from '../../../types/plugins'; + +export interface PluginRiskConsentBoxProps { + acknowledgeRisk: () => void; + currentEdition?: EditionKey; + riskConsent?: RiskConsent; +} + +export default function PluginRiskConsentBox(props: PluginRiskConsentBoxProps) { + const { currentEdition, riskConsent } = props; + + if (riskConsent === RiskConsent.Accepted) { + return null; + } + + return ( + <div className="boxed-group it__plugin_risk_consent_box"> + <h2>{translate('marketplace.risk_consent.title')}</h2> + <div className="boxed-group-inner"> + <p>{translate('marketplace.risk_consent.description')}</p> + {currentEdition === EditionKey.community && ( + <p className="spacer-top">{translate('marketplace.risk_consent.installation')}</p> + )} + <Button + className="display-block big-spacer-top button-primary" + onClick={props.acknowledgeRisk}> + {translate('marketplace.risk_consent.action')} + </Button> + </div> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx new file mode 100644 index 00000000000..66f28bddcdb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EditionKey } from '../../../../types/editions'; +import { RiskConsent } from '../../../../types/plugins'; +import PluginRiskConsentBox, { PluginRiskConsentBoxProps } from '../PluginRiskConsentBox'; + +it.each([[undefined], [RiskConsent.Accepted], [RiskConsent.NotAccepted], [RiskConsent.Required]])( + 'should render correctly for risk consent %s', + (riskConsent?: RiskConsent) => { + expect(shallowRender({ riskConsent })).toMatchSnapshot(); + } +); + +it('should render correctly for community edition', () => { + expect(shallowRender({ currentEdition: EditionKey.community })).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<PluginRiskConsentBoxProps> = {}) { + return shallow(<PluginRiskConsentBox acknowledgeRisk={jest.fn()} {...props} />); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap new file mode 100644 index 00000000000..39b0f9ea863 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly for community edition 1`] = ` +<div + className="boxed-group it__plugin_risk_consent_box" +> + <h2> + marketplace.risk_consent.title + </h2> + <div + className="boxed-group-inner" + > + <p> + marketplace.risk_consent.description + </p> + <p + className="spacer-top" + > + marketplace.risk_consent.installation + </p> + <Button + className="display-block big-spacer-top button-primary" + onClick={[MockFunction]} + > + marketplace.risk_consent.action + </Button> + </div> +</div> +`; + +exports[`should render correctly for risk consent ACCEPTED 1`] = `""`; + +exports[`should render correctly for risk consent NOT_ACCEPTED 1`] = ` +<div + className="boxed-group it__plugin_risk_consent_box" +> + <h2> + marketplace.risk_consent.title + </h2> + <div + className="boxed-group-inner" + > + <p> + marketplace.risk_consent.description + </p> + <Button + className="display-block big-spacer-top button-primary" + onClick={[MockFunction]} + > + marketplace.risk_consent.action + </Button> + </div> +</div> +`; + +exports[`should render correctly for risk consent REQUIRED 1`] = ` +<div + className="boxed-group it__plugin_risk_consent_box" +> + <h2> + marketplace.risk_consent.title + </h2> + <div + className="boxed-group-inner" + > + <p> + marketplace.risk_consent.description + </p> + <Button + className="display-block big-spacer-top button-primary" + onClick={[MockFunction]} + > + marketplace.risk_consent.action + </Button> + </div> +</div> +`; + +exports[`should render correctly for risk consent undefined 1`] = ` +<div + className="boxed-group it__plugin_risk_consent_box" +> + <h2> + marketplace.risk_consent.title + </h2> + <div + className="boxed-group-inner" + > + <p> + marketplace.risk_consent.description + </p> + <Button + className="display-block big-spacer-top button-primary" + onClick={[MockFunction]} + > + marketplace.risk_consent.action + </Button> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx b/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx index 12f4257256b..56742d4e086 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx @@ -28,6 +28,7 @@ import { Router, withRouter } from '../../../components/hoc/withRouter'; import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls'; import { hasGlobalPermission } from '../../../helpers/users'; import { ComponentQualifier } from '../../../types/component'; +import { Permissions } from '../../../types/permissions'; export interface ApplicationCreationProps { appState: Pick<T.AppState, 'qualifiers'>; @@ -43,7 +44,7 @@ export function ApplicationCreation(props: ApplicationCreationProps) { const canCreateApplication = appState.qualifiers.includes(ComponentQualifier.Application) && - hasGlobalPermission(currentUser, 'applicationcreator'); + hasGlobalPermission(currentUser, Permissions.ApplicationCreation); if (!canCreateApplication) { return null; diff --git a/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx b/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx index b9c251336bc..34678061215 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx @@ -23,6 +23,7 @@ import { Button } from 'sonar-ui-common/components/controls/buttons'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { withRouter } from '../../../components/hoc/withRouter'; import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; +import { Permissions } from '../../../types/permissions'; export interface EmptyInstanceProps { currentUser: T.CurrentUser; @@ -32,7 +33,7 @@ export interface EmptyInstanceProps { export function EmptyInstance(props: EmptyInstanceProps) { const { currentUser, router } = props; const showNewProjectButton = - isLoggedIn(currentUser) && hasGlobalPermission(currentUser, 'provisioning'); + isLoggedIn(currentUser) && hasGlobalPermission(currentUser, Permissions.ProjectCreation); return ( <div className="projects-empty-list"> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx index fb282ba3f49..d2c52c76eba 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx @@ -27,6 +27,7 @@ import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; import { IMPORT_COMPATIBLE_ALMS } from '../../../helpers/constants'; import { hasGlobalPermission } from '../../../helpers/users'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; +import { Permissions } from '../../../types/permissions'; import ProjectCreationMenuItem from './ProjectCreationMenuItem'; interface Props { @@ -38,8 +39,6 @@ interface State { boundAlms: Array<string>; } -const PROJECT_CREATION_PERMISSION = 'provisioning'; - const almSettingsValidators = { [AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url, [AlmKeys.BitbucketServer]: (_: AlmSettingsInstance) => true, @@ -68,7 +67,7 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> { fetchAlmBindings = async () => { const { currentUser } = this.props; - const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION); + const canCreateProject = hasGlobalPermission(currentUser, Permissions.ProjectCreation); // getAlmSettings requires branchesEnabled if (!canCreateProject) { @@ -94,7 +93,7 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> { const { className, currentUser } = this.props; const { boundAlms } = this.state; - const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION); + const canCreateProject = hasGlobalPermission(currentUser, Permissions.ProjectCreation); if (!canCreateProject) { return null; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx index ccd0eb4c0c6..3121467f77d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx @@ -19,14 +19,18 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import EmptyInstance from '../EmptyInstance'; +import { mockRouter } from '../../../../helpers/testMocks'; +import { EmptyInstance } from '../EmptyInstance'; it('renders correctly for SQ', () => { - expect(shallow(<EmptyInstance currentUser={{ isLoggedIn: false }} />)).toMatchSnapshot(); + expect( + shallow(<EmptyInstance currentUser={{ isLoggedIn: false }} router={mockRouter()} />) + ).toMatchSnapshot(); expect( shallow( <EmptyInstance currentUser={{ isLoggedIn: true, permissions: { global: ['provisioning'] } }} + router={mockRouter()} /> ) ).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/EmptyInstance-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/EmptyInstance-test.tsx.snap index 3c17a844515..247739f3cec 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/EmptyInstance-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/EmptyInstance-test.tsx.snap @@ -1,26 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly for SQ 1`] = ` -<EmptyInstance - currentUser={ - Object { - "isLoggedIn": false, - } - } -/> +<div + className="projects-empty-list" +> + <h3> + projects.no_projects.empty_instance + </h3> +</div> `; exports[`renders correctly for SQ 2`] = ` -<EmptyInstance - currentUser={ - Object { - "isLoggedIn": true, - "permissions": Object { - "global": Array [ - "provisioning", - ], - }, - } - } -/> +<div + className="projects-empty-list" +> + <h3> + projects.no_projects.empty_instance.new_project + </h3> + <div> + <p + className="big-spacer-top" + > + projects.no_projects.empty_instance.how_to_add_projects + </p> + <p + className="big-spacer-top" + > + <Button + onClick={[Function]} + > + my_account.create_new.TRK + </Button> + </p> + </div> +</div> `; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index 7c0abc8f42e..c10ecb3d411 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -30,6 +30,7 @@ import { getValues } from '../../api/settings'; import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; import { hasGlobalPermission } from '../../helpers/users'; import { getAppState, getCurrentUser, Store } from '../../store/rootReducer'; +import { Permissions } from '../../types/permissions'; import { SettingsKey } from '../../types/settings'; import CreateProjectForm from './CreateProjectForm'; import Header from './Header'; @@ -205,7 +206,7 @@ export class App extends React.PureComponent<Props, State> { <Header defaultProjectVisibility={defaultProjectVisibility} - hasProvisionPermission={hasGlobalPermission(currentUser, 'provisioning')} + hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)} onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange} onProjectCreate={this.openCreateProjectForm} /> diff --git a/server/sonar-web/src/main/js/types/permissions.ts b/server/sonar-web/src/main/js/types/permissions.ts new file mode 100644 index 00000000000..2da4b7d21c3 --- /dev/null +++ b/server/sonar-web/src/main/js/types/permissions.ts @@ -0,0 +1,25 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +export enum Permissions { + Admin = 'admin', + ProjectCreation = 'provisioning', + ApplicationCreation = 'applicationcreator' +} diff --git a/server/sonar-web/src/main/js/types/plugins.ts b/server/sonar-web/src/main/js/types/plugins.ts index f602016a712..cd4100e56e6 100644 --- a/server/sonar-web/src/main/js/types/plugins.ts +++ b/server/sonar-web/src/main/js/types/plugins.ts @@ -76,6 +76,12 @@ export enum PluginType { External = 'EXTERNAL' } +export enum RiskConsent { + Accepted = 'ACCEPTED', + NotAccepted = 'NOT_ACCEPTED', + Required = 'REQUIRED' +} + export function isAvailablePlugin(plugin: Plugin): plugin is AvailablePlugin { return (plugin as any).release !== undefined; } diff --git a/server/sonar-web/src/main/js/types/settings.ts b/server/sonar-web/src/main/js/types/settings.ts index c7ca6d250ab..0cf1661724b 100644 --- a/server/sonar-web/src/main/js/types/settings.ts +++ b/server/sonar-web/src/main/js/types/settings.ts @@ -20,7 +20,8 @@ export const enum SettingsKey { DaysBeforeDeletingInactiveBranchesAndPRs = 'sonar.dbcleaner.daysBeforeDeletingInactiveBranchesAndPRs', DefaultProjectVisibility = 'projects.default.visibility', - ServerBaseUrl = 'sonar.core.serverBaseURL' + ServerBaseUrl = 'sonar.core.serverBaseURL', + PluginRiskConsent = 'sonar.plugins.risk.consent' } export type Setting = SettingValue & { definition: SettingDefinition }; |