aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md5
-rw-r--r--server/sonar-docs/src/tooltips/project/visibility-private.md5
-rw-r--r--server/sonar-docs/src/tooltips/project/visibility-public-admin.md5
-rw-r--r--server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md5
-rw-r--r--server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md5
-rw-r--r--server/sonar-docs/src/tooltips/project/visibility-public.md5
-rw-r--r--server/sonar-web/src/main/js/api/components.ts2
-rw-r--r--server/sonar-web/src/main/js/api/permissions.ts6
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx9
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/badges.css14
-rw-r--r--server/sonar-web/src/main/js/app/types.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx44
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js3
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/App.js15
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/App.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap19
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap19
-rw-r--r--server/sonar-web/src/main/js/apps/projects/types.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/App.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap16
-rw-r--r--server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx123
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx69
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap53
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap13
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx20
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocTooltip.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx50
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap9
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap30
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx (renamed from server/sonar-web/src/main/js/components/common/PrivateBadge.tsx)22
-rw-r--r--server/sonar-web/src/main/js/helpers/organizations.ts4
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties6
57 files changed, 693 insertions, 234 deletions
diff --git a/server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md b/server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md
new file mode 100644
index 00000000000..757d33d3da2
--- /dev/null
+++ b/server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md
@@ -0,0 +1,5 @@
+This organization is subscribed to a paid plan, allowing private projects. Its private projects, members, Quality Profiles and Quality Gates are visible to members only.
+
+---
+
+See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)
diff --git a/server/sonar-docs/src/tooltips/project/visibility-private.md b/server/sonar-docs/src/tooltips/project/visibility-private.md
new file mode 100644
index 00000000000..99d9647adae
--- /dev/null
+++ b/server/sonar-docs/src/tooltips/project/visibility-private.md
@@ -0,0 +1,5 @@
+This project is private. Only the members of this organization are able to browse it and its source code.
+
+---
+
+See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)
diff --git a/server/sonar-docs/src/tooltips/project/visibility-public-admin.md b/server/sonar-docs/src/tooltips/project/visibility-public-admin.md
new file mode 100644
index 00000000000..defcc6b5ca3
--- /dev/null
+++ b/server/sonar-docs/src/tooltips/project/visibility-public-admin.md
@@ -0,0 +1,5 @@
+This project is public, which means anyone is able to browse its source code. Subscribe to a paid plan to get unlimited private projects in [Administration > Billing](/#sonarcloud#/organizations/#organization#/extension/billing/billing).
+
+---
+
+See also: [Pricing](/sonarcloud-pricing)
diff --git a/server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md
new file mode 100644
index 00000000000..54b8a3d76d8
--- /dev/null
+++ b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md
@@ -0,0 +1,5 @@
+This project is public, which means anyone is able to browse its source code. Go to your project's [Administration > Permissions](/#sonarcloud#/project_roles?id=#projectKey#) to make it private.
+
+---
+
+See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)
diff --git a/server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md
new file mode 100644
index 00000000000..3be0178c809
--- /dev/null
+++ b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md
@@ -0,0 +1,5 @@
+This project is public, which means anyone is able to browse its source code. Contact the project administrator to make it private.
+
+---
+
+See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)
diff --git a/server/sonar-docs/src/tooltips/project/visibility-public.md b/server/sonar-docs/src/tooltips/project/visibility-public.md
new file mode 100644
index 00000000000..b0d5c067689
--- /dev/null
+++ b/server/sonar-docs/src/tooltips/project/visibility-public.md
@@ -0,0 +1,5 @@
+This project is public, which means anyone is able to browse its source code. Contact the organization administrator if you want to make it private.
+
+---
+
+See also: [Pricing](/sonarcloud-pricing)
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index 7ae4d59aa41..a069c38b17e 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -162,7 +162,7 @@ export interface Component {
isFavorite?: boolean;
analysisDate?: string;
tags: string[];
- visibility: string;
+ visibility: Visibility;
leakPeriodDate?: string;
}
diff --git a/server/sonar-web/src/main/js/api/permissions.ts b/server/sonar-web/src/main/js/api/permissions.ts
index 189ed4ca8de..e14367bb6a7 100644
--- a/server/sonar-web/src/main/js/api/permissions.ts
+++ b/server/sonar-web/src/main/js/api/permissions.ts
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { BaseSearchProjectsParameters } from './components';
-import { PermissionTemplate } from '../app/types';
+import { PermissionTemplate, Visibility } from '../app/types';
import throwGlobalError from '../app/utils/throwGlobalError';
import { getJSON, post, postJSON, RequestData } from '../helpers/request';
@@ -294,14 +294,14 @@ export function getPermissionTemplateGroups(
export function changeProjectVisibility(
project: string,
- visibility: string
+ visibility: Visibility
): Promise<void | Response> {
return post('/api/projects/update_visibility', { project, visibility }).catch(throwGlobalError);
}
export function changeProjectDefaultVisibility(
organization: string,
- projectVisibility: string
+ projectVisibility: Visibility
): Promise<void | Response> {
return post('/api/projects/update_default_visibility', { organization, projectVisibility }).catch(
throwGlobalError
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
index 9052c611e68..83a4afb2822 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
@@ -29,7 +29,8 @@ import {
MainBranch,
LongLivingBranch,
PullRequest,
- BranchType
+ BranchType,
+ Visibility
} from '../../types';
import { STATUSES } from '../../../apps/background-tasks/constants';
import { waitAndUpdate } from '../../../helpers/testUtils';
@@ -80,12 +81,12 @@ it('changes component', () => {
(wrapper.instance() as ComponentContainer).mounted = true;
wrapper.setState({
branches: [{ isMain: true }],
- component: { qualifier: 'TRK', visibility: 'public' },
+ component: { qualifier: 'TRK', visibility: Visibility.Public },
loading: false
});
- (wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' });
- expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' });
+ (wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: Visibility.Private });
+ expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: Visibility.Private });
});
it("loads branches for module's project", async () => {
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 150c9c50923..e01fb137866 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
@@ -27,7 +27,6 @@ import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../s
import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
import OrganizationHelmet from '../../../../components/common/OrganizationHelmet';
import OrganizationLink from '../../../../components/ui/OrganizationLink';
-import PrivateBadge from '../../../../components/common/PrivateBadge';
import { collapsePath, limitComponentName } from '../../../../helpers/path';
import { getProjectUrl } from '../../../../helpers/urls';
@@ -67,9 +66,6 @@ export function ComponentNavHeader(props: Props) {
</>
)}
{renderBreadcrumbs(component.breadcrumbs)}
- {component.visibility === 'private' && (
- <PrivateBadge className="spacer-left" qualifier={component.qualifier} />
- )}
{props.currentBranchLike && (
<ComponentNavBranch
branchLikes={props.branchLikes}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
index 676a38a577b..ff1fdacdf46 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
@@ -29,7 +29,7 @@ it('should not render breadcrumbs with one element', () => {
name: 'My Project',
organization: 'org',
qualifier: 'TRK',
- visibility: 'public'
+ visibility: Visibility.Public
};
const result = shallow(
<ComponentNavHeader
@@ -49,7 +49,7 @@ it('should render organization', () => {
name: 'My Project',
organization: 'foo',
qualifier: 'TRK',
- visibility: 'public'
+ visibility: Visibility.Public
};
const organization = {
key: 'foo',
@@ -67,23 +67,3 @@ it('should render organization', () => {
);
expect(result).toMatchSnapshot();
});
-
-it('renders private badge', () => {
- const component = {
- breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
- key: 'my-project',
- name: 'My Project',
- organization: 'org',
- qualifier: 'TRK',
- visibility: 'private'
- };
- const result = shallow(
- <ComponentNavHeader
- branchLikes={[]}
- component={component}
- currentBranchLike={undefined}
- shouldOrganizationBeDisplayed={false}
- />
- );
- expect(result.find('PrivateBadge')).toHaveLength(1);
-});
diff --git a/server/sonar-web/src/main/js/app/styles/components/badges.css b/server/sonar-web/src/main/js/app/styles/components/badges.css
index 9d0e5a5a2d9..05d1f81238f 100644
--- a/server/sonar-web/src/main/js/app/styles/components/badges.css
+++ b/server/sonar-web/src/main/js/app/styles/components/badges.css
@@ -148,6 +148,18 @@ a.badge-focus:active {
.outline-badge.active {
color: var(--baseFontColor);
- border: 1px solid var(--blue);
+ border-color: var(--blue);
background-color: var(--lightBlue);
}
+
+.outline-badge.badge-info {
+ border-color: var(--blue);
+}
+
+.outline-badge.badge-icon {
+ padding-left: calc(var(--gridSize) / 2);
+}
+
+.outline-badge.badge-icon svg {
+ height: calc(var(--smallControlHeight) - 2px);
+}
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index bddfe372c4a..b546c8a212f 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -67,7 +67,7 @@ export interface Component extends LightComponent {
qualityGate?: { isDefault?: boolean; key: string; name: string };
tags?: string[];
version?: string;
- visibility?: string;
+ visibility?: Visibility;
}
interface ComponentConfiguration {
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
index 4cc5dd277f2..8d135c8fede 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
@@ -21,11 +21,15 @@ import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import OrganizationNavigation from '../navigation/OrganizationNavigation';
+import { fetchOrganization } from '../actions';
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 { Organization } from '../../../app/types';
+import { Organization, CurrentUser } from '../../../app/types';
+import {
+ getOrganizationByKey,
+ getCurrentUser,
+ getMyOrganizations
+} from '../../../store/rootReducer';
interface OwnProps {
children?: React.ReactNode;
@@ -34,7 +38,9 @@ interface OwnProps {
}
interface StateProps {
+ currentUser: CurrentUser;
organization?: Organization;
+ userOrganizations: Organization[];
}
interface DispatchToProps {
@@ -92,7 +98,12 @@ export class OrganizationPage extends React.PureComponent<Props, State> {
<div>
<Helmet defaultTitle={organization.name} titleTemplate={'%s - ' + organization.name} />
<Suggestions suggestions="organization_space" />
- <OrganizationNavigation location={this.props.location} organization={organization} />
+ <OrganizationNavigation
+ currentUser={this.props.currentUser}
+ location={this.props.location}
+ organization={organization}
+ userOrganizations={this.props.userOrganizations}
+ />
{this.props.children}
</div>
);
@@ -100,7 +111,9 @@ export class OrganizationPage extends React.PureComponent<Props, State> {
}
const mapStateToProps = (state: any, ownProps: OwnProps) => ({
- organization: getOrganizationByKey(state, ownProps.params.organizationKey)
+ currentUser: getCurrentUser(state),
+ organization: getOrganizationByKey(state, ownProps.params.organizationKey),
+ userOrganizations: getMyOrganizations(state)
});
const mapDispatchToProps = { fetchOrganization: fetchOrganization as any };
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx
index e1230f3a18c..5bf896954b5 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx
@@ -21,17 +21,14 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import { OrganizationPage } from '../OrganizationPage';
-const fetchOrganization = () => Promise.resolve();
+const fetchOrganization = jest.fn().mockResolvedValue(undefined);
+
+beforeEach(() => {
+ fetchOrganization.mockClear();
+});
it('smoke test', () => {
- const wrapper = shallow(
- <OrganizationPage
- fetchOrganization={fetchOrganization}
- location={{ pathname: 'foo' }}
- params={{ organizationKey: 'foo' }}>
- <div>hello</div>
- </OrganizationPage>
- );
+ const wrapper = getWrapper();
expect(wrapper.type()).toBeNull();
const organization = { key: 'foo', name: 'Foo', isDefault: false, canAdmin: false };
@@ -40,29 +37,28 @@ it('smoke test', () => {
});
it('not found', () => {
- const wrapper = shallow(
- <OrganizationPage
- fetchOrganization={fetchOrganization}
- location={{ pathname: 'foo' }}
- params={{ organizationKey: 'foo' }}>
- <div>hello</div>
- </OrganizationPage>
- );
+ const wrapper = getWrapper();
wrapper.setState({ loading: false });
expect(wrapper).toMatchSnapshot();
});
it('should correctly update when the organization changes', () => {
- const fetchOrganization = jest.fn(() => Promise.resolve());
- const wrapper = shallow(
+ const wrapper = getWrapper();
+ wrapper.setProps({ params: { organizationKey: 'bar' } });
+ expect(fetchOrganization).toHaveBeenCalledTimes(2);
+ expect(fetchOrganization.mock.calls).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
<OrganizationPage
+ currentUser={{ isLoggedIn: false }}
fetchOrganization={fetchOrganization}
location={{ pathname: 'foo' }}
- params={{ organizationKey: 'foo' }}>
+ params={{ organizationKey: 'foo' }}
+ userOrganizations={[]}
+ {...props}>
<div>hello</div>
</OrganizationPage>
);
- wrapper.setProps({ params: { organizationKey: 'bar' } });
- expect(fetchOrganization).toHaveBeenCalledTimes(2);
- expect(fetchOrganization.mock.calls).toMatchSnapshot();
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap
index 9802e06cdcb..c6b612738c1 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap
@@ -25,6 +25,11 @@ exports[`smoke test 1`] = `
suggestions="organization_space"
/>
<OrganizationNavigation
+ currentUser={
+ Object {
+ "isLoggedIn": false,
+ }
+ }
location={
Object {
"pathname": "foo",
@@ -38,6 +43,7 @@ exports[`smoke test 1`] = `
"name": "Foo",
}
}
+ userOrganizations={Array []}
/>
<div>
hello
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 112d2ef75ce..d74444d133b 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
@@ -23,19 +23,30 @@ import OrganizationNavigationMeta from './OrganizationNavigationMeta';
import OrganizationNavigationMenuContainer from './OrganizationNavigationMenuContainer';
import * as theme from '../../../app/theme';
import ContextNavBar from '../../../components/nav/ContextNavBar';
-import { Organization } from '../../../app/types';
+import { Organization, CurrentUser } from '../../../app/types';
interface Props {
+ currentUser: CurrentUser;
location: { pathname: string };
organization: Organization;
+ userOrganizations: Organization[];
}
-export default function OrganizationNavigation({ location, organization }: Props) {
+export default function OrganizationNavigation({
+ currentUser,
+ location,
+ organization,
+ userOrganizations
+}: Props) {
return (
<ContextNavBar height={theme.contextNavHeightRaw} id="context-navigation">
<div className="navbar-context-justified">
<OrganizationNavigationHeaderContainer organization={organization} />
- <OrganizationNavigationMeta organization={organization} />
+ <OrganizationNavigationMeta
+ currentUser={currentUser}
+ organization={organization}
+ userOrganizations={userOrganizations}
+ />
</div>
<OrganizationNavigationMenuContainer location={location} organization={organization} />
</ContextNavBar>
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
index 6815ae5dae9..10e2496c277 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
@@ -18,16 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Organization, HomePageType } from '../../../app/types';
import HomePageSelect from '../../../components/controls/HomePageSelect';
+import DocTooltip from '../../../components/docs/DocTooltip';
import { translate } from '../../../helpers/l10n';
import { isSonarCloud } from '../../../helpers/system';
+import { hasPrivateAccess, isPaidOrganization } from '../../../helpers/organizations';
+import { CurrentUser, HomePageType, Organization } from '../../../app/types';
interface Props {
+ currentUser: CurrentUser;
organization: Organization;
+ userOrganizations: Organization[];
}
-export default function OrganizationNavigationMeta({ organization }: Props) {
+export default function OrganizationNavigationMeta({
+ currentUser,
+ organization,
+ userOrganizations
+}: Props) {
+ const onSonarCloud = isSonarCloud();
return (
<div className="navbar-context-meta">
{organization.url != null && (
@@ -39,10 +48,17 @@ export default function OrganizationNavigationMeta({ organization }: Props) {
{organization.url}
</a>
)}
+ {onSonarCloud &&
+ isPaidOrganization(organization) &&
+ hasPrivateAccess(currentUser, organization, userOrganizations) && (
+ <DocTooltip className="spacer-right" doc="organizations/subscription-paid-plan">
+ <div className="outline-badge">{translate('organization.paid_plan.badge')}</div>
+ </DocTooltip>
+ )}
<div className="text-muted">
<strong>{translate('organization.key')}:</strong> {organization.key}
</div>
- {isSonarCloud() && (
+ {onSonarCloud && (
<div className="navbar-context-meta-secondary">
<HomePageSelect
currentPage={{ type: HomePageType.Organization, organization: organization.key }}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx
index 6f780c28e2c..ecfdd94f780 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx
@@ -26,12 +26,14 @@ it('render', () => {
expect(
shallow(
<OrganizationNavigation
+ currentUser={{ isLoggedIn: false }}
location={{ pathname: '/organizations/foo' }}
organization={{
key: 'foo',
name: 'Foo',
projectVisibility: Visibility.Public
}}
+ userOrganizations={[]}
/>
)
).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx
index 96d9f772c63..5009da6beb4 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx
@@ -20,20 +20,34 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationNavigationMeta from '../OrganizationNavigationMeta';
-import { Visibility } from '../../../../app/types';
+import { OrganizationSubscription } from '../../../../app/types';
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: () => true }));
+const organization = { key: 'foo', name: 'Foo', subscription: OrganizationSubscription.Free };
+
it('renders', () => {
expect(
shallow(
<OrganizationNavigationMeta
- organization={{
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- }}
+ currentUser={{ isLoggedIn: false }}
+ organization={organization}
+ userOrganizations={[]}
/>
)
).toMatchSnapshot();
});
+
+it('renders with private badge', () => {
+ expect(
+ shallow(
+ <OrganizationNavigationMeta
+ currentUser={{ isLoggedIn: true }}
+ organization={{ ...organization, subscription: OrganizationSubscription.Paid }}
+ userOrganizations={[organization]}
+ />
+ )
+ .find('DocTooltip')
+ .exists()
+ ).toBeTruthy();
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
index 240404ba19f..0da0558483a 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
@@ -18,6 +18,11 @@ exports[`render 1`] = `
}
/>
<OrganizationNavigationMeta
+ currentUser={
+ Object {
+ "isLoggedIn": false,
+ }
+ }
organization={
Object {
"key": "foo",
@@ -25,6 +30,7 @@ exports[`render 1`] = `
"projectVisibility": "public",
}
}
+ userOrganizations={Array []}
/>
</div>
<Connect(OrganizationNavigationMenu)
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
index 62ee5647128..3e7749343a5 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
@@ -46,6 +46,7 @@ import {
getMyOrganizations,
getOrganizationByKey
} from '../../../store/rootReducer';
+import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
interface StateToProps {
currentUser: CurrentUser;
@@ -105,18 +106,26 @@ export class Meta extends React.PureComponent<Props> {
render() {
const { organizationsEnabled } = this.context;
- const { branchLike, component, metrics } = this.props;
+ const { branchLike, component, metrics, organization } = this.props;
const { qualifier, description, visibility } = component;
const isProject = qualifier === 'TRK';
const isApp = qualifier === 'APP';
const isPrivate = visibility === Visibility.Private;
-
return (
<div className="overview-meta">
<div className="overview-meta-card">
<h4 className="overview-meta-header">
{translate('overview.about_this_project', qualifier)}
+ {component.visibility && (
+ <PrivacyBadgeContainer
+ className="spacer-left pull-right"
+ organization={organization}
+ qualifier={component.qualifier}
+ tooltipProps={{ projectKey: component.key }}
+ visibility={component.visibility}
+ />
+ )}
</h4>
{description !== undefined && <p className="overview-meta-description">{description}</p>}
{isProject && (
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
index 51d563afbe1..1fbf421aa71 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
@@ -24,6 +24,7 @@ import SearchForm from '../../shared/components/SearchForm';
import HoldersList from '../../shared/components/HoldersList';
import { translate } from '../../../../helpers/l10n';
import { PERMISSIONS_ORDER_BY_QUALIFIER } from '../constants';
+import { Visibility } from '../../../../app/types';
/*::
type Props = {|
@@ -89,7 +90,7 @@ export default class AllHoldersList extends React.PureComponent {
render() {
let order = PERMISSIONS_ORDER_BY_QUALIFIER[this.props.component.qualifier];
- if (this.props.visibility === 'public') {
+ if (this.props.visibility === Visibility.Public) {
order = without(order, 'user', 'codeviewer');
}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
index 5815264b6a4..dc5888fcd08 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
@@ -30,6 +30,7 @@ import PageError from '../../shared/components/PageError';
import * as api from '../../../../api/permissions';
import { translate } from '../../../../helpers/l10n';
import '../../styles.css';
+import { Visibility } from '../../../../app/types';
/*::
export type Props = {|
@@ -284,7 +285,7 @@ export default class App extends React.PureComponent {
};
handleVisibilityChange = (visibility /*: string */) => {
- if (visibility === 'public') {
+ if (visibility === Visibility.Public) {
this.openDisclaimer();
} else {
this.turnProjectToPrivate();
@@ -292,25 +293,25 @@ export default class App extends React.PureComponent {
};
turnProjectToPublic = () => {
- this.props.onComponentChange({ visibility: 'public' });
- api.changeProjectVisibility(this.props.component.key, 'public').then(
+ this.props.onComponentChange({ visibility: Visibility.Public });
+ api.changeProjectVisibility(this.props.component.key, Visibility.Public).then(
() => {
this.loadHolders();
},
error => {
- this.props.onComponentChange({ visibility: 'private' });
+ this.props.onComponentChange({ visibility: Visibility.Private });
}
);
};
turnProjectToPrivate = () => {
- this.props.onComponentChange({ visibility: 'private' });
- api.changeProjectVisibility(this.props.component.key, 'private').then(
+ this.props.onComponentChange({ visibility: Visibility.Private });
+ api.changeProjectVisibility(this.props.component.key, Visibility.Private).then(
() => {
this.loadHolders();
},
error => {
- this.props.onComponentChange({ visibility: 'public' });
+ this.props.onComponentChange({ visibility: Visibility.Public });
}
);
};
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
index 188130d59e1..5e9b873b841 100644
--- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
@@ -36,6 +36,7 @@ import { fetchMetrics } from '../../../store/rootActions';
import { getMetrics } from '../../../store/rootReducer';
import { Metric, Component } from '../../../app/types';
import '../styles.css';
+import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
interface OwnProps {
component: Component;
@@ -190,6 +191,15 @@ export class App extends React.PureComponent<Props, State> {
<div className="portfolio-meta-card">
<h4 className="portfolio-meta-header">
{translate('overview.about_this_portfolio')}
+ {component.visibility && (
+ <PrivacyBadgeContainer
+ className="spacer-left pull-right"
+ organization={component.organization}
+ qualifier={component.qualifier}
+ tooltipProps={{ projectKey: component.key }}
+ visibility={component.visibility}
+ />
+ )}
</h4>
<Summary component={component} measures={measures || {}} />
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
index b6a3c63c2b4..912bf578736 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
@@ -26,20 +26,19 @@ import Favorite from '../../../components/controls/Favorite';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import TagsList from '../../../components/tags/TagsList';
-import PrivateBadge from '../../../components/common/PrivateBadge';
+import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Project } from '../types';
+import { Organization } from '../../../app/types';
interface Props {
height: number;
- organization?: { key: string };
+ organization: Organization | undefined;
project: Project;
}
export default function ProjectCardLeak({ height, organization, project }: Props) {
const { measures } = project;
-
- const isPrivate = project.visibility === 'private';
const hasTags = project.tags.length > 0;
return (
@@ -60,23 +59,30 @@ export default function ProjectCardLeak({ height, organization, project }: Props
)}
<Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link>
</h2>
- {project.analysisDate && <ProjectCardQualityGate status={measures!['alert_status']} />}
+ {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
<div className="project-card-header-right">
- {isPrivate && <PrivateBadge className="spacer-left" qualifier="TRK" />}
+ <PrivacyBadgeContainer
+ className="spacer-left"
+ organization={organization || project.organization}
+ qualifier="TRK"
+ tooltipProps={{ projectKey: project.key }}
+ visibility={project.visibility}
+ />
+
{hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
</div>
</div>
{project.analysisDate &&
project.leakPeriodDate && (
<div className="project-card-dates note text-right pull-right">
- <DateFromNow date={project.leakPeriodDate!}>
+ <DateFromNow date={project.leakPeriodDate}>
{fromNow => (
<span className="project-card-leak-date pull-right">
{translateWithParameters('projects.leak_period_x', fromNow)}
</span>
)}
</DateFromNow>
- <DateTimeFormatter date={project.analysisDate!}>
+ <DateTimeFormatter date={project.analysisDate}>
{formattedDate => (
<span>
{translateWithParameters('projects.last_analysis_on_x', formattedDate)}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
index f653d14975d..859572fb80a 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
@@ -25,20 +25,20 @@ import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer
import Favorite from '../../../components/controls/Favorite';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import TagsList from '../../../components/tags/TagsList';
-import PrivateBadge from '../../../components/common/PrivateBadge';
+import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Project } from '../types';
+import { Organization } from '../../../app/types';
interface Props {
height: number;
- organization?: { key: string };
+ organization: Organization | undefined;
project: Project;
}
export default function ProjectCardOverall({ height, organization, project }: Props) {
const { measures } = project;
- const isPrivate = project.visibility === 'private';
const hasTags = project.tags.length > 0;
return (
@@ -61,7 +61,13 @@ export default function ProjectCardOverall({ height, organization, project }: Pr
</h2>
{project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
<div className="project-card-header-right">
- {isPrivate && <PrivateBadge className="spacer-left" qualifier="TRK" />}
+ <PrivacyBadgeContainer
+ className="spacer-left"
+ organization={organization || project.organization}
+ qualifier="TRK"
+ tooltipProps={{ projectKey: project.key }}
+ visibility={project.visibility}
+ />
{hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
index 3a25da57bf3..c2982771b7c 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ProjectCardLeak from '../ProjectCardLeak';
+import { Visibility } from '../../../../app/types';
const MEASURES = {
alert_status: 'OK',
@@ -36,11 +37,11 @@ const PROJECT = {
name: 'Foo',
organization: { key: 'org', name: 'org' },
tags: [],
- visibility: 'public'
+ visibility: Visibility.Public
};
it('should display analysis date and leak start date', () => {
- const card = shallow(<ProjectCardLeak height={100} project={PROJECT} />);
+ const card = shallow(<ProjectCardLeak height={100} organization={undefined} project={PROJECT} />);
expect(card.find('.project-card-dates').exists()).toBeTruthy();
expect(card.find('.project-card-dates').find('DateFromNow')).toHaveLength(1);
expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1);
@@ -48,28 +49,30 @@ it('should display analysis date and leak start date', () => {
it('should not display analysis date or leak start date', () => {
const project = { ...PROJECT, analysisDate: undefined };
- const card = shallow(<ProjectCardLeak height={100} project={project} />);
+ const card = shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />);
expect(card.find('.project-card-dates').exists()).toBeFalsy();
});
it('should display tags', () => {
const project = { ...PROJECT, tags: ['foo', 'bar'] };
expect(
- shallow(<ProjectCardLeak height={100} project={project} />)
+ shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />)
.find('TagsList')
.exists()
).toBeTruthy();
});
-it('should private badge', () => {
- const project = { ...PROJECT, visibility: 'private' };
+it('should display private badge', () => {
+ const project = { ...PROJECT, visibility: Visibility.Private };
expect(
- shallow(<ProjectCardLeak height={100} project={project} />)
- .find('PrivateBadge')
+ shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />)
+ .find('Connect(PrivacyBadge)')
.exists()
).toBeTruthy();
});
it('should display the leak measures and quality gate', () => {
- expect(shallow(<ProjectCardLeak height={100} project={PROJECT} />)).toMatchSnapshot();
+ expect(
+ shallow(<ProjectCardLeak height={100} organization={undefined} project={PROJECT} />)
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
index fc3e58bbd80..4ed5e6753ab 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ProjectCardOverall from '../ProjectCardOverall';
+import { Visibility } from '../../../../app/types';
const MEASURES = {
alert_status: 'OK',
@@ -35,17 +36,23 @@ const PROJECT = {
name: 'Foo',
organization: { key: 'org', name: 'org' },
tags: [],
- visibility: 'public'
+ visibility: Visibility.Public
};
it('should display analysis date (and not leak period) when defined', () => {
expect(
- shallow(<ProjectCardOverall height={100} project={PROJECT} />)
+ shallow(<ProjectCardOverall height={100} organization={undefined} project={PROJECT} />)
.find('.project-card-dates')
.exists()
).toBeTruthy();
expect(
- shallow(<ProjectCardOverall height={100} project={{ ...PROJECT, analysisDate: undefined }} />)
+ shallow(
+ <ProjectCardOverall
+ height={100}
+ organization={undefined}
+ project={{ ...PROJECT, analysisDate: undefined }}
+ />
+ )
.find('.project-card-dates')
.exists()
).toBeFalsy();
@@ -54,7 +61,7 @@ it('should display analysis date (and not leak period) when defined', () => {
it('should not display the quality gate', () => {
const project = { ...PROJECT, analysisDate: undefined };
expect(
- shallow(<ProjectCardOverall height={100} project={project} />)
+ shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
.find('ProjectCardOverallQualityGate')
.exists()
).toBeFalsy();
@@ -63,21 +70,23 @@ it('should not display the quality gate', () => {
it('should display tags', () => {
const project = { ...PROJECT, tags: ['foo', 'bar'] };
expect(
- shallow(<ProjectCardOverall height={100} project={project} />)
+ shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
.find('TagsList')
.exists()
).toBeTruthy();
});
-it('should private badge', () => {
- const project = { ...PROJECT, visibility: 'private' };
+it('should display private badge', () => {
+ const project = { ...PROJECT, visibility: Visibility.Private };
expect(
- shallow(<ProjectCardOverall height={100} project={project} />)
- .find('PrivateBadge')
+ shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
+ .find('Connect(PrivacyBadge)')
.exists()
).toBeTruthy();
});
it('should display the overall measures and quality gate', () => {
- expect(shallow(<ProjectCardOverall height={100} project={PROJECT} />)).toMatchSnapshot();
+ expect(
+ shallow(<ProjectCardOverall height={100} organization={undefined} project={PROJECT} />)
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
index fa106ed2ac6..1c7f8335013 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
@@ -47,7 +47,24 @@ exports[`should display the leak measures and quality gate 1`] = `
/>
<div
className="project-card-header-right"
- />
+ >
+ <Connect(PrivacyBadge)
+ className="spacer-left"
+ organization={
+ Object {
+ "key": "org",
+ "name": "org",
+ }
+ }
+ qualifier="TRK"
+ tooltipProps={
+ Object {
+ "projectKey": "foo",
+ }
+ }
+ visibility="public"
+ />
+ </div>
</div>
<div
className="project-card-dates note text-right pull-right"
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
index 263d0bf5fd0..039ca9bd89c 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
@@ -47,7 +47,24 @@ exports[`should display the overall measures and quality gate 1`] = `
/>
<div
className="project-card-header-right"
- />
+ >
+ <Connect(PrivacyBadge)
+ className="spacer-left"
+ organization={
+ Object {
+ "key": "org",
+ "name": "org",
+ }
+ }
+ qualifier="TRK"
+ tooltipProps={
+ Object {
+ "projectKey": "foo",
+ }
+ }
+ visibility="public"
+ />
+ </div>
</div>
<div
className="project-card-dates note text-right"
diff --git a/server/sonar-web/src/main/js/apps/projects/types.ts b/server/sonar-web/src/main/js/apps/projects/types.ts
index 2d339fb7d4c..e997ca3eae6 100644
--- a/server/sonar-web/src/main/js/apps/projects/types.ts
+++ b/server/sonar-web/src/main/js/apps/projects/types.ts
@@ -17,6 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Visibility } from '../../app/types';
+
export interface Project {
analysisDate?: string;
isFavorite?: boolean;
@@ -26,7 +28,7 @@ export interface Project {
name: string;
organization?: { key: string; name: string };
tags: string[];
- visibility: string;
+ visibility: Visibility;
}
export interface Facet {
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
index 688230948db..23d9bb2830c 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Risk from '../Risk';
+import { Visibility } from '../../../../app/types';
it('renders', () => {
const project1 = {
@@ -27,7 +28,7 @@ it('renders', () => {
measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734' },
name: 'Foo',
tags: [],
- visibility: 'public'
+ visibility: Visibility.Public
};
expect(
shallow(<Risk displayOrganizations={false} helpText="foobar" projects={[project1]} />)
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
index 4224630f25d..5d0acd4b3c3 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import SimpleBubbleChart from '../SimpleBubbleChart';
+import { Visibility } from '../../../../app/types';
it('renders', () => {
const project1 = {
@@ -28,7 +29,7 @@ it('renders', () => {
measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734', security_rating: '2' },
name: 'Foo',
tags: [],
- visibility: 'public'
+ visibility: Visibility.Public
};
expect(
shallow(
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
index 34d6db3ebae..2a5bdbeee27 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
@@ -35,7 +35,7 @@ import { translate } from '../../helpers/l10n';
export interface Props {
currentUser: { login: string };
hasProvisionPermission?: boolean;
- onVisibilityChange: (visibility: string) => void;
+ onVisibilityChange: (visibility: Visibility) => void;
organization: Organization;
topLevelQualifiers: string[];
}
@@ -215,19 +215,19 @@ export default class App extends React.PureComponent<Props, State> {
<Projects
currentUser={this.props.currentUser}
- ready={this.state.ready}
- projects={this.state.projects}
- selection={this.state.selection}
- onProjectSelected={this.onProjectSelected}
onProjectDeselected={this.onProjectDeselected}
+ onProjectSelected={this.onProjectSelected}
organization={this.props.organization}
+ projects={this.state.projects}
+ ready={this.state.ready}
+ selection={this.state.selection}
/>
<ListFooter
- ready={this.state.ready}
count={this.state.projects.length}
- total={this.state.total}
loadMore={this.loadMore}
+ ready={this.state.ready}
+ total={this.state.total}
/>
{this.state.createProjectForm && (
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 d208e4e5b9b..42f537dbbe6 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
@@ -35,7 +35,7 @@ interface StateProps {
interface DispatchProps {
fetchOrganization: (organization: string) => void;
- onVisibilityChange: (organization: Organization, visibility: string) => void;
+ onVisibilityChange: (organization: Organization, visibility: Visibility) => void;
}
interface OwnProps {
@@ -51,7 +51,7 @@ class AppContainer extends React.PureComponent<OwnProps & StateProps & DispatchP
}
}
- handleVisibilityChange = (visibility: string) => {
+ handleVisibilityChange = (visibility: Visibility) => {
if (this.props.organization) {
this.props.onVisibilityChange(this.props.organization, visibility);
}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
index 5537721739e..8853af4a079 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
@@ -21,8 +21,7 @@ import * as React from 'react';
import { Link } from 'react-router';
import ProjectRowActions from './ProjectRowActions';
import { Project } from './utils';
-import { Visibility } from '../../app/types';
-import PrivateBadge from '../../components/common/PrivateBadge';
+import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer';
import Checkbox from '../../components/controls/Checkbox';
import QualifierIcon from '../../components/icons-components/QualifierIcon';
import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
@@ -41,7 +40,7 @@ export default class ProjectRow extends React.PureComponent<Props> {
};
render() {
- const { project, selected } = this.props;
+ const { organization, project, selected } = this.props;
return (
<tr>
@@ -58,9 +57,12 @@ export default class ProjectRow extends React.PureComponent<Props> {
</td>
<td className="thin nowrap">
- {project.visibility === Visibility.Private && (
- <PrivateBadge qualifier={project.qualifier} />
- )}
+ <PrivacyBadgeContainer
+ organization={organization}
+ qualifier={project.qualifier}
+ tooltipProps={{ projectKey: project.key }}
+ visibility={project.visibility}
+ />
</td>
<td className="nowrap">
@@ -78,7 +80,7 @@ export default class ProjectRow extends React.PureComponent<Props> {
<td className="thin nowrap">
<ProjectRowActions
currentUser={this.props.currentUser}
- organization={this.props.organization}
+ organization={organization}
project={project}
/>
</td>
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
index f8b0f32445d..3299c3e33b4 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
@@ -129,8 +129,8 @@ it('creates project', () => {
it('changes default project visibility', () => {
const onVisibilityChange = jest.fn();
const wrapper = shallowRender({ onVisibilityChange });
- wrapper.find('Header').prop<Function>('onVisibilityChange')('private');
- expect(onVisibilityChange).toBeCalledWith('private');
+ wrapper.find('Header').prop<Function>('onVisibilityChange')(Visibility.Private);
+ expect(onVisibilityChange).toBeCalledWith(Visibility.Private);
});
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
index 1b78dda0438..9d462d5d584 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
@@ -53,13 +53,13 @@ it('changes visibility', () => {
click(wrapper.find('a[data-visibility="private"]'), {
currentTarget: {
blur() {},
- dataset: { visibility: 'private' }
+ dataset: { visibility: Visibility.Private }
}
});
expect(wrapper).toMatchSnapshot();
click(wrapper.find('.js-confirm'));
- expect(onConfirm).toBeCalledWith('private');
+ expect(onConfirm).toBeCalledWith(Visibility.Private);
});
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
index 96ea19b42a2..bc9e0ff3a6d 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
@@ -51,7 +51,7 @@ it('creates project', async () => {
change(wrapper.find('input[name="key"]'), 'key', {
currentTarget: { name: 'key', value: 'key' }
});
- wrapper.find('VisibilitySelector').prop<Function>('onChange')('private');
+ wrapper.find('VisibilitySelector').prop<Function>('onChange')(Visibility.Private);
wrapper.update();
expect(wrapper).toMatchSnapshot();
@@ -60,7 +60,7 @@ it('creates project', async () => {
name: 'name',
organization: 'org',
project: 'key',
- visibility: 'private'
+ visibility: Visibility.Private
});
expect(wrapper).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
index 25b2a6ac78d..63a914ea80a 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
@@ -22,7 +22,7 @@ import { shallow } from 'enzyme';
import Projects from '../Projects';
import { Visibility } from '../../../app/types';
-const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public };
const projects = [
{ key: 'a', name: 'A', qualifier: 'TRK', visibility: Visibility.Public },
{ key: 'b', name: 'B', qualifier: 'TRK', visibility: Visibility.Public }
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
index 6d05b0406f7..f97f8899d35 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
@@ -39,8 +39,14 @@ exports[`renders 1`] = `
<td
className="thin nowrap"
>
- <PrivateBadge
+ <Connect(PrivacyBadge)
qualifier="TRK"
+ tooltipProps={
+ Object {
+ "projectKey": "project",
+ }
+ }
+ visibility="private"
/>
</td>
<td
@@ -122,8 +128,14 @@ exports[`renders 2`] = `
<td
className="thin nowrap"
>
- <PrivateBadge
+ <Connect(PrivacyBadge)
qualifier="TRK"
+ tooltipProps={
+ Object {
+ "projectKey": "project",
+ }
+ }
+ visibility="private"
/>
</td>
<td
diff --git a/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
new file mode 100644
index 00000000000..5b05edbeb4d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
@@ -0,0 +1,123 @@
+/*
+ * 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 * as classNames from 'classnames';
+import { connect } from 'react-redux';
+import * as theme from '../../app/theme';
+import Tooltip from '../controls/Tooltip';
+import { translate } from '../../helpers/l10n';
+import { Visibility, Organization, CurrentUser } from '../../app/types';
+import { isSonarCloud } from '../../helpers/system';
+import { isCurrentUserMemberOf, isPaidOrganization } from '../../helpers/organizations';
+import { getCurrentUser, getOrganizationByKey, getMyOrganizations } from '../../store/rootReducer';
+import VisibleIcon from '../icons-components/VisibleIcon';
+import DocTooltip from '../docs/DocTooltip';
+
+interface StateToProps {
+ currentUser: CurrentUser;
+ organization?: Organization;
+ userOrganizations: Organization[];
+}
+
+interface OwnProps {
+ className?: string;
+ organization: Organization | string | undefined;
+ qualifier: string;
+ tooltipProps?: { projectKey: string };
+ visibility: Visibility;
+}
+
+interface Props extends OwnProps, StateToProps {
+ organization: Organization | undefined;
+}
+
+export function PrivacyBadge({
+ className,
+ currentUser,
+ organization,
+ qualifier,
+ userOrganizations,
+ tooltipProps,
+ visibility
+}: Props) {
+ const onSonarCloud = isSonarCloud();
+ if (
+ visibility !== Visibility.Private &&
+ (!onSonarCloud || !isCurrentUserMemberOf(currentUser, organization, userOrganizations))
+ ) {
+ return null;
+ }
+
+ let icon = null;
+ if (isPaidOrganization(organization) && visibility === Visibility.Public) {
+ icon = <VisibleIcon className="little-spacer-right" fill={theme.blue} />;
+ }
+
+ const badge = (
+ <div
+ className={classNames('outline-badge', className, {
+ 'badge-info': Boolean(icon),
+ 'badge-icon': Boolean(icon)
+ })}>
+ {icon}
+ {translate('visibility', visibility)}
+ </div>
+ );
+
+ if (onSonarCloud && organization) {
+ let docUrl = `project/visibility-${visibility}`;
+ if (visibility === Visibility.Public) {
+ if (icon) {
+ docUrl += '-paid-org';
+ }
+ if (organization.canAdmin) {
+ docUrl += '-admin';
+ }
+ }
+
+ return (
+ <DocTooltip
+ className={className}
+ doc={docUrl}
+ overlayProps={{ ...tooltipProps, organization: organization.key }}>
+ {badge}
+ </DocTooltip>
+ );
+ }
+
+ return (
+ <Tooltip overlay={translate('visibility', visibility, 'description', qualifier)}>
+ {badge}
+ </Tooltip>
+ );
+}
+
+const mapStateToProps = (state: any, { organization }: OwnProps) => {
+ if (typeof organization === 'string') {
+ organization = getOrganizationByKey(state, organization);
+ }
+ return {
+ currentUser: getCurrentUser(state),
+ organization,
+ userOrganizations: getMyOrganizations(state)
+ };
+};
+
+export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(PrivacyBadge);
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx
new file mode 100644
index 00000000000..336a20948da
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 { PrivacyBadge } from '../PrivacyBadgeContainer';
+import { Visibility, OrganizationSubscription } from '../../../app/types';
+import { isSonarCloud } from '../../../helpers/system';
+
+jest.mock('../../../helpers/system', () => ({ isSonarCloud: jest.fn().mockReturnValue(false) }));
+
+const organization = { key: 'foo', name: 'Foo' };
+const loggedInUser = { isLoggedIn: true, login: 'luke', name: 'Skywalker' };
+
+it('renders', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('do not render', () => {
+ expect(getWrapper({ visibility: Visibility.Public })).toMatchSnapshot();
+});
+
+it('renders public', () => {
+ (isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true);
+ expect(getWrapper({ visibility: Visibility.Public })).toMatchSnapshot();
+});
+
+it('renders public with icon', () => {
+ (isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true);
+ expect(
+ getWrapper({
+ organization: {
+ ...organization,
+ canAdmin: true,
+ subscription: OrganizationSubscription.Paid
+ },
+ visibility: Visibility.Public
+ })
+ ).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <PrivacyBadge
+ currentUser={loggedInUser}
+ organization={organization}
+ qualifier="TRK"
+ userOrganizations={[organization]}
+ visibility={Visibility.Private}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx
deleted file mode 100644
index 462b6f1aa04..00000000000
--- a/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx
+++ /dev/null
@@ -1,26 +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 PrivateBadge from '../PrivateBadge';
-
-it('renders', () => {
- expect(shallow(<PrivateBadge qualifier="TRK" />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap
new file mode 100644
index 00000000000..b0aef67a7db
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`do not render 1`] = `""`;
+
+exports[`renders 1`] = `
+<Tooltip
+ overlay="visibility.private.description.TRK"
+>
+ <div
+ className="outline-badge"
+ >
+ visibility.private
+ </div>
+</Tooltip>
+`;
+
+exports[`renders public 1`] = `
+<DocTooltip
+ doc="project/visibility-public"
+ overlayProps={
+ Object {
+ "organization": "foo",
+ }
+ }
+>
+ <div
+ className="outline-badge"
+ >
+ visibility.public
+ </div>
+</DocTooltip>
+`;
+
+exports[`renders public with icon 1`] = `
+<DocTooltip
+ doc="project/visibility-public-paid-org-admin"
+ overlayProps={
+ Object {
+ "organization": "foo",
+ }
+ }
+>
+ <div
+ className="outline-badge badge-info badge-icon"
+ >
+ <VisibleIcon
+ className="little-spacer-right"
+ fill="#4b9fd5"
+ />
+ visibility.public
+ </div>
+</DocTooltip>
+`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap
deleted file mode 100644
index b52275d7c3b..00000000000
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<Tooltip
- overlay="visibility.private.description.TRK"
->
- <div
- className="outline-badge"
- >
- visibility.private
- </div>
-</Tooltip>
-`;
diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
index 2d1f1c6579f..8e9c594a4fd 100644
--- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
@@ -29,13 +29,20 @@ import { separateFrontMatter } from '../../helpers/markdown';
import { isSonarCloud } from '../../helpers/system';
interface Props {
+ childProps?: { [k: string]: string };
className?: string;
content: string | undefined;
displayH1?: boolean;
isTooltip?: boolean;
}
-export default function DocMarkdownBlock({ className, content, displayH1, isTooltip }: Props) {
+export default function DocMarkdownBlock({
+ childProps,
+ className,
+ content,
+ displayH1,
+ isTooltip
+}: Props) {
const parsed = separateFrontMatter(content || '');
return (
<div className={classNames('markdown', className)}>
@@ -48,7 +55,7 @@ export default function DocMarkdownBlock({ className, content, displayH1, isTool
// do not render outer <div />
div: React.Fragment,
// use custom link to render documentation anchors
- a: isTooltip ? DocTooltipLink : DocLink,
+ a: isTooltip ? withChildProps(DocTooltipLink, childProps) : DocLink,
// used to handle `@include`
p: DocParagraph,
// use custom img tag to render documentation images
@@ -62,6 +69,15 @@ export default function DocMarkdownBlock({ className, content, displayH1, isTool
);
}
+function withChildProps<P>(
+ WrappedComponent: React.ComponentType<P & { customProps?: { [k: string]: string } }>,
+ childProps?: { [k: string]: string }
+) {
+ return function withChildProps(props: P) {
+ return <WrappedComponent customProps={childProps} {...props} />;
+ };
+}
+
function filterContent(content: string) {
const beginning = isSonarCloud() ? '<!-- sonarqube -->' : '<!-- sonarcloud -->';
const ending = isSonarCloud() ? '<!-- /sonarqube -->' : '<!-- /sonarcloud -->';
diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
index 754008144a4..3f7d4ec0195 100644
--- a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
@@ -28,6 +28,7 @@ interface Props {
children?: React.ReactNode;
/** Key of the documentation chunk */
doc: string;
+ overlayProps?: { [k: string]: string };
}
interface State {
@@ -82,7 +83,12 @@ export default class DocTooltip extends React.PureComponent<Props, State> {
{this.state.loading ? (
<i className="spinner" />
) : (
- <DocMarkdownBlock className="cut-margins" content={this.state.content} isTooltip={true} />
+ <DocMarkdownBlock
+ childProps={this.props.overlayProps}
+ className="cut-margins"
+ content={this.state.content}
+ isTooltip={true}
+ />
)}
</div>
);
diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx
index 897b3568e89..7160ab51370 100644
--- a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx
@@ -19,25 +19,45 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
+import { forEach } from 'lodash';
import DetachIcon from '../../components/icons-components/DetachIcon';
-export default function DocTooltipLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
- const { children, href, ...other } = props;
+interface OwnProps {
+ customProps?: { [k: string]: string };
+}
+
+type Props = OwnProps & React.AnchorHTMLAttributes<HTMLAnchorElement>;
+
+const SONARCLOUD_LINK = '/#sonarcloud#/';
+
+export default function DocTooltipLink({ children, customProps, href, ...other }: Props) {
+ if (customProps) {
+ forEach(customProps, (value, key) => {
+ if (href) {
+ href = href.replace(`#${key}#`, encodeURIComponent(value));
+ }
+ });
+ }
+
+ if (href && href.startsWith('/')) {
+ if (href.startsWith(SONARCLOUD_LINK)) {
+ href = `/${href.substr(SONARCLOUD_LINK.length)}`;
+ } else {
+ href = `/documentation/${href.substr(1)}`;
+ }
+
+ return (
+ <Link rel="noopener noreferrer" target="_blank" to={href} {...other}>
+ {children}
+ </Link>
+ );
+ }
+
return (
<>
- {href && href.startsWith('/') ? (
- <Link
- rel="noopener noreferrer"
- target="_blank"
- to={`/documentation/${href.substr(1)}`}
- {...other}>
- {children}
- </Link>
- ) : (
- <a href={href} rel="noopener noreferrer" target="_blank" {...other}>
- {children}
- </a>
- )}
+ <a href={href} rel="noopener noreferrer" target="_blank" {...other}>
+ {children}
+ </a>
<DetachIcon className="little-spacer-left little-spacer-right vertical-baseline" size={12} />
</>
);
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
index 898ff22a5a3..16deb2d6838 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
@@ -71,3 +71,15 @@ text`;
(isSonarCloud as jest.Mock).mockImplementation(() => true);
expect(shallow(<DocMarkdownBlock content={content} />)).toMatchSnapshot();
});
+
+it('should render with custom props for links', () => {
+ expect(
+ shallow(
+ <DocMarkdownBlock
+ childProps={{ foo: 'bar' }}
+ content="some [link](#quality-profiles)"
+ isTooltip={true}
+ />
+ ).find('withChildProps')
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx
index 21a44671719..e57b122d21a 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx
@@ -28,3 +28,9 @@ it('should render simple link', () => {
it('should render internal link', () => {
expect(shallow(<DocTooltipLink href="/foo/bar" />)).toMatchSnapshot();
});
+
+it('should render links with custom props', () => {
+ expect(
+ shallow(<DocTooltipLink customProps={{ bar: 'baz' }} href="/#sonarcloud#/foo/#bar#" />)
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
index e5364db5d6a..9e7a8b437fb 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
@@ -104,3 +104,12 @@ exports[`should render use custom component for links 1`] = `
link
</DocLink>
`;
+
+exports[`should render with custom props for links 1`] = `
+<withChildProps
+ href="#quality-profiles"
+ key="h-3"
+>
+ link
+</withChildProps>
+`;
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap
index efb4518b4ae..007500cb059 100644
--- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap
@@ -1,19 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render internal link 1`] = `
-<React.Fragment>
- <Link
- onlyActiveOnIndex={false}
- rel="noopener noreferrer"
- style={Object {}}
- target="_blank"
- to="/documentation/foo/bar"
- />
- <DetachIcon
- className="little-spacer-left little-spacer-right vertical-baseline"
- size={12}
- />
-</React.Fragment>
+<Link
+ onlyActiveOnIndex={false}
+ rel="noopener noreferrer"
+ style={Object {}}
+ target="_blank"
+ to="/documentation/foo/bar"
+/>
+`;
+
+exports[`should render links with custom props 1`] = `
+<Link
+ onlyActiveOnIndex={false}
+ rel="noopener noreferrer"
+ style={Object {}}
+ target="_blank"
+ to="/foo/baz"
+/>
`;
exports[`should render simple link 1`] = `
diff --git a/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx b/server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx
index 3fdad3d849e..c22ea4722f1 100644
--- a/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx
+++ b/server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx
@@ -18,21 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
-import Tooltip from '../controls/Tooltip';
-import { translate } from '../../helpers/l10n';
+import Icon, { IconProps } from './Icon';
-interface Props {
- className?: string;
- qualifier: string;
-}
-
-export default function PrivateBadge({ className, qualifier }: Props) {
+export default function VisibleIcon({ className, fill = 'currentColor', size }: IconProps) {
return (
- <Tooltip overlay={translate('visibility.private.description', qualifier)}>
- <div className={classNames('outline-badge', className)}>
- {translate('visibility.private')}
- </div>
- </Tooltip>
+ <Icon className={className} size={size}>
+ <path
+ d="M13.524 8.403q-1.093-1.697-2.74-2.539 0.439 0.748 0.439 1.618 0 1.331-0.946 2.276t-2.276 0.946-2.276-0.946-0.946-2.276q0-0.87 0.439-1.618-1.647 0.842-2.74 2.539 0.957 1.474 2.399 2.348t3.125 0.874 3.125-0.874 2.399-2.348zM8.345 5.641q0-0.144-0.101-0.245t-0.245-0.101q-0.899 0-1.543 0.644t-0.644 1.543q0 0.144 0.101 0.245t0.245 0.101 0.245-0.101 0.101-0.245q0-0.619 0.439-1.057t1.057-0.439q0.144 0 0.245-0.101t0.101-0.245zM14.444 8.403q0 0.245-0.144 0.496-1.007 1.654-2.708 2.65t-3.593 0.996-3.593-1-2.708-2.647q-0.144-0.252-0.144-0.496t0.144-0.496q1.007-1.647 2.708-2.647t3.593-1 3.593 1 2.708 2.647q0.144 0.252 0.144 0.496z"
+ style={{ fill }}
+ />
+ </Icon>
);
}
diff --git a/server/sonar-web/src/main/js/helpers/organizations.ts b/server/sonar-web/src/main/js/helpers/organizations.ts
index 3d57db7ff3b..97a57c14238 100644
--- a/server/sonar-web/src/main/js/helpers/organizations.ts
+++ b/server/sonar-web/src/main/js/helpers/organizations.ts
@@ -42,6 +42,8 @@ export function isCurrentUserMemberOf(
return Boolean(
organization &&
isLoggedIn(currentUser) &&
- (organization.canAdmin || userOrganizations.some(org => org.key === organization.key))
+ (organization.canAdmin ||
+ organization.isAdmin ||
+ userOrganizations.some(org => org.key === organization.key))
);
}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index cddf7f289fd..96bf8b0b98c 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -459,7 +459,9 @@ sidebar.tools=Tools
visibility.both=Public, Private
visibility.public=Public
-visibility.public.description=This project is public. Anyone can browse and see the source code.
+visibility.public.description.TRK=This project is public. Anyone can browse and see the source code.
+visibility.public.description.VW=This portfolio is public. Anyone can browse it.
+visibility.public.description.APP=This application is public. Anyone can browse it.
visibility.public.description.short=Anyone can browse and see the source code.
visibility.private=Private
visibility.private.description.TRK=This project is private. Only authorized users can browse and see the source code.
@@ -2573,11 +2575,13 @@ 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.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
+
#------------------------------------------------------------------------------
#
# EMBEDED DOCS