]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10676 Add documentation for team onboarding on SonarCloud "Members" page (...
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 25 May 2018 07:37:15 +0000 (09:37 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 25 May 2018 18:20:48 +0000 (20:20 +0200)
29 files changed:
server/sonar-docs/gatsby-node.js
server/sonar-docs/src/EmbedDocsSuggestions.json
server/sonar-docs/src/pages/branches/branches-faq.md
server/sonar-docs/src/pages/branches/index.md
server/sonar-docs/src/pages/branches/long-lived-branches.md
server/sonar-docs/src/pages/branches/short-lived-branches.md
server/sonar-docs/src/pages/organizations/index.md [new file with mode: 0644]
server/sonar-docs/src/pages/organizations/manage-team.md [new file with mode: 0644]
server/sonar-docs/src/templates/page.js
server/sonar-docs/src/tooltips/organizations/add-organization-member.md [new file with mode: 0644]
server/sonar-web/config/documentation-loader/fetch-matter.js
server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/documentation/components/App.tsx
server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
server/sonar-web/src/main/js/apps/documentation/utils.ts
server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap
server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7bee8372b44d05cf419236df150beba043737d3d..c9cd2fd483794082f0297080311143cf79f2dec3 100644 (file)
@@ -41,6 +41,9 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
         allMarkdownRemark {
           edges {
             node {
+              frontmatter {
+                scope
+              }
               fields {
                 slug
               }
@@ -50,14 +53,16 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
       }
     `).then(result => {
       result.data.allMarkdownRemark.edges.forEach(({ node }) => {
-        createPage({
-          path: node.fields.slug,
-          component: path.resolve('./src/templates/page.js'),
-          context: {
-            // Data passed to context is available in page queries as GraphQL variables.
-            slug: node.fields.slug
-          }
-        });
+        if (node.frontmatter.scope !== 'sonarcloud') {
+          createPage({
+            path: node.fields.slug,
+            component: path.resolve('./src/templates/page.js'),
+            context: {
+              // Data passed to context is available in page queries as GraphQL variables.
+              slug: node.fields.slug
+            }
+          });
+        }
       });
       resolve();
     });
index 91be7e4892e7a17ac40ee270498d97a9ecb82902..fce00fdf5497574feef6a5fc6db860549dd6a7fd 100644 (file)
     }
   ],
   "marketplace": [],
+  "organization_members": [
+    {
+      "link": "/documentation/organizations/manage-team",
+      "text": "Manage a Team",
+      "scope": "sonarcloud"
+    }
+  ],
+  "organization_projects": [
+    {
+      "link": "/documentation/organizations/index",
+      "text": "Organizations",
+      "scope": "sonarcloud"
+    }
+  ],
   "overview": [
     {
       "link": "/documentation/fixing-the-water-leak",
index 63bc9cab628f8161beacc8ad38f211972420dabd..58bacdcf22e924d1542daf7609cddc9e27cf6782 100644 (file)
@@ -2,8 +2,12 @@
 title: Frequently Asked Branches Questions
 ---
 
+<!-- sonarqube -->
+
 _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_
 
+<!-- /sonarqube -->
+
 **Q:** How long are branches retained?  
 **A:** Long-lived branches are retained until you delete them manually (**Administration > Branches**).
 Short-lived branches are deleted automatically after 30 days with no analysis.
index 29f168cff2f7345595cf61648fa4d1af5e2730c9..67de486046c036ab02a2c3fff5eb6d0504f130d5 100644 (file)
@@ -2,8 +2,12 @@
 title: Branches
 ---
 
+<!-- sonarqube -->
+
 _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_
 
+<!-- /sonarqube -->
+
 Branch analysis allows you to
 
 * analyze long-lived branches
index 6498ae65888b7881eb486420037ad41b02b2d63b..010edc13b9ce59db04e777fa16c66ffc10e82539 100644 (file)
@@ -2,8 +2,12 @@
 title: Long-lived Branches
 ---
 
+<!-- sonarqube -->
+
 _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_
 
+<!-- /sonarqube -->
+
 ## Status vs Quality Gate
 
 The same quality gate that is applied to the project as a whole is automatically applied to long-lived branches as well. This is not editable.
index cdf79fc9c7c9fd95d3a6ec793a70915322b4a36b..25ebadbe4ea1ef417b1fc7148a1c1376b99888e9 100644 (file)
@@ -2,8 +2,12 @@
 title: Short-lived Branches
 ---
 
+<!-- sonarqube -->
+
 _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_
 
+<!-- /sonarqube -->
+
 ## Status vs Quality Gate
 
 For short-lived branches, there is a kind of hard-coded quality gate focusing only on new issues. Its status is reflected by the green|red signal associated with each short-lived branch:
diff --git a/server/sonar-docs/src/pages/organizations/index.md b/server/sonar-docs/src/pages/organizations/index.md
new file mode 100644 (file)
index 0000000..5a6fc29
--- /dev/null
@@ -0,0 +1,21 @@
+---
+title: Organizations
+scope: sonarcloud
+---
+
+## Overview
+
+An organization is a space where a team or a whole company can collaborate across many projects.
+
+An organization consists of:
+* Projects, on which users collaborate
+* [Members](/organizations/manage-team), who can have different persmissions on the projects
+* [Quality Profiles](/quality-profiles) and [Quality Gates](/quality-gates), which can be customized and shared accross projects
+
+There are 2 kind of organizations:
+* **Personal organizations**. Each account has a personal organization linked to it. This is typically where open-source developers host their personal projects. It is not possible to delete this kind of organization.
+* **Standard organization**. This is the kind of organization that users want to create for their companies or for their open-source communities. As soon as you want to collaborate, it is a good idea to create such an organization.
+
+Organizations can be on:
+* **Free plan**. This is the default plan. Every project in an organization on the free plan is public.
+* **Paid plan**. This plan unlocks the ability to have private projects. Go to the "Billing" page of your organization to upgrade it to the paid plan.
diff --git a/server/sonar-docs/src/pages/organizations/manage-team.md b/server/sonar-docs/src/pages/organizations/manage-team.md
new file mode 100644 (file)
index 0000000..53d650f
--- /dev/null
@@ -0,0 +1,34 @@
+---
+title: Manage a Team
+scope: sonarcloud
+---
+
+Members can collaborate on the projects in the organizations to which they belong. Depending on their permisssions within the organization, members can:
+* Analyse projects
+* Manage project settings (permissions, visibility, quality profiles, ...)
+* Update issues
+* Manage quality gates and quality profiles
+* Administer the organization itself
+
+## Adding Members
+
+Adding members is done on the "Members" page of the organization, and this can be done only by an administrator of 
+the organization.
+
+Adding a user as a member is possible only if that user has already signed up on SonarCloud. If the user never authenticated to
+the system, the administrator will simply not be able to find the user in the search modal window.
+
+## Granting permissions
+
+Once added, a user can be granted permissions to perform various operations in the organization. It is up to the 
+administrator who added the user to make sure that she gets the relevant permissions.
+
+Organization admins will prefer to create groups to manage permissions, and add new users to those
+groups through the "Members" page. With such an approach, they won't have to manage individal permissions at
+project level for instance.
+
+## Future evolutions
+
+Future versions of SonarCloud will make this onboarding process easier thanks to better integrations with GitHub, 
+Bitbucket Cloud and VSTS: users won't have to sign up prior to joining an organization, and their permissions will 
+be retrieved at best from the ones existing on the other systems.
index 0a4347fdabbd02e6ead9d16cac92c2b4c193d6f2..de57fc1a2a479ba91240a290c81e23c951e242c8 100644 (file)
@@ -22,10 +22,13 @@ import Helmet from 'react-helmet';
 
 export default ({ data }) => {
   const page = data.markdownRemark;
-  const htmlWithInclusions = page.html.replace(/\<p\>@include (.*)\<\/p\>/, (_, path) => {
-    const chunk = data.allMarkdownRemark.edges.find(edge => edge.node.fields.slug === path);
-    return chunk ? chunk.node.html : '';
-  });
+  const htmlWithInclusions = cutSonarCloudContent(page.html).replace(
+    /\<p\>@include (.*)\<\/p\>/,
+    (_, path) => {
+      const chunk = data.allMarkdownRemark.edges.find(edge => edge.node.fields.slug === path);
+      return chunk ? chunk.node.html : '';
+    }
+  );
 
   return (
     <div css={{ paddingTop: 24, paddingBottom: 24 }}>
@@ -56,3 +59,19 @@ export const query = graphql`
     }
   }
 `;
+
+function cutSonarCloudContent(content) {
+  const beginning = '<!-- sonarcloud -->';
+  const ending = '<!-- /sonarcloud -->';
+
+  let newContent = content;
+  let start = newContent.indexOf(beginning);
+  let end = newContent.indexOf(ending);
+  while (start !== -1 && end !== -1) {
+    newContent = newContent.substring(0, start) + newContent.substring(end + ending.length);
+    start = newContent.indexOf(beginning);
+    end = newContent.indexOf(ending);
+  }
+
+  return newContent;
+}
diff --git a/server/sonar-docs/src/tooltips/organizations/add-organization-member.md b/server/sonar-docs/src/tooltips/organizations/add-organization-member.md
new file mode 100644 (file)
index 0000000..ef8bd37
--- /dev/null
@@ -0,0 +1,5 @@
+Add new members to this organization and manage their permissions. Note that users must have signed up on the service to be able to find and add them.
+
+---
+
+See also: [Manage a Team](/organizations/manage-team)
index 657a07f08e9fb44243be1b930dd98523985c5b77..424d491d94866ddc2339ee20fd0a056b333aa388 100644 (file)
@@ -37,7 +37,8 @@ module.exports = (root, files) => {
         name: path.basename(file).slice(0, -3),
         relativeName: file.slice(0, -3),
         title: headerData.title || file,
-        order: headerData.order || -1
+        order: headerData.order || -1,
+        scope: headerData.scope && headerData.scope.toLowerCase()
       };
     })
     .sort(compare);
index c400a69fabadde1af1aa9930b86971510627e1fa..bf1efda4cbbf917ccc4e7d63f66f492961c6d933 100644 (file)
  */
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
-//eslint-disable-next-line import/no-extraneous-dependencies
+// eslint-disable-next-line import/no-extraneous-dependencies
 import * as suggestionsJson from 'Docs/EmbedDocsSuggestions.json';
 import { SuggestionsContext } from './SuggestionsContext';
 
 export interface SuggestionLink {
-  text: string;
   link: string;
+  scope?: 'sonarcloud';
+  text: string;
 }
 
 interface SuggestionsJson {
-  [key: string]: Array<SuggestionLink>;
+  [key: string]: SuggestionLink[];
 }
 
 interface Props {
-  children: ({ suggestions }: { suggestions: Array<SuggestionLink> }) => React.ReactNode;
+  children: ({ suggestions }: { suggestions: SuggestionLink[] }) => React.ReactNode;
 }
 
 interface State {
-  suggestions: Array<SuggestionLink>;
+  suggestions: SuggestionLink[];
 }
 
 export default class SuggestionsProvider extends React.Component<Props, State> {
@@ -47,7 +48,11 @@ export default class SuggestionsProvider extends React.Component<Props, State> {
     suggestions: PropTypes.object
   };
 
-  state = { suggestions: [] };
+  static contextTypes = {
+    onSonarCloud: PropTypes.bool
+  };
+
+  state: State = { suggestions: [] };
 
   getChildContext = (): { suggestions: SuggestionsContext } => {
     return {
@@ -60,7 +65,7 @@ export default class SuggestionsProvider extends React.Component<Props, State> {
 
   fetchSuggestions = () => {
     const jsonList = suggestionsJson as SuggestionsJson;
-    let suggestions: Array<SuggestionLink> = [];
+    let suggestions: SuggestionLink[] = [];
     this.keys.forEach(key => {
       if (jsonList[key]) {
         suggestions = [...jsonList[key], ...suggestions];
@@ -80,6 +85,10 @@ export default class SuggestionsProvider extends React.Component<Props, State> {
   };
 
   render() {
-    return this.props.children({ suggestions: this.state.suggestions });
+    const suggestions = this.context.onSonarCloud
+      ? this.state.suggestions
+      : this.state.suggestions.filter(suggestion => suggestion.scope !== 'sonarcloud');
+
+    return this.props.children({ suggestions });
   }
 }
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx
new file mode 100644 (file)
index 0000000..32a957e
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 SuggestionsProvider from '../SuggestionsProvider';
+
+jest.mock(
+  'Docs/EmbedDocsSuggestions.json',
+  () => ({
+    pageA: [{ link: '/foo', text: 'Foo' }, { link: '/bar', text: 'Bar', scope: 'sonarcloud' }],
+    pageB: [{ link: '/qux', text: 'Qux' }]
+  }),
+  { virtual: true }
+);
+
+it('should add & remove suggestions', () => {
+  const children = jest.fn();
+  const wrapper = shallow(<SuggestionsProvider>{children}</SuggestionsProvider>);
+  const instance = wrapper.instance() as SuggestionsProvider;
+  expect(children).lastCalledWith({ suggestions: [] });
+
+  instance.addSuggestions('pageA');
+  expect(children).lastCalledWith({ suggestions: [{ link: '/foo', text: 'Foo' }] });
+
+  instance.addSuggestions('pageB');
+  expect(children).lastCalledWith({
+    suggestions: [{ link: '/qux', text: 'Qux' }, { link: '/foo', text: 'Foo' }]
+  });
+
+  instance.removeSuggestions('pageA');
+  expect(children).lastCalledWith({ suggestions: [{ link: '/qux', text: 'Qux' }] });
+});
+
+it('should show sonarcloud pages', () => {
+  const children = jest.fn();
+  const wrapper = shallow(<SuggestionsProvider>{children}</SuggestionsProvider>, {
+    context: { onSonarCloud: true }
+  });
+  const instance = wrapper.instance() as SuggestionsProvider;
+  expect(children).lastCalledWith({ suggestions: [] });
+
+  instance.addSuggestions('pageA');
+  expect(children).lastCalledWith({
+    suggestions: [{ link: '/foo', text: 'Foo' }, { link: '/bar', text: 'Bar', scope: 'sonarcloud' }]
+  });
+});
index c623b78944d51297edafb7163386816facf71e98..05ee09a677f28ce3723227ace92868daa70709a5 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import * as matter from 'gray-matter';
 import Helmet from 'react-helmet';
 import { Link } from 'react-router';
+import * as PropTypes from 'prop-types';
 import Menu from './Menu';
 import NotFound from '../../../app/components/NotFound';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
@@ -41,6 +42,11 @@ interface State {
 
 export default class App extends React.PureComponent<Props, State> {
   mounted = false;
+
+  static contextTypes = {
+    onSonarCloud: PropTypes.bool
+  };
+
   state: State = { loading: false, notFound: false };
 
   componentDidMount() {
@@ -65,7 +71,12 @@ export default class App extends React.PureComponent<Props, State> {
     import(`Docs/pages/${path === '' ? 'index' : path}.md`).then(
       ({ default: content }) => {
         if (this.mounted) {
-          this.setState({ content, loading: false, notFound: false });
+          const parsed = matter(content || '');
+          if (parsed.data.scope === 'sonarcloud' && !this.context.onSonarCloud) {
+            this.setState({ loading: false, notFound: true });
+          } else {
+            this.setState({ content, loading: false, notFound: false });
+          }
         }
       },
       () => {
index 6eb6a4c610dd84e0c39478161f9b099ed7a33e44..dffe69579ba0d80c2e5da3337e69c04a2c7d73db 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { Link } from 'react-router';
 import * as classNames from 'classnames';
+import * as PropTypes from 'prop-types';
 import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon';
 import {
   activeOrChildrenActive,
@@ -29,13 +30,22 @@ import {
 } from '../utils';
 import * as Docs from '../documentation.directory-loader';
 
+const pages = (Docs as any) as DocumentationEntry[];
+
 interface Props {
   splat?: string;
 }
 
 export default class Menu extends React.PureComponent<Props> {
+  static contextTypes = {
+    onSonarCloud: PropTypes.bool
+  };
+
   getMenuEntriesHierarchy = (root?: string): Array<DocumentationEntry> => {
-    const toplevelEntries = getEntryChildren(Docs as any, root);
+    const instancePages = this.context.onSonarCloud
+      ? pages
+      : pages.filter(page => page.scope !== 'sonarcloud');
+    const toplevelEntries = getEntryChildren(instancePages, root);
     toplevelEntries.forEach(entry => {
       const entryRoot = getEntryRoot(entry.relativeName);
       entry.children = entryRoot !== '' ? this.getMenuEntriesHierarchy(entryRoot) : [];
@@ -48,7 +58,7 @@ export default class Menu extends React.PureComponent<Props> {
     const opened = activeOrChildrenActive(this.props.splat || '', entry);
     const offset = 10 + 25 * depth;
     return (
-      <React.Fragment key={entry.name}>
+      <React.Fragment key={entry.relativeName}>
         <Link
           className={classNames('list-group-item', { active })}
           style={{ paddingLeft: offset }}
index 70ec0d1f12b01d24a3e48ea57f49cd7cb69c00f5..114c670ee6e7228876cd7006601b3eacaaaa6e58 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 export interface DocumentationEntry {
-  title: string;
-  order: string;
+  children: DocumentationEntry[];
   name: string;
+  order: string;
   relativeName: string;
-  children: Array<DocumentationEntry>;
+  scope?: 'sonarcloud';
+  title: string;
 }
 
 export function activeOrChildrenActive(root: string, entry: DocumentationEntry) {
@@ -39,10 +40,7 @@ export function getEntryRoot(name: string) {
   return name;
 }
 
-export function getEntryChildren(
-  entries: Array<DocumentationEntry>,
-  root?: string
-): Array<DocumentationEntry> {
+export function getEntryChildren(entries: DocumentationEntry[], root?: string) {
   return entries.filter(entry => {
     const parts = entry.relativeName.split('/');
     const depth = root ? root.split('/').length : 0;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js
deleted file mode 100644 (file)
index 97b2db4..0000000
+++ /dev/null
@@ -1,42 +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.
- */
-//@flow
-import React from 'react';
-
-/*::
-type Props = {
-  loading: boolean,
-  total?: number,
-  children?: React.Element<*>
-};
-*/
-
-export default class MembersPageHeader extends React.PureComponent {
-  /*:: props: Props; */
-
-  render() {
-    return (
-      <header className="page-header">
-        {this.props.loading && <i className="spinner" />}
-        {this.props.children}
-      </header>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx
new file mode 100644 (file)
index 0000000..9481d01
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import { translate } from '../../../helpers/l10n';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+
+interface Props {
+  children?: React.ReactNode;
+  loading: boolean;
+}
+
+export default function MembersPageHeader(props: Props) {
+  return (
+    <header className="page-header">
+      <h1 className="page-title">{translate('organization.members.page')}</h1>
+      <DeferredSpinner loading={props.loading} />
+      {props.children}
+      <p className="page-description">
+        <FormattedMessage
+          defaultMessage={translate('organization.members.page.description')}
+          id="organization.members.page.description"
+          values={{
+            link: (
+              <Link to="/documentation/organizations/manage-team">
+                {translate('organization.members.manage_a_team')}
+              </Link>
+            )
+          }}
+        />
+      </p>
+    </header>
+  );
+}
index cda04c3f200d52336a0fa2d6ebe2b699d73f9d16..b56d8b58188c195c562cc0fd82b4aa1a30b75cd5 100644 (file)
@@ -24,7 +24,9 @@ import MembersPageHeader from './MembersPageHeader';
 import MembersListHeader from './MembersListHeader';
 import MembersList from './MembersList';
 import AddMemberForm from './forms/AddMemberForm';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
 import ListFooter from '../../../components/controls/ListFooter';
+import DocTooltip from '../../../components/docs/DocTooltip';
 import { translate } from '../../../helpers/l10n';
 /*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
 /*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
@@ -89,31 +91,33 @@ export default class OrganizationMembers extends React.PureComponent {
     return (
       <div className="page page-limited">
         <Helmet title={translate('organization.members.page')} />
-        <MembersPageHeader loading={status.loading} total={status.total}>
+        <Suggestions suggestions="organization_members" />
+        <MembersPageHeader loading={status.loading}>
           {organization.canAdmin && (
             <div className="page-actions">
               <AddMemberForm
                 addMember={this.addMember}
-                organization={organization}
                 memberLogins={this.props.memberLogins}
+                organization={organization}
               />
+              <DocTooltip className="spacer-left" doc="organizations/add-organization-member" />
             </div>
           )}
         </MembersPageHeader>
-        <MembersListHeader total={status.total} handleSearch={this.handleSearchMembers} />
+        <MembersListHeader handleSearch={this.handleSearchMembers} total={status.total} />
         <MembersList
           members={members}
-          organizationGroups={this.props.organizationGroups}
           organization={organization}
+          organizationGroups={this.props.organizationGroups}
           removeMember={this.removeMember}
           updateMemberGroups={this.updateMemberGroups}
         />
         {status.total != null && (
           <ListFooter
             count={members.length}
-            total={status.total}
-            ready={!status.loading}
             loadMore={this.handleLoadMoreMembers}
+            ready={!status.loading}
+            total={status.total}
           />
         )}
       </div>
index 7f2fa5e839db0b16e9746cd84ec9d74fad0472df..09ae20f81513d49df6439a3a8a6adbd250dab84f 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import AllProjectsContainer from '../../projects/components/AllProjectsContainer';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
 
 interface Props {
   location: { pathname: string; query: { [x: string]: string } };
@@ -27,10 +28,13 @@ interface Props {
 
 export default function OrganizationProjects(props: Props) {
   return (
-    <AllProjectsContainer
-      isFavorite={false}
-      location={props.location}
-      organization={props.organization}
-    />
+    <>
+      <AllProjectsContainer
+        isFavorite={false}
+        location={props.location}
+        organization={props.organization}
+      />
+      <Suggestions suggestions="organization_projects" />
+    </>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js
deleted file mode 100644 (file)
index 030b6cc..0000000
+++ /dev/null
@@ -1,43 +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 React from 'react';
-import { shallow } from 'enzyme';
-import MembersPageHeader from '../MembersPageHeader';
-
-it('should render the members page header', () => {
-  const wrapper = shallow(<MembersPageHeader />);
-  expect(wrapper).toMatchSnapshot();
-  wrapper.setProps({ loading: true });
-  expect(wrapper.find('.spinner')).toMatchSnapshot();
-});
-
-it('should render the members page header with the total', () => {
-  const wrapper = shallow(<MembersPageHeader total="5" />);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render its children', () => {
-  const wrapper = shallow(
-    <MembersPageHeader loading={true} total="5">
-      <span>children test</span>
-    </MembersPageHeader>
-  );
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx
new file mode 100644 (file)
index 0000000..eb2dbe0
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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 MembersPageHeader from '../MembersPageHeader';
+
+it('should render', () => {
+  const wrapper = shallow(
+    <MembersPageHeader loading={true}>
+      <span>children test</span>
+    </MembersPageHeader>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap
deleted file mode 100644 (file)
index b4da660..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render its children 1`] = `
-<header
-  className="page-header"
->
-  <i
-    className="spinner"
-  />
-  <span>
-    children test
-  </span>
-</header>
-`;
-
-exports[`should render the members page header 1`] = `
-<header
-  className="page-header"
-/>
-`;
-
-exports[`should render the members page header 2`] = `
-<i
-  className="spinner"
-/>
-`;
-
-exports[`should render the members page header with the total 1`] = `
-<header
-  className="page-header"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
new file mode 100644 (file)
index 0000000..86a1603
--- /dev/null
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<header
+  className="page-header"
+>
+  <h1
+    className="page-title"
+  >
+    organization.members.page
+  </h1>
+  <DeferredSpinner
+    loading={true}
+    timeout={100}
+  />
+  <span>
+    children test
+  </span>
+  <p
+    className="page-description"
+  >
+    <FormattedMessage
+      defaultMessage="organization.members.page.description"
+      id="organization.members.page.description"
+      values={
+        Object {
+          "link": <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/documentation/organizations/manage-team"
+          >
+            organization.members.manage_a_team
+          </Link>,
+        }
+      }
+    />
+  </p>
+</header>
+`;
index 28a46b239f11d1976061b1d2dc3fed6571306bba..6fb9690cf4bc9c466e4cda5fea07848d9f3381b8 100644 (file)
@@ -9,9 +9,10 @@ exports[`should not render actions for non admin 1`] = `
     encodeSpecialCharacters={true}
     title="organization.members.page"
   />
-  <MembersPageHeader
-    total={2}
+  <Suggestions
+    suggestions="organization_members"
   />
+  <MembersPageHeader />
   <MembersListHeader
     handleSearch={[Function]}
     total={2}
@@ -60,9 +61,11 @@ exports[`should render actions for admin 1`] = `
     encodeSpecialCharacters={true}
     title="organization.members.page"
   />
+  <Suggestions
+    suggestions="organization_members"
+  />
   <MembersPageHeader
     loading={true}
-    total={2}
   >
     <div
       className="page-actions"
@@ -77,6 +80,10 @@ exports[`should render actions for admin 1`] = `
           }
         }
       />
+      <DocTooltip
+        className="spacer-left"
+        doc="organizations/add-organization-member"
+      />
     </div>
   </MembersPageHeader>
   <MembersListHeader
index 88fcfb65cc8d0a7ca1215fde824e8b70f6c7ebb4..8d3ae04e71d82e673a9a8251a1dc0c0bc6c3a0a0 100644 (file)
@@ -22,6 +22,7 @@ import * as classNames from 'classnames';
 import remark from 'remark';
 import reactRenderer from 'remark-react';
 import * as matter from 'gray-matter';
+import * as PropTypes from 'prop-types';
 import DocLink from './DocLink';
 import DocParagraph from './DocParagraph';
 import DocImg from './DocImg';
@@ -32,29 +33,52 @@ interface Props {
   displayH1?: boolean;
 }
 
-export default function DocMarkdownBlock({ className, content, displayH1 }: Props) {
-  const parsed = matter(content || '');
-  return (
-    <div className={classNames('markdown', className)}>
-      {displayH1 && <h1>{parsed.data.title}</h1>}
-      {
-        remark()
-          // .use(remarkInclude)
-          .use(reactRenderer, {
-            remarkReactComponents: {
-              // do not render outer <div />
-              div: React.Fragment,
-              // use custom link to render documentation anchors
-              a: DocLink,
-              // used to handle `@include`
-              p: DocParagraph,
-              // use custom img tag to render documentation images
-              img: DocImg
-            },
-            toHast: {}
-          })
-          .processSync(parsed.content).contents
-      }
-    </div>
-  );
+export default class DocMarkdownBlock extends React.PureComponent<Props> {
+  static contextTypes = {
+    onSonarCloud: PropTypes.bool
+  };
+
+  render() {
+    const { className, content, displayH1 } = this.props;
+    const parsed = matter(content || '');
+    return (
+      <div className={classNames('markdown', className)}>
+        {displayH1 && <h1>{parsed.data.title}</h1>}
+        {
+          remark()
+            // .use(remarkInclude)
+            .use(reactRenderer, {
+              remarkReactComponents: {
+                // do not render outer <div />
+                div: React.Fragment,
+                // use custom link to render documentation anchors
+                a: DocLink,
+                // used to handle `@include`
+                p: DocParagraph,
+                // use custom img tag to render documentation images
+                img: DocImg
+              },
+              toHast: {}
+            })
+            .processSync(filterContent(parsed.content, this.context.onSonarCloud)).contents
+        }
+      </div>
+    );
+  }
+}
+
+function filterContent(content: string, onSonarCloud: boolean) {
+  const beginning = onSonarCloud ? '<!-- sonarqube -->' : '<!-- sonarcloud -->';
+  const ending = onSonarCloud ? '<!-- /sonarqube -->' : '<!-- /sonarcloud -->';
+
+  let newContent = content;
+  let start = newContent.indexOf(beginning);
+  let end = newContent.indexOf(ending);
+  while (start !== -1 && end !== -1) {
+    newContent = newContent.substring(0, start) + newContent.substring(end + ending.length);
+    start = newContent.indexOf(beginning);
+    end = newContent.indexOf(ending);
+  }
+
+  return newContent;
 }
index 587343d7c0bf327ba5a7130cb094883dbccf40fa..ebe70a272478f84c29dbd765c3a9c437c5c5cbe7 100644 (file)
@@ -41,3 +41,32 @@ it('should render use custom component for links', () => {
     shallow(<DocMarkdownBlock content="some [link](#quality-profiles)" />).find('DocLink')
   ).toMatchSnapshot();
 });
+
+it.only('should cut sonarqube/sonarcloud content', () => {
+  const content = `
+some
+
+<!-- sonarqube -->
+sonarqube
+<!-- /sonarqube -->
+
+<!-- sonarcloud -->
+sonarcloud
+<!-- /sonarcloud -->
+
+<!-- sonarqube -->
+  long
+
+  multiline
+<!-- /sonarqube -->
+
+text`;
+
+  expect(
+    shallow(<DocMarkdownBlock content={content} />, { context: { onSonarCloud: false } })
+  ).toMatchSnapshot();
+
+  expect(
+    shallow(<DocMarkdownBlock content={content} />, { context: { onSonarCloud: true } })
+  ).toMatchSnapshot();
+});
index 7d44e66dc156e2684b8f3f69be9c2812ccb13d86..e5364db5d6aeee168059c815211f756533129281 100644 (file)
@@ -1,5 +1,79 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`should cut sonarqube/sonarcloud content 1`] = `
+<div
+  className="markdown"
+>
+  <React.Fragment
+    key="h-1"
+  >
+    <DocParagraph
+      key="h-2"
+    >
+      some
+    </DocParagraph>
+    
+
+    <DocParagraph
+      key="h-3"
+    >
+      sonarqube
+    </DocParagraph>
+    
+
+    <DocParagraph
+      key="h-4"
+    >
+        long
+    </DocParagraph>
+    
+
+    <DocParagraph
+      key="h-5"
+    >
+        multiline
+    </DocParagraph>
+    
+
+    <DocParagraph
+      key="h-6"
+    >
+      text
+    </DocParagraph>
+  </React.Fragment>
+</div>
+`;
+
+exports[`should cut sonarqube/sonarcloud content 2`] = `
+<div
+  className="markdown"
+>
+  <React.Fragment
+    key="h-1"
+  >
+    <DocParagraph
+      key="h-2"
+    >
+      some
+    </DocParagraph>
+    
+
+    <DocParagraph
+      key="h-3"
+    >
+      sonarcloud
+    </DocParagraph>
+    
+
+    <DocParagraph
+      key="h-4"
+    >
+      text
+    </DocParagraph>
+  </React.Fragment>
+</div>
+`;
+
 exports[`should render simple markdown 1`] = `
 <div
   className="markdown"
index 64a545329d5b8c122908e387f799abb2d475fed5..92c53af4b182b74e931083584408e6357ee7030a 100644 (file)
@@ -2543,6 +2543,7 @@ organization.updated=Organization details have been updated.
 organization.url=Url
 organization.url.description=Url of the homepage of the organization.
 organization.members.page=Members
+organization.members.page.description=Add users to the organization and grant them permissions to work on the projects. See {link} documentation.
 organization.members.add=Add a member
 organization.members.x_groups={0} group(s)
 organization.members.members=member(s)
@@ -2550,6 +2551,7 @@ organization.members.remove=Remove from organization's members
 organization.members.remove_x=Are you sure you want to remove {0} from {1}'s members ?
 organization.members.manage_groups=Manage groups
 organization.members.members_groups={0}'s groups:
+organization.members.manage_a_team=Manage a team
 organization.members.add_to_members=Add to members
 organization.default_visibility_of_new_projects=Default visibility of new projects:
 organization.change_visibility_form.header=Set Default Visibility of New Projects