From e8b90e724c5f8d4eac7607f8e40db222eb7555e9 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Fri, 25 May 2018 09:37:15 +0200 Subject: [PATCH] SONAR-10676 Add documentation for team onboarding on SonarCloud "Members" page (#251) --- server/sonar-docs/gatsby-node.js | 21 ++++-- .../sonar-docs/src/EmbedDocsSuggestions.json | 14 ++++ .../src/pages/branches/branches-faq.md | 4 + server/sonar-docs/src/pages/branches/index.md | 4 + .../src/pages/branches/long-lived-branches.md | 4 + .../pages/branches/short-lived-branches.md | 4 + .../src/pages/organizations/index.md | 21 ++++++ .../src/pages/organizations/manage-team.md | 34 +++++++++ server/sonar-docs/src/templates/page.js | 27 ++++++- .../organizations/add-organization-member.md | 5 ++ .../documentation-loader/fetch-matter.js | 3 +- .../embed-docs-modal/SuggestionsProvider.tsx | 25 +++++-- .../__tests__/SuggestionsProvider-test.tsx | 63 ++++++++++++++++ .../js/apps/documentation/components/App.tsx | 13 +++- .../js/apps/documentation/components/Menu.tsx | 14 +++- .../src/main/js/apps/documentation/utils.ts | 12 ++- .../components/MembersPageHeader.tsx | 52 +++++++++++++ .../components/OrganizationMembers.js | 16 ++-- .../components/OrganizationProjects.tsx | 14 ++-- .../__tests__/MembersPageHeader-test.js | 43 ----------- .../MembersPageHeader-test.tsx} | 33 +++------ .../MembersPageHeader-test.js.snap | 32 -------- .../MembersPageHeader-test.tsx.snap | 39 ++++++++++ .../OrganizationMembers-test.js.snap | 13 +++- .../js/components/docs/DocMarkdownBlock.tsx | 74 ++++++++++++------- .../docs/__tests__/DocMarkdownBlock-test.tsx | 29 ++++++++ .../DocMarkdownBlock-test.tsx.snap | 74 +++++++++++++++++++ .../resources/org/sonar/l10n/core.properties | 2 + 28 files changed, 522 insertions(+), 167 deletions(-) create mode 100644 server/sonar-docs/src/pages/organizations/index.md create mode 100644 server/sonar-docs/src/pages/organizations/manage-team.md create mode 100644 server/sonar-docs/src/tooltips/organizations/add-organization-member.md create mode 100644 server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx delete mode 100644 server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js rename server/sonar-web/src/main/js/apps/organizations/components/{MembersPageHeader.js => __tests__/MembersPageHeader-test.tsx} (67%) delete mode 100644 server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap diff --git a/server/sonar-docs/gatsby-node.js b/server/sonar-docs/gatsby-node.js index 7bee8372b44..c9cd2fd4837 100644 --- a/server/sonar-docs/gatsby-node.js +++ b/server/sonar-docs/gatsby-node.js @@ -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(); }); diff --git a/server/sonar-docs/src/EmbedDocsSuggestions.json b/server/sonar-docs/src/EmbedDocsSuggestions.json index 91be7e4892e..fce00fdf549 100644 --- a/server/sonar-docs/src/EmbedDocsSuggestions.json +++ b/server/sonar-docs/src/EmbedDocsSuggestions.json @@ -33,6 +33,20 @@ } ], "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", diff --git a/server/sonar-docs/src/pages/branches/branches-faq.md b/server/sonar-docs/src/pages/branches/branches-faq.md index 63bc9cab628..58bacdcf22e 100644 --- a/server/sonar-docs/src/pages/branches/branches-faq.md +++ b/server/sonar-docs/src/pages/branches/branches-faq.md @@ -2,8 +2,12 @@ title: Frequently Asked Branches Questions --- + + _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_ + + **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. diff --git a/server/sonar-docs/src/pages/branches/index.md b/server/sonar-docs/src/pages/branches/index.md index 29f168cff2f..67de486046c 100644 --- a/server/sonar-docs/src/pages/branches/index.md +++ b/server/sonar-docs/src/pages/branches/index.md @@ -2,8 +2,12 @@ title: Branches --- + + _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_ + + Branch analysis allows you to * analyze long-lived branches diff --git a/server/sonar-docs/src/pages/branches/long-lived-branches.md b/server/sonar-docs/src/pages/branches/long-lived-branches.md index 6498ae65888..010edc13b9c 100644 --- a/server/sonar-docs/src/pages/branches/long-lived-branches.md +++ b/server/sonar-docs/src/pages/branches/long-lived-branches.md @@ -2,8 +2,12 @@ title: Long-lived Branches --- + + _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_ + + ## 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. diff --git a/server/sonar-docs/src/pages/branches/short-lived-branches.md b/server/sonar-docs/src/pages/branches/short-lived-branches.md index cdf79fc9c7c..25ebadbe4ea 100644 --- a/server/sonar-docs/src/pages/branches/short-lived-branches.md +++ b/server/sonar-docs/src/pages/branches/short-lived-branches.md @@ -2,8 +2,12 @@ title: Short-lived Branches --- + + _Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_ + + ## 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 index 00000000000..5a6fc299309 --- /dev/null +++ b/server/sonar-docs/src/pages/organizations/index.md @@ -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 index 00000000000..53d650f3746 --- /dev/null +++ b/server/sonar-docs/src/pages/organizations/manage-team.md @@ -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. diff --git a/server/sonar-docs/src/templates/page.js b/server/sonar-docs/src/templates/page.js index 0a4347fdabb..de57fc1a2a4 100644 --- a/server/sonar-docs/src/templates/page.js +++ b/server/sonar-docs/src/templates/page.js @@ -22,10 +22,13 @@ import Helmet from 'react-helmet'; export default ({ data }) => { const page = data.markdownRemark; - const htmlWithInclusions = page.html.replace(/\@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( + /\@include (.*)\<\/p\>/, + (_, path) => { + const chunk = data.allMarkdownRemark.edges.find(edge => edge.node.fields.slug === path); + return chunk ? chunk.node.html : ''; + } + ); return (
@@ -56,3 +59,19 @@ export const query = graphql` } } `; + +function cutSonarCloudContent(content) { + const beginning = ''; + const ending = ''; + + 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 index 00000000000..ef8bd37c141 --- /dev/null +++ b/server/sonar-docs/src/tooltips/organizations/add-organization-member.md @@ -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) diff --git a/server/sonar-web/config/documentation-loader/fetch-matter.js b/server/sonar-web/config/documentation-loader/fetch-matter.js index 657a07f08e9..424d491d948 100644 --- a/server/sonar-web/config/documentation-loader/fetch-matter.js +++ b/server/sonar-web/config/documentation-loader/fetch-matter.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx index c400a69faba..bf1efda4cbb 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx @@ -19,25 +19,26 @@ */ 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; + [key: string]: SuggestionLink[]; } interface Props { - children: ({ suggestions }: { suggestions: Array }) => React.ReactNode; + children: ({ suggestions }: { suggestions: SuggestionLink[] }) => React.ReactNode; } interface State { - suggestions: Array; + suggestions: SuggestionLink[]; } export default class SuggestionsProvider extends React.Component { @@ -47,7 +48,11 @@ export default class SuggestionsProvider extends React.Component { 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 { fetchSuggestions = () => { const jsonList = suggestionsJson as SuggestionsJson; - let suggestions: Array = []; + 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 { }; 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 index 00000000000..32a957e9117 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx @@ -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({children}); + 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({children}, { + 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' }] + }); +}); diff --git a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx index c623b78944d..05ee09a677f 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx @@ -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 { 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 { 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 }); + } } }, () => { diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx index 6eb6a4c610d..dffe69579ba 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx @@ -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 { + static contextTypes = { + onSonarCloud: PropTypes.bool + }; + getMenuEntriesHierarchy = (root?: string): Array => { - 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 { const opened = activeOrChildrenActive(this.props.splat || '', entry); const offset = 10 + 25 * depth; return ( - + ; + 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, - root?: string -): Array { +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.tsx b/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx new file mode 100644 index 00000000000..9481d010480 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx @@ -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 ( +
+

{translate('organization.members.page')}

+ + {props.children} +

+ + {translate('organization.members.manage_a_team')} + + ) + }} + /> +

+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js index cda04c3f200..b56d8b58188 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js @@ -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 (
- + + {organization.canAdmin && (
+
)}
- + {status.total != null && ( )}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx index 7f2fa5e839d..09ae20f8151 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx @@ -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 ( - + <> + + + ); } 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 index 030b6cc6643..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js +++ /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(); - 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(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render its children', () => { - const wrapper = shallow( - - children test - - ); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx similarity index 67% rename from server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js rename to server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx index 97b2db4e0d7..eb2dbe02f4d 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx @@ -17,26 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -//@flow -import React from 'react'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import MembersPageHeader from '../MembersPageHeader'; -/*:: -type Props = { - loading: boolean, - total?: number, - children?: React.Element<*> -}; -*/ - -export default class MembersPageHeader extends React.PureComponent { - /*:: props: Props; */ - - render() { - return ( -
- {this.props.loading && } - {this.props.children} -
- ); - } -} +it('should render', () => { + const wrapper = shallow( + + children test + + ); + 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 index b4da660437e..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render its children 1`] = ` -
- - - children test - -
-`; - -exports[`should render the members page header 1`] = ` -
-`; - -exports[`should render the members page header 2`] = ` - -`; - -exports[`should render the members page header with the total 1`] = ` -
-`; 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 index 00000000000..86a16038cda --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +
+

+ organization.members.page +

+ + + children test + +

+ + organization.members.manage_a_team + , + } + } + /> +

+
+`; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap index 28a46b239f1..6fb9690cf4b 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap @@ -9,9 +9,10 @@ exports[`should not render actions for non admin 1`] = ` encodeSpecialCharacters={true} title="organization.members.page" /> - + +
+
- {displayH1 &&

{parsed.data.title}

} - { - remark() - // .use(remarkInclude) - .use(reactRenderer, { - remarkReactComponents: { - // do not render outer
- 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 - } -
- ); +export default class DocMarkdownBlock extends React.PureComponent { + static contextTypes = { + onSonarCloud: PropTypes.bool + }; + + render() { + const { className, content, displayH1 } = this.props; + const parsed = matter(content || ''); + return ( +
+ {displayH1 &&

{parsed.data.title}

} + { + remark() + // .use(remarkInclude) + .use(reactRenderer, { + remarkReactComponents: { + // do not render outer
+ 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 + } +
+ ); + } +} + +function filterContent(content: string, onSonarCloud: boolean) { + const beginning = onSonarCloud ? '' : ''; + const ending = onSonarCloud ? '' : ''; + + 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-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx index 587343d7c0b..ebe70a27247 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx +++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx @@ -41,3 +41,32 @@ it('should render use custom component for links', () => { shallow().find('DocLink') ).toMatchSnapshot(); }); + +it.only('should cut sonarqube/sonarcloud content', () => { + const content = ` +some + + +sonarqube + + + +sonarcloud + + + + long + + multiline + + +text`; + + expect( + shallow(, { context: { onSonarCloud: false } }) + ).toMatchSnapshot(); + + expect( + shallow(, { context: { onSonarCloud: true } }) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap index 7d44e66dc15..e5364db5d6a 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap @@ -1,5 +1,79 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should cut sonarqube/sonarcloud content 1`] = ` +
+ + + some + + + + + sonarqube + + + + + long + + + + + multiline + + + + + text + + +
+`; + +exports[`should cut sonarqube/sonarcloud content 2`] = ` +
+ + + some + + + + + sonarcloud + + + + + text + + +
+`; + exports[`should render simple markdown 1`] = `