Browse Source

SONAR-10676 Add documentation for team onboarding on SonarCloud "Members" page (#251)

tags/7.5
Stas Vilchik 6 years ago
parent
commit
e8b90e724c
28 changed files with 522 additions and 167 deletions
  1. 13
    8
      server/sonar-docs/gatsby-node.js
  2. 14
    0
      server/sonar-docs/src/EmbedDocsSuggestions.json
  3. 4
    0
      server/sonar-docs/src/pages/branches/branches-faq.md
  4. 4
    0
      server/sonar-docs/src/pages/branches/index.md
  5. 4
    0
      server/sonar-docs/src/pages/branches/long-lived-branches.md
  6. 4
    0
      server/sonar-docs/src/pages/branches/short-lived-branches.md
  7. 21
    0
      server/sonar-docs/src/pages/organizations/index.md
  8. 34
    0
      server/sonar-docs/src/pages/organizations/manage-team.md
  9. 23
    4
      server/sonar-docs/src/templates/page.js
  10. 5
    0
      server/sonar-docs/src/tooltips/organizations/add-organization-member.md
  11. 2
    1
      server/sonar-web/config/documentation-loader/fetch-matter.js
  12. 17
    8
      server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
  13. 63
    0
      server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx
  14. 12
    1
      server/sonar-web/src/main/js/apps/documentation/components/App.tsx
  15. 12
    2
      server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
  16. 5
    7
      server/sonar-web/src/main/js/apps/documentation/utils.ts
  17. 52
    0
      server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx
  18. 10
    6
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
  19. 9
    5
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx
  20. 0
    43
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js
  21. 11
    22
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx
  22. 0
    32
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap
  23. 39
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
  24. 10
    3
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap
  25. 49
    25
      server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
  26. 29
    0
      server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
  27. 74
    0
      server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
  28. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 13
- 8
server/sonar-docs/gatsby-node.js View 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();
});

+ 14
- 0
server/sonar-docs/src/EmbedDocsSuggestions.json View File

@@ -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",

+ 4
- 0
server/sonar-docs/src/pages/branches/branches-faq.md View 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.

+ 4
- 0
server/sonar-docs/src/pages/branches/index.md View 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

+ 4
- 0
server/sonar-docs/src/pages/branches/long-lived-branches.md View 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.

+ 4
- 0
server/sonar-docs/src/pages/branches/short-lived-branches.md View 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:

+ 21
- 0
server/sonar-docs/src/pages/organizations/index.md View File

@@ -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.

+ 34
- 0
server/sonar-docs/src/pages/organizations/manage-team.md View File

@@ -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.

+ 23
- 4
server/sonar-docs/src/templates/page.js View 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;
}

+ 5
- 0
server/sonar-docs/src/tooltips/organizations/add-organization-member.md View File

@@ -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)

+ 2
- 1
server/sonar-web/config/documentation-loader/fetch-matter.js View 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);

+ 17
- 8
server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx View File

@@ -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<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 });
}
}

+ 63
- 0
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx View File

@@ -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' }]
});
});

+ 12
- 1
server/sonar-web/src/main/js/apps/documentation/components/App.tsx View 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 });
}
}
},
() => {

+ 12
- 2
server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx View 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 }}

+ 5
- 7
server/sonar-web/src/main/js/apps/documentation/utils.ts View File

@@ -18,11 +18,12 @@
* 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;

+ 52
- 0
server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx View File

@@ -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>
);
}

+ 10
- 6
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js View 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>

+ 9
- 5
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx View 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" />
</>
);
}

+ 0
- 43
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js View File

@@ -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();
});

server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js → server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx View File

@@ -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 (
<header className="page-header">
{this.props.loading && <i className="spinner" />}
{this.props.children}
</header>
);
}
}
it('should render', () => {
const wrapper = shallow(
<MembersPageHeader loading={true}>
<span>children test</span>
</MembersPageHeader>
);
expect(wrapper).toMatchSnapshot();
});

+ 0
- 32
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap View File

@@ -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"
/>
`;

+ 39
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap View File

@@ -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>
`;

+ 10
- 3
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap View 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

+ 49
- 25
server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx View 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;
}

+ 29
- 0
server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx View 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();
});

+ 74
- 0
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap View 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"

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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

Loading…
Cancel
Save