Browse Source

MMF-1420 Ease management of Embedded Docs navigation (#699)

tags/7.5
Pascal Mugnier 5 years ago
parent
commit
7d7bfd09c1
79 changed files with 2632 additions and 562 deletions
  1. 39
    16
      server/sonar-docs/README.md
  2. 9
    11
      server/sonar-docs/gatsby-node.js
  3. 16
    2
      server/sonar-docs/package.json
  4. 29
    29
      server/sonar-docs/src/EmbedDocsSuggestions.json
  5. 178
    0
      server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js
  6. 52
    32
      server/sonar-docs/src/layouts/components/CategoryLink.js
  7. 34
    0
      server/sonar-docs/src/layouts/components/ExternalLink.js
  8. 4
    4
      server/sonar-docs/src/layouts/components/Footer.js
  9. 35
    0
      server/sonar-docs/src/layouts/components/HeadingAnchor.js
  10. 13
    17
      server/sonar-docs/src/layouts/components/HeadingsLink.js
  11. 15
    10
      server/sonar-docs/src/layouts/components/Search.js
  12. 1
    1
      server/sonar-docs/src/layouts/components/SearchEntryResult.js
  13. 72
    29
      server/sonar-docs/src/layouts/components/Sidebar.js
  14. 32
    0
      server/sonar-docs/src/layouts/components/icons/DetachIcon.js
  15. 2
    9
      server/sonar-docs/src/layouts/index.js
  16. 22
    8
      server/sonar-docs/src/layouts/utils.js
  17. 8
    0
      server/sonar-docs/src/pages/404.md
  18. 1
    0
      server/sonar-docs/src/pages/analysis/background-tasks.md
  19. 3
    1
      server/sonar-docs/src/pages/analysis/generic-issue.md
  20. 1
    0
      server/sonar-docs/src/pages/analysis/generic-test.md
  21. 2
    1
      server/sonar-docs/src/pages/analysis/overview.md
  22. 2
    1
      server/sonar-docs/src/pages/analysis/pull-request.md
  23. 2
    1
      server/sonar-docs/src/pages/analysis/scm-integration.md
  24. 19
    19
      server/sonar-docs/src/pages/branches/branches-faq.md
  25. 1
    0
      server/sonar-docs/src/pages/branches/long-lived-branches.md
  26. 7
    6
      server/sonar-docs/src/pages/branches/overview.md
  27. 1
    0
      server/sonar-docs/src/pages/branches/short-lived-branches.md
  28. 10
    5
      server/sonar-docs/src/pages/index.md
  29. 1
    1
      server/sonar-docs/src/pages/instance-administration/custom-measures.md
  30. 1
    0
      server/sonar-docs/src/pages/instance-administration/housekeeping.md
  31. 1
    1
      server/sonar-docs/src/pages/instance-administration/look-and-feel.md
  32. 1
    0
      server/sonar-docs/src/pages/instance-administration/quality-profiles.md
  33. 0
    10
      server/sonar-docs/src/pages/integrations/index.md
  34. 1
    0
      server/sonar-docs/src/pages/project-administration/webhooks.md
  35. 6
    6
      server/sonar-docs/src/pages/sonarcloud/analyze-a-project.md
  36. 1
    1
      server/sonar-docs/src/pages/sonarcloud/integrations/bitbucketcloud.md
  37. 1
    1
      server/sonar-docs/src/pages/sonarcloud/integrations/github.md
  38. 1
    1
      server/sonar-docs/src/pages/sonarcloud/integrations/vsts.md
  39. 4
    4
      server/sonar-docs/src/pages/sonarcloud/organizations/index.md
  40. 1
    1
      server/sonar-docs/src/pages/sonarcloud/organizations/manage-team.md
  41. 1
    1
      server/sonar-docs/src/pages/sonarcloud/organizations/organization-visibility.md
  42. 1
    1
      server/sonar-docs/src/pages/sonarcloud/privacy.md
  43. 1
    1
      server/sonar-docs/src/pages/sonarcloud/security.md
  44. 1
    1
      server/sonar-docs/src/pages/sonarcloud/sonarcloud-pricing.md
  45. 4
    3
      server/sonar-docs/src/pages/user-guide/fixing-the-water-leak.md
  46. 1
    1
      server/sonar-docs/src/pages/user-guide/keyboard-shortcuts.md
  47. 1
    0
      server/sonar-docs/src/pages/user-guide/metric-definitions.md
  48. 2
    1
      server/sonar-docs/src/pages/user-guide/quality-gates.md
  49. 1
    0
      server/sonar-docs/src/pages/user-guide/security-reports.md
  50. 4
    3
      server/sonar-docs/src/pages/user-guide/user-account.md
  51. 6
    7
      server/sonar-docs/src/templates/page.js
  52. 24
    0
      server/sonar-docs/static/README.md
  53. 53
    0
      server/sonar-docs/static/SonarCloudNavigationTree.json
  54. 46
    0
      server/sonar-docs/static/SonarQubeNavigationTree.json
  55. 54
    0
      server/sonar-docs/static/StaticNavigationTree.json
  56. 1282
    70
      server/sonar-docs/yarn.lock
  57. 10
    4
      server/sonar-web/src/main/js/apps/documentation/components/App.tsx
  58. 59
    39
      server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
  59. 58
    0
      server/sonar-web/src/main/js/apps/documentation/components/MenuBlock.tsx
  60. 37
    0
      server/sonar-web/src/main/js/apps/documentation/components/MenuExternalLink.tsx
  61. 46
    0
      server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx
  62. 1
    1
      server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx
  63. 5
    2
      server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx
  64. 8
    2
      server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx
  65. 10
    2
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx
  66. 74
    0
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuBlock-test.tsx
  67. 1
    1
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx
  68. 9
    2
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx
  69. 17
    3
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx
  70. 46
    64
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap
  71. 69
    0
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuBlock-test.tsx.snap
  72. 2
    2
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap
  73. 2
    2
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap
  74. 26
    4
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
  75. 0
    86
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/pages-test.ts
  76. 1
    6
      server/sonar-web/src/main/js/apps/documentation/pages.ts
  77. 33
    22
      server/sonar-web/src/main/js/apps/documentation/utils.ts
  78. 4
    1
      server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
  79. 3
    2
      server/sonar-web/src/main/js/helpers/markdown.js

+ 39
- 16
server/sonar-docs/README.md View File

@@ -62,30 +62,56 @@ cd sonar-enterprise/server/sonar-docs
yarn develop
```

## Testing
As documentation writers there are two ways it is possible for us to break the SonarQube build
* malformed markup
* broken links

Even without spinning up servers, you can double-check that your changes won't break the build.
**Test everything**
You can run all the tests, and make sure that both your markup is well-formed and your links are correct by running the build script:
```
cd sonar-enterprise/
./build.sh -x test -x obfuscate
```
**Test links only**
If you only want to double-check your links changes, you can
```
cd sonar-enterprise/server/sonar-docs
yarn jest
```

This will run the broken link test and stop at the first broken link it finds. Continue running this iteratively until it passes.

## Navigation trees
Controlling the navigation trees of the tree instances is covered in [static/README.md](static)


## Writing docs

### URLs
All urls _must_ end with a trailing slash (`/`).

### Header

Each documentation file should contain a header at the top of the file delimited by "---" top and bottom. The header holds file metadata:

* The `title` tag defines the title of the page for the index
* The `order` tag defines the order of the page for the index. (Floats are interpreted correctly)
* The `scope` tag defines to which product the doc applies. Omit `scope` to have a file show up everywhere:
* “sonarqube” - visible only for SonarQube and the static website
* “sonarcloud” - visible only for SonarCloud
* "static" - visible only on the static website
* The `url` tag is required and defines the path at which to publish the page. Reminder: end this with a trailing slash.

Ex.:

```
---
title: Demo page
order: 0
scope: static
url: /sonarcloud-pricing
---
```

Metadata tags can appear in any order, but by convention, `title` should come first.
** Metadata conventions**
* Metadata tags can appear in any order, but by convention, `title` should come first.
* The `url` tag is optional, but by convention, should be specified to both make the publish path explicit and avoid problems potentially caused by moving or renaming files.


### Includes

@@ -98,15 +124,12 @@ Basic syntax: `@include tooltips/quality-gates/quality-gate`

With special comments you can mark a page or a part of the content to be displayed only on SonarCloud, SonarQube or the static documentation website.

To display a page only in a certain context use the frontmatter option:

```md
---
scope: sonarcloud (or sonarqube, or static)
---
To drop in "SonarQube" or "SonarCloud" as appropriate, use:
```
{instance}
```

To display/hide a part of the content use special comments:
To display/hide some other part of the content, use special comments:

```md
<!-- sonarcloud -->
@@ -128,7 +151,7 @@ this content is displayed only in the static website
<!-- /static -->
```

You can also use inline comments:
You can also use these comments inline:

```md
this content is displayed on <!-- sonarcloud -->SonarCloud<!-- /sonarcloud --><!-- sonarqube -->SonarQube<!-- /sonarqube -->

+ 9
- 11
server/sonar-docs/gatsby-node.js View File

@@ -42,7 +42,7 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
edges {
node {
frontmatter {
scope
url
}
headings {
depth
@@ -57,16 +57,14 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
}
`).then(result => {
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
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
}
});
}
createPage({
path: node.frontmatter.url || 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();
});

+ 16
- 2
server/sonar-docs/package.json View File

@@ -22,17 +22,31 @@
"typography": "^0.16.16"
},
"scripts": {
"build": "gatsby build --prefix-paths",
"build": "yarn jest && gatsby build --prefix-paths",
"develop": "gatsby develop",
"start": "serve public/",
"format": "prettier --write 'src/**/*.{md,js}'"
},
"devDependencies": {
"prettier": "^1.12.0"
"glob-promise": "3.4.0",
"jest": "23.6.0",
"prettier": "^1.12.0",
"remark": "9.0.0",
"unist-util-visit": "1.4.0"
},
"prettier": {
"jsxBracketSameLine": true,
"printWidth": 100,
"singleQuote": true
},
"jest": {
"moduleFileExtensions": [
"js"
],
"testPathIgnorePatterns": [
"<rootDir>/node_modules",
"<rootDir>/.cache"
],
"testRegex": "(/__tests__/.*|\\-test)\\.(ts|tsx|js)$"
}
}

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

