]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10963 Improve privacy badges of projects and organization
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 6 Jul 2018 09:45:51 +0000 (11:45 +0200)
committerSonarTech <sonartech@sonarsource.com>
Wed, 11 Jul 2018 18:21:22 +0000 (20:21 +0200)
58 files changed:
server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md [new file with mode: 0644]
server/sonar-docs/src/tooltips/project/visibility-private.md [new file with mode: 0644]
server/sonar-docs/src/tooltips/project/visibility-public-admin.md [new file with mode: 0644]
server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md [new file with mode: 0644]
server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md [new file with mode: 0644]
server/sonar-docs/src/tooltips/project/visibility-public.md [new file with mode: 0644]
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/permissions.ts
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
server/sonar-web/src/main/js/app/styles/components/badges.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
server/sonar-web/src/main/js/apps/permissions/project/components/App.js
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/types.ts
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/PrivateBadge.tsx [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx
server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap
server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/organizations.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

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 (file)
index 0000000..757d33d
--- /dev/null
@@ -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 (file)
index 0000000..99d9647
--- /dev/null
@@ -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 (file)
index 0000000..defcc6b
--- /dev/null
@@ -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 (file)
index 0000000..54b8a3d
--- /dev/null
@@ -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 (file)
index 0000000..3be0178
--- /dev/null
@@ -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 (file)
index 0000000..b0d5c06
--- /dev/null
@@ -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)
index 7ae4d59aa4181fbc88609d22b8e7d9ec706751e5..a069c38b17e5203594abd335c0768fe4d05c7af7 100644 (file)
@@ -162,7 +162,7 @@ export interface Component {
   isFavorite?: boolean;
   analysisDate?: string;
   tags: string[];
-  visibility: string;
+  visibility: Visibility;
   leakPeriodDate?: string;
 }
 
index 189ed4ca8de1a40328fd1f2d32d901624d70d875..e14367bb6a7010e55bf3a98b5eea33374c0e3ef6 100644 (file)
@@ -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
index 9052c611e682844acc4683cced398929ce0e98b3..83a4afb28220a38008fdce7910c2fe91075777aa 100644 (file)
@@ -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 () => {
index 150c9c5092338383d263ddf8dc649c928825505f..e01fb137866e89c91eeef18fa021c2235eaa3b06 100644 (file)
@@ -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}
index 676a38a577b428a7cd8c7c72af2e28fcf078133f..ff1fdacdf465bae4a4cd9205bf9380de110c682c 100644 (file)
@@ -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);
-});
index 9d0e5a5a2d9f6e3f961729088e475384ddb00385..05d1f81238f26f5ae67549c5817093a5a268271f 100644 (file)
@@ -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);
+}
index bddfe372c4a96324131d5ed57c49f6fb106825c5..b546c8a212fa92557bca589f6cce0c9471afba44 100644 (file)
@@ -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 {
index 4cc5dd277f2fa104a53b0378186516fc90d84577..8d135c8fede4b9fa3bf98fc1e32340fc5757265d 100644 (file)
@@ -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 };
index e1230f3a18c3821debffaa2603941e6c0c3fb48c..5bf896954b58d3381cbbc85c456abbbec5952670 100644 (file)
@@ -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();
-});
+}
index 9802e06cdcbb1018ff22f8f043d4a8c13cc68604..c6b612738c1d9dcbd96411b598caa8baf587066a 100644 (file)
@@ -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
index 112d2ef75cef6d2258f64f389f48004cdf1a2701..d74444d133bb4528684f002019ffea05b7eb99f3 100644 (file)
@@ -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>
index 6815ae5dae9c872b30c2f6a1d1df9c39cb7ce8c5..10e2496c277e11d1b8afc7e21001b7c4befa443c 100644 (file)
  * 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 }}
index 6f780c28e2c17b93d8a06cfbdd5c8f8410a63646..ecfdd94f7808dd12861db0facf9a72b09c9a66f2 100644 (file)
@@ -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();
index 96d9f772c63f03fc9f2a5bf8ae3e902139350bcb..5009da6beb406943b8f49bbd9642b61ec3f27eb4 100644 (file)
 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();
+});
index 240404ba19f0ee207981d384fa177f84452c9c91..0da0558483a264bcf40352c1f36a475b5bf19897 100644 (file)
@@ -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)
index 62ee5647128624f5766812d9add5081139473035..3e7749343a551438749a5a86e0ed03c717668809 100644 (file)
@@ -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 && (
index 51d563afbe115010c638f05061532326f956e2aa..1fbf421aa71f134b5607f8765be5b7bce620fd92 100644 (file)
@@ -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');
     }
 
index 5815264b6a41c47d0bb62be3bf601fb763223996..dc5888fcd08b0ba2a136a14fa9c3fca74f0702fb 100644 (file)
@@ -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 });
       }
     );
   };
index 188130d59e11a01024fd4f6f079c3458e0e1c3c2..5e9b873b84127c087f22b220bbc773a434ccab56 100644 (file)
@@ -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>
index b6a3c63c2b45f0e162076767402e7e81fec0408c..912bf578736de6fa2eff8f3c66e71966ea668923 100644 (file)
@@ -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)}
index f653d14975ddd6611051116a03411bf30ec07f9b..859572fb80aefd419becc63db2bcdb0003de2537 100644 (file)
@@ -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>
index 3a25da57bf3b6770a09e1ecd9b78a952a567e12f..c2982771b7c42742b987903bb72c40810518648e 100644 (file)
@@ -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();
 });
