@@ -189,7 +189,6 @@ export function getMyProjects(data: { | |||
} | |||
export interface Component { | |||
organization: string; | |||
id: string; | |||
key: string; | |||
name: string; | |||
@@ -212,7 +211,6 @@ export function searchProjects( | |||
): Promise<{ | |||
components: Component[]; | |||
facets: Facet[]; | |||
organizations: Array<{ key: string; name: string }>; | |||
paging: T.Paging; | |||
}> { | |||
const url = '/api/components/search_projects'; | |||
@@ -232,7 +230,6 @@ export function changeKey(data: { from: string; to: string }) { | |||
} | |||
export interface SuggestionsResponse { | |||
organizations: Array<{ key: string; name: string }>; | |||
projects: Array<{ key: string; name: string }>; | |||
results: Array<{ | |||
items: Array<{ | |||
@@ -241,7 +238,6 @@ export interface SuggestionsResponse { | |||
key: string; | |||
match: string; | |||
name: string; | |||
organization: string; | |||
project: string; | |||
}>; | |||
more: number; |
@@ -75,7 +75,6 @@ export function getFacet( | |||
} | |||
export function searchIssueTags(data: { | |||
organization?: string; | |||
project?: string; | |||
ps?: number; | |||
q?: string; | |||
@@ -136,7 +135,6 @@ export function bulkChangeIssues(issueKeys: string[], query: RequestData): Promi | |||
} | |||
export function searchIssueAuthors(data: { | |||
organization?: string; | |||
project?: string; | |||
ps?: number; | |||
q?: string; |
@@ -1,38 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { getJSON } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getOrganizations(data: { | |||
organizations?: string; | |||
member?: boolean; | |||
}): Promise<{ | |||
organizations: T.Organization[]; | |||
paging: T.Paging; | |||
}> { | |||
return getJSON('/api/organizations/search', data).catch(throwGlobalError); | |||
} | |||
export function getOrganization(key: string): Promise<T.Organization | undefined> { | |||
return getJSON('/api/organizations/search', { organizations: key }).then( | |||
r => r.organizations.find((o: T.Organization) => o.key === key), | |||
throwGlobalError | |||
); | |||
} |
@@ -229,7 +229,6 @@ export function dissociateProject({ language, name: qualityProfile }: Profile, p | |||
export interface SearchUsersGroupsParameters { | |||
language: string; | |||
organization?: string; | |||
qualityProfile: string; | |||
q?: string; | |||
selected?: 'all' | 'selected' | 'deselected'; | |||
@@ -272,7 +271,6 @@ export function removeUser(parameters: AddRemoveUserParameters): Promise<void | | |||
export interface AddRemoveGroupParameters { | |||
group: string; | |||
language: string; | |||
organization?: string; | |||
qualityProfile: string; | |||
} | |||
@@ -25,10 +25,7 @@ export function getRulesApp(): Promise<GetRulesAppResponse> { | |||
return getJSON('/api/rules/app').catch(throwGlobalError); | |||
} | |||
export function searchRules(data: { | |||
organization?: string; | |||
[x: string]: any; | |||
}): Promise<SearchRulesResponse> { | |||
export function searchRules(data: { [x: string]: any }): Promise<SearchRulesResponse> { | |||
return getJSON('/api/rules/search', data).catch(throwGlobalError); | |||
} | |||
@@ -73,7 +70,7 @@ export function createRule(data: { | |||
); | |||
} | |||
export function deleteRule(parameters: { key: string; organization?: string }) { | |||
export function deleteRule(parameters: { key: string }) { | |||
return post('/api/rules/delete', parameters).catch(throwGlobalError); | |||
} | |||
@@ -1,83 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { fetchOrganization } from '../../../store/rootActions'; | |||
import { getOrganizationByKey, Store } from '../../../store/rootReducer'; | |||
import NotFound from '../NotFound'; | |||
import Extension from './Extension'; | |||
interface StateToProps { | |||
organization?: T.Organization; | |||
} | |||
interface DispatchProps { | |||
fetchOrganization: (organizationKey: string) => void; | |||
} | |||
interface OwnProps { | |||
location: {}; | |||
params: { | |||
extensionKey: string; | |||
organizationKey: string; | |||
pluginKey: string; | |||
}; | |||
} | |||
type Props = OwnProps & StateToProps & DispatchProps; | |||
class OrganizationPageExtension extends React.PureComponent<Props> { | |||
refreshOrganization = () => { | |||
return this.props.organization && this.props.fetchOrganization(this.props.organization.key); | |||
}; | |||
render() { | |||
const { extensionKey, pluginKey } = this.props.params; | |||
const { organization } = this.props; | |||
if (!organization) { | |||
return null; | |||
} | |||
const { actions = {} } = organization; | |||
let { pages = [] } = organization; | |||
if (actions.admin && organization.adminPages) { | |||
pages = pages.concat(organization.adminPages); | |||
} | |||
const extension = pages.find(p => p.key === `${pluginKey}/${extensionKey}`); | |||
return extension ? ( | |||
<Extension | |||
extension={extension} | |||
options={{ organization, refreshOrganization: this.refreshOrganization }} | |||
/> | |||
) : ( | |||
<NotFound withContainer={false} /> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state: Store, ownProps: OwnProps) => ({ | |||
organization: getOrganizationByKey(state, ownProps.params.organizationKey) | |||
}); | |||
const mapDispatchToProps = { fetchOrganization }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(OrganizationPageExtension); |
@@ -21,213 +21,47 @@ import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent'; | |||
import NavBar from 'sonar-ui-common/components/ui/NavBar'; | |||
import { parseDate } from 'sonar-ui-common/helpers/dates'; | |||
import { | |||
fetchPrismicFeatureNews, | |||
fetchPrismicRefs, | |||
PrismicFeatureNews | |||
} from '../../../../api/news'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import { isLoggedIn } from '../../../../helpers/users'; | |||
import { | |||
getAppState, | |||
getCurrentUser, | |||
getCurrentUserSetting, | |||
getGlobalSettingValue, | |||
Store | |||
} from '../../../../store/rootReducer'; | |||
import { setCurrentUserSetting } from '../../../../store/users'; | |||
import { getAppState, getCurrentUser, Store } from '../../../../store/rootReducer'; | |||
import { rawSizes } from '../../../theme'; | |||
import EmbedDocsPopupHelper from '../../embed-docs-modal/EmbedDocsPopupHelper'; | |||
import Search from '../../search/Search'; | |||
import './GlobalNav.css'; | |||
import GlobalNavBranding, { SonarCloudNavBranding } from './GlobalNavBranding'; | |||
import GlobalNavBranding from './GlobalNavBranding'; | |||
import GlobalNavMenu from './GlobalNavMenu'; | |||
import GlobalNavUserContainer from './GlobalNavUserContainer'; | |||
import GlobalNavUser from './GlobalNavUser'; | |||
const GlobalNavPlus = lazyLoadComponent(() => import('./GlobalNavPlus'), 'GlobalNavPlus'); | |||
const NotificationsSidebar = lazyLoadComponent( | |||
() => import('../../notifications/NotificationsSidebar'), | |||
'NotificationsSidebar' | |||
); | |||
const NavLatestNotification = lazyLoadComponent( | |||
() => import('../../notifications/NavLatestNotification'), | |||
'NavLatestNotification' | |||
); | |||
interface Props { | |||
accessToken?: string; | |||
appState: Pick<T.AppState, 'canAdmin' | 'globalPages' | 'organizationsEnabled' | 'qualifiers'>; | |||
export interface GlobalNavProps { | |||
appState: Pick<T.AppState, 'canAdmin' | 'globalPages' | 'qualifiers'>; | |||
currentUser: T.CurrentUser; | |||
location: { pathname: string }; | |||
notificationsLastReadDate?: Date; | |||
notificationsOptOut?: boolean; | |||
setCurrentUserSetting: (setting: T.CurrentUserSetting) => void; | |||
} | |||
interface State { | |||
notificationSidebar?: boolean; | |||
loadingNews: boolean; | |||
loadingMoreNews: boolean; | |||
news: PrismicFeatureNews[]; | |||
newsPaging?: T.Paging; | |||
newsRef?: string; | |||
} | |||
const PAGE_SIZE = 5; | |||
export class GlobalNav extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
loadingNews: false, | |||
loadingMoreNews: false, | |||
news: [], | |||
notificationSidebar: false | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
if (isSonarCloud()) { | |||
this.fetchFeatureNews(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchFeatureNews = () => { | |||
const { accessToken } = this.props; | |||
if (accessToken) { | |||
this.setState({ loadingNews: true }); | |||
fetchPrismicRefs() | |||
.then(({ ref }) => { | |||
if (this.mounted) { | |||
this.setState({ newsRef: ref }); | |||
} | |||
return ref; | |||
}) | |||
.then(ref => fetchPrismicFeatureNews({ accessToken, ref, ps: PAGE_SIZE })) | |||
.then( | |||
({ news, paging }) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loadingNews: false, | |||
news, | |||
newsPaging: paging | |||
}); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loadingNews: false }); | |||
} | |||
} | |||
); | |||
} | |||
}; | |||
fetchMoreFeatureNews = () => { | |||
const { accessToken } = this.props; | |||
const { newsPaging, newsRef } = this.state; | |||
if (accessToken && newsPaging && newsRef) { | |||
this.setState({ loadingMoreNews: true }); | |||
fetchPrismicFeatureNews({ | |||
accessToken, | |||
ref: newsRef, | |||
p: newsPaging.pageIndex + 1, | |||
ps: PAGE_SIZE | |||
}).then( | |||
({ news, paging }) => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
loadingMoreNews: false, | |||
news: [...state.news, ...news], | |||
newsPaging: paging | |||
})); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loadingMoreNews: false }); | |||
} | |||
} | |||
); | |||
} | |||
}; | |||
handleOpenNotificationSidebar = () => { | |||
this.setState({ notificationSidebar: true }); | |||
this.fetchFeatureNews(); | |||
}; | |||
handleCloseNotificationSidebar = () => { | |||
this.setState({ notificationSidebar: false }); | |||
const lastNews = this.state.news[0]; | |||
const readDate = lastNews ? parseDate(lastNews.publicationDate).getTime() : Date.now(); | |||
this.props.setCurrentUserSetting({ key: 'notifications.readDate', value: readDate.toString() }); | |||
}; | |||
render() { | |||
const { appState, currentUser } = this.props; | |||
const { news } = this.state; | |||
return ( | |||
<NavBar className="navbar-global" height={rawSizes.globalNavHeightRaw} id="global-navigation"> | |||
{isSonarCloud() ? <SonarCloudNavBranding /> : <GlobalNavBranding />} | |||
<GlobalNavMenu {...this.props} /> | |||
<ul className="global-navbar-menu global-navbar-menu-right"> | |||
{isSonarCloud() && isLoggedIn(currentUser) && news.length > 0 && ( | |||
<NavLatestNotification | |||
lastNews={news[0]} | |||
notificationsLastReadDate={this.props.notificationsLastReadDate} | |||
notificationsOptOut={this.props.notificationsOptOut} | |||
onClick={this.handleOpenNotificationSidebar} | |||
setCurrentUserSetting={this.props.setCurrentUserSetting} | |||
/> | |||
)} | |||
<EmbedDocsPopupHelper /> | |||
<Search appState={appState} currentUser={currentUser} /> | |||
{isLoggedIn(currentUser) && ( | |||
<GlobalNavPlus appState={appState} currentUser={currentUser} /> | |||
)} | |||
<GlobalNavUserContainer appState={appState} currentUser={currentUser} /> | |||
</ul> | |||
{isSonarCloud() && isLoggedIn(currentUser) && this.state.notificationSidebar && ( | |||
<NotificationsSidebar | |||
fetchMoreFeatureNews={this.fetchMoreFeatureNews} | |||
loading={this.state.loadingNews} | |||
loadingMore={this.state.loadingMoreNews} | |||
news={news} | |||
notificationsLastReadDate={this.props.notificationsLastReadDate} | |||
onClose={this.handleCloseNotificationSidebar} | |||
paging={this.state.newsPaging} | |||
/> | |||
)} | |||
</NavBar> | |||
); | |||
} | |||
export function GlobalNav(props: GlobalNavProps) { | |||
const { appState, currentUser, location } = props; | |||
return ( | |||
<NavBar className="navbar-global" height={rawSizes.globalNavHeightRaw} id="global-navigation"> | |||
<GlobalNavBranding /> | |||
<GlobalNavMenu appState={appState} currentUser={currentUser} location={location} /> | |||
<ul className="global-navbar-menu global-navbar-menu-right"> | |||
<EmbedDocsPopupHelper /> | |||
<Search currentUser={currentUser} /> | |||
{isLoggedIn(currentUser) && <GlobalNavPlus appState={appState} currentUser={currentUser} />} | |||
<GlobalNavUser currentUser={currentUser} /> | |||
</ul> | |||
</NavBar> | |||
); | |||
} | |||
const mapStateToProps = (state: Store) => { | |||
const accessToken = getGlobalSettingValue(state, 'sonar.prismic.accessToken'); | |||
const notificationsLastReadDate = getCurrentUserSetting(state, 'notifications.readDate'); | |||
const notificationsOptOut = getCurrentUserSetting(state, 'notifications.optOut') === 'true'; | |||
return { | |||
currentUser: getCurrentUser(state), | |||
appState: getAppState(state), | |||
accessToken: accessToken && accessToken.value, | |||
notificationsLastReadDate: notificationsLastReadDate | |||
? parseDate(Number(notificationsLastReadDate)) | |||
: undefined, | |||
notificationsOptOut | |||
appState: getAppState(state) | |||
}; | |||
}; | |||
const mapDispatchToProps = { | |||
setCurrentUserSetting | |||
}; | |||
export default connect(mapStateToProps, mapDispatchToProps)(GlobalNav); | |||
export default connect(mapStateToProps)(GlobalNav); |
@@ -28,7 +28,7 @@ import { getQualityGatesUrl } from '../../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
interface Props { | |||
appState: Pick<T.AppState, 'canAdmin' | 'globalPages' | 'organizationsEnabled' | 'qualifiers'>; | |||
appState: Pick<T.AppState, 'canAdmin' | 'globalPages' | 'qualifiers'>; | |||
currentUser: T.CurrentUser; | |||
location: { pathname: string }; | |||
} | |||
@@ -156,16 +156,15 @@ export default class GlobalNavMenu extends React.PureComponent<Props> { | |||
const governanceInstalled = this.props.appState.qualifiers.includes( | |||
ComponentQualifier.Portfolio | |||
); | |||
const { organizationsEnabled } = this.props.appState; | |||
return ( | |||
<ul className="global-navbar-menu"> | |||
{this.renderProjects()} | |||
{governanceInstalled && this.renderPortfolios()} | |||
{this.renderIssuesLink()} | |||
{!organizationsEnabled && this.renderRulesLink()} | |||
{!organizationsEnabled && this.renderProfilesLink()} | |||
{!organizationsEnabled && this.renderQualityGatesLink()} | |||
{this.renderRulesLink()} | |||
{this.renderProfilesLink()} | |||
{this.renderQualityGatesLink()} | |||
{this.renderAdministrationLink()} | |||
{this.renderMore()} | |||
</ul> |
@@ -17,7 +17,6 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; | |||
@@ -25,14 +24,11 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; | |||
import { Router, withRouter } from '../../../../components/hoc/withRouter'; | |||
import Avatar from '../../../../components/ui/Avatar'; | |||
import OrganizationListItem from '../../../../components/ui/OrganizationListItem'; | |||
import { isLoggedIn } from '../../../../helpers/users'; | |||
import { rawSizes } from '../../../theme'; | |||
interface Props { | |||
appState: { organizationsEnabled?: boolean }; | |||
currentUser: T.CurrentUser; | |||
organizations: T.Organization[]; | |||
router: Pick<Router, 'push'>; | |||
} | |||
@@ -55,9 +51,7 @@ export class GlobalNavUser extends React.PureComponent<Props> { | |||
}; | |||
renderAuthenticated() { | |||
const { organizations } = this.props; | |||
const currentUser = this.props.currentUser as T.LoggedInUser; | |||
const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; | |||
return ( | |||
<Dropdown | |||
className="js-user-authenticated" | |||
@@ -79,17 +73,6 @@ export class GlobalNavUser extends React.PureComponent<Props> { | |||
<li> | |||
<Link to="/account">{translate('my_account.page')}</Link> | |||
</li> | |||
{hasOrganizations && <li className="divider" role="separator" />} | |||
{hasOrganizations && ( | |||
<li> | |||
<Link to="/account/organizations">{translate('my_organizations')}</Link> | |||
</li> | |||
)} | |||
{hasOrganizations && | |||
sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( | |||
<OrganizationListItem key={organization.key} organization={organization} /> | |||
))} | |||
{hasOrganizations && <li className="divider" role="separator" />} | |||
<li> | |||
<a href="#" onClick={this.handleLogout}> | |||
{translate('layout.logout')} |
@@ -1,32 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { getMyOrganizations, Store } from '../../../../store/rootReducer'; | |||
import GlobalNavUser from './GlobalNavUser'; | |||
interface StateProps { | |||
organizations: T.Organization[]; | |||
} | |||
const mapStateToProps = (state: Store): StateProps => ({ | |||
organizations: getMyOrganizations(state) | |||
}); | |||
export default connect(mapStateToProps)(GlobalNavUser); |
@@ -19,71 +19,21 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
fetchPrismicFeatureNews, | |||
fetchPrismicRefs, | |||
PrismicFeatureNews | |||
} from '../../../../../api/news'; | |||
import { isSonarCloud } from '../../../../../helpers/system'; | |||
import { GlobalNav } from '../GlobalNav'; | |||
jest.mock('../../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { GlobalNav, GlobalNavProps } from '../GlobalNav'; | |||
// Solve redux warning issue "No reducer provided for key": | |||
// https://stackoverflow.com/questions/43375079/redux-warning-only-appearing-in-tests | |||
jest.mock('../../../../../store/rootReducer'); | |||
jest.mock('../../../../../api/news', () => { | |||
const prismicResult: PrismicFeatureNews[] = [ | |||
{ | |||
notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration', | |||
publicationDate: '2018-04-06', | |||
features: [ | |||
{ | |||
categories: [{ color: '#ff0000', name: 'Java' }], | |||
description: '10 new Java rules' | |||
} | |||
] | |||
}, | |||
{ | |||
notification: 'Some other notification', | |||
publicationDate: '2018-04-05', | |||
features: [ | |||
{ | |||
categories: [{ color: '#0000ff', name: 'BitBucket' }], | |||
description: 'BitBucket branch decoration', | |||
readMore: 'http://example.com' | |||
} | |||
] | |||
} | |||
]; | |||
return { | |||
fetchPrismicRefs: jest.fn().mockResolvedValue({ ref: 'master-ref' }), | |||
fetchPrismicFeatureNews: jest.fn().mockResolvedValue({ | |||
news: prismicResult, | |||
paging: { pageIndex: 1, pageSize: 10, total: 2 } | |||
}) | |||
}; | |||
}); | |||
const appState: GlobalNav['props']['appState'] = { | |||
const appState: GlobalNavProps['appState'] = { | |||
globalPages: [], | |||
canAdmin: false, | |||
organizationsEnabled: false, | |||
qualifiers: [] | |||
}; | |||
const location = { pathname: '' }; | |||
beforeEach(() => { | |||
(fetchPrismicRefs as jest.Mock).mockClear(); | |||
(fetchPrismicFeatureNews as jest.Mock).mockClear(); | |||
}); | |||
it('should render correctly', async () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => false); | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot('anonymous users'); | |||
@@ -93,28 +43,12 @@ it('should render correctly', async () => { | |||
await waitAndUpdate(wrapper); | |||
}); | |||
it('should render correctly if there are new features', async () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => true); | |||
const wrapper = shallowRender(); | |||
wrapper.setProps({ currentUser: { isLoggedIn: true } }); | |||
await waitAndUpdate(wrapper); | |||
expect(fetchPrismicRefs).toHaveBeenCalled(); | |||
expect(fetchPrismicFeatureNews).toHaveBeenCalled(); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('NavLatestNotification').exists()).toBe(true); | |||
click(wrapper.find('NavLatestNotification')); | |||
expect(wrapper.find('NotificationsSidebar').exists()).toBe(true); | |||
}); | |||
function shallowRender(props: Partial<GlobalNav['props']> = {}) { | |||
function shallowRender(props: Partial<GlobalNavProps> = {}) { | |||
return shallow( | |||
<GlobalNav | |||
accessToken="token" | |||
appState={appState} | |||
currentUser={{ isLoggedIn: false }} | |||
location={location} | |||
setCurrentUserSetting={jest.fn()} | |||
{...props} | |||
/> | |||
); |
@@ -19,64 +19,21 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockCurrentUser, mockLoggedInUser, mockRouter } from '../../../../../helpers/testMocks'; | |||
import { GlobalNavUser } from '../GlobalNavUser'; | |||
const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' }; | |||
const organizations: T.Organization[] = [ | |||
{ key: 'myorg', name: 'MyOrg', projectVisibility: 'public' }, | |||
{ key: 'foo', name: 'Foo', projectVisibility: 'public' }, | |||
{ key: 'bar', name: 'bar', projectVisibility: 'public' } | |||
]; | |||
const appState = { organizationsEnabled: true }; | |||
it('should render the right interface for anonymous user', () => { | |||
const currentUser = { isLoggedIn: false }; | |||
const wrapper = shallow( | |||
<GlobalNavUser | |||
appState={appState} | |||
currentUser={currentUser} | |||
organizations={[]} | |||
router={{ push: jest.fn() }} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(shallowRender({ currentUser: mockCurrentUser() })).toMatchSnapshot(); | |||
}); | |||
it('should render the right interface for logged in user', () => { | |||
const wrapper = shallow( | |||
<GlobalNavUser | |||
appState={appState} | |||
currentUser={currentUser} | |||
organizations={[]} | |||
router={{ push: jest.fn() }} | |||
/> | |||
); | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ open: true }); | |||
expect(wrapper.find('Dropdown')).toMatchSnapshot(); | |||
}); | |||
it('should render user organizations', () => { | |||
const wrapper = shallow( | |||
<GlobalNavUser | |||
appState={appState} | |||
currentUser={currentUser} | |||
organizations={organizations} | |||
router={{ push: jest.fn() }} | |||
/> | |||
function shallowRender(overrides: Partial<GlobalNavUser['props']> = {}) { | |||
return shallow<GlobalNavUser>( | |||
<GlobalNavUser currentUser={mockLoggedInUser()} router={mockRouter()} {...overrides} /> | |||
); | |||
wrapper.setState({ open: true }); | |||
expect(wrapper.find('Dropdown')).toMatchSnapshot(); | |||
}); | |||
it('should not render user organizations when they are not activated', () => { | |||
const wrapper = shallow( | |||
<GlobalNavUser | |||
appState={{ organizationsEnabled: false }} | |||
currentUser={currentUser} | |||
organizations={organizations} | |||
router={{ push: jest.fn() }} | |||
/> | |||
); | |||
wrapper.setState({ open: true }); | |||
expect(wrapper.find('Dropdown')).toMatchSnapshot(); | |||
}); | |||
} |
@@ -1,108 +1,5 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly if there are new features 1`] = ` | |||
<NavBar | |||
className="navbar-global" | |||
height={48} | |||
id="global-navigation" | |||
> | |||
<SonarCloudNavBranding /> | |||
<GlobalNavMenu | |||
accessToken="token" | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
} | |||
} | |||
location={ | |||
Object { | |||
"pathname": "", | |||
} | |||
} | |||
setCurrentUserSetting={[MockFunction]} | |||
/> | |||
<ul | |||
className="global-navbar-menu global-navbar-menu-right" | |||
> | |||
<NavLatestNotification | |||
lastNews={ | |||
Object { | |||
"features": Array [ | |||
Object { | |||
"categories": Array [ | |||
Object { | |||
"color": "#ff0000", | |||
"name": "Java", | |||
}, | |||
], | |||
"description": "10 new Java rules", | |||
}, | |||
], | |||
"notification": "10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration", | |||
"publicationDate": "2018-04-06", | |||
} | |||
} | |||
onClick={[Function]} | |||
setCurrentUserSetting={[MockFunction]} | |||
/> | |||
<EmbedDocsPopupHelper /> | |||
<withRouter(Search) | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
} | |||
} | |||
/> | |||
<GlobalNavPlus | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
} | |||
} | |||
/> | |||
<Connect(withRouter(GlobalNavUser)) | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
} | |||
} | |||
/> | |||
</ul> | |||
</NavBar> | |||
`; | |||
exports[`should render correctly: anonymous users 1`] = ` | |||
<NavBar | |||
className="navbar-global" | |||
@@ -111,12 +8,10 @@ exports[`should render correctly: anonymous users 1`] = ` | |||
> | |||
<Connect(GlobalNavBranding) /> | |||
<GlobalNavMenu | |||
accessToken="token" | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
@@ -130,36 +25,19 @@ exports[`should render correctly: anonymous users 1`] = ` | |||
"pathname": "", | |||
} | |||
} | |||
setCurrentUserSetting={[MockFunction]} | |||
/> | |||
<ul | |||
className="global-navbar-menu global-navbar-menu-right" | |||
> | |||
<EmbedDocsPopupHelper /> | |||
<withRouter(Search) | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": false, | |||
} | |||
} | |||
/> | |||
<Connect(withRouter(GlobalNavUser)) | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
<withRouter(GlobalNavUser) | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": false, | |||
@@ -178,12 +56,10 @@ exports[`should render correctly: logged in users 1`] = ` | |||
> | |||
<Connect(GlobalNavBranding) /> | |||
<GlobalNavMenu | |||
accessToken="token" | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
@@ -197,21 +73,12 @@ exports[`should render correctly: logged in users 1`] = ` | |||
"pathname": "", | |||
} | |||
} | |||
setCurrentUserSetting={[MockFunction]} | |||
/> | |||
<ul | |||
className="global-navbar-menu global-navbar-menu-right" | |||
> | |||
<EmbedDocsPopupHelper /> | |||
<withRouter(Search) | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
@@ -223,7 +90,6 @@ exports[`should render correctly: logged in users 1`] = ` | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
@@ -233,15 +99,7 @@ exports[`should render correctly: logged in users 1`] = ` | |||
} | |||
} | |||
/> | |||
<Connect(withRouter(GlobalNavUser)) | |||
appState={ | |||
Object { | |||
"canAdmin": false, | |||
"globalPages": Array [], | |||
"organizationsEnabled": false, | |||
"qualifiers": Array [], | |||
} | |||
} | |||
<withRouter(GlobalNavUser) | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, |
@@ -1,68 +1,5 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should not render user organizations when they are not activated 1`] = ` | |||
<Dropdown | |||
className="js-user-authenticated" | |||
overlay={ | |||
<ul | |||
className="menu" | |||
> | |||
<li | |||
className="menu-item" | |||
> | |||
<div | |||
className="text-ellipsis text-muted" | |||
title="foo" | |||
> | |||
<strong> | |||
foo | |||
</strong> | |||
</div> | |||
<div | |||
className="little-spacer-top text-ellipsis text-muted" | |||
title="foo@bar.baz" | |||
> | |||
foo@bar.baz | |||
</div> | |||
</li> | |||
<li | |||
className="divider" | |||
/> | |||
<li> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/account" | |||
> | |||
my_account.page | |||
</Link> | |||
</li> | |||
<li> | |||
<a | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
layout.logout | |||
</a> | |||
</li> | |||
</ul> | |||
} | |||
tagName="li" | |||
> | |||
<a | |||
className="dropdown-toggle navbar-avatar" | |||
href="#" | |||
title="foo" | |||
> | |||
<Connect(Avatar) | |||
hash="abcd1234" | |||
name="foo" | |||
size={32} | |||
/> | |||
</a> | |||
</Dropdown> | |||
`; | |||
exports[`should render the right interface for anonymous user 1`] = ` | |||
<li> | |||
<a | |||
@@ -87,18 +24,12 @@ exports[`should render the right interface for logged in user 1`] = ` | |||
> | |||
<div | |||
className="text-ellipsis text-muted" | |||
title="foo" | |||
title="Skywalker" | |||
> | |||
<strong> | |||
foo | |||
Skywalker | |||
</strong> | |||
</div> | |||
<div | |||
className="little-spacer-top text-ellipsis text-muted" | |||
title="foo@bar.baz" | |||
> | |||
foo@bar.baz | |||
</div> | |||
</li> | |||
<li | |||
className="divider" | |||
@@ -127,118 +58,10 @@ exports[`should render the right interface for logged in user 1`] = ` | |||
<a | |||
className="dropdown-toggle navbar-avatar" | |||
href="#" | |||
title="foo" | |||
> | |||
<Connect(Avatar) | |||
hash="abcd1234" | |||
name="foo" | |||
size={32} | |||
/> | |||
</a> | |||
</Dropdown> | |||
`; | |||
exports[`should render user organizations 1`] = ` | |||
<Dropdown | |||
className="js-user-authenticated" | |||
overlay={ | |||
<ul | |||
className="menu" | |||
> | |||
<li | |||
className="menu-item" | |||
> | |||
<div | |||
className="text-ellipsis text-muted" | |||
title="foo" | |||
> | |||
<strong> | |||
foo | |||
</strong> | |||
</div> | |||
<div | |||
className="little-spacer-top text-ellipsis text-muted" | |||
title="foo@bar.baz" | |||
> | |||
foo@bar.baz | |||
</div> | |||
</li> | |||
<li | |||
className="divider" | |||
/> | |||
<li> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/account" | |||
> | |||
my_account.page | |||
</Link> | |||
</li> | |||
<li | |||
className="divider" | |||
role="separator" | |||
/> | |||
<li> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/account/organizations" | |||
> | |||
my_organizations | |||
</Link> | |||
</li> | |||
<OrganizationListItem | |||
organization={ | |||
Object { | |||
"key": "bar", | |||
"name": "bar", | |||
"projectVisibility": "public", | |||
} | |||
} | |||
/> | |||
<OrganizationListItem | |||
organization={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
"projectVisibility": "public", | |||
} | |||
} | |||
/> | |||
<OrganizationListItem | |||
organization={ | |||
Object { | |||
"key": "myorg", | |||
"name": "MyOrg", | |||
"projectVisibility": "public", | |||
} | |||
} | |||
/> | |||
<li | |||
className="divider" | |||
role="separator" | |||
/> | |||
<li> | |||
<a | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
layout.logout | |||
</a> | |||
</li> | |||
</ul> | |||
} | |||
tagName="li" | |||
> | |||
<a | |||
className="dropdown-toggle navbar-avatar" | |||
href="#" | |||
title="foo" | |||
title="Skywalker" | |||
> | |||
<Connect(Avatar) | |||
hash="abcd1234" | |||
name="foo" | |||
name="Skywalker" | |||
size={32} | |||
/> | |||
</a> |
@@ -41,7 +41,6 @@ const SearchResults = lazyLoadComponent(() => import('./SearchResults')); | |||
const SearchResult = lazyLoadComponent(() => import('./SearchResult')); | |||
interface OwnProps { | |||
appState: Pick<T.AppState, 'organizationsEnabled'>; | |||
currentUser: T.CurrentUser; | |||
} | |||
@@ -52,7 +51,6 @@ interface State { | |||
loadingMore?: string; | |||
more: More; | |||
open: boolean; | |||
organizations: T.Dict<{ name: string }>; | |||
projects: T.Dict<{ name: string }>; | |||
query: string; | |||
results: Results; | |||
@@ -74,7 +72,6 @@ export class Search extends React.PureComponent<Props, State> { | |||
loading: false, | |||
more: {}, | |||
open: false, | |||
organizations: {}, | |||
projects: {}, | |||
query: '', | |||
results: {}, | |||
@@ -142,7 +139,6 @@ export class Search extends React.PureComponent<Props, State> { | |||
this.setState({ | |||
more: {}, | |||
open: false, | |||
organizations: {}, | |||
projects: {}, | |||
query: '', | |||
results: {}, | |||
@@ -187,7 +183,6 @@ export class Search extends React.PureComponent<Props, State> { | |||
this.setState(state => ({ | |||
loading: false, | |||
more, | |||
organizations: { ...state.organizations, ...keyBy(response.organizations, 'key') }, | |||
projects: { ...state.projects, ...keyBy(response.projects, 'key') }, | |||
results, | |||
selected: list.length > 0 ? list[0] : undefined, | |||
@@ -216,7 +211,6 @@ export class Search extends React.PureComponent<Props, State> { | |||
loading: false, | |||
loadingMore: undefined, | |||
more: { ...state.more, [qualifier]: 0 }, | |||
organizations: { ...state.organizations, ...keyBy(response.organizations, 'key') }, | |||
projects: { ...state.projects, ...keyBy(response.projects, 'key') }, | |||
results: { | |||
...state.results, | |||
@@ -326,13 +320,11 @@ export class Search extends React.PureComponent<Props, State> { | |||
renderResult = (component: ComponentResult) => ( | |||
<SearchResult | |||
appState={this.props.appState} | |||
component={component} | |||
innerRef={this.innerRef} | |||
key={component.key} | |||
onClose={this.closeSearch} | |||
onSelect={this.handleSelect} | |||
organizations={this.state.organizations} | |||
projects={this.state.projects} | |||
selected={this.state.selected === component.key} | |||
/> |
@@ -28,12 +28,10 @@ import { getComponentOverviewUrl } from '../../../helpers/urls'; | |||
import { ComponentResult } from './utils'; | |||
interface Props { | |||
appState: Pick<T.AppState, 'organizationsEnabled'>; | |||
component: ComponentResult; | |||
innerRef: (componentKey: string, node: HTMLElement | null) => void; | |||
onClose: () => void; | |||
onSelect: (componentKey: string) => void; | |||
organizations: T.Dict<{ name: string }>; | |||
projects: T.Dict<{ name: string }>; | |||
selected: boolean; | |||
} |
@@ -103,11 +103,7 @@ it('shows warning about short input', () => { | |||
function shallowRender(props: Partial<Search['props']> = {}) { | |||
return shallow<Search>( | |||
// @ts-ignore | |||
<Search | |||
appState={{ organizationsEnabled: false }} | |||
currentUser={{ isLoggedIn: false }} | |||
{...props} | |||
/> | |||
<Search currentUser={{ isLoggedIn: false }} {...props} /> | |||
); | |||
} | |||
@@ -35,8 +35,7 @@ it('renders match', () => { | |||
key: 'foo', | |||
name: 'foo', | |||
match: 'f<mark>o</mark>o', | |||
qualifier: 'TRK', | |||
organization: 'bar' | |||
qualifier: 'TRK' | |||
}; | |||
const wrapper = shallowRender({ component }); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -47,8 +46,7 @@ it('renders favorite', () => { | |||
isFavorite: true, | |||
key: 'foo', | |||
name: 'foo', | |||
qualifier: 'TRK', | |||
organization: 'bar' | |||
qualifier: 'TRK' | |||
}; | |||
const wrapper = shallowRender({ component }); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -59,8 +57,7 @@ it('renders recently browsed', () => { | |||
isRecentlyBrowsed: true, | |||
key: 'foo', | |||
name: 'foo', | |||
qualifier: 'TRK', | |||
organization: 'bar' | |||
qualifier: 'TRK' | |||
}; | |||
const wrapper = shallowRender({ component }); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -96,12 +93,10 @@ it('shows tooltip after delay', () => { | |||
function shallowRender(props: Partial<SearchResult['props']> = {}) { | |||
return shallow( | |||
<SearchResult | |||
appState={{ organizationsEnabled: false }} | |||
component={{ key: 'foo', name: 'foo', qualifier: 'TRK', organization: 'bar' }} | |||
component={{ key: 'foo', name: 'foo', qualifier: 'TRK' }} | |||
innerRef={jest.fn()} | |||
onClose={jest.fn()} | |||
onSelect={jest.fn()} | |||
organizations={{ bar: { name: 'bar' } }} | |||
projects={{ foo: { name: 'foo' } }} | |||
selected={false} | |||
{...props} |
@@ -39,7 +39,6 @@ export interface ComponentResult { | |||
key: string; | |||
match?: string; | |||
name: string; | |||
organization?: string; | |||
project?: string; | |||
qualifier: string; | |||
} |
@@ -48,7 +48,6 @@ import AboutStandards from './AboutStandards'; | |||
import EntryIssueTypes from './EntryIssueTypes'; | |||
interface Props { | |||
appState: Pick<T.AppState, 'defaultOrganization' | 'organizationsEnabled'>; | |||
currentUser: T.CurrentUser; | |||
customText?: string; | |||
fetchAboutPageSettings: () => Promise<void>; |
@@ -23,7 +23,7 @@ import { addWhitePageClass, removeWhitePageClass } from 'sonar-ui-common/helpers | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { searchProjects } from '../../../../api/components'; | |||
import { getFacet } from '../../../../api/issues'; | |||
import { mockAppState, mockCurrentUser, mockLocation } from '../../../../helpers/testMocks'; | |||
import { mockCurrentUser, mockLocation } from '../../../../helpers/testMocks'; | |||
import { AboutApp } from '../AboutApp'; | |||
import EntryIssueTypes from '../EntryIssueTypes'; | |||
@@ -88,7 +88,6 @@ it('should not display issues if the WS return an http error', async () => { | |||
function shallowRender(props: Partial<AboutApp['props']> = {}) { | |||
return shallow( | |||
<AboutApp | |||
appState={mockAppState()} | |||
currentUser={mockCurrentUser()} | |||
customText="Lorem ipsum" | |||
fetchAboutPageSettings={jest.fn().mockResolvedValue('')} |
@@ -19,19 +19,17 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import { connect } from 'react-redux'; | |||
import handleRequiredAuthentication from 'sonar-ui-common/helpers/handleRequiredAuthentication'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { areThereCustomOrganizations, getCurrentUser, Store } from '../../../store/rootReducer'; | |||
import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; | |||
import '../account.css'; | |||
import Nav from './Nav'; | |||
import UserCard from './UserCard'; | |||
interface Props { | |||
currentUser: T.CurrentUser; | |||
customOrganizations?: boolean; | |||
} | |||
export class Account extends React.PureComponent<Props> { | |||
@@ -57,7 +55,7 @@ export class Account extends React.PureComponent<Props> { | |||
<header className="account-header"> | |||
<div className="account-container clearfix"> | |||
<UserCard user={currentUser as T.LoggedInUser} /> | |||
<Nav customOrganizations={this.props.customOrganizations} /> | |||
<Nav /> | |||
</div> | |||
</header> | |||
@@ -67,9 +65,4 @@ export class Account extends React.PureComponent<Props> { | |||
} | |||
} | |||
const mapStateToProps = (state: Store) => ({ | |||
currentUser: getCurrentUser(state), | |||
customOrganizations: areThereCustomOrganizations(state) | |||
}); | |||
export default connect(mapStateToProps)(Account); | |||
export default withCurrentUser(Account); |
@@ -22,11 +22,7 @@ import { IndexLink, Link } from 'react-router'; | |||
import NavBarTabs from 'sonar-ui-common/components/ui/NavBarTabs'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
interface Props { | |||
customOrganizations?: boolean; | |||
} | |||
export default function Nav({ customOrganizations }: Props) { | |||
export default function Nav() { | |||
return ( | |||
<nav className="account-nav"> | |||
<NavBarTabs> | |||
@@ -45,20 +41,11 @@ export default function Nav({ customOrganizations }: Props) { | |||
{translate('my_account.notifications')} | |||
</Link> | |||
</li> | |||
{!customOrganizations && ( | |||
<li> | |||
<Link activeClassName="active" to="/account/projects/"> | |||
{translate('my_account.projects')} | |||
</Link> | |||
</li> | |||
)} | |||
{customOrganizations && ( | |||
<li> | |||
<Link activeClassName="active" to="/account/organizations"> | |||
{translate('my_account.organizations')} | |||
</Link> | |||
</li> | |||
)} | |||
<li> | |||
<Link activeClassName="active" to="/account/projects/"> | |||
{translate('my_account.projects')} | |||
</Link> | |||
</li> | |||
</NavBarTabs> | |||
</nav> | |||
); |
@@ -25,7 +25,6 @@ import ProjectModal from '../ProjectModal'; | |||
jest.mock('../../../../api/components', () => ({ | |||
getSuggestions: jest.fn().mockResolvedValue({ | |||
organizations: [{ key: 'org', name: 'Org' }], | |||
results: [ | |||
{ | |||
q: 'TRK', | |||
@@ -63,7 +62,6 @@ it('should return an empty list when I search non-existent elements', async () = | |||
{ q: 'TRK', items: [], more: 0 }, | |||
{ q: 'UTS', items: [], more: 0 } | |||
], | |||
organizations: [], | |||
projects: [] | |||
}); | |||
@@ -1,58 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; | |||
import OrganizationLink from '../../../components/ui/OrganizationLink'; | |||
interface Props { | |||
organization: T.Organization; | |||
} | |||
export default function OrganizationCard({ organization }: Props) { | |||
const { actions = {} } = organization; | |||
return ( | |||
<div className="account-project-card clearfix"> | |||
<aside className="account-project-side note"> | |||
<strong>{translate('organization.key')}:</strong> {organization.key} | |||
</aside> | |||
<h3 className="account-project-name"> | |||
<OrganizationAvatar organization={organization} /> | |||
<OrganizationLink className="spacer-left text-middle" organization={organization}> | |||
{organization.name} | |||
</OrganizationLink> | |||
{actions.admin && <span className="badge spacer-left">{translate('admin')}</span>} | |||
</h3> | |||
{!!organization.description && ( | |||
<div className="markdown spacer-top">{organization.description}</div> | |||
)} | |||
{!!organization.url && ( | |||
<div className="markdown spacer-top"> | |||
<a href={organization.url} rel="nofollow" title={organization.url}> | |||
{organization.url} | |||
</a> | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -1,45 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import OrganizationCard from './OrganizationCard'; | |||
interface Props { | |||
organizations: T.Organization[]; | |||
} | |||
export default function OrganizationsList({ organizations }: Props) { | |||
if (organizations.length === 0) { | |||
return <div>{translate('my_account.organizations.no_results')}</div>; | |||
} | |||
return ( | |||
<ul className="account-projects-list"> | |||
{sortBy(organizations, organization => organization.name.toLocaleLowerCase()).map( | |||
organization => ( | |||
<li key={organization.key}> | |||
<OrganizationCard organization={organization} /> | |||
</li> | |||
) | |||
)} | |||
</ul> | |||
); | |||
} |
@@ -1,113 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { Helmet } from 'react-helmet-async'; | |||
import { connect } from 'react-redux'; | |||
import { Link } from 'react-router'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { | |||
getAppState, | |||
getGlobalSettingValue, | |||
getMyOrganizations, | |||
Store | |||
} from '../../../store/rootReducer'; | |||
import { fetchIfAnyoneCanCreateOrganizations } from './actions'; | |||
import OrganizationsList from './OrganizationsList'; | |||
interface StateProps { | |||
anyoneCanCreate: boolean; | |||
canAdmin?: boolean; | |||
organizations: T.Organization[]; | |||
} | |||
interface DispatchProps { | |||
fetchIfAnyoneCanCreateOrganizations: () => Promise<void>; | |||
} | |||
interface Props extends StateProps, DispatchProps {} | |||
interface State { | |||
loading: boolean; | |||
} | |||
class UserOrganizations extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.props.fetchIfAnyoneCanCreateOrganizations().then(this.stopLoading, this.stopLoading); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
stopLoading = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
render() { | |||
const { anyoneCanCreate } = this.props; | |||
const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin); | |||
return ( | |||
<div className="account-body account-container"> | |||
<Helmet title={translate('my_account.organizations')} /> | |||
<div className="boxed-group"> | |||
{canCreateOrganizations && ( | |||
<div className="clearfix"> | |||
<div className="boxed-group-actions"> | |||
<Link className="button" to="/create-organization"> | |||
{translate('create')} | |||
</Link> | |||
</div> | |||
</div> | |||
)} | |||
<div className="boxed-group-inner"> | |||
{this.state.loading ? ( | |||
<i className="spinner" /> | |||
) : ( | |||
<OrganizationsList organizations={this.props.organizations} /> | |||
)} | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state: Store): StateProps => { | |||
const anyoneCanCreate = getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'); | |||
return { | |||
anyoneCanCreate: Boolean(anyoneCanCreate && anyoneCanCreate.value === 'true'), | |||
canAdmin: getAppState(state).canAdmin, | |||
organizations: getMyOrganizations(state) | |||
}; | |||
}; | |||
const mapDispatchToProps = { | |||
fetchIfAnyoneCanCreateOrganizations: fetchIfAnyoneCanCreateOrganizations as any | |||
} as DispatchProps; | |||
export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations); |
@@ -1,36 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { Dispatch } from 'redux'; | |||
import { getOrganizations } from '../../../api/organizations'; | |||
import { getValues } from '../../../api/settings'; | |||
import { receiveMyOrganizations } from '../../../store/organizations'; | |||
import { receiveValues } from '../../settings/store/values'; | |||
export const fetchMyOrganizations = () => (dispatch: Dispatch) => { | |||
return getOrganizations({ member: true }).then(({ organizations }) => { | |||
return dispatch(receiveMyOrganizations(organizations)); | |||
}); | |||
}; | |||
export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch) => { | |||
return getValues({ keys: 'sonar.organizations.anyoneCanCreate' }).then(values => { | |||
dispatch(receiveValues(values)); | |||
}); | |||
}; |
@@ -37,10 +37,6 @@ const routes = [ | |||
{ | |||
path: 'notifications', | |||
component: lazyLoadComponent(() => import('./notifications/Notifications')) | |||
}, | |||
{ | |||
path: 'organizations', | |||
component: lazyLoadComponent(() => import('./organizations/UserOrganizations')) | |||
} | |||
] | |||
} |
@@ -95,7 +95,6 @@ function shallowRender(props: Partial<App['props']> = {}) { | |||
breadcrumbs: [], | |||
name: 'foo', | |||
key: 'foo', | |||
organization: 'foo', | |||
qualifier: 'FOO' | |||
}} | |||
fetchBranchStatus={jest.fn()} |
@@ -29,7 +29,6 @@ it('should show the last element without clickable link', () => { | |||
component={{ | |||
key: 'foo', | |||
name: 'Foo', | |||
organization: 'foo', | |||
qualifier: 'TRK' | |||
}} | |||
handleSelect={() => {}} | |||
@@ -47,7 +46,6 @@ it('should correctly show a middle element', () => { | |||
component={{ | |||
key: 'foo', | |||
name: 'Foo', | |||
organization: 'foo', | |||
qualifier: 'TRK' | |||
}} | |||
handleSelect={() => {}} |
@@ -34,14 +34,12 @@ jest.mock('../../../../api/components', () => ({ | |||
const componentFoo = { | |||
key: 'foo', | |||
name: 'Foo', | |||
organization: 'bar', | |||
qualifier: 'TRK' | |||
}; | |||
const componentBar = { | |||
key: 'bar', | |||
name: 'Bar', | |||
organization: 'bar', | |||
qualifier: 'TRK' | |||
}; | |||
@@ -7,7 +7,6 @@ exports[`should display correctly for the list view 1`] = ` | |||
Object { | |||
"key": "bar", | |||
"name": "Bar", | |||
"organization": "bar", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -16,7 +15,6 @@ exports[`should display correctly for the list view 1`] = ` | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "bar", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -29,7 +27,6 @@ Object { | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "bar", | |||
"qualifier": "TRK", | |||
}, | |||
], |
@@ -26,7 +26,6 @@ const COMPONENTS = [ | |||
key: 'foo', | |||
measures: [], | |||
name: 'Foo', | |||
organization: 'foo', | |||
qualifier: 'TRK' | |||
} | |||
]; |
@@ -26,7 +26,6 @@ const COMPONENTS = [ | |||
key: 'foo', | |||
measures: [], | |||
name: 'Foo', | |||
organization: 'foo', | |||
qualifier: 'TRK' | |||
} | |||
]; | |||
@@ -46,7 +45,6 @@ it('should render with best values hidden', () => { | |||
key: 'bar', | |||
measures: [{ bestValue: true, metric: { key: 'coverage' } }], | |||
name: 'Bar', | |||
organization: 'foo', | |||
qualifier: 'TRK' | |||
} | |||
] | |||
@@ -70,7 +68,6 @@ function getWrapper(props = {}) { | |||
key: 'parent', | |||
measures: [], | |||
name: 'Parent', | |||
organization: 'foo', | |||
qualifier: 'TRK' | |||
}} | |||
view="tree" |
@@ -11,7 +11,6 @@ exports[`should renders correctly 1`] = ` | |||
"key": "foo", | |||
"measures": Array [], | |||
"name": "Foo", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -32,7 +31,6 @@ exports[`should renders correctly 1`] = ` | |||
"key": "foo", | |||
"measures": Array [], | |||
"name": "Foo", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -91,7 +89,6 @@ exports[`should renders with multiple measures 1`] = ` | |||
"key": "foo", | |||
"measures": Array [], | |||
"name": "Foo", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -127,7 +124,6 @@ exports[`should renders with multiple measures 1`] = ` | |||
"key": "foo", | |||
"measures": Array [], | |||
"name": "Foo", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
} | |||
} |
@@ -9,7 +9,6 @@ exports[`should render with best values hidden 1`] = ` | |||
"key": "foo", | |||
"measures": Array [], | |||
"name": "Foo", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
}, | |||
] | |||
@@ -38,7 +37,6 @@ exports[`should render with best values hidden 1`] = ` | |||
"key": "parent", | |||
"measures": Array [], | |||
"name": "Parent", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
@@ -72,7 +70,6 @@ exports[`should renders correctly 1`] = ` | |||
"key": "foo", | |||
"measures": Array [], | |||
"name": "Foo", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
}, | |||
] | |||
@@ -101,7 +98,6 @@ exports[`should renders correctly 1`] = ` | |||
"key": "parent", | |||
"measures": Array [], | |||
"name": "Parent", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
} | |||
} |
@@ -29,7 +29,7 @@ jest.mock('../../../../api/permissions', () => ({ | |||
{ | |||
id: '1', | |||
name: 'Default template', | |||
description: 'Default permission template of organization test', | |||
description: 'Default permission template', | |||
createdAt: '2019-02-07T17:23:26+0100', | |||
updatedAt: '2019-02-07T17:23:26+0100', | |||
permissions: [ |
@@ -42,7 +42,7 @@ exports[`should render correctly 2`] = ` | |||
"defaultFor": Array [ | |||
"TRK", | |||
], | |||
"description": "Default permission template of organization test", | |||
"description": "Default permission template", | |||
"id": "1", | |||
"name": "Default template", | |||
"permissions": Array [ |
@@ -45,7 +45,6 @@ interface OwnProps { | |||
onLoadMore: () => void; | |||
onFilter: (filter: string) => void; | |||
onSearch: (query: string) => void; | |||
organization?: T.Organization; | |||
query: string; | |||
revokePermissionFromGroup: (groupName: string, permission: string) => Promise<void>; | |||
revokePermissionFromUser: (login: string, permission: string) => Promise<void>; | |||
@@ -77,7 +76,7 @@ export class AllHoldersList extends React.PureComponent<Props> { | |||
render() { | |||
const { appState, filter, groups, groupsPaging, users, usersPaging } = this.props; | |||
const l10nPrefix = this.props.organization ? 'organizations_permissions' : 'global_permissions'; | |||
const l10nPrefix = 'global_permissions'; | |||
const hasPortfoliosEnabled = appState.qualifiers.includes(ComponentQualifier.Portfolio); | |||
const hasApplicationsEnabled = appState.qualifiers.includes(ComponentQualifier.Application); |
@@ -29,7 +29,6 @@ import { applyTemplateToProject, getPermissionTemplates } from '../../../../api/ | |||
interface Props { | |||
onApply?: () => void; | |||
onClose: () => void; | |||
organization?: string; | |||
project: { key: string; name: string }; | |||
} | |||
@@ -71,7 +70,6 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> { | |||
handleSubmit = () => { | |||
if (this.state.permissionTemplate) { | |||
return applyTemplateToProject({ | |||
organization: this.props.organization, | |||
projectKey: this.props.project.key, | |||
templateId: this.state.permissionTemplate | |||
}).then(() => { |
@@ -89,7 +89,6 @@ export default class PageHeader extends React.PureComponent<Props, State> { | |||
<ApplyTemplate | |||
onApply={this.props.loadHolders} | |||
onClose={this.handleApplyTemplateClose} | |||
organization={component.organization} | |||
project={component} | |||
/> | |||
)} |
@@ -45,7 +45,7 @@ jest.mock('../../../../../api/permissions', () => ({ | |||
it('render correctly', async () => { | |||
const wrapper = shallow( | |||
<ApplyTemplate onClose={jest.fn()} organization="foo" project={{ key: 'foo', name: 'Foo' }} /> | |||
<ApplyTemplate onClose={jest.fn()} project={{ key: 'foo', name: 'Foo' }} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); |
@@ -53,7 +53,7 @@ export default class ChangeDefaultVisibilityForm extends React.PureComponent<Pro | |||
return ( | |||
<Modal contentLabel="modal form" onRequestClose={this.props.onClose}> | |||
<header className="modal-head"> | |||
<h2>{translate('organization.change_visibility_form.header')}</h2> | |||
<h2>{translate('settings.projects.change_visibility_form.header')}</h2> | |||
</header> | |||
<div className="modal-body"> | |||
@@ -74,13 +74,13 @@ export default class ChangeDefaultVisibilityForm extends React.PureComponent<Pro | |||
))} | |||
<Alert variant="warning"> | |||
{translate('organization.change_visibility_form.warning')} | |||
{translate('settings.projects.change_visibility_form.warning')} | |||
</Alert> | |||
</div> | |||
<footer className="modal-foot"> | |||
<Button className="js-confirm" onClick={this.handleConfirmClick}> | |||
{translate('organization.change_visibility_form.submit')} | |||
{translate('settings.projects.change_visibility_form.submit')} | |||
</Button> | |||
<ResetButtonLink className="js-modal-close" onClick={this.props.onClose}> | |||
{translate('cancel')} |
@@ -55,7 +55,7 @@ export default class Header extends React.PureComponent<Props, State> { | |||
<div className="page-actions"> | |||
<span className="big-spacer-right"> | |||
<span className="text-middle"> | |||
{translate('organization.default_visibility_of_new_projects')}{' '} | |||
{translate('settings.projects.default_visibility_of_new_projects')}{' '} | |||
<strong> | |||
{defaultProjectVisibility ? translate('visibility', defaultProjectVisibility) : '—'} | |||
</strong> |
@@ -9,7 +9,7 @@ exports[`changes visibility 1`] = ` | |||
className="modal-head" | |||
> | |||
<h2> | |||
organization.change_visibility_form.header | |||
settings.projects.change_visibility_form.header | |||
</h2> | |||
</header> | |||
<div | |||
@@ -56,7 +56,7 @@ exports[`changes visibility 1`] = ` | |||
<Alert | |||
variant="warning" | |||
> | |||
organization.change_visibility_form.warning | |||
settings.projects.change_visibility_form.warning | |||
</Alert> | |||
</div> | |||
<footer | |||
@@ -66,7 +66,7 @@ exports[`changes visibility 1`] = ` | |||
className="js-confirm" | |||
onClick={[Function]} | |||
> | |||
organization.change_visibility_form.submit | |||
settings.projects.change_visibility_form.submit | |||
</Button> | |||
<ResetButtonLink | |||
className="js-modal-close" | |||
@@ -87,7 +87,7 @@ exports[`changes visibility 2`] = ` | |||
className="modal-head" | |||
> | |||
<h2> | |||
organization.change_visibility_form.header | |||
settings.projects.change_visibility_form.header | |||
</h2> | |||
</header> | |||
<div | |||
@@ -134,7 +134,7 @@ exports[`changes visibility 2`] = ` | |||
<Alert | |||
variant="warning" | |||
> | |||
organization.change_visibility_form.warning | |||
settings.projects.change_visibility_form.warning | |||
</Alert> | |||
</div> | |||
<footer | |||
@@ -144,7 +144,7 @@ exports[`changes visibility 2`] = ` | |||
className="js-confirm" | |||
onClick={[Function]} | |||
> | |||
organization.change_visibility_form.submit | |||
settings.projects.change_visibility_form.submit | |||
</Button> | |||
<ResetButtonLink | |||
className="js-modal-close" |
@@ -26,7 +26,7 @@ exports[`renders: default 1`] = ` | |||
<span | |||
className="text-middle" | |||
> | |||
organization.default_visibility_of_new_projects | |||
settings.projects.default_visibility_of_new_projects | |||
<strong> | |||
visibility.public | |||
@@ -70,7 +70,7 @@ exports[`renders: undefined visibility 1`] = ` | |||
<span | |||
className="text-middle" | |||
> | |||
organization.default_visibility_of_new_projects | |||
settings.projects.default_visibility_of_new_projects | |||
<strong> | |||
— |
@@ -23,6 +23,7 @@ import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getIdentityProviders, searchUsers } from '../../api/users'; | |||
import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | |||
import { withCurrentUser } from '../../components/hoc/withCurrentUser'; | |||
import { Location, Router, withRouter } from '../../components/hoc/withRouter'; | |||
import Header from './Header'; | |||
import Search from './Search'; | |||
@@ -32,7 +33,6 @@ import { parseQuery, Query, serializeQuery } from './utils'; | |||
interface Props { | |||
currentUser: { isLoggedIn: boolean; login?: string }; | |||
location: Pick<Location, 'query'>; | |||
organizationsEnabled?: boolean; | |||
router: Pick<Router, 'push'>; | |||
} | |||
@@ -70,14 +70,11 @@ export class UsersApp extends React.PureComponent<Props, State> { | |||
}; | |||
fetchIdentityProviders = () => | |||
getIdentityProviders().then( | |||
({ identityProviders }) => { | |||
if (this.mounted) { | |||
this.setState({ identityProviders }); | |||
} | |||
}, | |||
() => {} | |||
); | |||
getIdentityProviders().then(({ identityProviders }) => { | |||
if (this.mounted) { | |||
this.setState({ identityProviders }); | |||
} | |||
}); | |||
fetchUsers = ({ location } = this.props) => { | |||
this.setState({ loading: true }); | |||
@@ -127,7 +124,6 @@ export class UsersApp extends React.PureComponent<Props, State> { | |||
currentUser={this.props.currentUser} | |||
identityProviders={this.state.identityProviders} | |||
onUpdateUsers={this.fetchUsers} | |||
organizationsEnabled={this.props.organizationsEnabled} | |||
updateTokensCount={this.updateTokensCount} | |||
users={users} | |||
/> | |||
@@ -144,4 +140,4 @@ export class UsersApp extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
export default withRouter(UsersApp); | |||
export default withRouter(withCurrentUser(UsersApp)); |
@@ -1,29 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { areThereCustomOrganizations, getCurrentUser, Store } from '../../store/rootReducer'; | |||
import UsersApp from './UsersApp'; | |||
const mapStateToProps = (state: Store) => ({ | |||
currentUser: getCurrentUser(state), | |||
organizationsEnabled: areThereCustomOrganizations(state) | |||
}); | |||
export default connect(mapStateToProps)(UsersApp); |
@@ -25,7 +25,6 @@ interface Props { | |||
currentUser: { isLoggedIn: boolean; login?: string }; | |||
identityProviders: T.IdentityProvider[]; | |||
onUpdateUsers: () => void; | |||
organizationsEnabled?: boolean; | |||
updateTokensCount: (login: string, tokensCount: number) => void; | |||
users: T.User[]; | |||
} | |||
@@ -34,7 +33,6 @@ export default function UsersList({ | |||
currentUser, | |||
identityProviders, | |||
onUpdateUsers, | |||
organizationsEnabled, | |||
updateTokensCount, | |||
users | |||
}: Props) { | |||
@@ -47,7 +45,7 @@ export default function UsersList({ | |||
<th className="nowrap" /> | |||
<th className="nowrap">{translate('my_profile.scm_accounts')}</th> | |||
<th className="nowrap">{translate('users.last_connection')}</th> | |||
{!organizationsEnabled && <th className="nowrap">{translate('my_profile.groups')}</th>} | |||
<th className="nowrap">{translate('my_profile.groups')}</th> | |||
<th className="nowrap">{translate('users.tokens')}</th> | |||
<th className="nowrap"> </th> | |||
</tr> | |||
@@ -61,7 +59,6 @@ export default function UsersList({ | |||
isCurrentUser={currentUser.isLoggedIn && currentUser.login === user.login} | |||
key={user.login} | |||
onUpdateUsers={onUpdateUsers} | |||
organizationsEnabled={organizationsEnabled} | |||
updateTokensCount={updateTokensCount} | |||
user={user} | |||
/> |
@@ -81,7 +81,6 @@ function getWrapper(props: Partial<UsersApp['props']> = {}) { | |||
<UsersApp | |||
currentUser={currentUser} | |||
location={location} | |||
organizationsEnabled={true} | |||
router={{ push: jest.fn() }} | |||
{...props} | |||
/> |
@@ -42,13 +42,6 @@ it('should render correctly', () => { | |||
expect(getWrapper()).toMatchSnapshot(); | |||
}); | |||
it('should show a group column', () => { | |||
const wrapper = getWrapper({ organizationsEnabled: false }); | |||
expect(wrapper.find('th').filterWhere(elem => elem.text() === 'my_profile.groups')).toHaveLength( | |||
1 | |||
); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<UsersList | |||
@@ -62,7 +55,6 @@ function getWrapper(props = {}) { | |||
} | |||
]} | |||
onUpdateUsers={jest.fn()} | |||
organizationsEnabled={true} | |||
updateTokensCount={jest.fn()} | |||
users={users} | |||
{...props} |
@@ -34,7 +34,6 @@ exports[`should render correctly 1`] = ` | |||
} | |||
identityProviders={Array []} | |||
onUpdateUsers={[Function]} | |||
organizationsEnabled={true} | |||
updateTokensCount={[Function]} | |||
users={Array []} | |||
/> | |||
@@ -84,7 +83,6 @@ exports[`should render correctly 2`] = ` | |||
] | |||
} | |||
onUpdateUsers={[Function]} | |||
organizationsEnabled={true} | |||
updateTokensCount={[Function]} | |||
users={ | |||
Array [ |
@@ -24,6 +24,11 @@ exports[`should render correctly 1`] = ` | |||
> | |||
users.last_connection | |||
</th> | |||
<th | |||
className="nowrap" | |||
> | |||
my_profile.groups | |||
</th> | |||
<th | |||
className="nowrap" | |||
> | |||
@@ -41,7 +46,6 @@ exports[`should render correctly 1`] = ` | |||
isCurrentUser={true} | |||
key="luke" | |||
onUpdateUsers={[MockFunction]} | |||
organizationsEnabled={true} | |||
updateTokensCount={[MockFunction]} | |||
user={ | |||
Object { | |||
@@ -57,7 +61,6 @@ exports[`should render correctly 1`] = ` | |||
isCurrentUser={false} | |||
key="obi" | |||
onUpdateUsers={[MockFunction]} | |||
organizationsEnabled={true} | |||
updateTokensCount={[MockFunction]} | |||
user={ | |||
Object { |
@@ -33,7 +33,6 @@ interface Props { | |||
identityProvider?: T.IdentityProvider; | |||
isCurrentUser: boolean; | |||
onUpdateUsers: () => void; | |||
organizationsEnabled?: boolean; | |||
updateTokensCount: (login: string, tokensCount: number) => void; | |||
user: T.User; | |||
} | |||
@@ -49,7 +48,7 @@ export default class UserListItem extends React.PureComponent<Props, State> { | |||
handleCloseTokensForm = () => this.setState({ openTokenForm: false }); | |||
render() { | |||
const { identityProvider, onUpdateUsers, organizationsEnabled, user } = this.props; | |||
const { identityProvider, onUpdateUsers, user } = this.props; | |||
return ( | |||
<tr> | |||
@@ -63,11 +62,9 @@ export default class UserListItem extends React.PureComponent<Props, State> { | |||
<td className="thin nowrap text-middle"> | |||
<DateFromNow date={user.lastConnectionDate} hourPrecision={true} /> | |||
</td> | |||
{!organizationsEnabled && ( | |||
<td className="thin nowrap text-middle"> | |||
<UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} /> | |||
</td> | |||
)} | |||
<td className="thin nowrap text-middle"> | |||
<UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} /> | |||
</td> | |||
<td className="thin nowrap text-middle"> | |||
{user.tokensCount} | |||
<ButtonIcon |
@@ -42,14 +42,6 @@ it('should render correctly without last connection date', () => { | |||
expect(shallowRender({})).toMatchSnapshot(); | |||
}); | |||
it('should display a change password button', () => { | |||
expect( | |||
shallowRender({ organizationsEnabled: true }) | |||
.find('UserGroups') | |||
.exists() | |||
).toBe(false); | |||
}); | |||
it('should open the correct forms', () => { | |||
const wrapper = shallowRender(); | |||
click(wrapper.find('.js-user-tokens')); | |||
@@ -61,7 +53,6 @@ function shallowRender(props: Partial<UserListItem['props']> = {}) { | |||
<UserListItem | |||
isCurrentUser={false} | |||
onUpdateUsers={jest.fn()} | |||
organizationsEnabled={false} | |||
updateTokensCount={jest.fn()} | |||
user={user} | |||
{...props} |
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent' | |||
const routes = [ | |||
{ | |||
indexRoute: { component: lazyLoadComponent(() => import('./UsersAppContainer')) } | |||
indexRoute: { component: lazyLoadComponent(() => import('./UsersApp')) } | |||
} | |||
]; | |||
@@ -43,7 +43,7 @@ jest.mock('../../../../api/webhooks', () => ({ | |||
updateWebhook: jest.fn(() => Promise.resolve()) | |||
})); | |||
const component = { key: 'bar', organization: 'foo', qualifier: 'TRK' }; | |||
const component = { key: 'bar', qualifier: 'TRK' }; | |||
beforeEach(() => { | |||
(createWebhook as jest.Mock<any>).mockClear(); |
@@ -20,7 +20,6 @@ Array [ | |||
"componentKey": "foo.java", | |||
"componentLongName": "Foo.java", | |||
"componentName": "foo.java", | |||
"componentOrganization": "default-organization", | |||
"componentPath": "/foo.java", | |||
"componentQualifier": "FIL", | |||
"creationDate": "2016-08-15T15:25:38+0200", | |||
@@ -30,13 +29,11 @@ Array [ | |||
"key": "AWaqVGl3tut9VbnJvk6M", | |||
"line": 62, | |||
"message": "Make sure this file handling is safe here.", | |||
"organization": "default-organization", | |||
"project": "org.sonarsource.java:java", | |||
"projectEnabled": true, | |||
"projectKey": "org.sonarsource.java:java", | |||
"projectLongName": "SonarJava", | |||
"projectName": "SonarJava", | |||
"projectOrganization": "default-organization", | |||
"projectQualifier": "TRK", | |||
"rule": "squid:S4797", | |||
"ruleKey": "squid:S4797", |
@@ -46,13 +46,11 @@ jest.mock('../../../../api/issues', () => ({ | |||
creationDate: '2016-08-15T15:25:38+0200', | |||
updateDate: '2018-10-25T10:23:08+0200', | |||
type: 'SECURITY_HOTSPOT', | |||
organization: 'default-organization', | |||
fromHotspot: true | |||
} | |||
], | |||
components: [ | |||
{ | |||
organization: 'default-organization', | |||
key: 'org.sonarsource.java:java', | |||
enabled: true, | |||
qualifier: 'TRK', | |||
@@ -60,7 +58,6 @@ jest.mock('../../../../api/issues', () => ({ | |||
longName: 'SonarJava' | |||
}, | |||
{ | |||
organization: 'default-organization', | |||
key: 'foo.java', | |||
enabled: true, | |||
qualifier: 'FIL', |
@@ -1,48 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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. | |||
*/ | |||
.navbar-context-avatar { | |||
display: inline-flex; | |||
vertical-align: top; | |||
justify-content: center; | |||
align-items: center; | |||
width: calc(4 * var(--gridSize)); | |||
height: calc(4 * var(--gridSize)); | |||
border: 1px solid var(--barBorderColor); | |||
} | |||
.navbar-context-avatar.no-border { | |||
border: none; | |||
} | |||
.navbar-context-avatar.is-small { | |||
width: calc(2 * var(--gridSize)); | |||
height: calc(2 * var(--gridSize)); | |||
} | |||
.navbar-context-avatar img { | |||
vertical-align: top; | |||
max-width: 100%; | |||
max-height: 100%; | |||
} | |||
.navbar-context-avatar img, | |||
.navbar-context-avatar svg { | |||
transform: none; | |||
} |
@@ -1,66 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import GenericAvatar from 'sonar-ui-common/components/ui/GenericAvatar'; | |||
import './OrganizationAvatar.css'; | |||
interface Props { | |||
className?: string; | |||
organization: Pick<T.OrganizationBase, 'avatar' | 'name'>; | |||
small?: boolean; | |||
} | |||
interface State { | |||
imgLoadError: boolean; | |||
} | |||
export default class OrganizationAvatar extends React.PureComponent<Props, State> { | |||
state = { imgLoadError: false }; | |||
handleImgError = () => { | |||
this.setState({ imgLoadError: true }); | |||
}; | |||
render() { | |||
const { className, organization, small } = this.props; | |||
const { imgLoadError } = this.state; | |||
return ( | |||
<div | |||
className={classNames( | |||
'navbar-context-avatar', | |||
'rounded', | |||
{ 'no-border': !organization.avatar, 'is-small': small }, | |||
className | |||
)}> | |||
{organization.avatar && !imgLoadError ? ( | |||
<img | |||
alt={organization.name} | |||
className="rounded" | |||
onError={this.handleImgError} | |||
src={organization.avatar} | |||
/> | |||
) : ( | |||
<GenericAvatar name={organization.name} size={small ? 15 : 30} /> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -99,7 +99,6 @@ function isSameHomePage(a: T.HomePage, b: T.HomePage) { | |||
return ( | |||
a.type === b.type && | |||
(a as any).branch === (b as any).branch && | |||
(a as any).component === (b as any).component && | |||
(a as any).organization === (b as any).organization | |||
(a as any).component === (b as any).component | |||
); | |||
} |
@@ -20,21 +20,18 @@ exports[`should fetch notifications and render 1`] = ` | |||
Array [ | |||
Object { | |||
"channel": "channel1", | |||
"organization": "org", | |||
"project": "foo", | |||
"projectName": "Foo", | |||
"type": "type-global", | |||
}, | |||
Object { | |||
"channel": "channel1", | |||
"organization": "org", | |||
"project": "bar", | |||
"projectName": "Bar", | |||
"type": "type-common", | |||
}, | |||
Object { | |||
"channel": "channel2", | |||
"organization": "org", | |||
"project": "qux", | |||
"projectName": "Qux", | |||
"type": "type-common", |
@@ -34,22 +34,19 @@ jest.mock('../../../api/notifications', () => ({ | |||
channel: 'channel1', | |||
type: 'type-global', | |||
project: 'foo', | |||
projectName: 'Foo', | |||
organization: 'org' | |||
projectName: 'Foo' | |||
}, | |||
{ | |||
channel: 'channel1', | |||
type: 'type-common', | |||
project: 'bar', | |||
projectName: 'Bar', | |||
organization: 'org' | |||
projectName: 'Bar' | |||
}, | |||
{ | |||
channel: 'channel2', | |||
type: 'type-common', | |||
project: 'qux', | |||
projectName: 'Qux', | |||
organization: 'org' | |||
projectName: 'Qux' | |||
} | |||
], | |||
perProjectTypes: ['type-common'] |
@@ -1,51 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { mockStore } from '../../../helpers/testMocks'; | |||
import { withUserOrganizations } from '../withUserOrganizations'; | |||
jest.mock('../../../api/organizations', () => ({ getOrganizations: jest.fn() })); | |||
class X extends React.Component<{ | |||
fetchMyOrganizations: () => Promise<void>; | |||
userOrganizations: T.Organization[]; | |||
}> { | |||
render() { | |||
return <div />; | |||
} | |||
} | |||
const UnderTest = withUserOrganizations(X); | |||
it('should pass user organizations and logged in user', () => { | |||
const org = { key: 'my-org', name: 'My Organization' }; | |||
const wrapper = shallow(<UnderTest />, { | |||
context: { | |||
store: mockStore({ | |||
organizations: { byKey: { 'my-org': org }, my: ['my-org'] } | |||
}) | |||
}, | |||
disableLifecycleMethods: true | |||
}); | |||
const wrappedComponent = wrapper.dive(); | |||
expect(wrappedComponent.type()).toBe(X); | |||
expect(wrappedComponent.prop('userOrganizations')).toEqual([org]); | |||
}); |
@@ -1,53 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { fetchMyOrganizations } from '../../apps/account/organizations/actions'; | |||
import { getMyOrganizations, Store } from '../../store/rootReducer'; | |||
import { getWrappedDisplayName } from './utils'; | |||
interface OwnProps { | |||
fetchMyOrganizations: () => Promise<void>; | |||
userOrganizations: T.Organization[]; | |||
} | |||
export function withUserOrganizations<P>( | |||
WrappedComponent: React.ComponentType<P & Partial<OwnProps>> | |||
) { | |||
class Wrapper extends React.Component<P & OwnProps> { | |||
static displayName = getWrappedDisplayName(WrappedComponent, 'withUserOrganizations'); | |||
componentDidMount() { | |||
this.props.fetchMyOrganizations(); | |||
} | |||
render() { | |||
return <WrappedComponent {...this.props} />; | |||
} | |||
} | |||
const mapDispatchToProps = { fetchMyOrganizations: fetchMyOrganizations as any }; | |||
function mapStateToProps(state: Store) { | |||
return { userOrganizations: getMyOrganizations(state) }; | |||
} | |||
return connect(mapStateToProps, mapDispatchToProps)(Wrapper); | |||
} |
@@ -22,7 +22,7 @@ import * as React from 'react'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import IssueTags from '../IssueTags'; | |||
const issue = { key: 'issuekey', projectOrganization: 'foo', tags: ['mytag', 'test'] }; | |||
const issue = { key: 'issuekey', tags: ['mytag', 'test'] }; | |||
it('should render without the action when the correct rights are missing', () => { | |||
expect(shallowRender({ canSetTags: false, issue: { ...issue, tags: [] } })).toMatchSnapshot(); |
@@ -1,64 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { areThereCustomOrganizations, getOrganizationByKey, Store } from '../../store/rootReducer'; | |||
import OrganizationLink from '../ui/OrganizationLink'; | |||
interface OwnProps { | |||
organizationKey: string; | |||
} | |||
interface Props { | |||
link?: boolean; | |||
linkClassName?: string; | |||
organization: { key: string; name: string } | null; | |||
shouldBeDisplayed?: boolean; | |||
} | |||
function Organization(props: Props) { | |||
const { link = true, organization, shouldBeDisplayed } = props; | |||
if (!shouldBeDisplayed || !organization) { | |||
return null; | |||
} | |||
return ( | |||
<span> | |||
{link ? ( | |||
<OrganizationLink className={props.linkClassName} organization={organization}> | |||
{organization.name} | |||
</OrganizationLink> | |||
) : ( | |||
organization.name | |||
)} | |||
<span className="slash-separator" /> | |||
</span> | |||
); | |||
} | |||
const mapStateToProps = (state: Store, ownProps: OwnProps) => ({ | |||
organization: getOrganizationByKey(state, ownProps.organizationKey), | |||
shouldBeDisplayed: areThereCustomOrganizations(state) | |||
}); | |||
export default connect(mapStateToProps)(Organization); | |||
export const UnconnectedOrganization = Organization; |
@@ -1,42 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { UnconnectedOrganization } from '../Organization'; | |||
const organization = { key: 'foo', name: 'foo' }; | |||
it('should match snapshot', () => { | |||
expect( | |||
shallow(<UnconnectedOrganization organization={organization} shouldBeDisplayed={true} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should not be displayed', () => { | |||
expect( | |||
shallow( | |||
<UnconnectedOrganization organization={organization} shouldBeDisplayed={false} /> | |||
).type() | |||
).toBeNull(); | |||
expect( | |||
shallow(<UnconnectedOrganization organization={null} shouldBeDisplayed={true} />).type() | |||
).toBeNull(); | |||
}); |
@@ -1,19 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should match snapshot 1`] = ` | |||
<span> | |||
<OrganizationLink | |||
organization={ | |||
Object { | |||
"key": "foo", | |||
"name": "foo", | |||
} | |||
} | |||
> | |||
foo | |||
</OrganizationLink> | |||
<span | |||
className="slash-separator" | |||
/> | |||
</span> | |||
`; |
@@ -1,37 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { Link } from 'react-router'; | |||
interface Props { | |||
children?: React.ReactNode; | |||
organization: { key: string }; | |||
[x: string]: any; | |||
} | |||
export default function OrganizationLink(props: Props) { | |||
const { children, organization, ...other } = props; | |||
return ( | |||
<Link to={`/organizations/${organization.key}`} {...other}> | |||
{children} | |||
</Link> | |||
); | |||
} |
@@ -1,42 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import OrganizationAvatar from '../common/OrganizationAvatar'; | |||
import OrganizationLink from './OrganizationLink'; | |||
interface Props { | |||
organization: T.Organization; | |||
} | |||
export default function OrganizationListItem({ organization }: Props) { | |||
const { actions = {} } = organization; | |||
return ( | |||
<li> | |||
<OrganizationLink className="display-flex-center" organization={organization}> | |||
<div> | |||
<OrganizationAvatar organization={organization} small={true} /> | |||
<span className="spacer-left">{organization.name}</span> | |||
</div> | |||
{actions.admin && <span className="badge spacer-left">{translate('admin')}</span>} | |||
</OrganizationLink> | |||
</li> | |||
); | |||
} |
@@ -1,26 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 OrganizationLink from '../OrganizationLink'; | |||
it('renders', () => { | |||
expect(shallow(<OrganizationLink organization={{ key: 'org' }} />)).toMatchSnapshot(); | |||
}); |
@@ -1,37 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 OrganizationListItem from '../OrganizationListItem'; | |||
it('renders', () => { | |||
expect( | |||
shallow( | |||
<OrganizationListItem | |||
organization={{ | |||
actions: { admin: true }, | |||
key: 'org', | |||
name: 'org', | |||
projectVisibility: 'public' | |||
}} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -1,9 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/org" | |||
/> | |||
`; |
@@ -1,45 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<li> | |||
<OrganizationLink | |||
className="display-flex-center" | |||
organization={ | |||
Object { | |||
"actions": Object { | |||
"admin": true, | |||
}, | |||
"key": "org", | |||
"name": "org", | |||
"projectVisibility": "public", | |||
} | |||
} | |||
> | |||
<div> | |||
<OrganizationAvatar | |||
organization={ | |||
Object { | |||
"actions": Object { | |||
"admin": true, | |||
}, | |||
"key": "org", | |||
"name": "org", | |||
"projectVisibility": "public", | |||
} | |||
} | |||
small={true} | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
org | |||
</span> | |||
</div> | |||
<span | |||
className="badge spacer-left" | |||
> | |||
admin | |||
</span> | |||
</OrganizationLink> | |||
</li> | |||
`; |
@@ -1,56 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { hasPrivateAccess, isCurrentUserMemberOf } from '../organizations'; | |||
const org: T.Organization = { key: 'foo', name: 'Foo', subscription: 'PAID' }; | |||
const adminOrg = { actions: { admin: true }, key: 'bar', name: 'Bar' }; | |||
const randomOrg = { key: 'bar', name: 'Bar' }; | |||
const loggedIn = { | |||
isLoggedIn: true, | |||
login: 'luke', | |||
name: 'Skywalker' | |||
}; | |||
const loggedOut = { isLoggedIn: false }; | |||
describe('isCurrentUserMemberOf', () => { | |||
it('should be a member', () => { | |||
expect(isCurrentUserMemberOf(loggedIn, adminOrg, [])).toBe(true); | |||
expect(isCurrentUserMemberOf(loggedIn, org, [org])).toBe(true); | |||
}); | |||
it('should not be a member', () => { | |||
expect(isCurrentUserMemberOf(loggedIn, undefined, [])).toBe(false); | |||
expect(isCurrentUserMemberOf(loggedIn, org, [])).toBe(false); | |||
expect(isCurrentUserMemberOf(loggedIn, org, [randomOrg])).toBe(false); | |||
expect(isCurrentUserMemberOf(loggedOut, org, [org])).toBe(false); | |||
}); | |||
}); | |||
describe('hasPrivateAccess', () => { | |||
it('should have access', () => { | |||
expect(hasPrivateAccess(loggedIn, randomOrg, [])).toBe(true); | |||
expect(hasPrivateAccess(loggedIn, org, [org])).toBe(true); | |||
}); | |||
it('should not have access', () => { | |||
expect(hasPrivateAccess(loggedIn, org, [])).toBe(false); | |||
}); | |||
}); |
@@ -118,7 +118,7 @@ describe('#getComponentDrilldownUrl', () => { | |||
}); | |||
describe('#getQualityGate(s)Url', () => { | |||
it('should take organization key into account', () => { | |||
it('should work as expected', () => { | |||
expect(getQualityGatesUrl()).toEqual({ pathname: '/quality_gates' }); | |||
expect(getQualityGateUrl('bar')).toEqual({ pathname: '/quality_gates/show/bar' }); | |||
}); |
@@ -1,48 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { isLoggedIn } from './users'; | |||
export function isPaidOrganization(organization: T.Organization | undefined): boolean { | |||
return Boolean(organization && organization.subscription === 'PAID'); | |||
} | |||
export function hasPrivateAccess( | |||
currentUser: T.CurrentUser, | |||
organization: T.Organization | undefined, | |||
userOrganizations: T.Organization[] | |||
): boolean { | |||
return ( | |||
!isPaidOrganization(organization) || | |||
isCurrentUserMemberOf(currentUser, organization, userOrganizations) | |||
); | |||
} | |||
export function isCurrentUserMemberOf( | |||
currentUser: T.CurrentUser, | |||
organization: T.Organization | undefined, | |||
userOrganizations: T.Organization[] | |||
): boolean { | |||
return Boolean( | |||
organization && | |||
isLoggedIn(currentUser) && | |||
((organization.actions && organization.actions.admin) || | |||
userOrganizations.some(org => org.key === organization.key)) | |||
); | |||
} |
@@ -36,21 +36,6 @@ export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T | |||
}; | |||
} | |||
export function mockAlmOrganization(overrides: Partial<T.AlmOrganization> = {}): T.AlmOrganization { | |||
return { | |||
avatar: 'http://example.com/avatar', | |||
almUrl: 'https://github.com/foo', | |||
description: 'description-foo', | |||
key: 'foo', | |||
name: 'foo', | |||
personal: false, | |||
privateRepos: 0, | |||
publicRepos: 3, | |||
url: 'http://example.com/foo', | |||
...overrides | |||
}; | |||
} | |||
export function mockAnalysis(overrides: Partial<T.Analysis> = {}): T.Analysis { | |||
return { | |||
date: '2017-03-01T09:36:01+0100', | |||
@@ -99,7 +84,6 @@ export function mockAnalysisEvent(overrides: Partial<T.AnalysisEvent> = {}): T.A | |||
export function mockAppState(overrides: Partial<T.AppState> = {}): T.AppState { | |||
return { | |||
defaultOrganization: 'foo', | |||
edition: 'community', | |||
productionDatabase: true, | |||
qualifiers: ['TRK'], | |||
@@ -492,33 +476,6 @@ export function mockMeasureEnhanced(overrides: Partial<T.MeasureEnhanced> = {}): | |||
}; | |||
} | |||
export function mockOrganization(overrides: Partial<T.Organization> = {}): T.Organization { | |||
return { key: 'foo', name: 'Foo', ...overrides }; | |||
} | |||
export function mockOrganizationWithAdminActions( | |||
overrides: Partial<T.Organization> = {}, | |||
actionsOverrides: Partial<T.Organization['actions']> = {} | |||
) { | |||
return mockOrganization({ actions: { admin: true, ...actionsOverrides }, ...overrides }); | |||
} | |||
export function mockOrganizationWithAlm( | |||
overrides: Partial<T.Organization> = {}, | |||
almOverrides: Partial<T.Organization['alm']> = {} | |||
): T.Organization { | |||
return mockOrganization({ | |||
alm: { | |||
key: 'github', | |||
membersSync: false, | |||
personal: false, | |||
url: 'https://github.com/foo', | |||
...almOverrides | |||
}, | |||
...overrides | |||
}); | |||
} | |||
export function mockPeriod(overrides: Partial<T.Period> = {}): T.Period { | |||
return { | |||
date: '2019-04-23T02:12:32+0100', |
@@ -255,10 +255,6 @@ export function getCodeUrl( | |||
}; | |||
} | |||
export function getOrganizationUrl(organization: string) { | |||
return `/organizations/${organization}`; | |||
} | |||
export function getHomePageUrl(homepage: T.HomePage) { | |||
switch (homepage.type) { | |||
case 'APPLICATION': | |||
@@ -269,8 +265,6 @@ export function getHomePageUrl(homepage: T.HomePage) { | |||
return homepage.branch | |||
? getBranchUrl(homepage.component, homepage.branch) | |||
: getProjectUrl(homepage.component); | |||
case 'ORGANIZATION': | |||
return getOrganizationUrl(homepage.organization); | |||
case 'PORTFOLIO': | |||
return getPortfolioUrl(homepage.component); | |||
case 'PORTFOLIOS': |
@@ -1,101 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`Reducer should create organization 1`] = ` | |||
Object { | |||
"byKey": Object { | |||
"foo": Object { | |||
"actions": Object { | |||
"admin": true, | |||
}, | |||
"key": "foo", | |||
"name": "foo", | |||
}, | |||
}, | |||
"my": Array [ | |||
"foo", | |||
], | |||
} | |||
`; | |||
exports[`Reducer should delete organization 1`] = ` | |||
Object { | |||
"byKey": Object {}, | |||
"my": Array [], | |||
} | |||
`; | |||
exports[`Reducer should have initial state 1`] = ` | |||
Object { | |||
"byKey": Object {}, | |||
"my": Array [], | |||
} | |||
`; | |||
exports[`Reducer should receive my organizations 1`] = ` | |||
Object { | |||
"byKey": Object { | |||
"bar": Object { | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
"foo": Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
}, | |||
"my": Array [ | |||
"foo", | |||
"bar", | |||
], | |||
} | |||
`; | |||
exports[`Reducer should receive organizations 1`] = ` | |||
Object { | |||
"byKey": Object { | |||
"bar": Object { | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
"foo": Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
}, | |||
"my": Array [], | |||
} | |||
`; | |||
exports[`Reducer should receive organizations 2`] = ` | |||
Object { | |||
"byKey": Object { | |||
"bar": Object { | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
"foo": Object { | |||
"key": "foo", | |||
"name": "Qwe", | |||
}, | |||
}, | |||
"my": Array [], | |||
} | |||
`; | |||
exports[`Reducer should update organization 1`] = ` | |||
Object { | |||
"byKey": Object { | |||
"foo": Object { | |||
"actions": Object { | |||
"admin": true, | |||
}, | |||
"description": "description", | |||
"key": "foo", | |||
"name": "bar", | |||
}, | |||
}, | |||
"my": Array [ | |||
"foo", | |||
], | |||
} | |||
`; |
@@ -1,109 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 reducer, { | |||
areThereCustomOrganizations, | |||
createOrganization, | |||
deleteOrganization, | |||
getMyOrganizations, | |||
getOrganizationByKey, | |||
receiveMyOrganizations, | |||
receiveOrganizations, | |||
State, | |||
updateOrganization | |||
} from '../organizations'; | |||
const state0: State = { byKey: {}, my: [] }; | |||
describe('Reducer', () => { | |||
it('should have initial state', () => { | |||
// @ts-ignore `undefined` is passed when the redux store is created, | |||
// however should not be allowed by typings | |||
expect(reducer(undefined, {})).toMatchSnapshot(); | |||
}); | |||
it('should receive organizations', () => { | |||
const state1 = reducer( | |||
state0, | |||
receiveOrganizations([ | |||
{ key: 'foo', name: 'Foo' }, | |||
{ key: 'bar', name: 'Bar' } | |||
]) | |||
); | |||
expect(state1).toMatchSnapshot(); | |||
const state2 = reducer(state1, receiveOrganizations([{ key: 'foo', name: 'Qwe' }])); | |||
expect(state2).toMatchSnapshot(); | |||
}); | |||
it('should receive my organizations', () => { | |||
const state1 = reducer( | |||
state0, | |||
receiveMyOrganizations([ | |||
{ key: 'foo', name: 'Foo' }, | |||
{ key: 'bar', name: 'Bar' } | |||
]) | |||
); | |||
expect(state1).toMatchSnapshot(); | |||
}); | |||
it('should create organization', () => { | |||
const state1 = reducer(state0, createOrganization({ key: 'foo', name: 'foo' })); | |||
expect(state1).toMatchSnapshot(); | |||
}); | |||
it('should update organization', () => { | |||
const state1 = reducer(state0, createOrganization({ key: 'foo', name: 'foo' })); | |||
const state2 = reducer( | |||
state1, | |||
updateOrganization('foo', { name: 'bar', description: 'description' }) | |||
); | |||
expect(state2).toMatchSnapshot(); | |||
}); | |||
it('should delete organization', () => { | |||
const state1 = reducer(state0, createOrganization({ key: 'foo', name: 'foo' })); | |||
const state2 = reducer(state1, deleteOrganization('foo')); | |||
expect(state2).toMatchSnapshot(); | |||
}); | |||
}); | |||
describe('Selectors', () => { | |||
it('getOrganizationByKey', () => { | |||
const foo = { key: 'foo', name: 'Foo' }; | |||
const state = { ...state0, byKey: { foo } }; | |||
expect(getOrganizationByKey(state, 'foo')).toBe(foo); | |||
expect(getOrganizationByKey(state, 'bar')).toBeUndefined(); | |||
}); | |||
it('getMyOrganizations', () => { | |||
expect(getMyOrganizations(state0)).toEqual([]); | |||
const foo = { key: 'foo', name: 'Foo' }; | |||
expect(getMyOrganizations({ ...state0, byKey: { foo }, my: ['foo'] })).toEqual([foo]); | |||
}); | |||
it('areThereCustomOrganizations', () => { | |||
const foo = { key: 'foo', name: 'Foo' }; | |||
const bar = { key: 'bar', name: 'Bar' }; | |||
expect(areThereCustomOrganizations({ ...state0, byKey: {} })).toBe(false); | |||
expect(areThereCustomOrganizations({ ...state0, byKey: { foo } })).toBe(false); | |||
expect(areThereCustomOrganizations({ ...state0, byKey: { foo, bar } })).toBe(true); | |||
}); | |||
}); |
@@ -45,9 +45,7 @@ export function requireAuthorization() { | |||
const defaultValue: T.AppState = { | |||
authenticationError: false, | |||
authorizationError: false, | |||
defaultOrganization: '', | |||
edition: undefined, | |||
organizationsEnabled: false, | |||
productionDatabase: true, | |||
qualifiers: [], | |||
settings: {}, |
@@ -1,121 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { omit, uniq, without } from 'lodash'; | |||
import { combineReducers } from 'redux'; | |||
import { ActionType } from './utils/actions'; | |||
type ReceiveOrganizationsAction = | |||
| ActionType<typeof receiveOrganizations, 'RECEIVE_ORGANIZATIONS'> | |||
| ActionType<typeof receiveMyOrganizations, 'RECEIVE_MY_ORGANIZATIONS'>; | |||
type Action = | |||
| ReceiveOrganizationsAction | |||
| ActionType<typeof createOrganization, 'CREATE_ORGANIZATION'> | |||
| ActionType<typeof updateOrganization, 'UPDATE_ORGANIZATION'> | |||
| ActionType<typeof deleteOrganization, 'DELETE_ORGANIZATION'>; | |||
export interface State { | |||
byKey: T.Dict<T.Organization>; | |||
my: string[]; | |||
} | |||
export function receiveOrganizations(organizations: T.Organization[]) { | |||
return { type: 'RECEIVE_ORGANIZATIONS', organizations }; | |||
} | |||
export function receiveMyOrganizations(organizations: T.Organization[]) { | |||
return { type: 'RECEIVE_MY_ORGANIZATIONS', organizations }; | |||
} | |||
export function createOrganization(organization: T.Organization) { | |||
return { type: 'CREATE_ORGANIZATION', organization }; | |||
} | |||
export function updateOrganization(key: string, changes: T.OrganizationBase) { | |||
return { type: 'UPDATE_ORGANIZATION', key, changes }; | |||
} | |||
export function deleteOrganization(key: string) { | |||
return { type: 'DELETE_ORGANIZATION', key }; | |||
} | |||
function onReceiveOrganizations(state: State['byKey'], action: ReceiveOrganizationsAction) { | |||
const nextState = { ...state }; | |||
action.organizations.forEach(organization => { | |||
nextState[organization.key] = { ...state[organization.key], ...organization }; | |||
}); | |||
return nextState; | |||
} | |||
function byKey(state: State['byKey'] = {}, action: Action): State['byKey'] { | |||
switch (action.type) { | |||
case 'RECEIVE_ORGANIZATIONS': | |||
case 'RECEIVE_MY_ORGANIZATIONS': | |||
return onReceiveOrganizations(state, action); | |||
case 'CREATE_ORGANIZATION': | |||
return { | |||
...state, | |||
[action.organization.key]: { | |||
...action.organization, | |||
actions: { ...(action.organization.actions || {}), admin: true } | |||
} | |||
}; | |||
case 'UPDATE_ORGANIZATION': | |||
return { | |||
...state, | |||
[action.key]: { | |||
...state[action.key], | |||
key: action.key, | |||
...action.changes | |||
} | |||
}; | |||
case 'DELETE_ORGANIZATION': | |||
return omit(state, action.key); | |||
default: | |||
return state; | |||
} | |||
} | |||
function my(state: State['my'] = [], action: Action): State['my'] { | |||
switch (action.type) { | |||
case 'RECEIVE_MY_ORGANIZATIONS': | |||
return uniq([...state, ...action.organizations.map(o => o.key)]); | |||
case 'CREATE_ORGANIZATION': | |||
return uniq([...state, action.organization.key]); | |||
case 'DELETE_ORGANIZATION': | |||
return without(state, action.key); | |||
default: | |||
return state; | |||
} | |||
} | |||
export default combineReducers({ byKey, my }); | |||
export function getOrganizationByKey(state: State, key: string) { | |||
return state.byKey[key]; | |||
} | |||
export function getMyOrganizations(state: State) { | |||
return state.my.map(key => getOrganizationByKey(state, key)); | |||
} | |||
export function areThereCustomOrganizations(state: State) { | |||
return Object.keys(state.byKey).length > 1; | |||
} |
@@ -22,7 +22,6 @@ import { Dispatch } from 'redux'; | |||
import * as auth from '../api/auth'; | |||
import { getLanguages } from '../api/languages'; | |||
import { getAllMetrics } from '../api/metrics'; | |||
import { getOrganization } from '../api/organizations'; | |||
import { getQualityGateProjectStatus } from '../api/quality-gates'; | |||
import { getBranchLikeQuery } from '../helpers/branch-like'; | |||
import { extractStatusConditionsFromProjectStatus } from '../helpers/qualityGates'; | |||
@@ -32,7 +31,6 @@ import { registerBranchStatusAction } from './branches'; | |||
import { addGlobalErrorMessage } from './globalMessages'; | |||
import { receiveLanguages } from './languages'; | |||
import { receiveMetrics } from './metrics'; | |||
import { receiveOrganizations } from './organizations'; | |||
export function fetchLanguages() { | |||
return (dispatch: Dispatch) => { | |||
@@ -56,13 +54,6 @@ export function fetchMetrics() { | |||
}; | |||
} | |||
export const fetchOrganization = (key: string) => async (dispatch: Dispatch) => { | |||
const organization = await getOrganization(key); | |||
if (organization) { | |||
dispatch(receiveOrganizations([organization])); | |||
} | |||
}; | |||
export function fetchBranchStatus(branchLike: BranchLike, projectKey: string) { | |||
return (dispatch: Dispatch<any>) => { | |||
getQualityGateProjectStatus({ projectKey, ...getBranchLikeQuery(branchLike) }).then( |
@@ -25,7 +25,6 @@ import branches, * as fromBranches from './branches'; | |||
import globalMessages, * as fromGlobalMessages from './globalMessages'; | |||
import languages, * as fromLanguages from './languages'; | |||
import metrics, * as fromMetrics from './metrics'; | |||
import organizations, * as fromOrganizations from './organizations'; | |||
import users, * as fromUsers from './users'; | |||
export type Store = { | |||
@@ -34,7 +33,6 @@ export type Store = { | |||
globalMessages: fromGlobalMessages.State; | |||
languages: T.Languages; | |||
metrics: fromMetrics.State; | |||
organizations: fromOrganizations.State; | |||
users: fromUsers.State; | |||
// apps | |||
@@ -47,7 +45,6 @@ export default combineReducers<Store>({ | |||
globalMessages, | |||
languages, | |||
metrics, | |||
organizations, | |||
users, | |||
// apps | |||
@@ -86,18 +83,6 @@ export function getMetricByKey(state: Store, key: string) { | |||
return fromMetrics.getMetricByKey(state.metrics, key); | |||
} | |||
export function getOrganizationByKey(state: Store, key: string) { | |||
return fromOrganizations.getOrganizationByKey(state.organizations, key); | |||
} | |||
export function getMyOrganizations(state: Store) { | |||
return fromOrganizations.getMyOrganizations(state.organizations); | |||
} | |||
export function areThereCustomOrganizations(state: Store) { | |||
return getAppState(state).organizationsEnabled; | |||
} | |||
export function getGlobalSettingValue(state: Store, key: string) { | |||
return fromSettingsApp.getValue(state.settingsApp, key); | |||
} |
@@ -30,14 +30,6 @@ declare namespace T { | |||
installationUrl: string; | |||
} | |||
export interface AlmOrganization extends OrganizationBase { | |||
almUrl: string; | |||
key: string; | |||
personal: boolean; | |||
privateRepos: number; | |||
publicRepos: number; | |||
} | |||
export interface AlmRepository { | |||
label: string; | |||
installationKey: string; | |||
@@ -96,12 +88,10 @@ declare namespace T { | |||
authorizationError?: boolean; | |||
branchesEnabled?: boolean; | |||
canAdmin?: boolean; | |||
defaultOrganization: string; | |||
edition: 'community' | 'developer' | 'enterprise' | 'datacenter' | undefined; | |||
globalPages?: Extension[]; | |||
multipleAlmEnabled?: boolean; | |||
needIssueSync?: boolean; | |||
organizationsEnabled?: boolean; | |||
productionDatabase: boolean; | |||
qualifiers: string[]; | |||
settings: T.Dict<string>; | |||
@@ -167,7 +157,6 @@ declare namespace T { | |||
key: string; | |||
match?: string; | |||
name: string; | |||
organization?: string; | |||
path?: string; | |||
project?: string; | |||
qualifier: string; | |||
@@ -295,7 +284,6 @@ declare namespace T { | |||
| { type: 'ISSUES' } | |||
| { type: 'MY_ISSUES' } | |||
| { type: 'MY_PROJECTS' } | |||
| { type: 'ORGANIZATION'; organization: string } | |||
| { type: 'PORTFOLIO'; component: string } | |||
| { type: 'PORTFOLIOS' } | |||
| { type: 'PROJECT'; branch: string | undefined; component: string } | |||
@@ -306,7 +294,6 @@ declare namespace T { | |||
| 'ISSUES' | |||
| 'MY_ISSUES' | |||
| 'MY_PROJECTS' | |||
| 'ORGANIZATION' | |||
| 'PORTFOLIO' | |||
| 'PORTFOLIOS' | |||
| 'PROJECT' | |||
@@ -403,7 +390,6 @@ declare namespace T { | |||
export interface LightComponent { | |||
key: string; | |||
organization?: string; | |||
qualifier: string; | |||
} | |||
@@ -434,7 +420,6 @@ declare namespace T { | |||
homepage?: HomePage; | |||
isLoggedIn: true; | |||
local?: boolean; | |||
personalOrganization?: string; | |||
scmAccounts: string[]; | |||
settings?: CurrentUserSetting[]; | |||
} | |||
@@ -518,39 +503,6 @@ declare namespace T { | |||
projectName: string; | |||
} | |||
export interface OrganizationActions { | |||
admin?: boolean; | |||
delete?: boolean; | |||
provision?: boolean; | |||
executeAnalysis?: boolean; | |||
} | |||
export interface Organization extends OrganizationBase { | |||
actions?: OrganizationActions; | |||
alm?: { key: string; membersSync: boolean; personal: boolean; url: string }; | |||
adminPages?: Extension[]; | |||
canUpdateProjectsVisibilityToPrivate?: boolean; | |||
isDefault?: boolean; | |||
key: string; | |||
pages?: Extension[]; | |||
projectVisibility?: Visibility; | |||
subscription?: OrganizationSubscription; | |||
} | |||
export interface OrganizationBase { | |||
avatar?: string; | |||
description?: string; | |||
key?: string; | |||
name: string; | |||
url?: string; | |||
} | |||
export interface OrganizationMember extends UserActive { | |||
groupCount?: number; | |||
} | |||
export type OrganizationSubscription = 'FREE' | 'PAID' | 'SONARQUBE'; | |||
export interface Paging { | |||
pageIndex: number; | |||
pageSize: number; |
@@ -132,9 +132,6 @@ not_now=Not now | |||
off=Off | |||
on=On | |||
or=Or | |||
organization_key=Organization Key | |||
organization.bitbucket=Bitbucket team | |||
organization.github=GitHub organization | |||
open=Open | |||
optional=Optional | |||
order=Order | |||
@@ -262,7 +259,6 @@ logging_out=You're logging out, please wait... | |||
manage=Manage | |||
management=Management | |||
more_information=More information | |||
my_organizations=My Organizations | |||
new_violations=New violations | |||
new_window=New window | |||
no_data=No data | |||
@@ -1060,6 +1056,11 @@ settings.new_code_period.description2=This setting is the default for all projec | |||
settings.languages.select_a_language_placeholder=Select a language | |||
settings.projects.default_visibility_of_new_projects=Default visibility of new projects: | |||
settings.projects.change_visibility_form.header=Set Default Visibility of New Projects | |||
settings.projects.change_visibility_form.warning=This will not change the visibility of already existing projects. | |||
settings.projects.change_visibility_form.submit=Change Default Visibility | |||
settings.almintegration.title=Integration configurations | |||
settings.almintegration.description=ALM integrations allow SonarQube to interact with your ALM. This enables things like authentication, or providing analysis details and a Quality Gate to your Pull Requests directly in your ALM provider's interface. | |||
settings.almintegration.azure.info=Accounts that will be used to decorate Pull Requests need Code: Read & Write permission. {link} | |||
@@ -1853,10 +1854,6 @@ my_account.projects.description=Those projects are the ones you are administerin | |||
my_account.projects.no_results=You are not administering any project yet. | |||
my_account.projects.analyzed_x=Analyzed {0} | |||
my_account.projects.never_analyzed=Never analyzed | |||
my_account.organizations=Organizations | |||
my_account.organizations.description=Those organizations are the ones you are member of. | |||
my_account.organizations.no_results=You are not a member of any organizations yet. | |||
my_account.create_organization=Create Organization | |||
my_account.search_project=Search Project | |||
my_account.set_notifications_for=Search a project by name | |||
my_account.set_notifications_for.title=Add a project | |||
@@ -2413,9 +2410,7 @@ global_permissions.groups=Groups | |||
global_permissions.administer=Administer | |||
global_permissions.creator=Create | |||
global_permissions.admin=Administer System | |||
global_permissions.admin.sonarcloud=Administer Organization | |||
global_permissions.admin.desc=Ability to perform all administration functions for the instance. | |||
global_permissions.admin.desc.sonarcloud=Ability to perform all administration functions for the organization. | |||
global_permissions.profileadmin=Quality Profiles | |||
global_permissions.profileadmin.desc=Ability to perform any action on Quality Profiles. | |||
global_permissions.gateadmin=Quality Gates | |||
@@ -2432,23 +2427,6 @@ global_permissions.applicationcreator.desc=Ability to create an application. | |||
global_permissions.portfoliocreator=Portfolios | |||
global_permissions.portfoliocreator.desc=Ability to create a portfolio. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# ORGANIZATIONS PERMISSIONS | |||
# | |||
#------------------------------------------------------------------------------ | |||
organizations_permissions.admin=Administer Organization | |||
organizations_permissions.admin.desc=Ability to perform all administration functions for the organization. | |||
organizations_permissions.profileadmin=Administer Quality Profiles | |||
organizations_permissions.profileadmin.desc=Ability to perform any action on Quality Profiles. | |||
organizations_permissions.gateadmin=Administer Quality Gates | |||
organizations_permissions.gateadmin.desc=Ability to perform any action on quality gates. | |||
organizations_permissions.scan=Execute Analysis | |||
organizations_permissions.scan.desc=Ability to get all settings required to perform an analysis (including the secured settings like passwords) and to push analysis results to the {instance} server. | |||
organizations_permissions.provisioning=Create Projects | |||
organizations_permissions.provisioning.desc=Ability to initialize a project so its settings can be configured before the first analysis. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# PROJECTS PERMISSIONS | |||
@@ -3104,75 +3082,6 @@ about_page.scanners.gradle=SonarQube Scanner for Gradle | |||
about_page.scanners.jenkins=SonarQube Scanner for Jenkins | |||
about_page.scanners.ant=SonarQube Scanner for Ant | |||
#------------------------------------------------------------------------------ | |||
# | |||
# ORGANIZATIONS | |||
# | |||
#------------------------------------------------------------------------------ | |||
organization.avatar=Avatar | |||
organization.avatar.description=Url of a small image that represents the organization (preferably 30px height). | |||
organization.avatar.preview=Preview | |||
organization.bind_to_x=Bind this organization to {0} | |||
organization.go_to_settings_to_bind=Go to Organization Settings to bind it | |||
organization.bound=This organization is bound. | |||
organization.bound_to_x=This organization is bound to {0} | |||
organization.not_bound_to_x=This organization is not bound to {0} | |||
organization.created=Organization "{0}" has been created. | |||
organization.delete=Delete Organization | |||
organization.delete_x=Delete the "{0}" organization | |||
organization.delete.description=Delete this organization from {instance}. All projects belonging to the organization will be deleted as well. The operation cannot be undone. | |||
organization.delete.sonarcloud.paid_plan_info=Your current paid plan subscription will stop and you won't be charged anymore. | |||
organization.delete.question=Are you sure you want to delete this organization? | |||
organization.deleted=Organization has been deleted. | |||
organization.deleted_x=Organization "{0}" has been deleted. | |||
organization.description=Description | |||
organization.description.description=Description of the organization. | |||
organization.details=Organization details | |||
organization.key=Key | |||
organization.key.description=Key of the organization (up to 255 characters). All chars must be lower-case letters (a to z), digits or dash (but dash can neither be trailing nor heading). When not specified, the key is computed from the name. | |||
organization.name=Name | |||
organization.name.description=Name of the organization (up to 255 characters). | |||
organization.see_on_x=See on {0} | |||
organization.settings=Organization settings | |||
organization.updated=Organization details have been updated. | |||
organization.url=Url | |||
organization.url.description=Url of the homepage of the organization. | |||
organization.binding_with_x_easy_sync=Binding an organization from SonarCloud with {0} is an easy way to keep them synchronized. | |||
organization.app_will_be_installed_on_x=To bind this organization to {0}, the SonarCloud application will be installed. | |||
organization.members.page=Members | |||
organization.members.page.description=Add users to the organization and grant them permissions to work on the projects. See {link} documentation. | |||
organization.members.add=Add a member | |||
organization.members.add.multiple=Add members | |||
organization.members.x_groups={0} group(s) | |||
organization.members.members=member(s) | |||
organization.members.remove=Remove from organization's members | |||
organization.members.remove_x=Are you sure you want to remove {0} from {1}'s members ? | |||
organization.members.manage_groups=Manage groups | |||
organization.members.members_groups={0}'s groups: | |||
organization.members.manage_a_team=Manage a team | |||
organization.members.add_to_members=Add to members | |||
organization.members.config_synchro=Configure Synchronization | |||
organization.members.auto_sync_with_x=Automatic sync with {0} | |||
organization.members.auto_sync_members_from_org_x=Now your members can be automatically synchronized with your {0}. | |||
organization.members.auto_sync_total_help.github=You might not see all members from your GitHub organization yet, as they need to connect to SonarCloud at least once to appear in this list. | |||
organization.members.see_all_members_on_x=See all members on {0} | |||
organization.members.management.title=Members Management | |||
organization.members.management.description=Select your management mode for members of this organization. | |||
organization.members.management.manual=Manual | |||
organization.members.management.manual.add_members_manually=Admin add members manually from SonarCloud existing users | |||
organization.members.management.automatic=Automatic sync with {0} | |||
organization.members.management.automatic.synchronized_from_x=Members are synchronized automatically from your {0} | |||
organization.members.management.automatic.members_changes_reflected.github=If you add or remove a member on GitHub, SonarCloud immediately reflects the changes | |||
organization.members.management.automatic.warning_x=This will override your current Members, removing those that are not part of your {0}. | |||
organization.members.management.choose_members_permissions=Admin manages permissions for each member in SonarCloud | |||
organization.paid_plan.badge=Paid plan | |||
organization.default_visibility_of_new_projects=Default visibility of new projects: | |||
organization.change_visibility_form.header=Set Default Visibility of New Projects | |||
organization.change_visibility_form.warning=This will not change the visibility of already existing projects. | |||
organization.change_visibility_form.submit=Change Default Visibility | |||
organization.bind.success=Organization bound successfully | |||
#------------------------------------------------------------------------------ | |||
# | |||
# EMBEDED DOCS | |||
@@ -3319,57 +3228,6 @@ onboarding.create_project.gitlab.link=See on GitLab | |||
onboarding.create_project.gitlab.search_prompt=Search for projects | |||
onboarding.create_project.gitlab.set_up=Set up | |||
onboarding.create_organization.page.header=Create Organization | |||
onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects. | |||
onboarding.create_organization.organization_name=Key | |||
onboarding.create_organization.organization_name.description=Up to 255 characters. All chars must be lower-case letters (a to z), digits or dash (but dash can neither be trailing nor heading). The display name can be specified in the additional info. | |||
onboarding.create_organization.organization_name.error=The provided value doesn't match the expected format. | |||
onboarding.create_organization.organization_name.taken=This name is already taken. | |||
onboarding.create_organization.add_additional_info=Add additional info | |||
onboarding.create_organization.hide_additional_info=Hide additional info | |||
onboarding.create_organization.description=Description | |||
onboarding.create_organization.description.error=The provided value doesn't match the expected format. | |||
onboarding.create_organization.display_name=Display Name | |||
onboarding.create_organization.display_name.description=Up to 255 characters | |||
onboarding.create_organization.display_name.error=The provided value doesn't match the expected format. | |||
onboarding.create_organization.avatar=Avatar | |||
onboarding.create_organization.avatar.description=Url of a small image that represents the organization (preferably 30px height). | |||
onboarding.create_organization.avatar.error=The value must be a valid url. | |||
onboarding.create_organization.avatar.placeholder=Default avatar | |||
onboarding.create_organization.url=URL | |||
onboarding.create_organization.url.error=The value must be a valid url. | |||
onboarding.create_organization.enter_org_details=Enter your organization details | |||
onboarding.create_organization.create_manually=Create manually | |||
onboarding.create_organization.enter_payment_details=Enter payment details | |||
onboarding.create_organization.choose_plan=Choose a plan | |||
onboarding.create_organization.enter_your_coupon=Enter your coupon | |||
onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade | |||
onboarding.create_organization.ready=All set! Your organization is now ready to go | |||
onboarding.import_organization.bind=Bind Organization | |||
onboarding.import_organization.choose_unbound_installation_x=Choose one of your {0} that already have the SonarCloud application installed: | |||
onboarding.import_organization.import=Import Organization | |||
onboarding.import_organization.import_org_details=Import organization details | |||
onboarding.import_organization.org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue: | |||
onboarding.import_organization.org_not_found.tips_1=You must be an administrator of the organization | |||
onboarding.import_organization.org_not_found.tips_2=Try to uninstall and re-install the SonarCloud App (using the button bellow) | |||
onboarding.import_organization.choose_organization=Choose an organization... | |||
onboarding.import_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket | |||
onboarding.import_organization.choose_organization_button.github=Choose an organization on GitHub | |||
onboarding.import_organization.choose_the_organization_button.bitbucket=Choose the team on Bitbucket | |||
onboarding.import_organization.choose_the_organization_button.github=Choose the organization on GitHub | |||
onboarding.import_organization.installing=Finalize installation of the {0} application... | |||
onboarding.import_organization.personal.import_org_details=Import personal organization details | |||
onboarding.import_organization.private.disabled=Selecting private repository is not available yet and will come soon. Meanwhile, you need to create the project manually. | |||
onboarding.import_organization.import_from_x=Import from {0} | |||
onboarding.import_organization.bind_existing=Bind to an existing SonarCloud organization | |||
onboarding.import_organization.create_new=Create new SonarCloud organization from it | |||
onboarding.import_organization.already_bound_x=Your organization {avatar} {name} is already bound to the SonarCloud organization {boundAvatar} {boundName}. Try again and choose a different organization. | |||
onboarding.import_organization.members_sync_info_x=All members from your {0} {1} will be added to your SonarCloud organization. As they connect to SonarCloud with their {2} account, members will automatically have access to your SonarCloud organization and its projects. | |||
onboarding.import_organization.bind_members_not_sync_info_x=We'll keep your members, groups and permissions as they are today on SonarCloud. To sync your members with your {0}, enable members sync in your Members tab. | |||
onboarding.import_organization_x=Import {avatar} {name} into a SonarCloud organization | |||
onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName} | |||
onboarding.binding_organization=Binding organization | |||
onboarding.token.header=Provide a token | |||
onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point of time in your {link}. | |||
onboarding.token.text.user_account=user account |