@@ -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() { |
@@ -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} /> |
@@ -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); |
@@ -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={{ |
@@ -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'); | |||
}); | |||
}); |
@@ -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> | |||
`; |
@@ -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> | |||
); | |||
} |
@@ -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} />); | |||
} |
@@ -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> | |||
`; |
@@ -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; |
@@ -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"> |
@@ -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; |
@@ -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(); |
@@ -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> | |||
`; |
@@ -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} | |||
/> |
@@ -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' | |||
} |
@@ -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; | |||
} |
@@ -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 }; |
@@ -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 |