index fc3e58bbd80c2518c92ad9a7c27366fe077ff5d4..4ed5e6753ab5de920502f98ae190f670d4da0678 100644 (file)
@@ -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();
 });
index fa106ed2ac6daeb4f4d13898a1c8a4633442a219..1c7f833501300a76bcb44a87c587ffee87ba1eb5 100644 (file)
@@ -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"
index 263d0bf5fd05eaf0f9a10f2e30be0635a247d256..039ca9bd89ccbaf9ca18320539a671d6b243af89 100644 (file)
@@ -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"
index 2d339fb7d4c56fd47ae2aafba28ddd69f04054ce..e997ca3eae697fac57c80dec7a776165eb8723d9 100644 (file)
@@ -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 {
index 688230948db59137fa70fde115add8ba4a699e88..23d9bb2830c482583d9d0d01e8a59ba9009fed2d 100644 (file)
@@ -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]} />)
index 4224630f25d5f350e46e092af52854a61c079e13..5d0acd4b3c38d3d402e379696f7d861c3618de68 100644 (file)
@@ -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(
index 34d6db3ebaecfc046d68fa26e37b325364a0207f..2a5bdbeee2738c0ac424f2b22c882e2f1df524b9 100644 (file)
@@ -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 && (
index d208e4e5b9b8535e13e1b33a26c89b585a9ad592..42f537dbbe6d88484cd763447dab79e277c2d6bc 100644 (file)
@@ -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);
     }
index 5537721739ed30f6afaa26f044718173d55a732f..8853af4a079e4532fca5ad43e540d103e73d7324 100644 (file)
@@ -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>
index f8b0f32445dfa55b8ba32af917c83549c9c2e150..3299c3e33b480b275b3cfa3eee7e9338a63f8b74 100644 (file)
@@ -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] }) {
index 1b78dda043815592a8842632a997c019db25dd1c..9d462d5d5846ed106012c65a422b356615f512bf 100644 (file)
@@ -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] }) {
index 96ea19b42a262ec0bffaf7e57cab21db9132d417..bc9e0ff3a6d1c43330abfafb16505f8c399e4420 100644 (file)
@@ -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();
 
index 25b2a6ac78dce78a6291dbdd3e64c26eff6b3fe8..63a914ea80a79a5c2266582d79f26fb9c25ce352 100644 (file)
@@ -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 }
index 6d05b0406f7dc788720fa74966e1d02141aa20f3..f97f8899d353ba67a255fafdc641e3a7f7fcf407 100644 (file)
@@ -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 (file)
index 0000000..5b05edb
--- /dev/null
@@ -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/PrivateBadge.tsx b/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx
deleted file mode 100644 (file)
index 3fdad3d..0000000
+++ /dev/null
@@ -1,38 +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 * as classNames from 'classnames';
-import Tooltip from '../controls/Tooltip';
-import { translate } from '../../helpers/l10n';
-
-interface Props {
-  className?: string;
-  qualifier: string;
-}
-
-export default function PrivateBadge({ className, qualifier }: Props) {
-  return (
-    <Tooltip overlay={translate('visibility.private.description', qualifier)}>
-      <div className={classNames('outline-badge', className)}>
-        {translate('visibility.private')}
-      </div>
-    </Tooltip>
-  );
-}
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 (file)
index 0000000..336a209
--- /dev/null
@@ -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 (file)
index 462b6f1..0000000
+++ /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 (file)
index 0000000..b0aef67
--- /dev/null
@@ -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 (file)
index b52275d..0000000
+++ /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>
-`;
index 2d1f1c6579f9218487f62fa5af3f9021dc20635d..8e9c594a4fde0cd826bd6a2238c17ac5cbbb549a 100644 (file)
@@ -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 -->';
index 754008144a4eb25c1d9d0993b9bad55e53bac45f..3f7d4ec019547c03f73c33067490700e3d9dab4b 100644 (file)
@@ -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>
     );
index 897b3568e89073b68e7ebb1f1f6b5306e60dc504..7160ab513706c8f2b51b1d38eb9a4b771d7fd8e3 100644 (file)
  */
 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} />
     </>
   );
index 898ff22a5a375858be3d7169d1a386790e25ed1c..16deb2d683898249f2eda983c2905fac41612ed1 100644 (file)
@@ -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();
+});
index 21a4467171912db82e206f4a1a51253df00d1c9f..e57b122d21ae8b24e1659335f0a2085f92ceca05 100644 (file)
@@ -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();
+});
index e5364db5d6aeee168059c815211f756533129281..9e7a8b437fb573e06c2a5fc98fcdd12dcafc20c0 100644 (file)
@@ -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>
+`;
index efb4518b4ae7882c68778a52f7e146191d1bee4d..007500cb05903c802782bd043d425fe796d2db39 100644 (file)
@@ -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/icons-components/VisibleIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx
new file mode 100644 (file)
index 0000000..c22ea47
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 Icon, { IconProps } from './Icon';
+
+export default function VisibleIcon({ className, fill = 'currentColor', size }: IconProps) {
+  return (
+    <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>
+  );
+}
index 3d57db7ff3ba3775d677c79ed00216e1fd3dc4f2..97a57c142389325ee2a02a59898c544e2eff6588 100644 (file)
@@ -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))
   );
 }
index cddf7f289fd25b976170fc3474e659f13cde7435..96bf8b0b98cbc2ae73f1261130a78332dab362a5 100644 (file)
@@ -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