diff options
author | Pascal Mugnier <pascal.mugnier@sonarsource.com> | 2018-04-26 08:42:19 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-05-03 20:20:50 +0200 |
commit | b083d4376580b8dd252933025ca86ce5b98ce7af (patch) | |
tree | a0d05a812d6c70c3d8573106b12a1ec74f6faefa /server/sonar-web/src/main/js | |
parent | 0c996218c1a2c2542c3465a7bf1f38ee386132da (diff) | |
download | sonarqube-b083d4376580b8dd252933025ca86ce5b98ce7af.tar.gz sonarqube-b083d4376580b8dd252933025ca86ce5b98ce7af.zip |
SONAR-10612 Create documentation space in the web app
Diffstat (limited to 'server/sonar-web/src/main/js')
51 files changed, 716 insertions, 45 deletions
diff --git a/server/sonar-web/src/main/js/@types/gray-matter.d.ts b/server/sonar-web/src/main/js/@types/gray-matter.d.ts new file mode 100644 index 00000000000..ee5cb17e576 --- /dev/null +++ b/server/sonar-web/src/main/js/@types/gray-matter.d.ts @@ -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; diff --git a/server/sonar-web/src/main/js/app/components/NotFound.js b/server/sonar-web/src/main/js/app/components/NotFound.js index 3f1d81ac0be..37e2a27fa64 100644 --- a/server/sonar-web/src/main/js/app/components/NotFound.js +++ b/server/sonar-web/src/main/js/app/components/NotFound.js @@ -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> ); } diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx index 4a75361d7e5..d81f6bf8806 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json deleted file mode 100644 index ae1ce94f6fd..00000000000 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json +++ /dev/null @@ -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 " } - ] -} diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx index 833c382e066..c400a69faba 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx @@ -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 { diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 80c0438930e..bcdeb28cd2f 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -189,6 +189,10 @@ td.big-spacer-top { overflow: hidden; } +.max-width-100 { + max-width: 100%; +} + .width-100 { width: 100%; } diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 6d38859ecd7..35ba6778ddf 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/account/components/Account.js b/server/sonar-web/src/main/js/apps/account/components/Account.js index 596c837c096..0266a3efaf6 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Account.js +++ b/server/sonar-web/src/main/js/apps/account/components/Account.js @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js index 0c2770ca719..b8d27cf0e7f 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx index 3c9ebfc25f4..99f607b359f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx @@ -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>} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 19441b29218..f9ad0c2eafd 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js index b18f85ca24f..03e0dd641ec 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap index a5a8537c675..70583de970f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap @@ -5,6 +5,9 @@ exports[`should render correctly 1`] = ` className="layout-page" id="component-measures" > + <Suggestions + suggestions="component_measures" + /> <HelmetWrapper defer={true} encodeSpecialCharacters={true} diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx index 79e4a5e5d67..620b3f6a528 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap index cb97413281e..c9a08f12b4d 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx index f802bc704f8..a8f87cf9941 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap index 55f852e2689..6a3320cd191 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/App-test.tsx.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx new file mode 100644 index 00000000000..90b0bfb46ea --- /dev/null +++ b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx new file mode 100644 index 00000000000..6eb6a4c610d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js b/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js new file mode 100644 index 00000000000..447060a8f47 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js @@ -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') +}; diff --git a/server/sonar-web/src/main/js/apps/documentation/routes.ts b/server/sonar-web/src/main/js/apps/documentation/routes.ts new file mode 100644 index 00000000000..704e4739073 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/documentation/routes.ts @@ -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; diff --git a/server/sonar-web/src/main/js/apps/documentation/utils.ts b/server/sonar-web/src/main/js/apps/documentation/utils.ts new file mode 100644 index 00000000000..70ec0d1f12b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/documentation/utils.ts @@ -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') + ); + }); +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/App.tsx b/server/sonar-web/src/main/js/apps/groups/components/App.tsx index dac3048bf49..e44998e4bde 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/App.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index 3ab8f9023e8..df3a3a1374c 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -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)} diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx index 9446bfe471c..8b151058a49 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx index ff2bff3aa8c..097e642860f 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/App.js b/server/sonar-web/src/main/js/apps/permission-templates/components/App.js index 33e8bea47d0..df020fbe8a4 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/App.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/App.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx index ed4397179b8..d79c2bd51c8 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx @@ -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 /> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index cc920d30286..567853ebe55 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -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 diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap index 71e18479081..1500e14c91d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx index 4b6c1615d89..9d1d86e32dc 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx @@ -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 ? ( diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx index ff0be985150..158e002447c 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx @@ -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; } diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx index a1f5ed8163a..381e56d4fd1 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx @@ -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 /> diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx index 42756844899..685ceb6f749 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx @@ -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'); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index 24b422552fe..978feca7bba 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js index 81e1034c736..dbd4a441f63 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx index a831d023512..2fe1b621c97 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js index 00e3599079b..ad96321df75 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/App.js +++ b/server/sonar-web/src/main/js/apps/settings/components/App.js @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/system/components/App.tsx b/server/sonar-web/src/main/js/apps/system/components/App.tsx index 07ce3c1d325..2d4b850ae93 100644 --- a/server/sonar-web/src/main/js/apps/system/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/App.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx index 1cf4ee7e669..e0ea3203655 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap index c955a2eef06..68ea461d488 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx index cb2b5e5f4d2..0b79a15c483 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx @@ -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 }) => ( diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx index 9d33f1486e9..e29280e7d14 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap index 43c7785c6e0..e8934607b3e 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap @@ -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} diff --git a/server/sonar-web/src/main/js/components/docs/DocImg.tsx b/server/sonar-web/src/main/js/components/docs/DocImg.tsx new file mode 100644 index 00000000000..4df9753ccb1 --- /dev/null +++ b/server/sonar-web/src/main/js/components/docs/DocImg.tsx @@ -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} />; +} diff --git a/server/sonar-web/src/main/js/components/docs/DocInclude.tsx b/server/sonar-web/src/main/js/components/docs/DocInclude.tsx new file mode 100644 index 00000000000..b74eff2421c --- /dev/null +++ b/server/sonar-web/src/main/js/components/docs/DocInclude.tsx @@ -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} />; + } +} diff --git a/server/sonar-web/src/main/js/components/docs/DocLink.tsx b/server/sonar-web/src/main/js/components/docs/DocLink.tsx index fd19f91cd94..17b99af9c4e 100644 --- a/server/sonar-web/src/main/js/components/docs/DocLink.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocLink.tsx @@ -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 ( diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx index 7a74a9f4b10..88fcfb65cc8 100644 --- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx @@ -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> ); diff --git a/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx b/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx new file mode 100644 index 00000000000..54b34660482 --- /dev/null +++ b/server/sonar-web/src/main/js/components/docs/DocParagraph.tsx @@ -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} />; +} diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx index 0dc27dd9954..5c719443490 100644 --- a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx @@ -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 }); diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap index 28a2441e530..7d44e66dc15 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap @@ -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> `; |