@@ -3,152 +3,152 @@
"api_documentation": [],
"background_tasks": [
{
"link": "/documentation/analysis/background-tasks",
"link": "/documentation/analysis/background-tasks/",
"text": "About Background Tasks"
}
],
"code": [],
"coding_rules": [
{
"link": "/documentation/quality-profiles",
"link": "/documentation/quality-profiles/",
"text": "Quality Profiles"
},
{
"link": "/documentation/keyboard-shortcuts",
"link": "/documentation/keyboard-shortcuts/",
"text": "Keyboard Shortcuts"
}
],
"component_measures": [
{
"link": "/documentation/fixing-the-water-leak",
"link": "/documentation/fixing-the-water-leak/",
"text": "Fixing the Water Leak"
},
{
"link":"/documentation/metric-definitions",
"text":"Metric Definitions"
"link": "/documentation/metric-definitions/",
"text": "Metric Definitions"
},
{
"link": "/documentation/keyboard-shortcuts",
"link": "/documentation/keyboard-shortcuts/",
"text": "Keyboard Shortcuts"
}
],
"custom_measures": [
{
"link": "/documentation/custom-measures",
"link": "/documentation/custom-measures/",
"text": "About Custom Measures"
}
],
"custom_metrics": [
{
"link": "/documentation/custom-measures",
"link": "/documentation/custom-measures/",
"text": "Custom Measures"
}
],
"extension_billing": [
{
"link": "/documentation/sonarcloud-pricing",
"link": "/documentation/sonarcloud-pricing/",
"text": "Pricing",
"scope": "sonarcloud"
}
],
"global_permissions": [
{
"link": "/documentation/organizations/manage-team",
"link": "/documentation/organizations/manage-team/",
"text": "Manage a Team",
"scope": "sonarcloud"
}
],
"issues": [
{
"link": "/documentation/keyboard-shortcuts",
"link": "/documentation/keyboard-shortcuts/",
"text": "Keyboard Shortcuts"
}
],
"marketplace": [],
"organization_members": [
{
"link": "/documentation/organizations/manage-team",
"link": "/documentation/organizations/manage-team/",
"text": "Manage a Team",
"scope": "sonarcloud"
}
],
"organization_projects": [
{
"link": "/documentation/organizations/manage-team",
"link": "/documentation/organizations/manage-team/",
"text": "Manage a Team",
"scope": "sonarcloud"
}
],
"organization_space": [
{
"link": "/documentation/organizations/index",
"link": "/documentation/organizations/index/",
"text": "Organizations",
"scope": "sonarcloud"
}
],
"overview": [
{
"link": "/documentation/fixing-the-water-leak",
"link": "/documentation/fixing-the-water-leak/",
"text": "Fixing the Water Leak"
},
{
"link": "/documentation/branches/index",
"link": "/documentation/branches/index/",
"text": "Branches Overview"
},
{
"link": "/documentation/analysis/pull-request",
"link": "/documentation/analysis/pull-request/",
"text": "Analyzing Pull Requests"
}
],
"permission_templates": [],
"profiles": [
"profiles": [
{
"link": "/documentation/quality-profiles",
"link": "/documentation/quality-profiles/",
"text": "Quality Profiles"
}
],
"project_activity": [],
"project_quality_gate": [
{
"link": "/documentation/fixing-the-water-leak",
"link": "/documentation/fixing-the-water-leak/",
"text": "Fixing the Water Leak"
}
],
"project_quality_profiles": [
{
"link": "/documentation/quality-profiles",
"link": "/documentation/quality-profiles/",
"text": "About Quality Profiles"
}
],
"projects_management": [
{
"link": "/documentation/analyze-a-project",
"link": "/documentation/analyze-a-project/",
"text": "Analyze a Project",
"scope": "sonarcloud"
}
],
"projects": [
{
"link": "/documentation/analyze-a-project",
"link": "/documentation/analyze-a-project/",
"text": "Analyze a Project",
"scope": "sonarcloud"
}
],
"quality_gates": [
{
"link": "/documentation/fixing-the-water-leak",
"link": "/documentation/fixing-the-water-leak/",
"text": "Fixing the Water Leak"
}
],
"quality_profiles": [
{
"link": "/documentation/quality-profiles",
"link": "/documentation/quality-profiles/",
"text": "Quality Profiles"
}
],
"security_reports": [
{
"link": "/documentation/security-reports",
"link": "/documentation/security-reports/",
"text": "About Security Reports"
}
],
@@ -156,7 +156,7 @@
"system_info": [],
"user_groups": [
{
"link": "/documentation/organizations/manage-team",
"link": "/documentation/organizations/manage-team/",
"text": "Manage a Team",
"scope": "sonarcloud"
}
@@ -164,7 +164,7 @@
"users": [],
"webhooks": [
{
"link": "/documentation/webhooks",
"link": "/documentation/webhooks/",
"text": "About Webhooks"
}
]

+ 178
- 0
server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js View File

@@ -0,0 +1,178 @@
/*
* 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.
*/
const remark = require('remark');
const fs = require('fs');
const path = require('path');
const glob = require('glob-promise');
const visit = require('unist-util-visit');

it('should not have any broken link', async () => {
const root = path.resolve(__dirname + '/..');
const files = await glob(root + '/pages/**/*.md')
.then(files => files.map(file => file.substr(root.length + 1)))
.then(files =>
files.map(file => ({
path: file.slice(0, -3),
content: handleIncludes(fs.readFileSync(root + '/' + file, 'utf8'), root)
}))
);

const parsedFiles = files.map(file => {
return { ...separateFrontMatter(file.content), path: file.path };
});

const trees = [
'SonarCloudNavigationTree.json',
'SonarQubeNavigationTree.json',
'StaticNavigationTree.json'
];
trees.forEach(file => {
const tree = JSON.parse(fs.readFileSync(root + '/../static/' + file, 'utf8'));
tree.forEach(leaf => {
if (typeof leaf === 'object') {
if (leaf.children) {
leaf.children.forEach(child => {
// Check children markdown file path validity
const result = urlExists(parsedFiles, child);
if (!result) {
// Display custom error message
console.log('[', child, '] is not a valid link, in ', file);
}
expect(result).toBeTruthy();
});
}
} else {
// Check markdown file path validity
const result = urlExists(parsedFiles, leaf);
if (!result) {
console.log('[', leaf, '] is not a valid link, in ', file);
}
expect(result).toBeTruthy();
}
});
});

// Check if all url tag in frontmatter are valid and uniques
let urlLists = [];
parsedFiles.map(file => {
let result = file.frontmatter.url;
if (!result) {
console.log('[', file.path, '] has no url metadata');
}
expect(result).toBeTruthy();

result = file.frontmatter.url.startsWith('/');
if (!result) {
console.log('[', file.path, '] should starts with a slash ', file.frontmatter.url);
}
expect(result).toBeTruthy();

result = file.frontmatter.url.endsWith('/');
if (!result) {
console.log('[', file.path, '] should ends with a slash ', file.frontmatter.url);
}
expect(result).toBeTruthy();

result = !urlLists.includes(file.frontmatter.url);
if (!result) {
console.log('[', file.path, '] has an url that is not unique ', file.frontmatter.url);
}
expect(result).toBeTruthy();

urlLists = [...urlLists, file.frontmatter.url];
});

parsedFiles.map(file => {
const ast = remark().parse(file.content);
visit(ast, node => {
if (node.type === 'image' && !node.url.startsWith('http')) {
// Check image path validity
const result = fs.existsSync(root + '/' + node.url);
if (!result) {
console.log('[', node.url, '] is not a valid image path, in ', file.path + '.md');
}
expect(result).toBeTruthy();
} else if (
node.type === 'link' &&
!node.url.startsWith('http') &&
!node.url.startsWith('/#')
) {
// Check markdown file path validity
const result = urlExists(parsedFiles, node.url);
if (!result) {
console.log('[', node.url, '] is not a valid link, in ', file.path + '.md');
}
expect(result).toBeTruthy();
}
});
});
expect(true).toBeTruthy();
});

function urlExists(files, url) {
return files.find(f => f.frontmatter.url === url) !== undefined;
}

function handleIncludes(content, root) {
return content.replace(/@include (.+)/, (match, p) => {
const filePath = path.join(root, '..', `${p}.md`);
return fs.readFileSync(filePath, 'utf8');
});
}

function getFrontMatterPosition(lines) {
let firstLine;
let lastLine;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '---') {
if (firstLine === undefined) {
firstLine = i;
} else {
lastLine = i;
break;
}
}
}
return lastLine !== undefined ? { firstLine, lastLine } : undefined;
}

function parseFrontMatter(lines) {
const data = {};
for (let i = 0; i < lines.length; i++) {
const tokens = lines[i].split(':').map(x => x.trim());
if (tokens.length === 2) {
data[tokens[0]] = tokens[1];
}
}
return data;
}

function separateFrontMatter(content) {
const lines = content.split('\n');
const position = getFrontMatterPosition(lines);
if (position) {
const frontmatter = parseFrontMatter(lines.slice(position.firstLine + 1, position.lastLine));
const content = lines.slice(position.lastLine + 1).join('\n');
return { frontmatter, content };
} else {
return { frontmatter: {}, content };
}
}

+ 52
- 32
server/sonar-docs/src/layouts/components/CategoryLink.js View File

@@ -21,39 +21,59 @@ import * as React from 'react';
import Link from 'gatsby-link';
import SubpageLink from './SubpageLink';
import HeadingsLink from './HeadingsLink';
import { sortNodes } from '../utils';
import ChevronDownIcon from './icons/ChevronDownIcon';
import ChevronUpIcon from './icons/ChevronUpIcon';

