Browse Source

SONAR-10612 Create documentation space in the web app

tags/7.5
Pascal Mugnier 6 years ago
parent
commit
b083d43765
62 changed files with 960 additions and 55 deletions
  1. 53
    0
      server/sonar-docs/src/EmbedDocsSuggestions.json
  2. 44
    0
      server/sonar-web/config/documentation-loader/fetch-matter.js
  3. 39
    0
      server/sonar-web/config/documentation-loader/index.js
  4. 24
    0
      server/sonar-web/config/documentation-loader/parse-directory.js
  5. 3
    1
      server/sonar-web/config/paths.js
  6. 14
    2
      server/sonar-web/config/webpack.config.js
  7. 3
    0
      server/sonar-web/package.json
  8. 1
    1
      server/sonar-web/scripts/start.js
  9. 131
    0
      server/sonar-web/src/main/js/@types/gray-matter.d.ts
  10. 12
    5
      server/sonar-web/src/main/js/app/components/NotFound.js
  11. 2
    1
      server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
  12. 0
    11
      server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json
  13. 2
    1
      server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
  14. 4
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  15. 2
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  16. 2
    0
      server/sonar-web/src/main/js/apps/account/components/Account.js
  17. 2
    0
      server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
  18. 2
    0
      server/sonar-web/src/main/js/apps/code/components/App.tsx
  19. 2
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
  20. 2
    0
      server/sonar-web/src/main/js/apps/component-measures/components/App.js
  21. 3
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
  22. 2
    0
      server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx
  23. 6
    0
      server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap
  24. 2
    0
      server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx
  25. 6
    0
      server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap
  26. 130
    0
      server/sonar-web/src/main/js/apps/documentation/components/App.tsx
  27. 77
    0
      server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
  28. 24
    0
      server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js
  29. 32
    0
      server/sonar-web/src/main/js/apps/documentation/routes.ts
  30. 54
    0
      server/sonar-web/src/main/js/apps/documentation/utils.ts
  31. 2
    0
      server/sonar-web/src/main/js/apps/groups/components/App.tsx
  32. 2
    0
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  33. 2
    0
      server/sonar-web/src/main/js/apps/marketplace/App.tsx
  34. 2
    0
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
  35. 2
    0
      server/sonar-web/src/main/js/apps/permission-templates/components/App.js
  36. 2
    0
      server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
  37. 2
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
  38. 3
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
  39. 2
    0
      server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
  40. 4
    4
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
  41. 2
    0
      server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
  42. 4
    4
      server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
  43. 2
    0
      server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
  44. 2
    0
      server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
  45. 2
    0
      server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
  46. 2
    0
      server/sonar-web/src/main/js/apps/settings/components/App.js
  47. 2
    0
      server/sonar-web/src/main/js/apps/system/components/App.tsx
  48. 2
    0
      server/sonar-web/src/main/js/apps/users/UsersApp.tsx
  49. 6
    0
      server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
  50. 2
    0
      server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
  51. 2
    0
      server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
  52. 6
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
  53. 30
    0
      server/sonar-web/src/main/js/components/docs/DocImg.tsx
  54. 70
    0
      server/sonar-web/src/main/js/components/docs/DocInclude.tsx
  55. 6
    12
      server/sonar-web/src/main/js/components/docs/DocLink.tsx
  56. 16
    4
      server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
  57. 35
    0
      server/sonar-web/src/main/js/components/docs/DocParagraph.tsx
  58. 1
    1
      server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
  59. 2
    2
      server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
  60. 5
    1
      server/sonar-web/tsconfig.json
  61. 50
    5
      server/sonar-web/yarn.lock
  62. 8
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

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

@@ -0,0 +1,53 @@
{
"account": [],
"api_documentation": [],
"background_tasks": [],
"code": [],
"coding_rules": [
{
"link": "/documentation/quality-profiles",
"text": "Quality Profiles"
}
],
"component_measures": [
{
"link": "/documentation/fixing-the-water-leak",
"text": "Fixing the Water Leak"
}
],
"custom_measures": [],
"custom_metrics": [],
"global_permissions": [],
"issues": [],
"marketplace": [],
"overview": [
{
"link": "/documentation/fixing-the-water-leak",
"text": "Fixing the Water Leak"
}
],
"permission_templates": [],
"profiles": [],
"project_activity": [],
"project_quality_gate": [],
"project_quality_profiles": [],
"projects_management": [],
"projects": [],
"quality_gates": [
{
"link": "/documentation/fixing-the-water-leak",
"text": "Fixing the Water Leak"
}
],
"quality_profiles": [
{
"link": "/documentation/quality-profiles",
"text": "Quality Profiles"
}
],
"settings": [],
"system_info": [],
"user_groups": [],
"users": [],
"webhooks": []
}

+ 44
- 0
server/sonar-web/config/documentation-loader/fetch-matter.js View File

@@ -0,0 +1,44 @@
/*
* 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 fs = require('fs');
const path = require('path');
const matter = require('gray-matter');

const compare = (a, b) => {
if (a.order === b.order) return a.title.localeCompare(b.title);
if (a.order === -1) return 1;
if (b.order === -1) return -1;
return a.order - b.order;
};

module.exports = (root, files) => {
return files
.map(file => {
const content = fs.readFileSync(root + '/' + file, 'utf8');
const headerData = matter(content).data;
return {
name: path.basename(file).slice(0, -3),
relativeName: file.slice(0, -3),
title: headerData.title || file,
order: headerData.order || -1
};
})
.sort(compare);
};

+ 39
- 0
server/sonar-web/config/documentation-loader/index.js View File

@@ -0,0 +1,39 @@
/*
* 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 path = require('path');
const parseDirectory = require('./parse-directory');
const fetchMatter = require('./fetch-matter');

module.exports = function(source) {
this.cacheable();

const failure = this.async();
const success = failure.bind(null, null);

const config = this.exec(source, this.resourcePath);
const root = path.resolve(path.dirname(this.resourcePath), config.root);
this.addContextDependency(root);

parseDirectory(root)
.then(files => fetchMatter(root, files))
.then(result => `module.exports = ${JSON.stringify(result)};`)
.then(success)
.catch(failure);
};

+ 24
- 0
server/sonar-web/config/documentation-loader/parse-directory.js View File

@@ -0,0 +1,24 @@
/*
* 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 glob = require('glob-promise');

module.exports = root => {
return glob(root + '/**/*.md').then(files => files.map(file => file.substr(root.length + 1)));
};

