Browse Source

SONAR-14606 Plugins require risk acknowledgment

tags/8.9.0.43852
Jeremy Davis 3 years ago
parent
commit
83774f0672
19 changed files with 443 additions and 158 deletions
  1. 3
    1
      server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx
  2. 56
    4
      server/sonar-web/src/main/js/apps/marketplace/App.tsx
  3. 9
    7
      server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
  4. 28
    0
      server/sonar-web/src/main/js/apps/marketplace/__tests__/App-test.tsx
  5. 59
    0
      server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx
  6. 0
    116
      server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap
  7. 55
    0
      server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx
  8. 39
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx
  9. 100
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap
  10. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx
  11. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx
  12. 3
    4
      server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
  13. 6
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx
  14. 30
    19
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/EmptyInstance-test.tsx.snap
  15. 2
    1
      server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
  16. 25
    0
      server/sonar-web/src/main/js/types/permissions.ts
  17. 6
    0
      server/sonar-web/src/main/js/types/plugins.ts
  18. 2
    1
      server/sonar-web/src/main/js/types/settings.ts
  19. 16
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 3
- 1
server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx View File

@@ -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() {

+ 56
- 4
server/sonar-web/src/main/js/apps/marketplace/App.tsx View File

@@ -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} />

+ 9
- 7
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx View File

@@ -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);

+ 28
- 0
server/sonar-web/src/main/js/apps/marketplace/__tests__/App-test.tsx View File

@@ -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={{

+ 59
- 0
server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx View File

@@ -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');
});
});

+ 0
- 116
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -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>
`;

+ 55
- 0
server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx View File

@@ -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>
);
}

+ 39
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx View File

@@ -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} />);
}

+ 100
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap View File

@@ -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>
`;

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx View File

@@ -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;

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx View File

@@ -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">

+ 3
- 4
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx View File

@@ -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;

+ 6
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx View File

@@ -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();

+ 30
- 19
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/EmptyInstance-test.tsx.snap View File

@@ -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>
`;

+ 2
- 1
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx View File

@@ -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}
/>

+ 25
- 0
server/sonar-web/src/main/js/types/permissions.ts View File

@@ -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'
}

+ 6
- 0
server/sonar-web/src/main/js/types/plugins.ts View File

@@ -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;
}

+ 2
- 1
server/sonar-web/src/main/js/types/settings.ts View File

@@ -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 };

+ 16
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2699,7 +2699,9 @@ marketplace.page.you_are_running.developer=You are currently running a Developer
marketplace.page.you_are_running.enterprise=You are currently running an Enterprise Edition.
marketplace.page.you_are_running.datacenter=You are currently running a Data Center Edition.
marketplace.page.plugins=Plugins
marketplace.page.plugins.description=Plugins available in the MarketPlace are not provided or supported by SonarSource. Please reach out directly to their maintainers for support.
marketplace.page.plugins.description=Plugins available in the Marketplace are not provided or supported by SonarSource. Please reach out directly to their maintainers for support.
marketplace.page.plugins.description2=Installing a plugin is a manual operation. Please refer to the {link}.
marketplace.page.plugins.description2.link=documentation
marketplace.plugin_list.no_plugins.all=No installed plugins or updates available
marketplace.plugin_list.no_plugins.installed=No installed plugins
marketplace.plugin_list.no_plugins.updates=No plugin updates available
@@ -2747,6 +2749,17 @@ marketplace.release_notes=Release Notes
marketplace.how_to_setup_cluster_url=Further configuration is required to set up a cluster. See {url} documentation.
marketplace.search=Search by features, tags, or categories...

marketplace.risk_consent.title=Installation of plugins
marketplace.risk_consent.description=Plugins are not provided by SonarSource and are therefore installed at your own risk. SonarSource disclaims all liability for installing and using such plugins.
marketplace.risk_consent.installation=You can install plugins directly from the list below after you acknowledge the risk.
marketplace.risk_consent.action=I understand the risk.

plugin_risk_consent.title=Installation of plugins
plugin_risk_consent.description=A third-party plugin has been detected.
plugin_risk_consent.description2=Plugins are not provided by SonarSource and are therefore installed at your own risk. SonarSource disclaims all liability for installing and using such plugins.
plugin_risk_consent.description3=If you wish to uninstall the plugin(s) instead, you may refer to the {link}.
plugin_risk_consent.description3.link=documentation
plugin_risk_consent.action=I understand the risk

#------------------------------------------------------------------------------
#
@@ -3961,6 +3974,8 @@ indexation.page_unavailable.title.portfolios=Portfolios page is temporarily unav
indexation.page_unavailable.title={componentQualifier} {componentName} is temporarily unavailable
indexation.page_unavailable.description=This page will be available after the data is reloaded. This might take a while depending on the amount of projects and issues in your SonarQube instance.
indexation.page_unavailable.description.additional_information=You can keep analyzing your projects during this process.


#------------------------------------------------------------------------------
#
# HOMEPAGE

Loading…
Cancel
Save