@@ -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(); | |||
}); |
@@ -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", |
@@ -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. |
@@ -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 |
@@ -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. |
@@ -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: |
@@ -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. |
@@ -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. |
@@ -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; | |||
} |
@@ -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) |
@@ -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); |
@@ -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 }); | |||
} | |||
} |
@@ -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' }] | |||
}); | |||
}); |
@@ -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 }); | |||
} | |||
} | |||
}, | |||
() => { |
@@ -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 }} |
@@ -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; |
@@ -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> | |||
); | |||
} |
@@ -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> |
@@ -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" /> | |||
</> | |||
); | |||
} |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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" | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -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 |
@@ -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; | |||
} |
@@ -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(); | |||
}); |
@@ -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" |
@@ -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 |