+ 3
- 1
server/sonar-web/config/paths.js View File

@@ -22,5 +22,7 @@ const path = require('path');
module.exports = {
appBuild: path.join(__dirname, '../build/webapp'),
appPublic: path.join(__dirname, '../public'),
appHtml: path.join(__dirname, '../public/index.html')
appHtml: path.join(__dirname, '../public/index.html'),
docRoot: path.join(__dirname, '../../sonar-docs/src'),
docImages: path.join(__dirname, '../../sonar-docs/src/images')
};

+ 14
- 2
server/sonar-web/config/webpack.config.js View File

@@ -36,7 +36,7 @@ module.exports = ({ production = true }) => ({
extensions: ['.ts', '.tsx', '.js', '.json'],
// import from 'Docs/foo.md' is rewritten to import from 'sonar-docs/src/foo.md'
alias: {
Docs: path.resolve(__dirname, '../../sonar-docs/src/tooltips')
Docs: path.resolve(__dirname, '../../sonar-docs/src')
}
},
entry: [
@@ -79,13 +79,25 @@ module.exports = ({ production = true }) => ({
},
{ test: require.resolve('lodash'), loader: 'expose-loader?_' },
{ test: require.resolve('react'), loader: 'expose-loader?React' },
{ test: require.resolve('react-dom'), loader: 'expose-loader?ReactDOM' }
{ test: require.resolve('react-dom'), loader: 'expose-loader?ReactDOM' },
{
test: /\.directory-loader\.js$/,
loader: path.resolve(__dirname, 'documentation-loader/index.js')
}
].filter(Boolean)
},
plugins: [
// `allowExternal: true` to remove files outside of the current dir
production && new CleanWebpackPlugin([paths.appBuild], { allowExternal: true, verbose: false }),

production &&
new CopyWebpackPlugin([
{
from: paths.docImages,
to: paths.appBuild + '/images/embed-doc/images'
}
]),

production &&
new CopyWebpackPlugin([
{

+ 3
- 0
server/sonar-web/package.json View File

@@ -16,6 +16,7 @@
"d3-shape": "1.2.0",
"date-fns": "1.29.0",
"formik": "0.11.7",
"gray-matter": "4.0.1",
"history": "3.3.0",
"intl-relativeformat": "2.1.0",
"keymaster": "1.6.2",
@@ -86,6 +87,8 @@
"eslint-plugin-sonarjs": "0.1.0",
"expose-loader": "0.7.5",
"flow-bin": "^0.52.0",
"glob": "7.1.2",
"glob-promise": "3.4.0",
"html-webpack-plugin": "3.0.6",
"jest": "22.0.6",
"lint-staged": "4.3.0",

+ 1
- 1
server/sonar-web/scripts/start.js View File

@@ -90,7 +90,7 @@ function runDevServer(compiler, host, port, protocol) {
},
compress: true,
clientLogLevel: 'none',
contentBase: paths.appPublic,
contentBase: [paths.appPublic, paths.docRoot],
disableHostCheck: true,
hot: true,
publicPath: config.output.publicPath,

+ 131
- 0
server/sonar-web/src/main/js/@types/gray-matter.d.ts View File

@@ -0,0 +1,131 @@
/*
* 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.
*/
/**
* Takes a string or object with `content` property, extracts
* and parses front-matter from the string, then returns an object
* with `data`, `content` and other [useful properties](#returned-object).
*
* ```js
* var matter = require('gray-matter');
* console.log(matter('---\ntitle: Home\n---\nOther stuff'));
* //=> { data: { title: 'Home'}, content: 'Other stuff' }
* ```
* @param {Object|String} `input` String, or object with `content` string
* @param {Object} `options`
* @return {Object}
* @api public
*/
declare function matter<I extends matter.Input, O extends matter.GrayMatterOption<I, O>>(
input: I | { content: I },
options?: O
): matter.GrayMatterFile<I>;

declare namespace matter {
type Input = string | Buffer;
interface GrayMatterOption<I extends Input, O extends GrayMatterOption<I, O>> {
parser?: () => void;
// eslint-disable-next-line no-eval
eval?: boolean;
excerpt?: boolean | ((input: I, options: O) => string);
excerpt_separator?: string;
engines?: {
[index: string]:
| ((input: string) => object)
| { parse: (input: string) => object; stringify?: (data: object) => string };
};
language?: string;
delimiters?: string | [string, string];
}
interface GrayMatterFile<I extends Input> {
data: { [key: string]: any };
content: string;
excerpt?: string;
orig: Buffer | I;
language: string;
matter: string;
stringify(lang: string): string;
}

/**
* Stringify an object to YAML or the specified language, and
* append it to the given string. By default, only YAML and JSON
* can be stringified. See the [engines](#engines) section to learn
* how to stringify other languages.
*
* ```js
* console.log(matter.stringify('foo bar baz', {title: 'Home'}));
* // results in:
* // ---
* // title: Home
* // ---
* // foo bar baz
* ```
* @param {String|Object} `file` The content string to append to stringified front-matter, or a file object with `file.content` string.
* @param {Object} `data` Front matter to stringify.
* @param {Object} `options` [Options](#options) to pass to gray-matter and [js-yaml].
* @return {String} Returns a string created by wrapping stringified yaml with delimiters, and appending that to the given string.
*/
export function stringify<O extends GrayMatterOption<string, O>>(
file: string | { content: string },
data: object,
options?: GrayMatterOption<string, O>
): string;

/**
* Synchronously read a file from the file system and parse
* front matter. Returns the same object as the [main function](#matter).
*
* ```js
* var file = matter.read('./content/blog-post.md');
* ```
* @param {String} `filepath` file path of the file to read.
* @param {Object} `options` [Options](#options) to pass to gray-matter.
* @return {Object} Returns [an object](#returned-object) with `data` and `content`
*/
export function read<O extends GrayMatterOption<string, O>>(
fp: string,
options?: GrayMatterOption<string, O>
): matter.GrayMatterFile<string>;

/**
* Returns true if the given `string` has front matter.
* @param {String} `string`
* @param {Object} `options`
* @return {Boolean} True if front matter exists.
*/
export function test<O extends matter.GrayMatterOption<string, O>>(
str: string,
options?: GrayMatterOption<string, O>
): boolean;

/**
* Detect the language to use, if one is defined after the
* first front-matter delimiter.
* @param {String} `string`
* @param {Object} `options`
* @return {Object} Object with `raw` (actual language string), and `name`, the language with whitespace trimmed
*/
export function language<O extends matter.GrayMatterOption<string, O>>(
str: string,
options?: GrayMatterOption<string, O>
): { name: string; raw: string };
}

export = matter;

+ 12
- 5
server/sonar-web/src/main/js/app/components/NotFound.js View File

@@ -21,11 +21,18 @@ import React from 'react';
import { Link } from 'react-router';
import SimpleContainer from './SimpleContainer';

export default function NotFound() {
/*::
type Props = {
withContainer?: boolean;
};
*/

export default function NotFound({ withContainer = true } /*: Props*/) {
const Container = withContainer ? SimpleContainer : React.Fragment;
return (
<SimpleContainer>
<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
<Container>
<div className="page-wrapper-simple" id="bd">
<div className="page-simple" id="nonav">
<h2 className="big-spacer-bottom">The page you were looking for does not exist.</h2>
<p className="spacer-bottom">
You may have mistyped the address or the page may have moved.
@@ -35,6 +42,6 @@ export default function NotFound() {
</p>
</div>
</div>
</SimpleContainer>
</Container>
);
}

+ 2
- 1
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx View File

@@ -23,6 +23,7 @@ import { Link } from 'react-router';
import { SuggestionLink } from './SuggestionsProvider';
import BubblePopup, { BubblePopupPosition } from '../../../components/common/BubblePopup';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';

interface Props {
onClose: () => void;
@@ -72,7 +73,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
alt={text}
className="spacer-right"
height="18"
src={'/images/embed-doc/' + icon}
src={`${getBaseUrl()}/images/embed-doc/${icon}`}
width="18"
/>
{text}

+ 0
- 11
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json View File

@@ -1,11 +0,0 @@
{
"projects": [
{ "link": "#", "text": "Foo Suggestion " },
{ "link": "#", "text": "Bar Suggestion " }
],
"profiles": [
{ "link": "#", "text": "Foo Suggestion " },
{ "link": "#", "text": "Bar Suggestion " },
{ "link": "#", "text": "Baz Suggestion " }
]
}

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

@@ -19,7 +19,8 @@
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import * as suggestionsJson from './EmbedDocsSuggestions.json';
//eslint-disable-next-line import/no-extraneous-dependencies
import * as suggestionsJson from 'Docs/EmbedDocsSuggestions.json';
import { SuggestionsContext } from './SuggestionsContext';

export interface SuggestionLink {

+ 4
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css View File

@@ -189,6 +189,10 @@ td.big-spacer-top {
overflow: hidden;
}

.max-width-100 {
max-width: 100%;
}

.width-100 {
width: 100%;
}

+ 2
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -75,6 +75,7 @@ import settingsRoutes from '../../apps/settings/routes';
import systemRoutes from '../../apps/system/routes';
import usersRoutes from '../../apps/users/routes';
import webAPIRoutes from '../../apps/web-api/routes';
import documentationRoutes from '../../apps/documentation/routes';
import webhooksRoutes from '../../apps/webhooks/routes';
import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes';
import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
@@ -170,6 +171,7 @@ const startReactApp = () => {
<Route path="account" childRoutes={accountRoutes} />
<Route path="coding_rules" childRoutes={codingRulesRoutes} />
<Route path="component" childRoutes={componentRoutes} />
<Route path="documentation" childRoutes={documentationRoutes} />
<Route path="explore" component={Explore}>
<Route path="issues" component={ExploreIssues} />
<Route path="projects" component={ExploreProjects} />

+ 2
- 0
server/sonar-web/src/main/js/apps/account/components/Account.js View File

@@ -25,6 +25,7 @@ import UserCard from './UserCard';
import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import '../account.css';

class Account extends React.PureComponent {
@@ -44,6 +45,7 @@ class Account extends React.PureComponent {
const title = translate('my_account.page');
return (
<div id="account-page">
<Suggestions suggestions="account" />
<Helmet defaultTitle={title} titleTemplate={'%s - ' + title} />
<header className="account-header">
<div className="account-container clearfix">

+ 2
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js View File

@@ -29,6 +29,7 @@ import Footer from './Footer';
import StatsContainer from '../components/StatsContainer';
import Search from '../components/Search';
import Tasks from '../components/Tasks';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
getTypes,
getActivity,
@@ -227,6 +228,7 @@ class BackgroundTasksApp extends React.PureComponent {

return (
<div className="page page-limited">
<Suggestions suggestions="background_tasks" />
<Helmet title={translate('background_tasks.page')} />
<Header component={component} />


+ 2
- 0
server/sonar-web/src/main/js/apps/code/components/App.tsx View File

@@ -29,6 +29,7 @@ import { retrieveComponentChildren, retrieveComponent, loadMoreChildren } from '
import { Component, BranchLike } from '../../../app/types';
import ListFooter from '../../../components/controls/ListFooter';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { isSameBranchLike } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
import { parseError } from '../../../helpers/request';
@@ -198,6 +199,7 @@ export default class App extends React.PureComponent<Props, State> {

return (
<div className="page page-limited">
<Suggestions suggestions="code" />
<Helmet title={sourceViewer !== undefined ? sourceViewer.name : translate('code.page')} />

{error && <div className="alert alert-danger">{error}</div>}

+ 2
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx View File

@@ -27,6 +27,7 @@ import FacetsList from './FacetsList';
import PageActions from './PageActions';
import RuleDetails from './RuleDetails';
import RuleListItem from './RuleListItem';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
Facets,
Query,
@@ -466,6 +467,7 @@ export default class App extends React.PureComponent<Props, State> {

return (
<>
<Suggestions suggestions="coding_rules" />
<Helmet title={translate('coding_rules.page')}>
<meta content="noindex" name="robots" />
</Helmet>

+ 2
- 0
server/sonar-web/src/main/js/apps/component-measures/components/App.js View File

@@ -27,6 +27,7 @@ import Sidebar from '../sidebar/Sidebar';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery } from '../utils';
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
getLocalizedMetricDomain,
translateWithParameters,
@@ -177,6 +178,7 @@ export default class App extends React.PureComponent {
const metric = metrics[query.metric];
return (
<div className="layout-page" id="component-measures">
<Suggestions suggestions="component_measures" />
<Helmet title={this.getHelmetTitle(metric, query)} />

<ScreenPositionHelper className="layout-page-side-outer">

+ 3
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap View File

@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = `
className="layout-page"
id="component-measures"
>
<Suggestions
suggestions="component_measures"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}

+ 2
- 0
server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx View File

@@ -28,6 +28,7 @@ import {
deleteCustomMeasure
} from '../../../api/measures';
import { Paging, CustomMeasure } from '../../../app/types';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import ListFooter from '../../../components/controls/ListFooter';
import { translate } from '../../../helpers/l10n';

@@ -133,6 +134,7 @@ export default class App extends React.PureComponent<Props, State> {

return (
<>
<Suggestions suggestions="custom_measures" />
<Helmet title={translate('custom_measures.page')} />
<div className="page page-limited">
<Header

+ 6
- 0
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -2,6 +2,9 @@

exports[`should work 1`] = `
<React.Fragment>
<Suggestions
suggestions="custom_measures"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -20,6 +23,9 @@ exports[`should work 1`] = `

exports[`should work 2`] = `
<React.Fragment>
<Suggestions
suggestions="custom_measures"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}

+ 2
- 0
server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx View File

@@ -22,6 +22,7 @@ import { Helmet } from 'react-helmet';
import { MetricProps } from './Form';
import Header from './Header';
import List from './List';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
getMetricDomains,
getMetricTypes,
@@ -146,6 +147,7 @@ export default class App extends React.PureComponent<Props, State> {

return (
<>
<Suggestions suggestions="custom_metrics" />
<Helmet title={translate('custom_metrics.page')} />
<div className="page page-limited" id="custom-metrics-page">
<Header domains={domains} loading={loading} onCreate={this.handleCreate} types={types} />

+ 6
- 0
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -2,6 +2,9 @@

exports[`should work 1`] = `
<React.Fragment>
<Suggestions
suggestions="custom_metrics"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -21,6 +24,9 @@ exports[`should work 1`] = `

exports[`should work 2`] = `
<React.Fragment>
<Suggestions
suggestions="custom_metrics"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}

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

@@ -0,0 +1,130 @@
/*
* 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 matter from 'gray-matter';
import Helmet from 'react-helmet';
import { Link } from 'react-router';
import Menu from './Menu';
import NotFound from '../../../app/components/NotFound';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { translate } from '../../../helpers/l10n';

interface Props {
params: { splat?: string };
}

interface State {
content?: string;
loading: boolean;
notFound: boolean;
}

export default class App extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false, notFound: false };

componentDidMount() {
this.mounted = true;
this.fetchContent(this.props.params.splat || 'index');
}

componentWillReceiveProps(nextProps: Props) {
const newSplat = nextProps.params.splat || 'index';
if (newSplat !== this.props.params.splat) {
this.setState({ content: undefined });
this.fetchContent(newSplat);
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchContent = (path: string) => {
this.setState({ loading: true });
import(`Docs/pages/${path === '' ? 'index' : path}.md`).then(
({ default: content }) => {
if (this.mounted) {
this.setState({ content, loading: false, notFound: false });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false, notFound: true });
}
}
);
};

renderContent() {
if (this.state.loading) {
return <DeferredSpinner />;
}

if (this.state.notFound) {
return <NotFound withContainer={false} />;
}

return (
<div className="boxed-group">
<DocMarkdownBlock
className="cut-margins boxed-group-inner"
content={this.state.content}
displayH1={true}
/>
</div>
);
}

render() {
const pageTitle = matter(this.state.content || '').data.title;
const mainTitle = translate('documentation.page');
const isIndex = !this.props.params.splat || this.props.params.splat === '';
return (
<div className="layout-page">
<Helmet title={isIndex || this.state.notFound ? mainTitle : `${pageTitle} - ${mainTitle}`}>
<meta content="noindex nofollow" name="robots" />
</Helmet>
<ScreenPositionHelper className="layout-page-side-outer">
{({ top }) => (
<div className="layout-page-side" style={{ top }}>
<div className="layout-page-side-inner">
<div className="layout-page-filters">
<div className="web-api-page-header">
<Link to="/documentation/">
<h1>{translate('documentation.page')}</h1>
</Link>
</div>
<Menu splat={this.props.params.splat} />
</div>
</div>
</div>
)}
</ScreenPositionHelper>

<div className="layout-page-main">
<div className="layout-page-main-inner">{this.renderContent()}</div>
</div>
</div>
);
}
}

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

@@ -0,0 +1,77 @@
/*
* 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 { Link } from 'react-router';
import * as classNames from 'classnames';
import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon';
import {
activeOrChildrenActive,
DocumentationEntry,
getEntryChildren,
getEntryRoot
} from '../utils';
import * as Docs from '../documentation.directory-loader';

interface Props {
splat?: string;
}

export default class Menu extends React.PureComponent<Props> {
getMenuEntriesHierarchy = (root?: string): Array<DocumentationEntry> => {
const toplevelEntries = getEntryChildren(Docs as any, root);
toplevelEntries.forEach(entry => {
const entryRoot = getEntryRoot(entry.relativeName);
entry.children = entryRoot !== '' ? this.getMenuEntriesHierarchy(entryRoot) : [];
});
return toplevelEntries;
};

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

render() {
return (
<div className="api-documentation-results panel">
<div className="list-group">
{this.getMenuEntriesHierarchy().map(entry => this.renderEntry(entry, 0))}
</div>
</div>
);
}
}

+ 24
- 0
server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js View File

@@ -0,0 +1,24 @@
/*
* 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 path = require('path');

module.exports = {
root: path.resolve(__dirname, '../../../../../../sonar-docs/src/pages')
};

+ 32
- 0
server/sonar-web/src/main/js/apps/documentation/routes.ts 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 { lazyLoad } from '../../components/lazyLoad';

const routes = [
{
indexRoute: { component: lazyLoad(() => import('./components/App')) }
},
{
path: '**',
indexRoute: { component: lazyLoad(() => import('./components/App')) }
}
];

export default routes;

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

@@ -0,0 +1,54 @@
/*
* 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.
*/
export interface DocumentationEntry {
title: string;
order: string;
name: string;
relativeName: string;
children: Array<DocumentationEntry>;
}

export function activeOrChildrenActive(root: string, entry: DocumentationEntry) {
return root.indexOf(getEntryRoot(entry.relativeName)) === 0;
}

export function getEntryRoot(name: string) {
if (name.endsWith('index')) {
return name
.split('/')
.slice(0, -1)
.join('/');
}
return name;
}

export function getEntryChildren(
entries: Array<DocumentationEntry>,
root?: string
): Array<DocumentationEntry> {
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')
);
});
}

+ 2
- 0
server/sonar-web/src/main/js/apps/groups/components/App.tsx View File

@@ -21,6 +21,7 @@ import * as React from 'react';
import { Helmet } from 'react-helmet';
import Header from './Header';
import List from './List';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { searchUsersGroups, deleteGroup, updateGroup, createGroup } from '../../../api/user_groups';
import { Group, Paging } from '../../../app/types';
import ListFooter from '../../../components/controls/ListFooter';
@@ -140,6 +141,7 @@ export default class App extends React.PureComponent<Props, State> {

return (
<>
<Suggestions suggestions="user_groups" />
<Helmet title={translate('user_groups.page')} />
<div className="page page-limited" id="groups-page">
<Header loading={loading} onCreate={this.handleCreate} />

+ 2
- 0
server/sonar-web/src/main/js/apps/issues/components/App.tsx View File

@@ -34,6 +34,7 @@ import PageActions from './PageActions';
import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList';
import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader';
import Sidebar from '../sidebar/Sidebar';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import * as actions from '../actions';
import {
areMyIssuesSelected,
@@ -1003,6 +1004,7 @@ export default class App extends React.PureComponent<Props, State> {
const selectedIndex = this.getSelectedIndex();
return (
<div className="layout-page issues" id="issues-page">
<Suggestions suggestions="issues" />
<Helmet title={openIssue ? openIssue.message : translate('issues.page')} />

{this.renderSide(openIssue)}

+ 2
- 0
server/sonar-web/src/main/js/apps/marketplace/App.tsx View File

@@ -27,6 +27,7 @@ import Footer from './Footer';
import PluginsList from './PluginsList';
import Search from './Search';
import { filterPlugins, parseQuery, Query, serializeQuery } from './utils';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import {
getAvailablePlugins,
getInstalledPluginsWithUpdates,
@@ -136,6 +137,7 @@ export default class App extends React.PureComponent<Props, State> {

return (
<div className="page page-limited" id="marketplace-page">
<Suggestions suggestions="marketplace" />
<Helmet title={translate('marketplace.page')} />
<Header />
<EditionBoxes

+ 2
- 0
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx View File

@@ -29,6 +29,7 @@ import Coverage from '../main/Coverage';
import Duplications from '../main/Duplications';
import Meta from '../meta/Meta';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { getMeasuresAndMeta } from '../../../api/measures';
import { getAllTimeMachineData, History } from '../../../api/time-machine';
import { parseDate } from '../../../helpers/dates';
@@ -237,6 +238,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
return (
<div className="page page-limited">
<div className="overview page-with-sidebar">
<Suggestions suggestions="overview" />
<Helmet>
<link href={getPathUrlAsString(getProjectUrl(component.key))} rel="canonical" />
</Helmet>

+ 2
- 0
server/sonar-web/src/main/js/apps/permission-templates/components/App.js View File

@@ -22,6 +22,7 @@ import PropTypes from 'prop-types';
import Home from './Home';
import Template from './Template';
import OrganizationHelmet from '../../../components/common/OrganizationHelmet';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { getPermissionTemplates } from '../../../api/permissions';
import { sortPermissions, mergePermissionsToTemplates, mergeDefaultsToTemplates } from '../utils';
import { translate } from '../../../helpers/l10n';
@@ -103,6 +104,7 @@ export default class App extends React.PureComponent {
const { id } = this.props.location.query;
return (
<div>
<Suggestions suggestions="permission_templates" />
<OrganizationHelmet
title={translate('permission_templates.page')}
organization={this.props.organization}

+ 2
- 0
server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx View File

@@ -22,6 +22,7 @@ import Helmet from 'react-helmet';
import PageHeader from './PageHeader';
import AllHoldersListContainer from './AllHoldersListContainer';
import PageError from '../../shared/components/PageError';
import Suggestions from '../../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../../helpers/l10n';
import { Organization } from '../../../../app/types';
import '../../styles.css';
@@ -33,6 +34,7 @@ interface Props {
export default function App({ organization }: Props) {
return (
<div className="page page-limited">
<Suggestions suggestions="global_permissions" />
<Helmet title={translate('global_permissions.permission')} />
<PageHeader organization={organization} />
<PageError />

+ 2
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js View File

@@ -25,6 +25,7 @@ import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
import ProjectActivityGraphs from './ProjectActivityGraphs';
import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import './projectActivity.css';
/*:: import type { Analysis, MeasureHistory, Metric, Query } from '../types'; */

@@ -61,6 +62,7 @@ export default function ProjectActivityApp(props /*: Props */) {
const canDeleteAnalyses = configuration ? configuration.showHistory : false;
return (
<div id="project-activity" className="page page-limited">
<Suggestions suggestions="project_activity" />
<Helmet title={translate('project_activity.page')} />

<ProjectActivityPageHeader

+ 3
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap View File

@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = `
className="page page-limited"
id="project-activity"
>
<Suggestions
suggestions="project_activity"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}

+ 2
- 0
server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx View File

@@ -28,6 +28,7 @@ import {
dissociateGateWithProject,
QualityGate
} from '../../api/quality-gates';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
import { Component } from '../../app/types';
@@ -128,6 +129,7 @@ export default class App extends React.PureComponent<Props> {

return (
<div id="project-quality-gate" className="page page-limited">
<Suggestions suggestions="project_quality_gate" />
<Helmet title={translate('project_quality_gate.page')} />
<Header />
{loading ? (

+ 4
- 4
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx View File

@@ -34,7 +34,7 @@ jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({
}));

import * as React from 'react';
import { mount } from 'enzyme';
import { shallow } from 'enzyme';
import App from '../App';
import { Component } from '../../../app/types';

@@ -73,7 +73,7 @@ beforeEach(() => {

it('checks permissions', () => {
handleRequiredAuthorization.mockClear();
mount(
shallow(
<App
component={{ ...component, configuration: undefined } as Component}
onComponentChange={jest.fn()}
@@ -85,7 +85,7 @@ it('checks permissions', () => {
it('fetches quality gates', () => {
fetchQualityGates.mockClear();
getGateForProject.mockClear();
mount(<App component={component} onComponentChange={jest.fn()} />);
shallow(<App component={component} onComponentChange={jest.fn()} />);
expect(fetchQualityGates).toBeCalledWith({ organization: 'org' });
expect(getGateForProject).toBeCalledWith({ organization: 'org', project: 'component' });
});
@@ -140,7 +140,7 @@ function randomGate(id: string, isDefault = false) {
}

function mountRender(allGates: any[], gate?: any) {
const wrapper = mount(<App component={component} onComponentChange={jest.fn()} />);
const wrapper = shallow(<App component={component} onComponentChange={jest.fn()} />);
wrapper.setState({ allGates, loading: false, gate });
return wrapper;
}

+ 2
- 0
server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx View File

@@ -28,6 +28,7 @@ import {
Profile
} from '../../api/quality-profiles';
import { Component } from '../../app/types';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
import { translate, translateWithParameters } from '../../helpers/l10n';
@@ -118,6 +119,7 @@ export default class QualityProfiles extends React.PureComponent<Props, State> {

return (
<div className="page page-limited">
<Suggestions suggestions="project_quality_profiles" />
<Helmet title={translate('project_quality_profiles.page')} />

<Header />

+ 4
- 4
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx View File

@@ -33,7 +33,7 @@ jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({
}));

import * as React from 'react';
import { mount } from 'enzyme';
import { shallow } from 'enzyme';
import App from '../App';

const associateProject = require('../../../api/quality-profiles').associateProject as jest.Mock<
@@ -66,13 +66,13 @@ const component = {

it('checks permissions', () => {
handleRequiredAuthorization.mockClear();
mount(<App component={{ ...component, configuration: undefined }} />);
shallow(<App component={{ ...component, configuration: undefined }} />);
expect(handleRequiredAuthorization).toBeCalled();
});

it('fetches profiles', () => {
searchQualityProfiles.mockClear();
mount(<App component={component} />);
shallow(<App component={component} />);
expect(searchQualityProfiles.mock.calls).toHaveLength(2);
expect(searchQualityProfiles).toBeCalledWith({ organization: 'org' });
expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', project: 'foo' });
@@ -82,7 +82,7 @@ it('changes profile', () => {
associateProject.mockClear();
dissociateProject.mockClear();
addGlobalSuccessMessage.mockClear();
const wrapper = mount(<App component={component} />);
const wrapper = shallow(<App component={component} />);

const fooJava = randomProfile('foo-java', 'java');
const fooJs = randomProfile('foo-js', 'js');

+ 2
- 0
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx View File

@@ -26,6 +26,7 @@ import Projects from './Projects';
import CreateProjectForm from './CreateProjectForm';
import { PAGE_SIZE, Project } from './utils';
import ListFooter from '../../components/controls/ListFooter';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { getComponents } from '../../api/components';
import { Organization } from '../../app/types';
import { toNotSoISOString } from '../../helpers/dates';
@@ -164,6 +165,7 @@ export default class App extends React.PureComponent<Props, State> {
render() {
return (
<div className="page page-limited" id="projects-management-page">
<Suggestions suggestions="projects_management" />
<Helmet title={translate('projects_management')} />

<Header

+ 2
- 0
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js View File

@@ -23,6 +23,7 @@ import Helmet from 'react-helmet';
import ListHeader from './ListHeader';
import List from './List';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { fetchQualityGates } from '../../../api/quality-gates';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
@@ -79,6 +80,7 @@ export default class QualityGatesApp extends Component {
const defaultTitle = translate('quality_gates.page');
return (
<div id="quality-gates-page" className="layout-page">
<Suggestions suggestions="quality_gates" />
<Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />

<ScreenPositionHelper className="layout-page-side-outer">

+ 2
- 0
server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx View File

@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import { searchQualityProfiles, getExporters, Actions } from '../../../api/quality-profiles';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { sortProfiles } from '../utils';
import { translate } from '../../../helpers/l10n';
import OrganizationHelmet from '../../../components/common/OrganizationHelmet';
@@ -109,6 +110,7 @@ export default class App extends React.PureComponent<Props, State> {
render() {
return (
<div className="page page-limited">
<Suggestions suggestions="quality_profiles" />
<OrganizationHelmet
title={translate('quality_profiles.page')}
organization={this.props.organization}

+ 2
- 0
server/sonar-web/src/main/js/apps/settings/components/App.js View File

@@ -24,6 +24,7 @@ import PageHeader from './PageHeader';
import CategoryDefinitionsList from './CategoryDefinitionsList';
import AllCategoriesList from './AllCategoriesList';
import WildcardsHelp from './WildcardsHelp';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import '../styles.css';

@@ -68,6 +69,7 @@ export default class App extends React.PureComponent {

return (
<div id="settings-page" className="page page-limited">
<Suggestions suggestions="settings" />
<Helmet title={translate('settings.page')} />

<PageHeader component={this.props.component} />

+ 2
- 0
server/sonar-web/src/main/js/apps/system/components/App.tsx View File

@@ -24,6 +24,7 @@ import ClusterSysInfos from './ClusterSysInfos';
import PageHeader from './PageHeader';
import StandaloneSysInfos from './StandaloneSysInfos';
import SystemUpgradeNotif from './system-upgrade/SystemUpgradeNotif';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import { ClusterSysInfo, getSystemInfo, SysInfo } from '../../../api/system';
import {
@@ -128,6 +129,7 @@ export default class App extends React.PureComponent<Props, State> {
const { loading, sysInfoData } = this.state;
return (
<div className="page page-limited">
<Suggestions suggestions="system_info" />
<Helmet title={translate('system_info.page')} />
<SystemUpgradeNotif />
<PageHeader

+ 2
- 0
server/sonar-web/src/main/js/apps/users/UsersApp.tsx View File

@@ -26,6 +26,7 @@ import Search from './Search';
import UsersList from './UsersList';
import { parseQuery, Query, serializeQuery } from './utils';
import ListFooter from '../../components/controls/ListFooter';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { getIdentityProviders, searchUsers } from '../../api/users';
import { Paging, IdentityProvider, User } from '../../app/types';
import { translate } from '../../helpers/l10n';
@@ -124,6 +125,7 @@ export default class UsersApp extends React.PureComponent<Props, State> {
const { loading, paging, users } = this.state;
return (
<div id="users-page" className="page page-limited">
<Suggestions suggestions="users" />
<Helmet title={translate('users.page')} />
<Header loading={loading} onUpdateUsers={this.fetchUsers} />
<Search query={query} updateQuery={this.updateQuery} />

+ 6
- 0
server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap View File

@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = `
className="page page-limited"
id="users-page"
>
<Suggestions
suggestions="users"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -43,6 +46,9 @@ exports[`should render correctly 2`] = `
className="page page-limited"
id="users-page"
>
<Suggestions
suggestions="users"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}

+ 2
- 0
server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx View File

@@ -29,6 +29,7 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe
import { getActionKey, isDomainPathActive } from '../utils';
import { scrollToElement } from '../../../helpers/scrolling';
import { translate } from '../../../helpers/l10n';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import '../styles/web-api.css';

interface Props {
@@ -148,6 +149,7 @@ export default class WebApiApp extends React.PureComponent<Props, State> {

return (
<div className="layout-page">
<Suggestions suggestions="api_documentation" />
<Helmet title={translate('api_documentation.page')} />
<ScreenPositionHelper className="layout-page-side-outer">
{({ top }) => (

+ 2
- 0
server/sonar-web/src/main/js/apps/webhooks/components/App.tsx View File

@@ -23,6 +23,7 @@ import PageActions from './PageActions';
import PageHeader from './PageHeader';
import WebhooksList from './WebhooksList';
import { createWebhook, deleteWebhook, searchWebhooks, updateWebhook } from '../../../api/webhooks';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { LightComponent, Organization, Webhook } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

@@ -113,6 +114,7 @@ export default class App extends React.PureComponent<Props, State> {

return (
<>
<Suggestions suggestions="webhooks" />
<Helmet title={translate('webhooks.page')} />

<div className="page page-limited">

+ 6
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -2,6 +2,9 @@

exports[`should be in loading status 1`] = `
<React.Fragment>
<Suggestions
suggestions="webhooks"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -25,6 +28,9 @@ exports[`should be in loading status 1`] = `

exports[`should fetch webhooks and display them 1`] = `
<React.Fragment>
<Suggestions
suggestions="webhooks"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}

+ 30
- 0
server/sonar-web/src/main/js/components/docs/DocImg.tsx View File

@@ -0,0 +1,30 @@
/*
* 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 DocImg(props: React.ImgHTMLAttributes<HTMLImageElement>) {
const { alt, src, ...other } = props;

if (process.env.NODE_ENV === 'development') {
return <img alt={alt} className="max-width-100" src={'/' + src} {...other} />;
}

return <img alt={alt} className="max-width-100" src={'/images/embed-doc/' + src} {...other} />;
}

+ 70
- 0
server/sonar-web/src/main/js/components/docs/DocInclude.tsx View File

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

interface Props {
path: string;
}

interface State {
content?: string;
}

export default class DocInclude extends React.PureComponent<Props, State> {
mounted = false;
state: State = {};

componentDidMount() {
this.mounted = true;
this.fetchContent();
}

componentWillReceiveProps(nextProps: Props) {
if (nextProps.path !== this.props.path) {
this.setState({ content: undefined });
}
}

componentDidUpdate(prevProps: Props) {
if (prevProps.path !== this.props.path) {
this.fetchContent();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchContent = () => {
import(`Docs/${this.props.path}.md`).then(
({ default: content }) => {
if (this.mounted) {
this.setState({ content });
}
},
() => {}
);
};

render() {
return <DocMarkdownBlock content={this.state.content} />;
}
}

+ 6
- 12
server/sonar-web/src/main/js/components/docs/DocLink.tsx View File

@@ -23,18 +23,12 @@ import { Link } from 'react-router';
export default function DocLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const { children, href, ...other } = props;

if (process.env.NODE_ENV === 'development') {
if (href && href.startsWith('#')) {
return (
<>
{/* TODO implement after SONAR-10612 Create documentation space in the web app */}
<Link to="" {...other}>
{children}
</Link>
<strong className="little-spacer-left text-danger">[TODO]</strong>
</>
);
}
if (href && href.startsWith('/')) {
return (
<Link to={`/documentation/${href.substr(1)}`} {...other}>
{children}
</Link>
);
}

return (

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

@@ -21,27 +21,39 @@ import * as React from 'react';
import * as classNames from 'classnames';
import remark from 'remark';
import reactRenderer from 'remark-react';
import * as matter from 'gray-matter';
import DocLink from './DocLink';
import DocParagraph from './DocParagraph';
import DocImg from './DocImg';

interface Props {
className?: string;
content: string | undefined;
displayH1?: boolean;
}

export default function DocMarkdownBlock({ className, content }: Props) {
export default function DocMarkdownBlock({ className, content, displayH1 }: Props) {
const parsed = matter(content || '');
return (
<div className={classNames('markdown', className)}>
{displayH1 && <h1>{parsed.data.title}</h1>}
{
remark()
// .use(remarkInclude)
.use(reactRenderer, {
remarkReactComponents: {
// do not render outer <div />
div: React.Fragment,
// use custom link to render documentation anchors
a: DocLink
}
a: DocLink,
// used to handle `@include`
p: DocParagraph,
// use custom img tag to render documentation images
img: DocImg
},
toHast: {}
})
.processSync(content).contents
.processSync(parsed.content).contents
}
</div>
);

+ 35
- 0
server/sonar-web/src/main/js/components/docs/DocParagraph.tsx 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';
import DocInclude from './DocInclude';

const INCLUDE = '@include';

export default function DocParagraph(props: React.HTMLAttributes<HTMLParagraphElement>) {
if (Array.isArray(props.children) && props.children.length === 1) {
const child = props.children[0];
if (typeof child === 'string' && child.startsWith(INCLUDE)) {
const includePath = child.substr(INCLUDE.length + 1);
return <DocInclude path={includePath} />;
}
}

return <p {...props} />;
}

+ 1
- 1
server/sonar-web/src/main/js/components/docs/DocTooltip.tsx View File

@@ -59,7 +59,7 @@ export default class DocTooltip extends React.PureComponent<Props, State> {

fetchContent = () => {
this.setState({ loading: true });
import(`Docs/${this.props.doc}.md`).then(
import(`Docs/tooltips/${this.props.doc}.md`).then(
({ default: content }) => {
if (this.mounted) {
this.setState({ content, loading: false });

+ 2
- 2
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap View File

@@ -7,7 +7,7 @@ exports[`should render simple markdown 1`] = `
<React.Fragment
key="h-1"
>
<p
<DocParagraph
key="h-2"
>
this is
@@ -17,7 +17,7 @@ exports[`should render simple markdown 1`] = `
bold
</em>
text
</p>
</DocParagraph>
</React.Fragment>
</div>
`;

+ 5
- 1
server/sonar-web/tsconfig.json View File

@@ -12,7 +12,11 @@
"lib": ["es2017", "dom"],
"module": "esnext",
"moduleResolution": "node",
"sourceMap": true
"sourceMap": true,
"baseUrl": ".",
"paths": {
"*": ["./src/main/js/@types/*"]
}
},
"include": ["./src/main/js/**/*"]
}

+ 50
- 5
server/sonar-web/yarn.lock View File

@@ -43,6 +43,18 @@
"@types/cheerio" "*"
"@types/react" "*"

"@types/events@*":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"

"@types/glob@*":
version "5.0.35"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a"
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"

"@types/history@^3":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.2.tgz#b6affa240cb10b5f841c6443d8a24d7f3fc8bb0c"
@@ -59,6 +71,10 @@
version "4.14.102"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.102.tgz#586a3e22385fc79b07cef9c5a1c8a5387986fbc8"

"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"

"@types/node@*":
version "6.0.90"
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.90.tgz#0ed74833fa1b73dcdb9409dcb1c97ec0a8b13b02"
@@ -3506,13 +3522,19 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"

glob-promise@3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20"
dependencies:
"@types/glob" "*"

glob2base@^0.0.12:
version "0.0.12"
resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56"
dependencies:
find-index "^0.1.1"

glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
glob@7.1.2, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3591,6 +3613,15 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"

gray-matter@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.1.tgz#375263c194f0d9755578c277e41b1c1dfdf22c7d"
dependencies:
js-yaml "^3.11.0"
kind-of "^6.0.2"
section-matter "^1.0.0"
strip-bom-string "^1.0.0"

growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -4774,6 +4805,13 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"

js-yaml@^3.11.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"

js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.1:
version "3.10.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
@@ -5122,10 +5160,6 @@ lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"

lodash.cond@^4.3.0:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"

lodash.escape@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698"
@@ -7463,6 +7497,13 @@ schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.5:
ajv "^6.1.0"
ajv-keywords "^3.1.0"

section-matter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
dependencies:
extend-shallow "^2.0.1"
kind-of "^6.0.0"

select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7959,6 +8000,10 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"

strip-bom-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"

strip-bom@3.0.0, strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"

+ 8
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2416,6 +2416,14 @@ api_documentation.changelog=Changelog
api_documentation.search=Search by name...


#------------------------------------------------------------------------------
#
# DOCUMENTATION PAGE
#
#------------------------------------------------------------------------------
documentation.page=Documentation


#------------------------------------------------------------------------------
#
# CODE

Loading…
Cancel
Save