@@ -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": [] | |||
} |
@@ -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); | |||
}; |
@@ -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); | |||
}; |
@@ -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))); | |||
}; |
@@ -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') | |||
}; |
@@ -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([ | |||
{ |
@@ -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", |
@@ -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, |
@@ -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; |
@@ -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> | |||
); | |||
} |
@@ -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} |
@@ -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 " } | |||
] | |||
} |
@@ -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 { |
@@ -189,6 +189,10 @@ td.big-spacer-top { | |||
overflow: hidden; | |||
} | |||
.max-width-100 { | |||
max-width: 100%; | |||
} | |||
.width-100 { | |||
width: 100%; | |||
} |
@@ -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} /> |
@@ -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"> |
@@ -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} /> | |||
@@ -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>} |
@@ -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> |
@@ -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"> |
@@ -5,6 +5,9 @@ exports[`should render correctly 1`] = ` | |||
className="layout-page" | |||
id="component-measures" | |||
> | |||
<Suggestions | |||
suggestions="component_measures" | |||
/> | |||
<HelmetWrapper | |||
defer={true} | |||
encodeSpecialCharacters={true} |
@@ -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 |
@@ -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} |
@@ -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} /> |
@@ -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} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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') | |||
}; |
@@ -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; |
@@ -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') | |||
); | |||
}); | |||
} |
@@ -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} /> |
@@ -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)} |
@@ -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 |
@@ -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> |
@@ -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} |
@@ -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 /> |
@@ -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 |
@@ -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} |
@@ -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 ? ( |
@@ -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; | |||
} |
@@ -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 /> |
@@ -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'); |
@@ -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 |
@@ -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"> |
@@ -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} |
@@ -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} /> |
@@ -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 |
@@ -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} /> |
@@ -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} |
@@ -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 }) => ( |
@@ -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"> |
@@ -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} |
@@ -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} />; | |||
} |
@@ -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} />; | |||
} | |||
} |
@@ -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 ( |
@@ -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> | |||
); |
@@ -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} />; | |||
} |
@@ -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 }); |
@@ -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> | |||
`; |
@@ -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/**/*"] | |||
} |
@@ -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" |
@@ -2416,6 +2416,14 @@ api_documentation.changelog=Changelog | |||
api_documentation.search=Search by name... | |||
#------------------------------------------------------------------------------ | |||
# | |||
# DOCUMENTATION PAGE | |||
# | |||
#------------------------------------------------------------------------------ | |||
documentation.page=Documentation | |||
#------------------------------------------------------------------------------ | |||
# | |||
# CODE |