export default function CategoryLink({ node, location, headers, onToggle }) {
const hasChild = node.pages && node.pages.length > 0;
const prefix = process.env.GATSBY_USE_PREFIX === '1' ? '/' + process.env.GATSBY_DOCS_VERSION : '';
const { slug } = node.fields;
const isCurrentPage = location.pathname === prefix + slug;
const open = location.pathname.startsWith(prefix + slug);
return (
<div>
<h2 className={isCurrentPage || open ? 'active' : ''}>
<Link to={slug} title={node.frontmatter.title}>
{hasChild && open && <ChevronUpIcon />}
{hasChild && !open && <ChevronDownIcon />}
{node.frontmatter.title}
</Link>
</h2>
{isCurrentPage && <HeadingsLink headers={headers} />}
{hasChild &&
open && (
<div className="sub-menu">
{sortNodes(node.pages).map(page => (
<SubpageLink
key={page.fields.slug}
headers={headers}
displayHeading={location.pathname === prefix + page.fields.slug}
node={page}
/>
))}
</div>
)}
</div>
);
export default class CategoryLink extends React.PureComponent {
constructor(props) {
super(props);
this.state = { open: props.open };
}

toggle = event => {
event.preventDefault();
event.stopPropagation();
this.props.onToggle(this.props.title);
};

render() {
const { node, location, headers, children, title, open } = this.props;
const prefix =
process.env.GATSBY_USE_PREFIX === '1' ? '/' + process.env.GATSBY_DOCS_VERSION : '';
const url = node ? node.frontmatter.url || node.fields.slug : '';
const isCurrentPage = location.pathname === prefix + url;
return (
<div>
<h2 className={isCurrentPage || open ? 'active' : ''}>
{node ? (
<Link to={url} title={node.frontmatter.title}>
{node.frontmatter.title}
</Link>
) : (
<a href="#" onClick={this.toggle}>
{open ? <ChevronUpIcon /> : <ChevronDownIcon />}
{title}
</a>
)}
</h2>
{isCurrentPage && <HeadingsLink headers={headers} />}
{children &&
open && (
<div className="sub-menu">
{children.map(page => {
const url = page.frontmatter.url || page.fields.slug;
return (
<SubpageLink
displayHeading={location.pathname === prefix + url}
headers={headers}
key={url}
node={page}
/>
);
})}
</div>
)}
</div>
);
}
}

+ 34
- 0
server/sonar-docs/src/layouts/components/ExternalLink.js View File

@@ -0,0 +1,34 @@
/*
* 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 DetachIcon from './icons/DetachIcon';

export function ExternalLink({ external, title }) {
return (
<div>
<h2>
<a href={external} target="_blank">
<DetachIcon />
{title}
</a>
</h2>
</div>
);
}

+ 4
- 4
server/sonar-docs/src/layouts/components/Footer.js View File

@@ -23,10 +23,10 @@ export default function Footer() {
return (
<div className="page-footer">
<a
href="https://creativecommons.org/licenses/by-nc/3.0/us/"
rel="noopener noreferrer"
target="_blank"
title="Creative Commons License"
href="https://creativecommons.org/licenses/by-nc/3.0/us/">
title="Creative Commons License">
<img
alt="Creative Commons License"
src="https://licensebuttons.net/l/by-nc/3.0/us/88x31.png"
@@ -35,9 +35,9 @@ export default function Footer() {
© 2008-2017, SonarSource S.A, Switzerland. Except where otherwise noted, content in this space
is licensed under a{' '}
<a
href="https://creativecommons.org/licenses/by-nc/3.0/us/"
rel="noopener noreferrer"
target="_blank"
href="https://creativecommons.org/licenses/by-nc/3.0/us/">
target="_blank">
Creative Commons Attribution-NonCommercial 3.0 United States License.
</a>{' '}
SONARQUBE is a trademark of SonarSource SA. All other trademarks and copyrights are the

+ 35
- 0
server/sonar-docs/src/layouts/components/HeadingAnchor.js View File

@@ -0,0 +1,35 @@
/*
* 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';

export default function HeadingAnchor(props) {
const onClick = event => {
event.stopPropagation();
event.preventDefault();
props.clickHandler(props.index);
};
return (
<li>
<a className={props.active ? 'active' : ''} href={'#header-' + props.index} onClick={onClick}>
{props.value}
</a>
</li>
);
}

+ 13
- 17
server/sonar-docs/src/layouts/components/HeadingsLink.js View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import HeadingAnchor from './HeadingAnchor';

export default class HeadingsLink extends React.Component {
skipScrollingHandler = false;
@@ -51,7 +52,7 @@ export default class HeadingsLink extends React.Component {
highlightHeading = scrollTop => {
let headingIndex = 0;
for (let i = 0; i < this.state.headers.length; i++) {
if (document.querySelector('#header-' + (i + 1)).offsetTop > scrollTop + 40) {
if (document.querySelector('#header-' + (i + 1)).offsetTop > scrollTop + 200) {
break;
}
headingIndex = i;
@@ -71,8 +72,8 @@ export default class HeadingsLink extends React.Component {
node.classList.add('targetted-heading');
if (scrollTo) {
this.skipScrollingHandler = true;
window.scrollTo(0, node.offsetTop - 30);
this.highlightHeading(node.offsetTop - 30);
window.scrollTo(0, node.offsetTop - 200);
this.highlightHeading(node.offsetTop - 200);
}
}
};
@@ -87,12 +88,8 @@ export default class HeadingsLink extends React.Component {
this.highlightHeading(scrollTop);
};

clickHandler = target => {
return event => {
event.stopPropagation();
event.preventDefault();
this.markH2(target, true);
};
clickHandler = index => {
this.markH2(index, true);
};

render() {
@@ -106,14 +103,13 @@ export default class HeadingsLink extends React.Component {
<ul>
{headers.map((header, index) => {
return (
<li key={index + 1}>
<a
onClick={this.clickHandler(index + 1)}
href={'#header-' + (index + 1)}
className={this.state.activeIndex === index ? 'active' : ''}>
{header.value}
</a>
</li>
<HeadingAnchor
active={this.state.activeIndex === index}
clickHandler={this.clickHandler}
key={index}
index={index + 1}
value={header.value}
/>
);
})}
</ul>

+ 15
- 10
server/sonar-docs/src/layouts/components/Search.js View File

@@ -18,8 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React, { Component } from 'react';
import lunr, { LunrIndex } from 'lunr';
import lunr from 'lunr';
import ClearIcon from './icons/ClearIcon';
import { getUrlsList } from '../utils';

// Search component
export default class Search extends Component {
@@ -36,13 +37,17 @@ export default class Search extends Component {

this.metadataWhitelist = ['position'];

props.pages.forEach(page =>
this.add({
id: page.id,
title: page.frontmatter.title,
text: page.html.replace(/<(?:.|\n)*?>/gm, '').replace(/&#x3C;(?:.|\n)*?>/gm, '')
})
);
props.pages
.filter(page =>
getUrlsList(props.navigation).includes(page.frontmatter.url || page.fields.slug)
)
.forEach(page =>
this.add({
id: page.id,
text: page.html.replace(/<(?:.|\n)*?>/gm, '').replace(/&#x3C;(?:.|\n)*?>/gm, ''),
title: page.frontmatter.title
})
);
});
}

@@ -67,9 +72,9 @@ export default class Search extends Component {
return {
page: {
id: page.id,
slug: page.fields.slug,
text: page.html.replace(/<(?:.|\n)*?>/gm, '').replace(/&#x3C;(?:.|\n)*?>/gm, ''),
title: page.frontmatter.title,
text: page.html.replace(/<(?:.|\n)*?>/gm, '').replace(/&#x3C;(?:.|\n)*?>/gm, '')
url: page.frontmatter.url || page.fields.slug
},
highlights,
longestTerm

+ 1
- 1
server/sonar-docs/src/layouts/components/SearchEntryResult.js View File

@@ -23,7 +23,7 @@ import { highlightMarks, cutWords } from '../utils';

export default function SearchResultEntry({ active, result }) {
return (
<Link className={active ? 'active search-result' : 'search-result'} to={result.page.slug}>
<Link className={active ? 'active search-result' : 'search-result'} to={result.page.url}>
<SearchResultTitle result={result} />
<SearchResultText result={result} />
</Link>

+ 72
- 29
server/sonar-docs/src/layouts/components/Sidebar.js View File

@@ -19,20 +19,46 @@
*/
import React from 'react';
import Link from 'gatsby-link';
import { fromPairs } from 'lodash';
import { sortNodes } from '../utils';
import CategoryLink from './CategoryLink';
import VersionSelect from './VersionSelect';
import Search from './Search';
import SearchEntryResult from './SearchEntryResult';
import NavigationTree from '../../../static/StaticNavigationTree.json';
import { ExternalLink } from './ExternalLink';

export default class Sidebar extends React.PureComponent {
state = { loaded: false, query: '', results: [], versions: [] };
constructor(props) {
super(props);
this.state = {
loaded: false,
openBlockTitle: this.getOpenBlockFromLocation(this.props.location),
query: '',
results: [],
versions: []
};
}

componentDidMount() {
this.loadVersions();
}

componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.setState({ openBlockTitle: this.getOpenBlockFromLocation(this.props.location) });
}
}

// A block is opened if the current page is set to one of his children
getOpenBlockFromLocation({ pathname }) {
const element = NavigationTree.find(
item =>
typeof item === 'object' &&
item.children &&
item.children.some(child => pathname.endsWith(child))
);
return element ? element.title : '';
}

