aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-06-29 16:33:30 +0200
committerSonarTech <sonartech@sonarsource.com>2018-07-11 20:21:21 +0200
commit477cfbd296c49853604563e24fc2c9987b4f751e (patch)
tree02346fa12e2f500fd0bf8b0e712e99a242d61609
parent65950a3720eab9c67e2d99ca6e7973de13a54bcb (diff)
downloadsonarqube-477cfbd296c49853604563e24fc2c9987b4f751e.tar.gz
sonarqube-477cfbd296c49853604563e24fc2c9987b4f751e.zip
SONAR-10945 QP and QG pages should only be visible only to members of paid organizations
* SONAR-10968 Warn user about project privacy after billing upgrade * SONAR-10949 Show QG, QP, members and rules only when the user has correct access * SONAR-10959 Do not display Rules, QP and QG pages for non members of paid organizations * SONAR-10961 Do not display Members page for non member of private organizations * Remove rule permalink in issues page for non members of paid orgs * Do not display QP, QG on project overview page
-rw-r--r--server/sonar-docs/src/pages/organizations/organization-and-project-privacy.md6
-rw-r--r--server/sonar-web/src/main/js/api/organizations.ts30
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/types.ts26
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/actions.js2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/MembersList.js4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/OrganizationAdminContainer.tsx)53
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroupCheckbox.js4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js)68
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAccessContainer-test.tsx88
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAdminContainer-test.tsx60
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAccessContainer-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAdminContainer-test.tsx.snap)2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMenu-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenu-test.tsx.snap136
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/routes.ts52
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx16
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts84
-rw-r--r--server/sonar-web/src/main/js/helpers/organizations.ts56
-rw-r--r--server/sonar-web/src/main/js/store/organizations/duck.ts (renamed from server/sonar-web/src/main/js/store/organizations/duck.js)178
40 files changed, 704 insertions, 465 deletions
diff --git a/server/sonar-docs/src/pages/organizations/organization-and-project-privacy.md b/server/sonar-docs/src/pages/organizations/organization-and-project-privacy.md
new file mode 100644
index 00000000000..639f9ce1685
--- /dev/null
+++ b/server/sonar-docs/src/pages/organizations/organization-and-project-privacy.md
@@ -0,0 +1,6 @@
+---
+title: Organization and Project Privacy
+scope: sonarcloud
+---
+
+## TODO
diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts
index b793d7412c3..95918a609b3 100644
--- a/server/sonar-web/src/main/js/api/organizations.ts
+++ b/server/sonar-web/src/main/js/api/organizations.ts
@@ -19,33 +19,15 @@
*/
import { getJSON, post, postJSON, RequestData } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-import { Paging } from '../app/types';
+import { LightOrganization, Paging } from '../app/types';
-interface GetOrganizationsParameters {
+export function getOrganizations(data: {
organizations?: string;
member?: boolean;
-}
-
-interface GetOrganizationsResponse {
- organizations: Array<{
- avatar?: string;
- description?: string;
- guarded: boolean;
- isAdmin: boolean;
- key: string;
- name: string;
- url?: string;
- }>;
- paging: {
- pageIndex: number;
- pageSize: number;
- total: number;
- };
-}
-
-export function getOrganizations(
- data: GetOrganizationsParameters
-): Promise<GetOrganizationsResponse> {
+}): Promise<{
+ organizations: LightOrganization[];
+ paging: Paging;
+}> {
return getJSON('/api/organizations/search', data);
}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js
index dc9980c1c1f..f93fc31fba3 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js
+++ b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js
@@ -24,7 +24,7 @@ import ExtensionContainer from './ExtensionContainer';
import ExtensionNotFound from './ExtensionNotFound';
import { getOrganizationByKey } from '../../../store/rootReducer';
import { fetchOrganization } from '../../../apps/organizations/actions';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../../app/types'; */
/*::
type Props = {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
index 325d8e0b613..150c9c50923 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
@@ -105,8 +105,7 @@ function renderBreadcrumbs(breadcrumbs: Breadcrumb[]) {
}
const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => ({
- organization:
- ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization),
+ organization: getOrganizationByKey(state, ownProps.component.organization),
shouldOrganizationBeDisplayed: areThereCustomOrganizations(state)
});
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index 97168176c94..6d359757c55 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -337,22 +337,32 @@ export interface Notification {
projectName?: string;
type: string;
}
+export interface LightOrganization {
+ avatar?: string;
+ description?: string;
+ guarded?: boolean;
+ isAdmin?: boolean;
+ key: string;
+ name: string;
+ subscription?: OrganizationSubscription;
+ url?: string;
+}
-export interface Organization {
+export interface Organization extends LightOrganization {
adminPages?: { key: string; name: string }[];
- avatar?: string;
canAdmin?: boolean;
canDelete?: boolean;
canProvisionProjects?: boolean;
canUpdateProjectsVisibilityToPrivate?: boolean;
- description?: string;
- isAdmin?: boolean;
isDefault?: boolean;
- key: string;
- name: string;
pages?: { key: string; name: string }[];
- projectVisibility: Visibility;
- url?: string;
+ projectVisibility?: Visibility;
+}
+
+export enum OrganizationSubscription {
+ Free = 'FREE',
+ Paid = 'PAID',
+ SonarQube = 'SONARQUBE'
}
export interface Paging {
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
index 110956c04fb..def6cd3f6d5 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
@@ -39,6 +39,7 @@ import { PopupPlacement } from '../../../components/ui/popups';
interface Props {
canWrite: boolean | undefined;
+ hidePermalink?: boolean;
hideSimilarRulesFilter?: boolean;
onFilterChange: (changes: Partial<Query>) => void;
onTagsChange: (tags: string[]) => void;
@@ -230,21 +231,22 @@ export default class RuleDetailsMeta extends React.PureComponent<Props> {
};
render() {
- const { ruleDetails } = this.props;
+ const { hidePermalink, ruleDetails } = this.props;
const hasTypeData = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
return (
<div className="js-rule-meta">
<header className="page-header">
<div className="pull-right">
<span className="note text-middle">{ruleDetails.key}</span>
- {!ruleDetails.isExternal && (
- <Link
- className="coding-rules-detail-permalink link-no-underline spacer-left text-middle"
- title={translate('permalink')}
- to={getRuleUrl(ruleDetails.key, this.props.organization)}>
- <LinkIcon />
- </Link>
- )}
+ {!ruleDetails.isExternal &&
+ !hidePermalink && (
+ <Link
+ className="coding-rules-detail-permalink link-no-underline spacer-left text-middle"
+ title={translate('permalink')}
+ to={getRuleUrl(ruleDetails.key, this.props.organization)}>
+ <LinkIcon />
+ </Link>
+ )}
{!this.props.hideSimilarRulesFilter && (
<SimilarRulesFilter onFilterChange={this.props.onFilterChange} rule={ruleDetails} />
)}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx
index 1380e6c0bfc..fd496d8f03e 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx
@@ -85,6 +85,14 @@ it('should edit tags', () => {
expect(onTagsChange).toBeCalledWith(['foo', 'bar']);
});
+it('should not display rule permalink', () => {
+ expect(
+ getWrapper({ hidePermalink: true })
+ .find('.coding-rules-detail-permalink')
+ .exists()
+ ).toBeFalsy();
+});
+
function getWrapper(props = {}) {
return shallow(
<RuleDetailsMeta
diff --git a/server/sonar-web/src/main/js/apps/organizations/actions.js b/server/sonar-web/src/main/js/apps/organizations/actions.js
index c70eb93dd58..2de045b6e89 100644
--- a/server/sonar-web/src/main/js/apps/organizations/actions.js
+++ b/server/sonar-web/src/main/js/apps/organizations/actions.js
@@ -27,7 +27,7 @@ import { onFail } from '../../store/rootActions';
import { getOrganizationMembersState } from '../../store/rootReducer';
import { addGlobalSuccessMessage } from '../../store/globalMessages/duck';
import { translate, translateWithParameters } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../app/types'; */
/*:: import type { Member } from '../../store/organizationsMembers/actions'; */
const PAGE_SIZE = 50;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js
index 5905d330a4c..4ed74e4169e 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js
@@ -21,12 +21,12 @@
import React from 'react';
import MembersListItem from './MembersListItem';
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../app/types'; */
/*::
type Props = {
members: Array<Member>,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
organization: Organization,
removeMember: Member => void,
updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
index 792f218ffac..b17f5ee3991 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
@@ -29,13 +29,13 @@ import ActionsDropdown, {
ActionsDropdownItem
} from '../../../components/controls/ActionsDropdown';
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../app/types'; */
/*::
type Props = {
member: Member,
organization: Organization,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
removeMember: Member => void,
updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void
};
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAdminContainer.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx
index 1e1ebd4142b..aab0246bd05 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAdminContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx
@@ -20,11 +20,13 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { RouterState } from 'react-router';
-import { getOrganizationByKey } from '../../../store/rootReducer';
+import { getOrganizationByKey, getCurrentUser } from '../../../store/rootReducer';
import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization';
-import { Organization } from '../../../app/types';
+import { Organization, CurrentUser, isLoggedIn } from '../../../app/types';
+import { isCurrentUserMemberOf, hasPrivateAccess } from '../../../helpers/organizations';
interface StateToProps {
+ currentUser: CurrentUser;
organization?: Organization;
}
@@ -32,9 +34,11 @@ interface OwnProps extends RouterState {
children: JSX.Element;
}
-interface Props extends StateToProps, Pick<OwnProps, 'children' | 'location'> {}
+interface Props extends StateToProps, Pick<OwnProps, 'children' | 'location'> {
+ hasAccess: (props: Props) => boolean;
+}
-export class OrganizationAdmin extends React.PureComponent<Props> {
+export class OrganizationAccess extends React.PureComponent<Props> {
componentDidMount() {
this.checkPermissions();
}
@@ -43,16 +47,14 @@ export class OrganizationAdmin extends React.PureComponent<Props> {
this.checkPermissions();
}
- isOrganizationAdmin = () => this.props.organization && this.props.organization.canAdmin;
-
checkPermissions = () => {
- if (!this.isOrganizationAdmin()) {
+ if (!this.props.hasAccess(this.props)) {
handleRequiredAuthorization();
}
};
render() {
- if (!this.isOrganizationAdmin()) {
+ if (!this.props.hasAccess(this.props)) {
return null;
}
return React.cloneElement(this.props.children, {
@@ -63,7 +65,40 @@ export class OrganizationAdmin extends React.PureComponent<Props> {
}
const mapStateToProps = (state: any, ownProps: OwnProps) => ({
+ currentUser: getCurrentUser(state),
organization: getOrganizationByKey(state, ownProps.params.organizationKey)
});
-export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(OrganizationAdmin);
+const OrganizationAccessContainer = connect<StateToProps, {}, OwnProps>(mapStateToProps)(
+ OrganizationAccess
+);
+
+export function OrganizationPrivateAccess(props: OwnProps) {
+ return (
+ <OrganizationAccessContainer
+ hasAccess={({ organization }: StateToProps) => hasPrivateAccess(organization)}
+ {...props}
+ />
+ );
+}
+
+export function OrganizationMembersAccess(props: OwnProps) {
+ return (
+ <OrganizationAccessContainer
+ hasAccess={({ organization }: StateToProps) => isCurrentUserMemberOf(organization)}
+ {...props}
+ />
+ );
+}
+
+export function hasAdminAccess({
+ currentUser,
+ organization
+}: Pick<StateToProps, 'currentUser' | 'organization'>) {
+ const isAdmin = isLoggedIn(currentUser) && organization && organization.canAdmin;
+ return Boolean(isAdmin);
+}
+
+export function OrganizationAdminAccess(props: OwnProps) {
+ return <OrganizationAccessContainer hasAccess={hasAdminAccess} {...props} />;
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js
index 770c9587bac..b78290609b8 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js
@@ -23,10 +23,9 @@ import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { translate } from '../../../helpers/l10n';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-import { getOrganizationByKey } from '../../../store/rootReducer';
import { updateOrganization } from '../actions';
import { SubmitButton } from '../../../components/ui/buttons';
+/*:: import type { Organization } from '../../../app/types'; */
/*::
type Props = {
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroupCheckbox.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroupCheckbox.js
index f62310aa435..95fd1e085e9 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroupCheckbox.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroupCheckbox.js
@@ -20,11 +20,11 @@
//@flow
import React from 'react';
import Checkbox from '../../../components/controls/Checkbox';
-/*:: import type { OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Group } from '../../../app/types'; */
/*::
type Props = {
- group: OrgGroup,
+ group: Group,
checked: boolean,
onCheck: (string, boolean) => void
};
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
index b56d8b58188..fdafac5ca40 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
@@ -28,14 +28,14 @@ import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import ListFooter from '../../../components/controls/ListFooter';
import DocTooltip from '../../../components/docs/DocTooltip';
import { translate } from '../../../helpers/l10n';
-/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../app/types'; */
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
/*::
type Props = {
members: Array<Member>,
memberLogins: Array<string>,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
status: { loading?: boolean, total?: number, pageIndex?: number, query?: string },
organization: Organization,
fetchOrganizationMembers: (organizationKey: string, query?: string) => void,
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
index 67077233600..4cc5dd277f2 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-// @flow
-import React from 'react';
+import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import OrganizationNavigation from '../navigation/OrganizationNavigation';
@@ -26,41 +25,38 @@ import NotFound from '../../../app/components/NotFound';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { fetchOrganization } from '../actions';
import { getOrganizationByKey } from '../../../store/rootReducer';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-
-/*::
-type OwnProps = {
- params: { organizationKey: string }
-};
-*/
-
-/*::
-type Props = {
- children?: React.Element<*>,
- location: Object,
- organization: null | Organization,
- params: { organizationKey: string },
- fetchOrganization: string => Promise<*>
-};
-*/
-
-/*::
-type State = {
- loading: boolean
-};
-*/
-
-export class OrganizationPage extends React.PureComponent {
- /*:: mounted: boolean; */
- /*:: props: Props; */
- state /*: State */ = { loading: true };
+import { Organization } from '../../../app/types';
+
+interface OwnProps {
+ children?: React.ReactNode;
+ location: { pathname: string };
+ params: { organizationKey: string };
+}
+
+interface StateProps {
+ organization?: Organization;
+}
+
+interface DispatchToProps {
+ fetchOrganization: (organizationKey: string) => Promise<void>;
+}
+
+type Props = OwnProps & StateProps & DispatchToProps;
+
+interface State {
+ loading: boolean;
+}
+
+export class OrganizationPage extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: true };
componentDidMount() {
this.mounted = true;
this.updateOrganization(this.props.params.organizationKey);
}
- componentWillReceiveProps(nextProps /*: Props */) {
+ componentWillReceiveProps(nextProps: Props) {
if (nextProps.params.organizationKey !== this.props.params.organizationKey) {
this.updateOrganization(nextProps.params.organizationKey);
}
@@ -76,7 +72,7 @@ export class OrganizationPage extends React.PureComponent {
}
};
- updateOrganization = (organizationKey /*: string */) => {
+ updateOrganization = (organizationKey: string) => {
this.setState({ loading: true });
this.props.fetchOrganization(organizationKey).then(this.stopLoading, this.stopLoading);
};
@@ -103,10 +99,12 @@ export class OrganizationPage extends React.PureComponent {
}
}
-const mapStateToProps = (state, ownProps /*: OwnProps */) => ({
+const mapStateToProps = (state: any, ownProps: OwnProps) => ({
organization: getOrganizationByKey(state, ownProps.params.organizationKey)
});
-const mapDispatchToProps = { fetchOrganization };
+const mapDispatchToProps = { fetchOrganization: fetchOrganization as any };
-export default connect(mapStateToProps, mapDispatchToProps)(OrganizationPage);
+export default connect<StateProps, DispatchToProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
+ OrganizationPage
+);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAccessContainer-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAccessContainer-test.tsx
new file mode 100644
index 00000000000..17b1fd80bb1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAccessContainer-test.tsx
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import { Location } from 'history';
+import { hasAdminAccess, OrganizationAccess } from '../OrganizationAccessContainer';
+import { Visibility } from '../../../../app/types';
+
+jest.mock('../../../../app/utils/handleRequiredAuthorization', () => ({ default: jest.fn() }));
+
+const locationMock = {} as Location;
+
+const currentUser = {
+ isLoggedIn: false
+};
+
+const loggedInUser = {
+ isLoggedIn: true,
+ login: 'luke',
+ name: 'Skywalker',
+ showOnboardingTutorial: false
+};
+
+const organization = {
+ canAdmin: false,
+ key: 'foo',
+ name: 'Foo',
+ projectVisibility: Visibility.Public
+};
+
+const adminOrganization = { ...organization, canAdmin: true };
+
+describe('component', () => {
+ it('should render children', () => {
+ expect(
+ shallow(
+ <OrganizationAccess
+ currentUser={loggedInUser}
+ hasAccess={() => true}
+ location={locationMock}
+ organization={adminOrganization}>
+ <div>hello</div>
+ </OrganizationAccess>
+ )
+ ).toMatchSnapshot();
+ });
+
+ it('should not render anything', () => {
+ expect(
+ shallow(
+ <OrganizationAccess
+ currentUser={loggedInUser}
+ hasAccess={() => false}
+ location={locationMock}
+ organization={adminOrganization}>
+ <div>hello</div>
+ </OrganizationAccess>
+ ).type()
+ ).toBeNull();
+ });
+});
+
+describe('access functions', () => {
+ it('should correctly handle access to admin only space', () => {
+ expect(
+ hasAdminAccess({ currentUser: loggedInUser, organization: adminOrganization })
+ ).toBeTruthy();
+ expect(hasAdminAccess({ currentUser, organization: adminOrganization })).toBeFalsy();
+ expect(hasAdminAccess({ currentUser: loggedInUser, organization })).toBeFalsy();
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAdminContainer-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAdminContainer-test.tsx
deleted file mode 100644
index 4f7f2669758..00000000000
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAdminContainer-test.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import { Location } from 'history';
-import { OrganizationAdmin } from '../OrganizationAdminContainer';
-import { Visibility } from '../../../../app/types';
-
-jest.mock('../../../../app/utils/handleRequiredAuthorization', () => ({ default: jest.fn() }));
-
-const locationMock = {} as Location;
-
-it('should render children', () => {
- const organization = {
- canAdmin: true,
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- };
- expect(
- shallow(
- <OrganizationAdmin organization={organization} location={locationMock}>
- <div>hello</div>
- </OrganizationAdmin>
- )
- ).toMatchSnapshot();
-});
-
-it('should not render anything', () => {
- const organization = {
- canAdmin: false,
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- };
- expect(
- shallow(
- <OrganizationAdmin organization={organization} location={locationMock}>
- <div>hello</div>
- </OrganizationAdmin>
- ).type()
- ).toBeNull();
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAdminContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAccessContainer-test.tsx.snap
index 349db2dd14c..df4447403e6 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAdminContainer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAccessContainer-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render children 1`] = `
+exports[`component should render children 1`] = `
<div
location={Object {}}
organization={
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js
index 90e04927efb..1fe1b0d7d6a 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js
@@ -24,7 +24,7 @@ import { searchMembers } from '../../../../api/organizations';
import Modal from '../../../../components/controls/Modal';
import { translate } from '../../../../helpers/l10n';
import { SubmitButton, ResetButtonLink, Button } from '../../../../components/ui/buttons';
-/*:: import type { Organization } from '../../../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../../../app/types'; */
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
/*::
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
index bf15a60e97a..563bf17505c 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
@@ -26,14 +26,14 @@ import { translate, translateWithParameters } from '../../../../helpers/l10n';
import OrganizationGroupCheckbox from '../OrganizationGroupCheckbox';
import { SubmitButton, ResetButtonLink } from '../../../../components/ui/buttons';
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization, OrgGroup } from '../../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../../app/types'; */
/*::
type Props = {
onClose: () => void;
member: Member,
organization: Organization,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void
};
*/
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
index e1b48aa57c8..b33c53f0907 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
@@ -23,7 +23,7 @@ import Modal from '../../../../components/controls/Modal';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { SubmitButton, ResetButtonLink } from '../../../../components/ui/buttons';
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization } from '../../../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../../../app/types'; */
/*::
type Props = {
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
index f9b1e0ddf81..3ee87fc0f36 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
@@ -30,7 +30,7 @@ interface Props {
organization: Organization;
}
-export default function OrganizationNavigation({ organization, location }: Props) {
+export default function OrganizationNavigation({ location, organization }: Props) {
return (
<ContextNavBar height={theme.contextNavHeightRaw} id="context-navigation">
<div className="navbar-context-justified">
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx
index bbce6164647..e63d8b4b0b3 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx
@@ -25,6 +25,7 @@ import { Organization } from '../../../app/types';
import NavBarTabs from '../../../components/nav/NavBarTabs';
import { translate } from '../../../helpers/l10n';
import { getQualityGatesUrl } from '../../../helpers/urls';
+import { hasPrivateAccess, isCurrentUserMemberOf } from '../../../helpers/organizations';
interface Props {
location: { pathname: string };
@@ -49,26 +50,36 @@ export default function OrganizationNavigationMenu({ location, organization }: P
{translate('issues.page')}
</Link>
</li>
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/quality_profiles`}>
- {translate('quality_profiles.page')}
- </Link>
- </li>
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/rules`}>
- {translate('coding_rules.page')}
- </Link>
- </li>
- <li>
- <Link activeClassName="active" to={getQualityGatesUrl(organization.key)}>
- {translate('quality_gates.page')}
- </Link>
- </li>
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/members`}>
- {translate('organization.members.page')}
- </Link>
- </li>
+ {hasPrivateAccess(organization) && (
+ <>
+ <li>
+ <Link
+ activeClassName="active"
+ to={`/organizations/${organization.key}/quality_profiles`}>
+ {translate('quality_profiles.page')}
+ </Link>
+ </li>
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/rules`}>
+ {translate('coding_rules.page')}
+ </Link>
+ </li>
+ <li>
+ <Link activeClassName="active" to={getQualityGatesUrl(organization.key)}>
+ {translate('quality_gates.page')}
+ </Link>
+ </li>
+ </>
+ )}
+
+ {isCurrentUserMemberOf(organization) && (
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/members`}>
+ {translate('organization.members.page')}
+ </Link>
+ </li>
+ )}
+
<OrganizationNavigationExtensions location={location} organization={organization} />
{organization.canAdmin && (
<OrganizationNavigationAdministration location={location} organization={organization} />
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMenu-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMenu-test.tsx
index 55e8fc5e802..251e41573de 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMenu-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMenu-test.tsx
@@ -21,19 +21,27 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationNavigationMenu from '../OrganizationNavigationMenu';
import { Visibility } from '../../../../app/types';
+import { isCurrentUserMemberOf, hasPrivateAccess } from '../../../../helpers/organizations';
+
+jest.mock('../../../../helpers/organizations', () => ({
+ isCurrentUserMemberOf: jest.fn().mockReturnValue(true),
+ hasPrivateAccess: jest.fn().mockReturnValue(true)
+}));
+
+const organization = {
+ key: 'foo',
+ name: 'Foo',
+ projectVisibility: Visibility.Public
+};
+
+beforeEach(() => {
+ (isCurrentUserMemberOf as jest.Mock<any>).mockClear();
+ (hasPrivateAccess as jest.Mock<any>).mockClear();
+});
it('renders', () => {
expect(
- shallow(
- <OrganizationNavigationMenu
- location={{ pathname: '' }}
- organization={{
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- }}
- />
- )
+ shallow(<OrganizationNavigationMenu location={{ pathname: '' }} organization={organization} />)
).toMatchSnapshot();
});
@@ -42,12 +50,7 @@ it('renders for admin', () => {
shallow(
<OrganizationNavigationMenu
location={{ pathname: '' }}
- organization={{
- canAdmin: true,
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- }}
+ organization={{ ...organization, canAdmin: true }}
/>
)
).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenu-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenu-test.tsx.snap
index 1177b7604b7..a51bf8cc513 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenu-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenu-test.tsx.snap
@@ -31,40 +31,42 @@ exports[`renders 1`] = `
issues.page
</Link>
</li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/quality_profiles"
- >
- quality_profiles.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/rules"
- >
- coding_rules.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/organizations/foo/quality_gates",
+ <React.Fragment>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/quality_profiles"
+ >
+ quality_profiles.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/rules"
+ >
+ coding_rules.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/organizations/foo/quality_gates",
+ }
}
- }
- >
- quality_gates.page
- </Link>
- </li>
+ >
+ quality_gates.page
+ </Link>
+ </li>
+ </React.Fragment>
<li>
<Link
activeClassName="active"
@@ -123,40 +125,42 @@ exports[`renders for admin 1`] = `
issues.page
</Link>
</li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/quality_profiles"
- >
- quality_profiles.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/rules"
- >
- coding_rules.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/organizations/foo/quality_gates",
+ <React.Fragment>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/quality_profiles"
+ >
+ quality_profiles.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/rules"
+ >
+ coding_rules.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/organizations/foo/quality_gates",
+ }
}
- }
- >
- quality_gates.page
- </Link>
- </li>
+ >
+ quality_gates.page
+ </Link>
+ </li>
+ </React.Fragment>
<li>
<Link
activeClassName="active"
diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.ts b/server/sonar-web/src/main/js/apps/organizations/routes.ts
index 02c2bdaec00..18aa7c3f4ba 100644
--- a/server/sonar-web/src/main/js/apps/organizations/routes.ts
+++ b/server/sonar-web/src/main/js/apps/organizations/routes.ts
@@ -54,25 +54,47 @@ const routes = [
]
},
{
- path: 'members',
- component: lazyLoad(() => import('./components/OrganizationMembersContainer'))
- },
- {
- path: 'rules',
- component: OrganizationContainer,
- childRoutes: codingRulesRoutes
- },
- {
- path: 'quality_profiles',
- childRoutes: qualityProfilesRoutes
+ component: lazyLoad(() =>
+ import('./components/OrganizationAccessContainer').then(lib => ({
+ default: lib.OrganizationMembersAccess
+ }))
+ ),
+ childRoutes: [
+ {
+ path: 'members',
+ component: lazyLoad(() => import('./components/OrganizationMembersContainer'))
+ }
+ ]
},
{
- path: 'quality_gates',
- component: OrganizationContainer,
- childRoutes: qualityGatesRoutes
+ component: lazyLoad(() =>
+ import('./components/OrganizationAccessContainer').then(lib => ({
+ default: lib.OrganizationPrivateAccess
+ }))
+ ),
+ childRoutes: [
+ {
+ path: 'rules',
+ component: OrganizationContainer,
+ childRoutes: codingRulesRoutes
+ },
+ {
+ path: 'quality_profiles',
+ childRoutes: qualityProfilesRoutes
+ },
+ {
+ path: 'quality_gates',
+ component: OrganizationContainer,
+ childRoutes: qualityGatesRoutes
+ }
+ ]
},
{
- component: lazyLoad(() => import('./components/OrganizationAdminContainer')),
+ component: lazyLoad(() =>
+ import('./components/OrganizationAccessContainer').then(lib => ({
+ default: lib.OrganizationAdminAccess
+ }))
+ ),
childRoutes: [
{ path: 'delete', component: lazyLoad(() => import('./components/OrganizationDelete')) },
{ path: 'edit', component: lazyLoad(() => import('./components/OrganizationEdit')) },
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
index c2145d14c3b..5eb22f4735d 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
@@ -32,6 +32,7 @@ import { Visibility, Component, Metric, BranchLike } from '../../../app/types';
import { History } from '../../../api/time-machine';
import { translate } from '../../../helpers/l10n';
import { MeasureEnhanced } from '../../../helpers/measures';
+import { hasPrivateAccess } from '../../../helpers/organizations';
interface Props {
branchLike?: BranchLike;
@@ -47,10 +48,40 @@ export default class Meta extends React.PureComponent<Props> {
organizationsEnabled: PropTypes.bool
};
+ renderQualityInfos() {
+ const { organizationsEnabled } = this.context;
+ const { organization, qualifier, qualityProfiles, qualityGate } = this.props.component;
+ const isProject = qualifier === 'TRK';
+
+ if (!isProject || (organizationsEnabled && !hasPrivateAccess(organization))) {
+ return null;
+ }
+
+ return (
+ <div className="overview-meta-card">
+ {qualityGate && (
+ <MetaQualityGate
+ organization={organizationsEnabled ? organization : undefined}
+ qualityGate={qualityGate}
+ />
+ )}
+
+ {qualityProfiles &&
+ qualityProfiles.length > 0 && (
+ <MetaQualityProfiles
+ headerClassName={qualityGate ? 'big-spacer-top' : undefined}
+ organization={organizationsEnabled ? organization : undefined}
+ profiles={qualityProfiles}
+ />
+ )}
+ </div>
+ );
+ }
+
render() {
const { organizationsEnabled } = this.context;
const { branchLike, component, metrics } = this.props;
- const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;
+ const { qualifier, description, visibility } = component;
const isProject = qualifier === 'TRK';
const isApp = qualifier === 'APP';
@@ -77,25 +108,7 @@ export default class Meta extends React.PureComponent<Props> {
qualifier={component.qualifier}
/>
- {isProject && (
- <div className="overview-meta-card">
- {qualityGate && (
- <MetaQualityGate
- organization={organizationsEnabled ? component.organization : undefined}
- qualityGate={qualityGate}
- />
- )}
-
- {qualityProfiles &&
- qualityProfiles.length > 0 && (
- <MetaQualityProfiles
- headerClassName={qualityGate ? 'big-spacer-top' : undefined}
- organization={organizationsEnabled ? component.organization : undefined}
- profiles={qualityProfiles}
- />
- )}
- </div>
- )}
+ {this.renderQualityInfos()}
{isProject && <MetaLinks component={component} />}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
index 26fb48fa1c8..d208e4e5b9b 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
@@ -21,7 +21,7 @@ import * as React from 'react';
import { connect } from 'react-redux';
import App from './App';
import forSingleOrganization from '../organizations/forSingleOrganization';
-import { Organization, LoggedInUser } from '../../app/types';
+import { Organization, LoggedInUser, Visibility } from '../../app/types';
import { getAppState, getOrganizationByKey, getCurrentUser } from '../../store/rootReducer';
import { receiveOrganizations } from '../../store/organizations/duck';
import { changeProjectDefaultVisibility } from '../../api/permissions';
@@ -85,7 +85,7 @@ const mapStateToProps = (state: any, ownProps: any) => ({
ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
});
-const onVisibilityChange = (organization: Organization, visibility: string) => (
+const onVisibilityChange = (organization: Organization, visibility: Visibility) => (
dispatch: Function
) => {
const currentVisibility = organization.projectVisibility;
@@ -97,7 +97,7 @@ const onVisibilityChange = (organization: Organization, visibility: string) => (
const mapDispatchToProps = (dispatch: Function) => ({
fetchOrganization: (key: string) => dispatch(fetchOrganization(key)),
- onVisibilityChange: (organization: Organization, visibility: string) =>
+ onVisibilityChange: (organization: Organization, visibility: Visibility) =>
dispatch(onVisibilityChange(organization, visibility))
});
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
index b3e90fd06b2..b721a8f7e76 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
@@ -21,7 +21,7 @@ import * as React from 'react';
import { Link } from 'react-router';
import { FormattedMessage } from 'react-intl';
import { createProject } from '../../api/components';
-import { Organization } from '../../app/types';
+import { Organization, Visibility } from '../../app/types';
import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
import VisibilitySelector from '../../components/common/VisibilitySelector';
import Modal from '../../components/controls/Modal';
@@ -40,7 +40,7 @@ interface State {
key: string;
loading: boolean;
name: string;
- visibility: string;
+ visibility?: Visibility;
// add index declaration to be able to do `this.setState({ [name]: value });`
[x: string]: any;
}
@@ -81,7 +81,7 @@ export default class CreateProjectForm extends React.PureComponent<Props, State>
this.setState({ [name]: value });
};
- handleVisibilityChange = (visibility: string) => {
+ handleVisibilityChange = (visibility: Visibility) => {
this.setState({ visibility });
};
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
index 2e5411384af..4d75c04cc18 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
@@ -54,18 +54,19 @@ export default class Header extends React.PureComponent<Props, State> {
<h1 className="page-title">{translate('projects_management')}</h1>
<div className="page-actions">
- {!isSonarCloud() && (
- <span className="big-spacer-right">
- <span className="text-middle">
- {translate('organization.default_visibility_of_new_projects')}{' '}
- <strong>{translate('visibility', organization.projectVisibility)}</strong>
+ {!isSonarCloud() &&
+ organization.projectVisibility && (
+ <span className="big-spacer-right">
+ <span className="text-middle">
+ {translate('organization.default_visibility_of_new_projects')}{' '}
+ <strong>{translate('visibility', organization.projectVisibility)}</strong>
+ </span>
+ <EditButton
+ className="js-change-visibility spacer-left button-small"
+ onClick={this.handleChangeVisibilityClick}
+ />
</span>
- <EditButton
- className="js-change-visibility spacer-left button-small"
- onClick={this.handleChangeVisibilityClick}
- />
- </span>
- )}
+ )}
{this.props.hasProvisionPermission && (
<Button id="create-project" onClick={this.props.onProjectCreate}>
{translate('qualifiers.create.TRK')}
diff --git a/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
index 10e25bce6ee..7d6633d1189 100644
--- a/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
+++ b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
@@ -20,25 +20,26 @@
import * as React from 'react';
import * as classNames from 'classnames';
import { translate } from '../../helpers/l10n';
+import { Visibility } from '../../app/types';
interface Props {
canTurnToPrivate?: boolean;
className?: string;
- onChange: (x: string) => void;
- visibility: string;
+ onChange: (visibility: Visibility) => void;
+ visibility?: Visibility;
}
export default class VisibilitySelector extends React.PureComponent<Props> {
handlePublicClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- this.props.onChange('public');
+ this.props.onChange(Visibility.Public);
};
handlePrivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- this.props.onChange('private');
+ this.props.onChange(Visibility.Private);
};
render() {
@@ -46,12 +47,12 @@ export default class VisibilitySelector extends React.PureComponent<Props> {
<div className={this.props.className}>
<a
className="link-base-color link-no-underline"
- id="visibility-public"
href="#"
+ id="visibility-public"
onClick={this.handlePublicClick}>
<i
className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'public'
+ 'is-checked': this.props.visibility === Visibility.Public
})}
/>
<span className="spacer-left">{translate('visibility.public')}</span>
@@ -60,12 +61,12 @@ export default class VisibilitySelector extends React.PureComponent<Props> {
{this.props.canTurnToPrivate ? (
<a
className="link-base-color link-no-underline huge-spacer-left"
- id="visibility-private"
href="#"
+ id="visibility-private"
onClick={this.handlePrivateClick}>
<i
className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
+ 'is-checked': this.props.visibility === Visibility.Private
})}
/>
<span className="spacer-left">{translate('visibility.private')}</span>
@@ -74,7 +75,7 @@ export default class VisibilitySelector extends React.PureComponent<Props> {
<span className="huge-spacer-left text-muted cursor-not-allowed" id="visibility-private">
<i
className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
+ 'is-checked': this.props.visibility === Visibility.Private
})}
/>
<span className="spacer-left">{translate('visibility.private')}</span>
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx
index 655563b8932..73c2b7771e6 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx
+++ b/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx
@@ -21,28 +21,37 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import VisibilitySelector from '../VisibilitySelector';
import { click } from '../../../helpers/testUtils';
+import { Visibility } from '../../../app/types';
it('changes visibility', () => {
const onChange = jest.fn();
const wrapper = shallow(
- <VisibilitySelector canTurnToPrivate={true} onChange={onChange} visibility="public" />
+ <VisibilitySelector
+ canTurnToPrivate={true}
+ onChange={onChange}
+ visibility={Visibility.Public}
+ />
);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('#visibility-private'));
- expect(onChange).toBeCalledWith('private');
+ expect(onChange).toBeCalledWith(Visibility.Private);
- wrapper.setProps({ visibility: 'private' });
+ wrapper.setProps({ visibility: Visibility.Private });
expect(wrapper).toMatchSnapshot();
click(wrapper.find('#visibility-public'));
- expect(onChange).toBeCalledWith('public');
+ expect(onChange).toBeCalledWith(Visibility.Public);
});
it('renders disabled', () => {
expect(
shallow(
- <VisibilitySelector canTurnToPrivate={false} onChange={jest.fn()} visibility="public" />
+ <VisibilitySelector
+ canTurnToPrivate={false}
+ onChange={jest.fn()}
+ visibility={Visibility.Public}
+ />
)
).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx
index e154a145773..647f4a4fade 100644
--- a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx
@@ -25,10 +25,11 @@ import DeferredSpinner from '../common/DeferredSpinner';
import RuleDetailsMeta from '../../apps/coding-rules/components/RuleDetailsMeta';
import RuleDetailsDescription from '../../apps/coding-rules/components/RuleDetailsDescription';
import '../../apps/coding-rules/styles.css';
+import { hasPrivateAccess } from '../../helpers/organizations';
interface Props {
onLoad: (details: { name: string }) => void;
- organization: string | undefined;
+ organizationKey: string | undefined;
ruleKey: string;
}
@@ -50,7 +51,7 @@ export default class WorkspaceRuleDetails extends React.PureComponent<Props, Sta
componentDidUpdate(prevProps: Props) {
if (
prevProps.ruleKey !== this.props.ruleKey ||
- prevProps.organization !== this.props.organization
+ prevProps.organizationKey !== this.props.organizationKey
) {
this.fetchRuleDetails();
}
@@ -63,8 +64,8 @@ export default class WorkspaceRuleDetails extends React.PureComponent<Props, Sta
fetchRuleDetails = () => {
this.setState({ loading: true });
Promise.all([
- getRulesApp({ organization: this.props.organization }),
- getRuleDetails({ key: this.props.ruleKey, organization: this.props.organization })
+ getRulesApp({ organization: this.props.organizationKey }),
+ getRuleDetails({ key: this.props.ruleKey, organization: this.props.organizationKey })
]).then(
([{ repositories }, { rule }]) => {
if (this.mounted) {
@@ -87,23 +88,26 @@ export default class WorkspaceRuleDetails extends React.PureComponent<Props, Sta
noOp = () => {};
render() {
+ const { organizationKey } = this.props;
+
return (
<DeferredSpinner loading={this.state.loading}>
{this.state.ruleDetails && (
<>
<RuleDetailsMeta
canWrite={false}
+ hidePermalink={!hasPrivateAccess(organizationKey)}
hideSimilarRulesFilter={true}
onFilterChange={this.noOp}
onTagsChange={this.noOp}
- organization={this.props.organization}
+ organization={organizationKey}
referencedRepositories={this.state.referencedRepositories}
ruleDetails={this.state.ruleDetails}
/>
<RuleDetailsDescription
canWrite={false}
onChange={this.noOp}
- organization={this.props.organization}
+ organization={organizationKey}
ruleDetails={this.state.ruleDetails}
/>
</>
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx
index 513f2ca8311..bb37eab9a0b 100644
--- a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx
@@ -58,7 +58,7 @@ export default class WorkspaceRuleViewer extends React.PureComponent<Props> {
<div className="workspace-viewer-container" style={{ height: this.props.height }}>
<WorkspaceRuleDetails
onLoad={this.handleLoaded}
- organization={rule.organization}
+ organizationKey={rule.organization}
ruleKey={rule.key}
/>
</div>
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx
index e1b5f68840f..e755c5686d5 100644
--- a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx
@@ -21,6 +21,12 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import WorkspaceRuleDetails from '../WorkspaceRuleDetails';
import { waitAndUpdate } from '../../../helpers/testUtils';
+import { OrganizationSubscription, Visibility } from '../../../app/types';
+import { hasPrivateAccess } from '../../../helpers/organizations';
+
+jest.mock('../../../helpers/organizations', () => ({
+ hasPrivateAccess: jest.fn().mockReturnValue(true)
+}));
jest.mock('../../../api/rules', () => ({
getRulesApp: jest.fn(() =>
@@ -29,9 +35,20 @@ jest.mock('../../../api/rules', () => ({
getRuleDetails: jest.fn(() => Promise.resolve({ rule: { key: 'foo', name: 'Foo' } }))
}));
+const organization = {
+ key: 'foo',
+ name: 'Foo',
+ projectVisibility: Visibility.Public,
+ subscription: OrganizationSubscription.Paid
+};
+
+beforeEach(() => {
+ (hasPrivateAccess as jest.Mock<any>).mockClear();
+});
+
it('should render', async () => {
const wrapper = shallow(
- <WorkspaceRuleDetails onLoad={jest.fn()} organization="org" ruleKey="foo" />
+ <WorkspaceRuleDetails onLoad={jest.fn()} organizationKey={undefined} ruleKey="foo" />
);
expect(wrapper).toMatchSnapshot();
@@ -42,8 +59,18 @@ it('should render', async () => {
it('should call back on load', async () => {
const onLoad = jest.fn();
const wrapper = shallow(
- <WorkspaceRuleDetails onLoad={onLoad} organization="org" ruleKey="foo" />
+ <WorkspaceRuleDetails onLoad={onLoad} organizationKey={undefined} ruleKey="foo" />
);
await waitAndUpdate(wrapper);
expect(onLoad).toBeCalledWith({ name: 'Foo' });
});
+
+it('should render without permalink', async () => {
+ (hasPrivateAccess as jest.Mock<any>).mockReturnValueOnce(false);
+ const wrapper = shallow(
+ <WorkspaceRuleDetails onLoad={jest.fn()} organizationKey={organization.key} ruleKey="foo" />
+ );
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('RuleDetailsMeta').prop('hidePermalink')).toBeTruthy();
+});
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap
index 682c1d5ac77..566b4a80db2 100644
--- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap
@@ -15,10 +15,10 @@ exports[`should render 2`] = `
<React.Fragment>
<RuleDetailsMeta
canWrite={false}
+ hidePermalink={false}
hideSimilarRulesFilter={true}
onFilterChange={[Function]}
onTagsChange={[Function]}
- organization="org"
referencedRepositories={
Object {
"repo": Object {
@@ -38,7 +38,6 @@ exports[`should render 2`] = `
<RuleDetailsDescription
canWrite={false}
onChange={[Function]}
- organization="org"
ruleDetails={
Object {
"key": "foo",
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap
index f8bc63a9a99..00b9e074c45 100644
--- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap
@@ -30,7 +30,7 @@ exports[`should render 1`] = `
>
<WorkspaceRuleDetails
onLoad={[Function]}
- organization="org"
+ organizationKey="org"
ruleKey="foo"
/>
</div>
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts
new file mode 100644
index 00000000000..17429129955
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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';
+import { getCurrentUser, getMyOrganizations } from '../../store/rootReducer';
+import { OrganizationSubscription } from '../../app/types';
+
+jest.mock('../../app/utils/getStore', () => ({
+ default: () => ({
+ getState: jest.fn()
+ })
+}));
+
+jest.mock('../../store/rootReducer', () => ({
+ getCurrentUser: jest.fn().mockReturnValue({
+ isLoggedIn: true,
+ login: 'luke',
+ name: 'Skywalker',
+ showOnboardingTutorial: false
+ }),
+ getMyOrganizations: jest.fn().mockReturnValue([])
+}));
+
+const organization = {
+ key: 'foo',
+ name: 'Foo',
+ subscription: OrganizationSubscription.Paid
+};
+
+const loggedOut = { isLoggedIn: false };
+
+beforeEach(() => {
+ (getCurrentUser as jest.Mock<any>).mockClear();
+ (getMyOrganizations as jest.Mock<any>).mockClear();
+});
+
+describe('isCurrentUserMemberOf', () => {
+ it('should be a member', () => {
+ expect(isCurrentUserMemberOf({ key: 'bar', name: 'Bar', canAdmin: true })).toBeTruthy();
+
+ (getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([organization]);
+ expect(isCurrentUserMemberOf(organization)).toBeTruthy();
+ });
+
+ it('should not be a member', () => {
+ expect(isCurrentUserMemberOf(undefined)).toBeFalsy();
+ expect(isCurrentUserMemberOf(organization)).toBeFalsy();
+
+ (getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([{ key: 'bar', name: 'Bar' }]);
+ expect(isCurrentUserMemberOf(organization)).toBeFalsy();
+
+ (getCurrentUser as jest.Mock<any>).mockReturnValueOnce(loggedOut);
+ expect(isCurrentUserMemberOf(organization)).toBeFalsy();
+ });
+});
+
+describe('hasPrivateAccess', () => {
+ it('should have access', () => {
+ expect(hasPrivateAccess({ key: 'bar', name: 'Bar' })).toBeTruthy();
+
+ (getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([organization]);
+ expect(hasPrivateAccess(organization)).toBeTruthy();
+ });
+
+ it('should not have access', () => {
+ expect(hasPrivateAccess(organization)).toBeFalsy();
+ });
+});
diff --git a/server/sonar-web/src/main/js/helpers/organizations.ts b/server/sonar-web/src/main/js/helpers/organizations.ts
new file mode 100644
index 00000000000..8489cb9ed12
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/organizations.ts
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 getStore from '../app/utils/getStore';
+import { Organization, isLoggedIn, OrganizationSubscription } from '../app/types';
+import { getCurrentUser, getMyOrganizations, getOrganizationByKey } from '../store/rootReducer';
+
+function getRealOrganization(
+ organization?: Organization | string,
+ state?: any
+): Organization | undefined {
+ if (typeof organization === 'string') {
+ state = state || getStore().getState();
+ return getOrganizationByKey(state, organization);
+ }
+
+ return organization;
+}
+
+function isPaidOrganization(organization: Organization | undefined): boolean {
+ return Boolean(organization && organization.subscription === OrganizationSubscription.Paid);
+}
+
+export function hasPrivateAccess(organization: Organization | string | undefined): boolean {
+ const realOrg = getRealOrganization(organization);
+ return !isPaidOrganization(realOrg) || isCurrentUserMemberOf(realOrg);
+}
+
+export function isCurrentUserMemberOf(organization: Organization | string | undefined): boolean {
+ const state = getStore().getState();
+ const currentUser = getCurrentUser(state);
+ const userOrganizations = getMyOrganizations(state);
+ const realOrg = getRealOrganization(organization, state);
+ return Boolean(
+ realOrg &&
+ isLoggedIn(currentUser) &&
+ (realOrg.canAdmin || userOrganizations.some(org => org.key === realOrg.key))
+ );
+}
diff --git a/server/sonar-web/src/main/js/store/organizations/duck.js b/server/sonar-web/src/main/js/store/organizations/duck.ts
index 1571e3a40f9..8b6c76e3993 100644
--- a/server/sonar-web/src/main/js/store/organizations/duck.js
+++ b/server/sonar-web/src/main/js/store/organizations/duck.ts
@@ -17,118 +17,67 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-// @flow
import { combineReducers } from 'redux';
import { omit, uniq, without } from 'lodash';
+import { Group, Organization } from '../../app/types';
-/*::
-export type Organization = {
- adminPages?: Array<{ key: string, name: string }>,
- avatar?: string,
- canAdmin?: boolean,
- canDelete?: boolean,
- canProvisionProjects?: boolean,
- canUpdateProjectsVisibilityToPrivate?: boolean,
- description?: string,
- isAdmin: bool,
- key: string,
- name: string,
- pages?: Array<{ key: string, name: string }>,
- projectVisibility: string,
- url?: string
-};
-*/
-
-/*::
-export type OrgGroup = {
- id: string,
- default: boolean,
- description: string,
- membersCount: number,
- name: string
-};
-*/
-
-/*::
-type ReceiveOrganizationsAction = {
- type: 'RECEIVE_ORGANIZATIONS',
- organizations: Array<Organization>
-};
-*/
+interface ReceiveOrganizationsAction {
+ type: 'RECEIVE_ORGANIZATIONS';
+ organizations: Organization[];
+}
-/*::
-type ReceiveMyOrganizationsAction = {
- type: 'RECEIVE_MY_ORGANIZATIONS',
- organizations: Array<Organization>
-};
-*/
+interface ReceiveMyOrganizationsAction {
+ type: 'RECEIVE_MY_ORGANIZATIONS';
+ organizations: Organization[];
+}
-/*::
-type ReceiveOrganizationGroups = {
- type: 'RECEIVE_ORGANIZATION_GROUPS',
- key: string,
- groups: Array<OrgGroup>
-};
-*/
+interface ReceiveOrganizationGroups {
+ type: 'RECEIVE_ORGANIZATION_GROUPS';
+ key: string;
+ groups: Group[];
+}
-/*::
-type CreateOrganizationAction = {
- type: 'CREATE_ORGANIZATION',
- organization: Organization
-};
-*/
+interface CreateOrganizationAction {
+ type: 'CREATE_ORGANIZATION';
+ organization: Organization;
+}
-/*::
-type UpdateOrganizationAction = {
- type: 'UPDATE_ORGANIZATION',
- key: string,
- changes: {}
-};
-*/
+interface UpdateOrganizationAction {
+ type: 'UPDATE_ORGANIZATION';
+ key: string;
+ changes: {};
+}
-/*::
-type DeleteOrganizationAction = {
- type: 'DELETE_ORGANIZATION',
- key: string
-};
-*/
+interface DeleteOrganizationAction {
+ type: 'DELETE_ORGANIZATION';
+ key: string;
+}
-/*::
type Action =
| ReceiveOrganizationsAction
| ReceiveMyOrganizationsAction
| ReceiveOrganizationGroups
| CreateOrganizationAction
| UpdateOrganizationAction
- | DeleteOrganizationAction; */
+ | DeleteOrganizationAction;
-/*::
-type ByKeyState = {
- [key: string]: Organization
-};
-*/
+interface ByKeyState {
+ [key: string]: Organization;
+}
-/*::
-type GroupsState = {
- [key: string]: Array<OrgGroup>
-};
-*/
+interface GroupsState {
+ [key: string]: Group[];
+}
-/*::
-type MyState = Array<string>;
-*/
+type MyState = string[];
-/*::
-type State = {
- byKey: ByKeyState,
- my: MyState,
- groups: GroupsState
-};
-*/
+interface State {
+ byKey: ByKeyState;
+ my: MyState;
+ groups: GroupsState;
+}
-export function receiveOrganizations(
- organizations /*: Array<Organization> */
-) /*: ReceiveOrganizationsAction */ {
+export function receiveOrganizations(organizations: Organization[]): ReceiveOrganizationsAction {
return {
type: 'RECEIVE_ORGANIZATIONS',
organizations
@@ -136,18 +85,15 @@ export function receiveOrganizations(
}
export function receiveMyOrganizations(
- organizations /*: Array<Organization> */
-) /*: ReceiveMyOrganizationsAction */ {
+ organizations: Organization[]
+): ReceiveMyOrganizationsAction {
return {
type: 'RECEIVE_MY_ORGANIZATIONS',
organizations
};
}
-export function receiveOrganizationGroups(
- key /*: string */,
- groups /*: Array<OrgGroup> */
-) /*: receiveOrganizationGroups */ {
+export function receiveOrganizationGroups(key: string, groups: Group[]): ReceiveOrganizationGroups {
return {
type: 'RECEIVE_ORGANIZATION_GROUPS',
key,
@@ -155,19 +101,14 @@ export function receiveOrganizationGroups(
};
}
-export function createOrganization(
- organization /*: Organization */
-) /*: CreateOrganizationAction */ {
+export function createOrganization(organization: Organization): CreateOrganizationAction {
return {
type: 'CREATE_ORGANIZATION',
organization
};
}
-export function updateOrganization(
- key /*: string */,
- changes /*: {} */
-) /*: UpdateOrganizationAction */ {
+export function updateOrganization(key: string, changes: {}): UpdateOrganizationAction {
return {
type: 'UPDATE_ORGANIZATION',
key,
@@ -175,7 +116,7 @@ export function updateOrganization(
};
}
-export function deleteOrganization(key /*: string */) /*: DeleteOrganizationAction */ {
+export function deleteOrganization(key: string): DeleteOrganizationAction {
return {
type: 'DELETE_ORGANIZATION',
key
@@ -183,9 +124,9 @@ export function deleteOrganization(key /*: string */) /*: DeleteOrganizationActi
}
function onReceiveOrganizations(
- state /*: ByKeyState */,
- action /*: ReceiveOrganizationsAction | ReceiveMyOrganizationsAction */
-) /*: ByKeyState */ {
+ state: ByKeyState,
+ action: ReceiveOrganizationsAction | ReceiveMyOrganizationsAction
+): ByKeyState {
const nextState = { ...state };
action.organizations.forEach(organization => {
nextState[organization.key] = { ...state[organization.key], ...organization };
@@ -193,7 +134,7 @@ function onReceiveOrganizations(
return nextState;
}
-function byKey(state /*: ByKeyState */ = {}, action /*: Action */) {
+function byKey(state: ByKeyState = {}, action: Action) {
switch (action.type) {
case 'RECEIVE_ORGANIZATIONS':
case 'RECEIVE_MY_ORGANIZATIONS':
@@ -216,7 +157,7 @@ function byKey(state /*: ByKeyState */ = {}, action /*: Action */) {
}
}
-function my(state /*: MyState */ = [], action /*: Action */) {
+function my(state: MyState = [], action: Action) {
switch (action.type) {
case 'RECEIVE_MY_ORGANIZATIONS':
return uniq([...state, ...action.organizations.map(o => o.key)]);
@@ -229,7 +170,7 @@ function my(state /*: MyState */ = [], action /*: Action */) {
}
}
-function groups(state /*: GroupsState */ = {}, action /*: Action */) {
+function groups(state: GroupsState = {}, action: Action) {
if (action.type === 'RECEIVE_ORGANIZATION_GROUPS') {
return { ...state, [action.key]: action.groups };
}
@@ -238,21 +179,18 @@ function groups(state /*: GroupsState */ = {}, action /*: Action */) {
export default combineReducers({ byKey, my, groups });
-export function getOrganizationByKey(state /*: State */, key /*: string */) /*: Organization */ {
+export function getOrganizationByKey(state: State, key: string): Organization | undefined {
return state.byKey[key];
}
-export function getOrganizationGroupsByKey(
- state /*: State */,
- key /*: string */
-) /*: Array<OrgGroup> */ {
+export function getOrganizationGroupsByKey(state: State, key: string): Group[] {
return state.groups[key] || [];
}
-export function getMyOrganizations(state /*: State */) /*: Array<Organization> */ {
- return state.my.map(key => getOrganizationByKey(state, key));
+export function getMyOrganizations(state: State): Organization[] {
+ return state.my.map(key => getOrganizationByKey(state, key) as Organization);
}
-export function areThereCustomOrganizations(state /*: State */) /*: boolean */ {
+export function areThereCustomOrganizations(state: State): boolean {
return Object.keys(state.byKey).length > 1;
}