loadVersions() {
fetch('/DocsVersions.json').then(response =>
response.json().then(json => {
@@ -41,21 +67,43 @@ export default class Sidebar extends React.PureComponent {
);
}

getPagesHierarchy() {
const categories = sortNodes(
this.props.pages.filter(p => p.fields.slug.split('/').length === 3)
);
const pages = this.props.pages.filter(p => p.fields.slug.split('/').length > 3);
const categoriesObject = fromPairs(categories.map(c => [c.fields.slug, { ...c, pages: [] }]));
pages.forEach(page => {
const parentSlug = page.fields.slug
.split('/')
.slice(0, 2)
.join('/');
categoriesObject[parentSlug + '/'].pages.push(page);
getNodeFromUrl = url => {
return this.props.pages.find(p => p.fields.slug === url + '/' || p.frontmatter.url === url);
};

handleToggle = title => {
this.setState(state => ({ openBlockTitle: state.openBlockTitle === title ? '' : title }));
};

renderCategories = tree => {
return tree.map(item => {
if (typeof item === 'object') {
if (item.children) {
return (
<CategoryLink
children={item.children.map(child => this.getNodeFromUrl(child))}
headers={this.props.headers}
key={item.title}
location={this.props.location}
onToggle={this.handleToggle}
open={item.title === this.state.openBlockTitle}
title={item.title}
/>
);
} else {
return <ExternalLink external={item.url} key={item.title} title={item.title} />;
}
}
return (
<CategoryLink
headers={this.props.headers}
key={item}
location={this.props.location}
node={this.getNodeFromUrl(item)}
/>
);
});
return categoriesObject;
}
};

renderResults = () => {
return (
@@ -79,7 +127,6 @@ export default class Sidebar extends React.PureComponent {
};

render() {
const nodes = this.getPagesHierarchy();
const isOnCurrentVersion =
this.state.versions.find(v => v.value === this.props.version) !== undefined;
return (
@@ -89,9 +136,9 @@ export default class Sidebar extends React.PureComponent {
<img
alt="Continuous Code Quality"
css={{ verticalAlign: 'top', margin: 0 }}
width="160"
src="/images/SonarQubeIcon.svg"
title="Continuous Code Quality"
width="160"
/>
</Link>
<VersionSelect
@@ -110,17 +157,13 @@ export default class Sidebar extends React.PureComponent {
)}
</div>
<div className="page-indexes">
<Search pages={this.props.pages} onResultsChange={this.handleSearch} />
<Search
navigation={NavigationTree}
onResultsChange={this.handleSearch}
pages={this.props.pages}
/>
{this.state.query !== '' && this.renderResults()}
{this.state.query === '' &&
Object.keys(nodes).map(key => (
<CategoryLink
key={key}
headers={this.props.headers}
node={nodes[key]}
location={this.props.location}
/>
))}
{this.state.query === '' && this.renderCategories(NavigationTree)}
</div>
</div>
);

+ 32
- 0
server/sonar-docs/src/layouts/components/icons/DetachIcon.js View File

@@ -0,0 +1,32 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import Icon from './Icon';

export default function DetachIcon({ className, fill = 'currentColor', size }) {
return (
<Icon className={className} size={size}>
<path
d="M12 9.25v2.5A2.25 2.25 0 0 1 9.75 14h-6.5A2.25 2.25 0 0 1 1 11.75v-6.5A2.25 2.25 0 0 1 3.25 3h5.5c.14 0 .25.11.25.25v.5c0 .14-.11.25-.25.25h-5.5C2.562 4 2 4.563 2 5.25v6.5c0 .688.563 1.25 1.25 1.25h6.5c.688 0 1.25-.563 1.25-1.25v-2.5c0-.14.11-.25.25-.25h.5c.14 0 .25.11.25.25zm3-6.75v4c0 .273-.227.5-.5.5a.497.497 0 0 1-.352-.148l-1.375-1.375L7.68 10.57a.27.27 0 0 1-.18.078.27.27 0 0 1-.18-.078l-.89-.89a.27.27 0 0 1-.078-.18.27.27 0 0 1 .078-.18l5.093-5.093-1.375-1.375A.497.497 0 0 1 10 2.5c0-.273.227-.5.5-.5h4c.273 0 .5.227.5.5z"
style={{ fill }}
/>
</Icon>
);
}

+ 2
- 9
server/sonar-docs/src/layouts/index.js View File

@@ -38,13 +38,7 @@ export default function Layout(props) {
location={props.location}
pages={props.data.allMarkdownRemark.edges
.map(e => e.node)
.filter(n => !n.fields.slug.startsWith('/tooltips'))
.filter(
n =>
!n.frontmatter.scope ||
n.frontmatter.scope === 'sonarqube' ||
n.frontmatter.scope === 'static'
)}
.filter(n => !n.fields.slug.startsWith('/tooltips'))}
searchIndex={props.data.siteSearchIndex}
version={version}
/>
@@ -95,8 +89,7 @@ export const query = graphql`
}
frontmatter {
title
order
scope
url
}
fields {
slug

+ 22
- 8
server/sonar-docs/src/layouts/utils.js View File

@@ -1,12 +1,26 @@
import { sortBy } from 'lodash';
/*
* 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 { sortBy, flatten } from 'lodash';

export function sortNodes(nodes) {
return nodes.sort((a, b) => {
if (a.frontmatter.order) {
return b.frontmatter.order ? a.frontmatter.order - b.frontmatter.order : 1;
}
return a.frontmatter.title < b.frontmatter.title ? -1 : 1;
});
export function getUrlsList(navigation) {
return flatten(navigation.map(item => item.children || [item]));
}

const WORDS = 6;

+ 8
- 0
server/sonar-docs/src/pages/404.md View File

@@ -0,0 +1,8 @@
---
title: Page not found
url: /404/
---

# Error

This page does not exist

+ 1
- 0
server/sonar-docs/src/pages/analysis/background-tasks.md View File

@@ -1,5 +1,6 @@
---
title: Background Tasks
url: /analysis/background-tasks/
---

A Background Task can be:

server/sonar-docs/src/pages/analysis/generic_issue.md → server/sonar-docs/src/pages/analysis/generic-issue.md View File

@@ -1,5 +1,6 @@
---
title: Generic Issue Data
url: /analysis/generic-issue/
---

SonarQube supports a generic import format for raising "external" issues in code. It is intended to allow you to import the issues from your favorite linter even if no plugin exists for it.
@@ -11,7 +12,7 @@ External issues suffer from two important limitations:

External issues and the rules that raise them must be managed in the configuration of your linter.

## Import
## Import
The analysis parameter `sonar.externalIssuesReportPaths` accepts a comma-delimited list of paths to reports.

Each report must contain, at top-level, an array of `Issue` objects named `issues`.
@@ -39,6 +40,7 @@ Each report must contain, at top-level, an array of `Issue` objects named `issue
* `startColumn` - integer, optional. 0-indexed
* `endColumn` - integer, optional. 0-indexed

## Example
Here is an example of the expected format:

{ "issues": [

server/sonar-docs/src/pages/analysis/generic_test.md → server/sonar-docs/src/pages/analysis/generic-test.md View File

@@ -1,5 +1,6 @@
---
title: Generic Test Data
url: /analysis/generic-test/
---

Out of the box, SonarQube supports generic formats for test coverage and test execution import. If your coverage engines' native output formats aren't supported by your language plugins, simply covert them to these formats.

server/sonar-docs/src/pages/analysis/index.md → server/sonar-docs/src/pages/analysis/overview.md View File

@@ -1,5 +1,6 @@
---
title: Analyzing Source Code
title: Overview
url: /analysis/overview/
---

Once the SonarQube platform has been installed, you're ready to install an analyzer and begin creating projects. To do that, you must install and configure the scanner that is most appropriate for your needs. Do you build with:

+ 2
- 1
server/sonar-docs/src/pages/analysis/pull-request.md View File

@@ -1,5 +1,6 @@
---
title: Pull Request Analysis
url: /analysis/pull-request/
---

<!-- sonarqube -->
@@ -24,7 +25,7 @@ PR analyses on SonarQube are deleted automatically after 30 days with no analysi

<!-- sonarcloud -->
## Integrations for GitHub, Bitbucket Cloud and VSTS
If your repositories are hosted on GitHub, Bitbucket Cloud or VSTS, check out first the dedicated ["Integrations" pages](/integrations/index). Chances are that you do not need to read this page further since those integrations handle the configuration and analysis parameters for you.
If your repositories are hosted on GitHub, Bitbucket Cloud or VSTS, check out first the dedicated Integrations for: [BitBucketCloud](/integrations/bitbucketcloud/), [GitHub](/integrations/github/), and [VSTS](/integrations/vsts/). Chances are that you do not need to read this page further since those integrations handle the configuration and analysis parameters for you.
<!-- /sonarcloud -->

## Analysis Parameters

server/sonar-docs/src/pages/analysis/scm_integration.md → server/sonar-docs/src/pages/analysis/scm-integration.md View File

@@ -1,12 +1,13 @@
---
title: SCM Integration
url: /analysis/scm-integration/
---

Collecting SCM data during code analysis can unlock a number of SonarQube features:

* Automatic Issue Assignment
* code annotation (blame data) in the Code Viewer
* SCM-driven detection of new code (to help with [Fixing the Water Leak](/fixing-the-water-leak)). Without SCM data, SonarQube determines new code using analysis dates (to timestamp modification of lines).
* SCM-driven detection of new code (to help with [Fixing the Water Leak](/user-guide/fixing-the-water-leak/)). Without SCM data, SonarQube determines new code using analysis dates (to timestamp modification of lines).

### Turning it on/off
SCM integration requires support for your individual SCM provider. Git and SVN are supported by default. <!-- sonarqube -->For other SCM providers, see the Marketplace.<!-- /sonarqube -->

+ 19
- 19
server/sonar-docs/src/pages/branches/branches-faq.md View File

@@ -1,6 +1,6 @@
---
title: Frequently Asked Branches Questions
order: 99
url: /branches/branches-faq/
---

<!-- sonarqube -->
@@ -10,32 +10,32 @@ _Branch analysis is available as part of [Developer Edition](https://redirect.so
<!-- /sonarqube -->


**Q:** How long are branches retained?
**A:** Long-lived branches are retained until you delete them manually (**Administration > Branches**).
## How long are branches retained?
Long-lived branches are retained until you delete them manually (**Administration > Branches**).
Short-lived branches are deleted automatically after 30 days with no analysis.
This can be updated in **Configuration > General > Number of days before purging inactive short living branches**.

**Q:** Do I need to have my project stored in an SCM such as Git or SVN to use this feature?
**A:** No, you don't need to be connected to a SCM. But if you use Git or SVN we can better track the new files on short-lived branches and so better report new issues on the files that really changed during the life of the short-lived branch.
## Does my project need to be stored in an SCM like Git or SVN?
No, you don't need to be connected to a SCM. But if you use Git or SVN we can better track the new files on short-lived branches and so better report new issues on the files that really changed during the life of the short-lived branch.

**Q:** If I flag an Issue as "Won't Fix" or "False-Positive", will it be replicated as such when merging my short-lived branch into the Master?
**A:** Yes. Each time there is an analysis of a long-lived branch, we look at the issues on the short-lived branches and try to synchronize them with the newly raised issues on the long-lived branch. In case you made some changes on the issues (false-positive, won't fix), these changes will be reported on the long-lived branch.
## What if I mark an Issue "Won't Fix" or "False-Positive" in a branch?
It be replicated as such when merging my short-lived branch into the Master. Each time there is an analysis of a long-lived branch, we look at the issues on the short-lived branches and try to synchronize them with the newly raised issues on the long-lived branch. In case you made some changes on the issues (false-positive, won't fix), these changes will be reported on the long-lived branch.

**Q:** Can I still use `sonar.branch`?
**A:** `sonar.branch` is deprecated. You can still use it but it will behave the same way it always has: a separate project will be created. We encourage you to smoothly migrate your users to the new parameter `sonar.branch.name`.
## Can I still use 'sonar.branch'?
`sonar.branch` is deprecated. You can still use it but it will behave the same way it always has: a separate project will be created. We encourage you to smoothly migrate your users to the new parameter `sonar.branch.name`.
Please note you cannot use `sonar.branch` together with `sonar.branch.name`.

**Q:** Can I manually delete a branch?
**A:** This can be achieved by going into the Administration menu at Project's level, then Branches.
## Can I manually delete a branch?
This can be achieved by going into the Administration menu at Project's level, then Branches.

**Q:** How do I control the lifespan of a short-lived branch?
**A:** As a global admin, you can set the parameter sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches to control how many days you want to keep an inactive short-lived branch.
## How do I control the lifespan of a short-lived branch?
As a global admin, you can set the parameter `sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches` to control how many days you want to keep an inactive short-lived branch.

**Q:** Does the payload of the Webhook contain extra information related to Branches?
**A:** Yes, an extra node called "branch" is added to the payload.
## Does the payload of the Webhook include branch information?
Yes, an extra node called "branch" is added to the payload.

**Q:** When are Webhooks called?
**A:** When the computation of the background task is done for a given branch but also when an issue is updated on a short-lived branch.
## When are Webhooks called?
When the computation of the background task is done for a given branch but also when an issue is updated on a short-lived branch.

**Q:** What is the impact on my LOCs consumption vs my license?
**A:** The LOC of your largest branch are counted toward your license limit. All other branches are ignored.
## What is the impact on my LOCs consumption vs my license?
The LOC of your largest branch are counted toward your license limit. All other branches are ignored.

+ 1
- 0
server/sonar-docs/src/pages/branches/long-lived-branches.md View File

@@ -1,5 +1,6 @@
---
title: Long-lived Branches
url: /branches/long-lived-branches/
---

<!-- sonarqube -->

server/sonar-docs/src/pages/branches/index.md → server/sonar-docs/src/pages/branches/overview.md View File

@@ -1,5 +1,6 @@
---
title: Branches
title: Overview
url: /branches/overview/
---

<!-- sonarqube -->
@@ -30,7 +31,7 @@ This corresponds to Pull/Merge Requests or Feature Branches. This kind of branch

![conceptual illustration of short-lived branches.](/images/short-lived-branch-concept.png)

For more, see [Short-lived Branches](/branches/short-lived-branches)
For more, see [Short-lived Branches](/branches/short-lived-branches/)

### Long-lived

@@ -44,7 +45,7 @@ This kind of branch will:

![conceptual illustration of long-lived branches.](/images/long-lived-branch-concept.png)

For more, see [Long-lived Branches](/branches/long-lived-branches)
For more, see [Long-lived Branches](/branches/long-lived-branches/)

### Master / Main Branch

@@ -76,6 +77,6 @@ This can be updated globally in **Configuration > General > Detection** of long-
Once a branch type has been set, it cannot be changed. Explicitly, you cannot transform a long-lived to short-lived branch, or vice-versa.

## See also
* [Short-lived Branches](short-lived-branches)
* [Long-lived Branches](long-lived-branches)
* [Frequently Asked Questions](branches-faq)
* [Short-lived Branches](/branches/short-lived-branches/)
* [Long-lived Branches](/branches/long-lived-branches/)
* [Frequently Asked Questions](/branches/branches-faq/)

+ 1
- 0
server/sonar-docs/src/pages/branches/short-lived-branches.md View File

@@ -1,5 +1,6 @@
---
title: Short-lived Branches
url: /branches/short-lived-branches/
---

<!-- sonarqube -->

+ 10
- 5
server/sonar-docs/src/pages/index.md View File

@@ -1,18 +1,23 @@
---
title: Documentation
url: /
---

<!-- sonarqube -->

[SonarQube](http://www.sonarqube.org/)® software (previously called Sonar) is an open source quality management platform, dedicated to continuously analyze and measure technical quality, from project portfolio to method. If you wish to extend the SonarQube platform with open source plugins, have a look at our [plugin library](https://docs.sonarqube.org/display/PLUG/Plugin+Library).

## I write code

* [Fixing the Water Leak](/fixing-the-water-leak)
* [Quality Gates](/quality-gates)
* [Quality Profiles](/quality-profiles)
<!-- /sonarqube -->
* [Fixing the Water Leak](/user-guide/fixing-the-water-leak/)
* [Quality Gates](/user-guide/quality-gates/)
* [Quality Profiles](/instance-administration/quality-profiles/)
<!-- /sonarqube -->

<!-- sonarcloud -->

SonarCloud is the leading product for Continuous Code Quality online, totally free for open-source projects. It supports all major programming languages, including Java, C#, JavaScript, TypeScript, C/C++ and many more. If your code is closed source, SonarCloud also offers a paid plan to run private analyses.

SonarCloud offers end-to-end integrations for teams leveraging [GitHub](/integrations/github), [VSTS](/integrations/vsts), or [Bitbucket Cloud](/integrations/bitbucketcloud) in their development processes.
SonarCloud offers end-to-end integrations for teams leveraging [GitHub](/integrations/github/), [VSTS](/integrations/vsts/), or [Bitbucket Cloud](/integrations/bitbucketcloud/) in their development processes.

<!-- /sonarcloud -->

server/sonar-docs/src/pages/custom-measures.md → server/sonar-docs/src/pages/instance-administration/custom-measures.md View File

@@ -1,6 +1,6 @@
---
title: Custom Measures
scope: sonarqube
url: /instance-administration/custom-measures/
---

SonarQube collects a maximum of measures in an automated manner but there are some measures for which this is not possible, such as when: the information is not available for collection, the measure is computed by a human, and so on. Whatever the reason, SonarQube provides a service to inject those measures manually and allow you to benefit from other services: the Manual Measures service. The manual measures entered will be picked during the next analysis of the project and thereafter treated as "normal" measures.

server/sonar-docs/src/pages/housekeeping.md → server/sonar-docs/src/pages/instance-administration/housekeeping.md View File

@@ -1,5 +1,6 @@
---
title: Housekeeping
url: /instance-administration/housekeeping/
---

When you run a new analysis of your project, some data that was previously available is cleaned out of the database. For example the source code of the previous analysis, measures at directory and file levels, and so on are automatically removed at the end of a new analysis. Additionally, some old analysis snapshots are also removed.

server/sonar-docs/src/pages/look-and-feel.md → server/sonar-docs/src/pages/instance-administration/look-and-feel.md View File

@@ -1,6 +1,6 @@
---
title: Look and Feel
scope: sonarqube
url: /instance-administration/look-and-feel/
---

## Home logo

server/sonar-docs/src/pages/quality-profiles.md → server/sonar-docs/src/pages/instance-administration/quality-profiles.md View File

@@ -1,5 +1,6 @@
---
title: Quality Profiles
url: /instance-administration/quality-profiles/
---

## Overview

+ 0
- 10
server/sonar-docs/src/pages/integrations/index.md View File

@@ -1,10 +0,0 @@
---
title: Integrations
scope: sonarcloud
---

SonarCloud integrates with the following cloud services to help developers get the most out of their code:

* [Integration with GitHub](/integrations/github)
* [Integration with Bitbucket Cloud](/integrations/bitbucketcloud)
* [Integration with VSTS](/integrations/vsts)

server/sonar-docs/src/pages/webhooks.md → server/sonar-docs/src/pages/project-administration/webhooks.md View File

@@ -1,5 +1,6 @@
---
title: Webhooks
url: /project-administration/webhooks/
---

Webhooks notify external services when a project analysis is complete. An HTTP POST request including a JSON payload is sent to each URL. URLs may be specified at both the project and global levels. Project-level specification does not replace global-level webhooks. All hooks at both levels are called.

server/sonar-docs/src/pages/analyze-a-project.md → server/sonar-docs/src/pages/sonarcloud/analyze-a-project.md View File

@@ -1,14 +1,14 @@
---
title: Analyze a Project
scope: sonarcloud
url: /analyze-a-project/
---

## Prepare your organization

A project must belong to an [organization](/organizations/index). Create one if you intend to collaborate with your team mates, or use your personal organization for test purposes.
A project must belong to an [organization](/organizations/overview/). Create one if you intend to collaborate with your team mates, or use your personal organization for test purposes.

[[info]]
| ** Important note for private code:** Newly created organizations and personal organizations are under a free plan by default. This means projects analyzed on these organizations are public by default: the code will be browsable by anyone. If you want private projects, you should [upgrade your organization to a paid plan](/sonarcloud-pricing) in the "Administration > Billing" page of your organization.
| ** Important note for private code:** Newly created organizations and personal organizations are under a free plan by default. This means projects analyzed on these organizations are public by default: the code will be browsable by anyone. If you want private projects, you should [upgrade your organization to a paid plan](/sonarcloud-pricing/) in the "Administration > Billing" page of your organization.

Find the key of your organization, you will need it at later stages. It can be found on the top right corner of your organization's header.

@@ -19,8 +19,8 @@ existing CI scripts.

Depending on which cloud solution you are using for your developments, you can rely on dedicated integrations to help you:

* VSTS: [read our dedicated documentation](/integrations/vsts)
* Bitbucket Cloud: [read our dedicated documentation](/integrations/bitbucketcloud)
* GitHub: [read our dedicated documentation](/integrations/github)
* VSTS: [read our dedicated documentation](/integrations/vsts/)
* Bitbucket Cloud: [read our dedicated documentation](/integrations/bitbucketcloud/)
* GitHub: [read our dedicated documentation](/integrations/github/)

If you are not using those solutions, you will have to find out what command to execute to run the analysis. Our [tutorial](/#sonarcloud#/onboarding) will help you on this.

server/sonar-docs/src/pages/integrations/bitbucketcloud.md → server/sonar-docs/src/pages/sonarcloud/integrations/bitbucketcloud.md View File

@@ -1,6 +1,6 @@
---
title: Integration with Bitbucket Cloud
scope: sonarcloud
url: /integrations/bitbucketcloud/
---

## Authentication

server/sonar-docs/src/pages/integrations/github.md → server/sonar-docs/src/pages/sonarcloud/integrations/github.md View File

@@ -1,6 +1,6 @@
---
title: Integration with GitHub
scope: sonarcloud
url: /integrations/github/
---

## Authentication

server/sonar-docs/src/pages/integrations/vsts.md → server/sonar-docs/src/pages/sonarcloud/integrations/vsts.md View File

@@ -1,6 +1,6 @@
---
title: Integration with VSTS
scope: sonarcloud
url: /integrations/vsts/
---



server/sonar-docs/src/pages/organizations/index.md → server/sonar-docs/src/pages/sonarcloud/organizations/index.md View File

@@ -1,6 +1,6 @@
---
title: Organizations
scope: sonarcloud
url: /organizations/overview/
---

## Overview
@@ -9,8 +9,8 @@ An organization is a space where a team or a whole company can collaborate acros

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
* [Members](/organizations/manage-team/), who can have different persmissions on the projects
* [Quality Profiles](/instance-administration/quality-profiles/) and [Quality Gates](/user-guide/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.
@@ -20,4 +20,4 @@ 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.

Depending on which plan the organization is in, its [visibility](/organizations/organization-visibility) will change.
Depending on which plan the organization is in, its [visibility](/organizations/organization-visibility/) will change.

server/sonar-docs/src/pages/organizations/manage-team.md → server/sonar-docs/src/pages/sonarcloud/organizations/manage-team.md View File

@@ -1,6 +1,6 @@
---
title: Manage a Team
scope: sonarcloud
url: /organizations/manage-team/
---

Members can collaborate on the projects in the organizations to which they belong. Depending on their permisssions within the organization, members can:

server/sonar-docs/src/pages/organizations/organization-visibility.md → server/sonar-docs/src/pages/sonarcloud/organizations/organization-visibility.md View File

@@ -1,6 +1,6 @@
---
title: Organization Visibility
scope: sonarcloud
url: /organizations/organization-visibility/
---

## Free plan organization

server/sonar-docs/src/pages/privacy.md → server/sonar-docs/src/pages/sonarcloud/privacy.md View File

@@ -1,6 +1,6 @@
---
title: Privacy
scope: sonarcloud
url: /privacy/
---

The privacy policy specifies how data collected on this website is used. Thank you for visiting our website and your interest in our services and products. As the protection of your personal data is an important concern for us, we will explain below what information we collect during your visit to our website, as they are processed and whether or how these may be used.

server/sonar-docs/src/pages/security.md → server/sonar-docs/src/pages/sonarcloud/security.md View File

@@ -1,6 +1,6 @@
---
title: SonarCloud Security
scope: sonarcloud
url: /security/
---

We know that your code is very important to you and your business. We also know that no one wants proven bugs or vulnerabilities found on their source code to be unveiled to third-parties. This is why we take security extremely seriously.

server/sonar-docs/src/pages/sonarcloud-pricing.md → server/sonar-docs/src/pages/sonarcloud/sonarcloud-pricing.md View File

@@ -1,6 +1,6 @@
---
title: Pricing
scope: sonarcloud
url: /sonarcloud-pricing/
---

Subscribing to a paid plan on SonarCloud allows you to analyze unlimited private projects. You can make your code visible by members of your organization only.

server/sonar-docs/src/pages/fixing-the-water-leak.md → server/sonar-docs/src/pages/user-guide/fixing-the-water-leak.md View File

@@ -1,5 +1,6 @@
---
title: Fixing the Water Leak
url: /user-guide/fixing-the-water-leak/
---

## What is the Water Leak
@@ -19,7 +20,7 @@ Typically in this traditional approach, just before release a periodic code qual

Instead, why not apply the same simple logic you use at home to the way you manage code quality? Fixing the leak means putting the focus on the “new” code, i.e. the code that was added or changed since the last release. Then things get much easier:

* The [Quality Gate](/quality-gates) can be run every day, and passing it is achievable. There are no surprises at release time.
* The [Quality Gate](/user-guide/quality-gates/) can be run every day, and passing it is achievable. There are no surprises at release time.
* It's pretty difficult for developers to push back on problems they introduced the previous day. Instead, they're generally happy to fix the problems while the code is still fresh.
* There is a clear ownership of code quality
* The criteria for go/no-go are consistent across applications, and are shared among teams. Indeed new code is new code, regardless of which application it is done in
@@ -32,5 +33,5 @@ As a bonus, the code that gets changed the most has the highest maintainability,
<!-- sonarqube -->SonarQube<!-- /sonarqube --><!-- sonarcloud -->SonarCloud<!-- /sonarcloud --> offers two main tools to help you find your leaks:

* New Code metrics show the variance in your measures between the current code and a specific point you choose in its history, typically the `previous_version`
* New Code is primarily detected based on SCM "blame" data starting from the first analysis within your New Code Period (formerly the "Leak Period"), with fallback mechanisms when needed. See [SCM integration](/analysis/scm-integration) for more details.
* [Quality Gates](/quality-gates) allow you to set boolean thresholds against which your code is measured. Use them with differential metrics to ensure that your code quality moves in the right direction over time.
* New Code is primarily detected based on SCM "blame" data starting from the first analysis within your New Code Period (formerly the "Leak Period"), with fallback mechanisms when needed. See [SCM integration](/analysis/scm-integration/) for more details.
* [Quality Gates](/user-guide/quality-gates/) allow you to set boolean thresholds against which your code is measured. Use them with differential metrics to ensure that your code quality moves in the right direction over time.

server/sonar-docs/src/pages/keyboard-shortcuts.md → server/sonar-docs/src/pages/user-guide/keyboard-shortcuts.md View File

@@ -1,6 +1,6 @@
---
title: Keyboard Shortcuts
order: 99
url: /user-guide/keyboard-shortcuts/
---

## Global

server/sonar-docs/src/pages/metric-definitions.md → server/sonar-docs/src/pages/user-guide/metric-definitions.md View File

@@ -1,5 +1,6 @@
---
title: Metric Definitions
url: /user-guide/metric-definitions/
---

## Table of Contents

server/sonar-docs/src/pages/quality-gates.md → server/sonar-docs/src/pages/user-guide/quality-gates.md View File

@@ -1,5 +1,6 @@
---
title: Quality Gates
url: /user-guide/quality-gates/
---

## Overview
@@ -23,7 +24,7 @@ Which is why you can define as many quality gates as you wish. Quality Gates are

## Use the Best Quality Gate Configuration

The quality gate "Sonar way" is provided by SonarSource, activated by default and considered as built-in and so read-only. It represents our view of the best way to implement the [Fixing the Water Leak](/fixing-the-water-leak) concept. At each SonarQube release, we adjust automatically this default quality gate according to SonarQube's capabilities.
The quality gate "Sonar way" is provided by SonarSource, activated by default and considered as built-in and so read-only. It represents our view of the best way to implement the [Fixing the Water Leak](/user-guide/fixing-the-water-leak/) concept. At each SonarQube release, we adjust automatically this default quality gate according to SonarQube's capabilities.

Three metrics allow you to enforce a given Rating of Reliability, Security and Maintainability, not just overall but also on new code. These metrics are recommended and come as part of the default quality gate. We strongly advise you to adjust your own quality gates to use them to make feedback more clear to your developers looking at their quality gate on their project page.


server/sonar-docs/src/pages/security-reports.md → server/sonar-docs/src/pages/user-guide/security-reports.md View File

@@ -1,5 +1,6 @@
---
title: Security Reports
url: /user-guide/security-reports/
---

## What do the Security Reports show?

server/sonar-docs/src/pages/user-account.md → server/sonar-docs/src/pages/user-guide/user-account.md View File

@@ -1,8 +1,9 @@
---
title: User Account
url: /user-guide/user-account/
---

As a <!-- sonarqube -->SonarQube<!-- /sonarqube --><!-- sonarcloud -->SonarCloud<!-- /sonarcloud --> user you have your own space where you can see the things that are relevant to you:
As a {instance} user you have your own space where you can see the things that are relevant to you:

## Home Page

@@ -13,7 +14,7 @@ It gives you a summary of:

## Security

In addition to being able to change your password, if your instance is not using a 3rd party authentication mechanism such as LDAP or any OAuth provider (GitHub, Google Account, ...), you can manage your own [authentication tokens](/user-token).
In addition to being able to change your password, if your instance is not using a 3rd party authentication mechanism such as LDAP or any OAuth provider (GitHub, Google Account, ...), you can manage your own authentication tokens.

You can create as many Token as you want. Once a Token is created, you can use it to publish analysis to a project where you have the [execute analysis](/security/authorization) permission.
You can create as many Token as you want. Once a Token is created, you can use it to publish analysis to a project where you have the execute analysis permission.


+ 6
- 7
server/sonar-docs/src/templates/page.js View File

@@ -51,10 +51,11 @@ export default class Page extends React.PureComponent {
htmlWithInclusions = removeTableOfContents(htmlWithInclusions);
htmlWithInclusions = createAnchorForHeadings(htmlWithInclusions, realHeadingsList);
htmlWithInclusions = replaceDynamicLinks(htmlWithInclusions);
htmlWithInclusions = replaceInstanceTag(htmlWithInclusions);

return (
<div css={{ paddingTop: 24, paddingBottom: 24 }}>
<Helmet title={page.frontmatter.title}>
<Helmet title={page.frontmatter.title || 'Documentation'}>
<html lang="en" />
</Helmet>
<HeaderList headers={realHeadingsList} />
@@ -99,6 +100,10 @@ export const query = graphql`
}
`;

function replaceInstanceTag(content) {
return content.replace('{instance}', 'SonarQube');
}

function replaceDynamicLinks(content) {
const version = process.env.GATSBY_DOCS_VERSION || '';
const usePrefix = process.env.GATSBY_USE_PREFIX === '1';
@@ -115,12 +120,6 @@ function replaceDynamicLinks(content) {
'<a href="http$1" target="_blank">$2</a>'
);

// Add trailing slash to local link
content = content.replace(
/\<a href="(?!http)(.*)(?!\/)"\>(.*)\<\/a\>/gim,
'<a href="$1/">$2</a>'
);

return content.replace(
/\<a href="(.*)\/#(?:sonarqube|sonarcloud|sonarqube-admin)#.*"\>(.*)\<\/a\>/gim,
'$2'

+ 24
- 0
server/sonar-docs/static/README.md View File

@@ -0,0 +1,24 @@
These three `*.NavigationTree.json` files control the navigation trees of the three versions of the documentation.

Each one contains a JSON array. Array elements may either be:

* a path string
* a node

**Nodes contain two elements:**
* title - string. This is the "parent" node name to be used in the navigation tree
* children - array of path strings

**Paths**
* begin with '/'
* end with '/'
* match the `url:` value of a page.
* **do not** include the trailing `.md` in the file name

**What is the URL value of a page?**
The url value can be implicitly defined by the document's path under the `pages` directory, or explicitly overridden by in the page metadata by setting `url: [path here]`.

Paths must always start and end with '/'. That includes:
* page metadata
* navigation tree files
* links between pages

+ 53
- 0
server/sonar-docs/static/SonarCloudNavigationTree.json View File

@@ -0,0 +1,53 @@
[
"/privacy/",
"/security/",
"/sonarcloud-pricing/",
"/analyze-a-project/",
{
"title": "Analyzing Source Code",
"children": [
"/analysis/overview/",
"/analysis/background-tasks/",
"/analysis/generic-issue/",
"/analysis/generic-test/",
"/analysis/pull-request/",
"/analysis/scm-integration/"
]
},
{
"title": "Integrations",
"children": ["/integrations/bitbucketcloud/", "/integrations/github/", "/integrations/vsts/"]
},
{
"title": "Branches",
"children": [
"/branches/overview/",
"/branches/short-lived-branches/",
"/branches/long-lived-branches/",
"/branches/branches-faq/"
]
},
{
"title": "User Guide",
"children": [
"/user-guide/fixing-the-water-leak/",
"/user-guide/keyboard-shortcuts/",
"/user-guide/quality-gates/",
"/user-guide/metric-definitions/",
"/user-guide/security-reports/",
"/user-guide/user-account/"
]
},
{
"title": "Project Administration",
"children": ["/project-administration/webhooks/"]
},
{
"title": "Organizations",
"children": [
"/organizations/overview/",
"/organizations/manage-team/",
"/organizations/organization-visibility/"
]
}
]

+ 46
- 0
server/sonar-docs/static/SonarQubeNavigationTree.json View File

@@ -0,0 +1,46 @@
[
{
"title": "Analyzing Source Code",
"children": [
"/analysis/overview/",
"/analysis/background-tasks/",
"/analysis/generic-issue/",
"/analysis/generic-test/",
"/analysis/pull-request/",
"/analysis/scm-integration/"
]
},
{
"title": "Branches",
"children": [
"/branches/overview/",
"/branches/short-lived-branches/",
"/branches/long-lived-branches/",
"/branches/branches-faq/"
]
},
{
"title": "User Guide",
"children": [
"/user-guide/fixing-the-water-leak/",
"/user-guide/keyboard-shortcuts/",
"/user-guide/quality-gates/",
"/user-guide/metric-definitions/",
"/user-guide/security-reports/",
"/user-guide/user-account/"
]
},
{
"title": "Project Administration",
"children": ["/project-administration/webhooks/"]
},
{
"title": "Instance Administration",
"children": [
"/instance-administration/custom-measures/",
"/instance-administration/housekeeping/",
"/instance-administration/look-and-feel/",
"/instance-administration/quality-profiles/"
]
}
]

+ 54
- 0
server/sonar-docs/static/StaticNavigationTree.json View File

@@ -0,0 +1,54 @@
[
{
"title": "Requirements",
"children": []
},
{
"title": "Setup and Upgrade",
"children": []
},
{
"title": "Analyzing Source Code",
"children": [
"/analysis/overview/",
"/analysis/background-tasks/",
"/analysis/generic-issue/",
"/analysis/generic-test/",
"/analysis/pull-request/",
"/analysis/scm-integration/"
]
},
{
"title": "Branches",
"children": [
"/branches/overview/",
"/branches/short-lived-branches/",
"/branches/long-lived-branches/",
"/branches/branches-faq/"
]
},
{
"title": "User Guide",
"children": [
"/user-guide/fixing-the-water-leak/",
"/user-guide/keyboard-shortcuts/",
"/user-guide/quality-gates/",
"/user-guide/metric-definitions/",
"/user-guide/security-reports/",
"/user-guide/user-account/"
]
},
{
"title": "Project Administration",
"children": ["/project-administration/webhooks/"]
},
{
"title": "Instance Administration",
"children": [
"/instance-administration/custom-measures/",
"/instance-administration/housekeeping/",
"/instance-administration/look-and-feel/",
"/instance-administration/quality-profiles/"
]
}
]

+ 1282
- 70
server/sonar-docs/yarn.lock
File diff suppressed because it is too large
View File


+ 10
- 4
server/sonar-web/src/main/js/apps/documentation/components/App.tsx View File

@@ -20,6 +20,8 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { Link } from 'react-router';
import * as navigationTreeSonarQube from 'Docs/../static/SonarQubeNavigationTree.json';
import * as navigationTreeSonarCloud from 'Docs/../static/SonarCloudNavigationTree.json';
import Sidebar from './Sidebar';
import getPages from '../pages';
import NotFound from '../../../app/components/NotFound';
@@ -27,6 +29,7 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe
import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock';
import { translate } from '../../../helpers/l10n';
import { isSonarCloud } from '../../../helpers/system';
import { DocsNavigationItem } from '../utils';
import '../styles.css';

interface Props {
@@ -52,8 +55,11 @@ export default class App extends React.PureComponent<Props> {
}

render() {
const { splat = 'index' } = this.props.params;
const page = this.pages.find(p => p.relativeName === splat);
const tree = isSonarCloud()
? ((navigationTreeSonarCloud as any).default as DocsNavigationItem[])
: ((navigationTreeSonarQube as any).default as DocsNavigationItem[]);
const { splat = '' } = this.props.params;
const page = this.pages.find(p => p.url === '/' + splat);
const mainTitle = translate('documentation.page');

if (!page) {
@@ -71,7 +77,7 @@ export default class App extends React.PureComponent<Props> {

return (
<div className="layout-page">
<Helmet title={isIndex ? mainTitle : `${page.title} - ${mainTitle}`}>
<Helmet title={isIndex || !page.title ? mainTitle : `${page.title} - ${mainTitle}`}>
{!isSonarCloud() && <meta content="noindex nofollow" name="robots" />}
</Helmet>

@@ -85,7 +91,7 @@ export default class App extends React.PureComponent<Props> {
<h1>{translate('documentation.page')}</h1>
</Link>
</div>
<Sidebar pages={this.pages} splat={splat} />
<Sidebar navigation={tree} pages={this.pages} splat={splat} />
</div>
</div>
</div>

+ 59
- 39
server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx View File

@@ -18,59 +18,79 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
import { sortBy } from 'lodash';
import MenuBlock from './MenuBlock';
import { MenuItem } from './MenuItem';
import { MenuExternalLink } from './MenuExternalLink';
import {
getEntryChildren,
DocumentationEntry,
activeOrChildrenActive,
getEntryRoot
DocsNavigationBlock,
getNodeFromUrl,
isDocsNavigationBlock,
isDocsNavigationExternalLink,
DocsNavigationItem
} from '../utils';
import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon';

interface Props {
navigation: DocsNavigationItem[];
pages: DocumentationEntry[];
splat: string;
}

type EntryWithChildren = DocumentationEntry & { children?: DocumentationEntry[] };
interface State {
openBlockTitle: string;
}

export default class Menu extends React.PureComponent<Props> {
getMenuEntriesHierarchy = (root?: string): EntryWithChildren[] => {
const topLevelEntries = getEntryChildren(this.props.pages, root);
return sortBy(
topLevelEntries.map(entry => {
const entryRoot = getEntryRoot(entry.relativeName);
const children = entryRoot !== '' ? this.getMenuEntriesHierarchy(entryRoot) : [];
return { ...entry, children };
}),
entry => entry.order
);
};
export default class Menu extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
openBlockTitle: this.getOpenBlockFromLocation(this.props.splat)
};
}

renderEntry = (entry: EntryWithChildren, depth: number): React.ReactNode => {
const active = entry.relativeName === this.props.splat;
const opened = activeOrChildrenActive(this.props.splat || '', entry);
const offset = 10 + 25 * depth;
const { children = [] } = entry;
return (
<React.Fragment key={entry.relativeName}>
<Link
className={classNames('list-group-item', { active })}
style={{ paddingLeft: offset }}
to={'/documentation/' + entry.relativeName}>
<h3 className="list-group-item-heading">
{children.length > 0 && <OpenCloseIcon className="little-spacer-right" open={opened} />}
{entry.title}
</h3>
</Link>
{opened && children.map(entry => this.renderEntry(entry, depth + 1))}
</React.Fragment>
componentWillReceiveProps(nextProps: Props) {
if (this.props.splat !== nextProps.splat) {
this.setState({ openBlockTitle: this.getOpenBlockFromLocation(nextProps.splat) });
}
}

getOpenBlockFromLocation(splat: string) {
const element = this.props.navigation.find(
item => isDocsNavigationBlock(item) && item.children.some(child => '/' + splat === child)
);
return element ? (element as DocsNavigationBlock).title : '';
}

toggleBlock = (title: string) => {
this.setState(state => ({ openBlockTitle: state.openBlockTitle === title ? '' : title }));
};

render() {
return <>{this.getMenuEntriesHierarchy().map(entry => this.renderEntry(entry, 0))}</>;
return this.props.navigation.map(item => {
if (isDocsNavigationBlock(item)) {
return (
<MenuBlock
block={item}
key={item.title}
onToggle={this.toggleBlock}
open={this.state.openBlockTitle === item.title}
pages={this.props.pages}
splat={this.props.splat}
title={item.title}
/>
);
}
if (isDocsNavigationExternalLink(item)) {
return <MenuExternalLink key={item.title} title={item.title} url={item.url} />;
}
return (
<MenuItem
indent={false}
key={item}
node={getNodeFromUrl(this.props.pages, item)}
splat={this.props.splat}
/>
);
});
}
}

+ 58
- 0
server/sonar-web/src/main/js/apps/documentation/components/MenuBlock.tsx View File

@@ -0,0 +1,58 @@
/*
* 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 { MenuItem } from './MenuItem';
import { DocumentationEntry, DocsNavigationBlock, getNodeFromUrl } from '../utils';
import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon';

interface Props {
block: DocsNavigationBlock;
onToggle: (title: string) => void;
open: boolean;
pages: DocumentationEntry[];
splat: string;
title: string;
}

export default class MenuBlock extends React.PureComponent<Props> {
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.stopPropagation();
event.preventDefault();
this.props.onToggle(this.props.title);
};

render() {
const { open, block, pages, title, splat } = this.props;
return (
<>
<a className="list-group-item" href="#" onClick={this.handleClick}>
<h3 className="list-group-item-heading">
<OpenCloseIcon className="little-spacer-right" open={this.props.open} />
{title}
</h3>
</a>
{open &&
block.children.map(item => (
<MenuItem indent={true} key={item} node={getNodeFromUrl(pages, item)} splat={splat} />
))}
</>
);
}
}

+ 37
- 0
server/sonar-web/src/main/js/apps/documentation/components/MenuExternalLink.tsx View File

@@ -0,0 +1,37 @@
/*
* 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 DetachIcon from '../../../components/icons-components/DetachIcon';

interface Props {
title: string;
url: string;
}

export function MenuExternalLink({ title, url }: Props) {
return (
<a href={url} key={title} target="_blank">
<h3 className="list-group-item-heading">
<DetachIcon className="spacer-right" />
{title}
</h3>
</a>
);
}

+ 46
- 0
server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { Link } from 'react-router';
import { DocumentationEntry } from '../utils';

interface Props {
indent: boolean;
node: DocumentationEntry | undefined;
splat: string;
}

export function MenuItem({ indent, node, splat }: Props) {
if (!node) {
return null;
}

const active = node.url === '/' + splat;
return (
<Link
className={classNames('list-group-item', { active })}
key={node.url}
style={{ paddingLeft: indent ? 31 : 10 }}
to={'/documentation' + node.url}>
<h3 className="list-group-item-heading">{node.title}</h3>
</Link>
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx View File

@@ -37,7 +37,7 @@ export default function SearchResultEntry({ active, result }: Props) {
return (
<Link
className={classNames('list-group-item', { active })}
to={'/documentation/' + result.page.relativeName}>
to={'/documentation' + result.page.url}>
<SearchResultTitle result={result} />
<SearchResultText result={result} />
</Link>

+ 5
- 2
server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx View File

@@ -21,9 +21,10 @@ import * as React from 'react';
import lunr, { LunrIndex } from 'lunr';
import { sortBy } from 'lodash';
import SearchResultEntry, { SearchResult } from './SearchResultEntry';
import { DocumentationEntry } from '../utils';
import { DocumentationEntry, getUrlsList, DocsNavigationItem } from '../utils';

interface Props {
navigation: DocsNavigationItem[];
pages: DocumentationEntry[];
query: string;
splat: string;
@@ -41,7 +42,9 @@ export default class SearchResults extends React.PureComponent<Props> {

this.metadataWhitelist = ['position'];

props.pages.forEach(page => this.add(page));
props.pages
.filter(page => getUrlsList(props.navigation).includes(page.url))
.forEach(page => this.add(page));
});
}


+ 8
- 2
server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx View File

@@ -20,10 +20,11 @@
import * as React from 'react';
import Menu from './Menu';
import SearchResults from './SearchResults';
import { DocumentationEntry } from '../utils';
import { DocumentationEntry, DocsNavigationItem } from '../utils';
import SearchBox from '../../../components/controls/SearchBox';

interface Props {
navigation: DocsNavigationItem[];
pages: DocumentationEntry[];
splat: string;
}
@@ -53,12 +54,17 @@ export default class Sidebar extends React.PureComponent<Props, State> {
<div className="list-group">
{this.state.query ? (
<SearchResults
navigation={this.props.navigation}
pages={this.props.pages}
query={this.state.query}
splat={this.props.splat}
/>
) : (
<Menu pages={this.props.pages} splat={this.props.splat} />
<Menu
navigation={this.props.navigation}
pages={this.props.pages}
splat={this.props.splat}
/>
)}
</div>
</div>

+ 10
- 2
server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx View File

@@ -22,7 +22,7 @@ import { shallow } from 'enzyme';
import Menu from '../Menu';

function createPage(title: string, relativeName: string, text = '') {
return { relativeName, title, order: -1, text, content: text };
return { relativeName, url: '/' + relativeName, title, text, content: text };
}

const pages = [
@@ -44,5 +44,13 @@ const pages = [
];

it('should render hierarchical menu', () => {
expect(shallow(<Menu pages={pages} splat="lorem/origin" />)).toMatchSnapshot();
expect(
shallow(
<Menu
navigation={[{ title: 'Block', children: ['/lorem/index', '/lorem/origin'] }, 'foobar']}
pages={pages}
splat="lorem/origin"
/>
)
).toMatchSnapshot();
});

+ 74
- 0
server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuBlock-test.tsx View File

@@ -0,0 +1,74 @@
/*
* 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 MenuBlock from '../MenuBlock';

const block = {
title: 'Foo',
children: ['/bar/', '/baz/']
};

const pages = [
{
content: 'bar',
relativeName: '/bar/',
text: 'bar',
title: 'Bar',
url: '/bar/'
},
{
content: 'baz',
relativeName: '/baz/',
text: 'baz',
title: 'baz',
url: '/baz/'
}
];

it('should render a closed menu block', () => {
expect(
shallow(
<MenuBlock
block={block}
onToggle={jest.fn()}
open={false}
pages={pages}
splat="/foobar/"
title="Foobarbaz"
/>
)
).toMatchSnapshot();
});

it('should render an opened menu block', () => {
expect(
shallow(
<MenuBlock
block={block}
onToggle={jest.fn()}
open={true}
pages={pages}
splat="/foo/"
title="Foo"
/>
)
).toMatchSnapshot();
});

+ 1
- 1
server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx View File

@@ -27,8 +27,8 @@ import SearchResultEntry, {

const page = {
content: '',
order: -1,
relativeName: 'foo/bar',
url: '/foo/bar',
text: 'Foobar is a universal variable understood to represent whatever is being discussed.',
title: 'Foobar'
};

+ 9
- 2
server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx View File

@@ -37,7 +37,7 @@ jest.mock('lunr', () => ({
}));

function createPage(title: string, relativeName: string, text = '') {
return { relativeName, title, order: -1, text, content: text };
return { relativeName, url: '/' + relativeName, title, text, content: text };
}

const pages = [
@@ -59,7 +59,14 @@ const pages = [
];

it('should search', () => {
const wrapper = shallow(<SearchResults pages={pages} query="from" splat="foobar" />);
const wrapper = shallow(
<SearchResults
navigation={['lorem/index', 'lorem/origin', 'foobar']}
pages={pages}
query="from"
splat="foobar"
/>
);
expect(wrapper).toMatchSnapshot();
expect(lunr).toBeCalled();
expect((wrapper.instance() as SearchResults).index.search).toBeCalledWith('from~1 from*');

+ 17
- 3
server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx View File

@@ -22,7 +22,7 @@ import { shallow } from 'enzyme';
import Sidebar from '../Sidebar';

function createPage(title: string, relativeName: string, text = '') {
return { relativeName, title, order: -1, text, content: text };
return { relativeName, url: '/' + relativeName, title, text, content: text };
}

const pages = [
@@ -31,11 +31,25 @@ const pages = [
];

it('should render menu', () => {
expect(shallow(<Sidebar pages={pages} splat="foobar" />)).toMatchSnapshot();
expect(
shallow(
<Sidebar
navigation={[{ title: 'Block', children: ['/lorem/index'] }, 'foobar']}
pages={pages}
splat="foobar"
/>
)
).toMatchSnapshot();
});

it('should search', () => {
const wrapper = shallow(<Sidebar pages={pages} splat="foobar" />);
const wrapper = shallow(
<Sidebar
navigation={[{ title: 'Block', children: ['/lorem/index'] }, 'foobar']}
pages={pages}
splat="foobar"
/>
);
wrapper.find('SearchBox').prop<Function>('onChange')('foo');
wrapper.update();
expect(wrapper).toMatchSnapshot();

+ 46
- 64
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap View File

@@ -1,70 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render hierarchical menu 1`] = `
<React.Fragment>
<React.Fragment
key="lorem/index"
>
<Link
className="list-group-item"
onlyActiveOnIndex={false}
style={
Object {
"paddingLeft": 10,
}
Array [
<MenuBlock
block={
Object {
"children": Array [
"/lorem/index",
"/lorem/origin",
],
"title": "Block",
}
to="/documentation/lorem/index"
>
<h3
className="list-group-item-heading"
>
<OpenCloseIcon
className="little-spacer-right"
open={true}
/>
Lorem Ipsum
</h3>
</Link>
<React.Fragment
key="lorem/origin"
>
<Link
className="list-group-item active"
onlyActiveOnIndex={false}
style={
Object {
"paddingLeft": 35,
}
}
to="/documentation/lorem/origin"
>
<h3
className="list-group-item-heading"
>
Where does it come from?
</h3>
</Link>
</React.Fragment>
</React.Fragment>
<React.Fragment
key="foobar"
>
<Link
className="list-group-item"
onlyActiveOnIndex={false}
style={
}
key="Block"
onToggle={[Function]}
open={true}
pages={
Array [
Object {
"paddingLeft": 10,
}
}
to="/documentation/foobar"
>
<h3
className="list-group-item-heading"
>
Where does Foobar come from?
</h3>
</Link>
</React.Fragment>
</React.Fragment>
"content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
"relativeName": "lorem/index",
"text": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
"title": "Lorem Ipsum",
"url": "/lorem/index",
},
Object {
"content": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.",
"relativeName": "lorem/origin",
"text": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.",
"title": "Where does it come from?",
"url": "/lorem/origin",
},
Object {
"content": "Foobar is a universal variable understood to represent whatever is being discussed.",
"relativeName": "foobar",
"text": "Foobar is a universal variable understood to represent whatever is being discussed.",
"title": "Where does Foobar come from?",
"url": "/foobar",
},
]
}
splat="lorem/origin"
title="Block"
/>,
<MenuItem
indent={false}
key="foobar"
splat="lorem/origin"
/>,
]
`;

+ 69
- 0
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuBlock-test.tsx.snap View File

@@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render a closed menu block 1`] = `
<React.Fragment>
<a
className="list-group-item"
href="#"
onClick={[Function]}
>
<h3
className="list-group-item-heading"
>
<OpenCloseIcon
className="little-spacer-right"
open={false}
/>
Foobarbaz
</h3>
</a>
</React.Fragment>
`;

exports[`should render an opened menu block 1`] = `
<React.Fragment>
<a
className="list-group-item"
href="#"
onClick={[Function]}
>
<h3
className="list-group-item-heading"
>
<OpenCloseIcon
className="little-spacer-right"
open={true}
/>
Foo
</h3>
</a>
<MenuItem
indent={true}
key="/bar/"
node={
Object {
"content": "bar",
"relativeName": "/bar/",
"text": "bar",
"title": "Bar",
"url": "/bar/",
}
}
splat="/foo/"
/>
<MenuItem
indent={true}
key="/baz/"
node={
Object {
"content": "baz",
"relativeName": "/baz/",
"text": "baz",
"title": "baz",
"url": "/baz/",
}
}
splat="/foo/"
/>
</React.Fragment>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap View File

@@ -14,10 +14,10 @@ exports[`SearchResultEntry should render 1`] = `
"longestTerm": "",
"page": Object {
"content": "",
"order": -1,
"relativeName": "foo/bar",
"text": "Foobar is a universal variable understood to represent whatever is being discussed.",
"title": "Foobar",
"url": "/foo/bar",
},
}
}
@@ -29,10 +29,10 @@ exports[`SearchResultEntry should render 1`] = `
"longestTerm": "",
"page": Object {
"content": "",
"order": -1,
"relativeName": "foo/bar",
"text": "Foobar is a universal variable understood to represent whatever is being discussed.",
"title": "Foobar",
"url": "/foo/bar",
},
}
}

+ 2
- 2
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap View File

@@ -24,10 +24,10 @@ exports[`should search 1`] = `
"longestTerm": "from",
"page": Object {
"content": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.",
"order": -1,
"relativeName": "lorem/origin",
"text": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.",
"title": "Where does it come from?",
"url": "/lorem/origin",
},
}
}
@@ -48,10 +48,10 @@ exports[`should search 1`] = `
"longestTerm": "from",
"page": Object {
"content": "Foobar is a universal variable understood to represent whatever is being discussed.",
"order": -1,
"relativeName": "foobar",
"text": "Foobar is a universal variable understood to represent whatever is being discussed.",
"title": "Where does Foobar come from?",
"url": "/foobar",
},
}
}

+ 26
- 4
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap View File

@@ -16,21 +16,32 @@ exports[`should render menu 1`] = `
className="list-group"
>
<Menu
navigation={
Array [
Object {
"children": Array [
"/lorem/index",
],
"title": "Block",
},
"foobar",
]
}
pages={
Array [
Object {
"content": "",
"order": -1,
"relativeName": "lorem/index",
"text": "",
"title": "Lorem Ipsum",
"url": "/lorem/index",
},
Object {
"content": "",
"order": -1,
"relativeName": "foobar",
"text": "",
"title": "Where does Foobar come from?",
"url": "/foobar",
},
]
}
@@ -57,21 +68,32 @@ exports[`should search 1`] = `
className="list-group"
>
<SearchResults
navigation={
Array [
Object {
"children": Array [
"/lorem/index",
],
"title": "Block",
},
"foobar",
]
}
pages={
Array [
Object {
"content": "",
"order": -1,
"relativeName": "lorem/index",
"text": "",
"title": "Lorem Ipsum",
"url": "/lorem/index",
},
Object {
"content": "",
"order": -1,
"relativeName": "foobar",
"text": "",
"title": "Where does Foobar come from?",
"url": "/foobar",
},
]
}

+ 0
- 86
server/sonar-web/src/main/js/apps/documentation/components/__tests__/pages-test.ts View File

@@ -1,86 +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 getPages from '../../pages';
import { isSonarCloud } from '../../../../helpers/system';

// mock `remark` and `remark-react` to work around the issue with cjs imports
jest.mock('remark', () => {
const remark = require.requireActual('remark');
return { default: remark };
});

jest.mock('unist-util-visit', () => {
const exp = require.requireActual('unist-util-visit');
return { default: exp };
});

jest.mock('../../documentation.directory-loader', () => [
{
path: 'all',
content: `
---
title: All
---

all all all`
},
{
path: 'sonarqube-foo',
content: `
---
title: Foo
scope: sonarqube
---

foo foo foo`
},
{
path: 'sonarcloud-bar',
content: `
---
title: Bar
scope: sonarcloud
---

bar bar bar`
},
{
path: 'static-baz',
content: `
---
title: Baz
scope: static
---

baz baz baz`
}
]);

jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));

it('should filter pages for SonarQube', () => {
(isSonarCloud as jest.Mock).mockReturnValue(false);
expect(getPages().map(page => page.title)).toEqual(['All', 'Foo']);
});

it('should filter pages for SonarCloud', () => {
(isSonarCloud as jest.Mock).mockReturnValue(true);
expect(getPages().map(page => page.title)).toEqual(['All', 'Bar']);
});

+ 1
- 6
server/sonar-web/src/main/js/apps/documentation/pages.ts View File

@@ -22,7 +22,6 @@ import visit from 'unist-util-visit';
import { DocumentationEntry, DocumentationEntryScope } from './utils';
import * as Docs from './documentation.directory-loader';
import { separateFrontMatter, filterContent } from '../../helpers/markdown';
import { isSonarCloud } from '../../helpers/system';

export default function getPages(): DocumentationEntry[] {
return Docs.map((file: any) => {
@@ -32,6 +31,7 @@ export default function getPages(): DocumentationEntry[] {

return {
relativeName: file.path,
url: parsed.frontmatter.url || `/${file.path}`,
title: parsed.frontmatter.title,
order: Number(parsed.frontmatter.order || -1),
scope: parsed.frontmatter.scope
@@ -40,11 +40,6 @@ export default function getPages(): DocumentationEntry[] {
text,
content: file.content
};
}).filter((page: DocumentationEntry) => {
if (!page.scope) {
return true;
}
return isSonarCloud() ? page.scope === 'sonarcloud' : page.scope === 'sonarqube';
});
}


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

@@ -17,42 +17,53 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { sortBy } from 'lodash';
import { sortBy, flatten } from 'lodash';

export type DocumentationEntryScope = 'sonarqube' | 'sonarcloud' | 'static';

export interface DocsNavigationBlock {
title: string;
children: string[];
}

export interface DocsNavigationExternalLink {
title: string;
url: string;
}

export interface DocumentationEntry {
content: string;
order: number;
relativeName: string;
scope?: DocumentationEntryScope;
text: string;
title: string;
url: string;
}

export function activeOrChildrenActive(root: string, entry: DocumentationEntry) {
return root.indexOf(getEntryRoot(entry.relativeName)) === 0;
export type DocsNavigationItem = string | DocsNavigationBlock | DocsNavigationExternalLink;

export function isDocsNavigationBlock(item: DocsNavigationItem): item is DocsNavigationBlock {
return typeof item === 'object' && !(item as any).url;
}

export function getEntryRoot(name: string) {
if (name.endsWith('index')) {
return name
.split('/')
.slice(0, -1)
.join('/');
}
return name;
export function isDocsNavigationExternalLink(
item: DocsNavigationItem
): item is DocsNavigationExternalLink {
return typeof item === 'object' && (item as any).url;
}

export function getUrlsList(navigation: DocsNavigationItem[]): string[] {
return flatten(
navigation
.filter(item => !isDocsNavigationExternalLink(item))
.map(
(item: string | DocsNavigationBlock) =>
isDocsNavigationBlock(item) ? item.children : [item]
)
);
}

export function getEntryChildren(entries: DocumentationEntry[], root?: string) {
return entries.filter(entry => {
const parts = entry.relativeName.split('/');
const depth = root ? root.split('/').length : 0;
return (
(!root || entry.relativeName.indexOf(root) === 0) &&
((parts.length === depth + 1 && parts[depth] !== 'index') || parts[depth + 1] === 'index')
);
});
export function getNodeFromUrl(pages: DocumentationEntry[], url: string) {
return pages.find(p => p.url === url);
}

const WORDS = 6;

+ 4
- 1
server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx View File

@@ -33,7 +33,10 @@ jest.mock('remark-react', () => {
return { default: remarkReact };
});

jest.mock('../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));
jest.mock('../../../helpers/system', () => ({
getInstance: jest.fn(),
isSonarCloud: jest.fn()
}));

it('should render simple markdown', () => {
expect(shallow(<DocMarkdownBlock content="this is *bold* text" />)).toMatchSnapshot();

+ 3
- 2
server/sonar-web/src/main/js/helpers/markdown.js View File

@@ -72,8 +72,9 @@ function parseFrontMatter(lines) {
* @returns {string}
*/
function filterContent(content) {
const { isSonarCloud } = require('./system');
const contentWithoutStatic = cutConditionalContent(content, 'static');
const { isSonarCloud, getInstance } = require('./system');
const contentWithInstance = content.replace('{instance}', getInstance());
const contentWithoutStatic = cutConditionalContent(contentWithInstance, 'static');
return isSonarCloud()
? cutConditionalContent(contentWithoutStatic, 'sonarqube')
: cutConditionalContent(contentWithoutStatic, 'sonarcloud');

Loading…
Cancel
Save