aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-docs/src/layouts
diff options
context:
space:
mode:
authorPascal Mugnier <pascal.mugnier@sonarsource.com>2018-09-10 11:32:33 +0200
committersonartech <sonartech@sonarsource.com>2018-09-19 10:50:57 +0200
commit55822b5c264747a28161902119482ef7f000ab66 (patch)
treea5a984280429502ffe41219b21502bece97727b1 /server/sonar-docs/src/layouts
parent80418249fe5b1f8cf3ee10f4f80d538351fd106c (diff)
downloadsonarqube-55822b5c264747a28161902119482ef7f000ab66.tar.gz
sonarqube-55822b5c264747a28161902119482ef7f000ab66.zip
MMF-1377 Finalize the static documentation site (#673)
Diffstat (limited to 'server/sonar-docs/src/layouts')
-rw-r--r--server/sonar-docs/src/layouts/components/CategoryLink.js59
-rw-r--r--server/sonar-docs/src/layouts/components/Footer.js47
-rw-r--r--server/sonar-docs/src/layouts/components/HeaderList.js41
-rw-r--r--server/sonar-docs/src/layouts/components/HeaderListProvider.js47
-rw-r--r--server/sonar-docs/src/layouts/components/HeadingsLink.js93
-rw-r--r--server/sonar-docs/src/layouts/components/OutsideClickHandler.js54
-rw-r--r--server/sonar-docs/src/layouts/components/Search.js98
-rw-r--r--server/sonar-docs/src/layouts/components/SearchEntryResult.js74
-rw-r--r--server/sonar-docs/src/layouts/components/Sidebar.js128
-rw-r--r--server/sonar-docs/src/layouts/components/SubpageLink.js35
-rw-r--r--server/sonar-docs/src/layouts/components/VersionSelect.js68
-rw-r--r--server/sonar-docs/src/layouts/components/icons/AlertWarnIcon.js32
-rw-r--r--server/sonar-docs/src/layouts/components/icons/ChevronDownIcon.js32
-rw-r--r--server/sonar-docs/src/layouts/components/icons/ChevronUpIcon.js32
-rw-r--r--server/sonar-docs/src/layouts/components/icons/DownloadIcon.js36
-rw-r--r--server/sonar-docs/src/layouts/components/icons/Icon.js52
-rw-r--r--server/sonar-docs/src/layouts/index.js135
-rw-r--r--server/sonar-docs/src/layouts/utils.js103
18 files changed, 1132 insertions, 34 deletions
diff --git a/server/sonar-docs/src/layouts/components/CategoryLink.js b/server/sonar-docs/src/layouts/components/CategoryLink.js
new file mode 100644
index 00000000000..2cfc743b1ec
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/CategoryLink.js
@@ -0,0 +1,59 @@
+/*
+ * 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 'gatsby-link';
+import SubpageLink from './SubpageLink';
+import HeadingsLink from './HeadingsLink';
+import { sortNodes } from '../utils';
+import ChevronDownIcon from './icons/ChevronDownIcon';
+import ChevronUpIcon from './icons/ChevronUpIcon';
+
+export default function CategoryLink({ node, location, headers, onToggle }) {
+ const hasChild = node.pages && node.pages.length > 0;
+ const prefix = process.env.GATSBY_USE_PREFIX === '1' ? '/' + process.env.GATSBY_DOCS_VERSION : '';
+ const { slug } = node.fields;
+ const isCurrentPage = location.pathname === prefix + slug;
+ const open = location.pathname.startsWith(prefix + slug);
+ return (
+ <div>
+ <h2 className={isCurrentPage || open ? 'active' : ''}>
+ <Link to={slug} title={node.frontmatter.title}>
+ {hasChild && open && <ChevronUpIcon />}
+ {hasChild && !open && <ChevronDownIcon />}
+ {node.frontmatter.title}
+ </Link>
+ </h2>
+ {isCurrentPage && <HeadingsLink headers={headers} />}
+ {hasChild &&
+ open && (
+ <div className="sub-menu">
+ {sortNodes(node.pages).map(page => (
+ <SubpageLink
+ key={page.fields.slug}
+ headers={headers}
+ displayHeading={location.pathname === prefix + page.fields.slug}
+ node={page}
+ />
+ ))}
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/Footer.js b/server/sonar-docs/src/layouts/components/Footer.js
new file mode 100644
index 00000000000..e807022e79b
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/Footer.js
@@ -0,0 +1,47 @@
+/*
+ * 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 Footer() {
+ return (
+ <div className="page-footer">
+ <a
+ rel="noopener noreferrer"
+ target="_blank"
+ title="Creative Commons License"
+ href="https://creativecommons.org/licenses/by-nc/3.0/us/">
+ <img
+ alt="Creative Commons License"
+ src="https://licensebuttons.net/l/by-nc/3.0/us/88x31.png"
+ />
+ </a>
+ © 2008-2017, SonarSource S.A, Switzerland. Except where otherwise noted, content in this space
+ is licensed under a{' '}
+ <a
+ rel="noopener noreferrer"
+ target="_blank"
+ href="https://creativecommons.org/licenses/by-nc/3.0/us/">
+ Creative Commons Attribution-NonCommercial 3.0 United States License.
+ </a>{' '}
+ SONARQUBE is a trademark of SonarSource SA. All other trademarks and copyrights are the
+ property of their respective owners.
+ </div>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/HeaderList.js b/server/sonar-docs/src/layouts/components/HeaderList.js
new file mode 100644
index 00000000000..56a029e3b66
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/HeaderList.js
@@ -0,0 +1,41 @@
+/*
+ * 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 PropTypes from 'prop-types';
+
+export default class HeaderList extends React.PureComponent {
+ static contextTypes = {
+ headers: PropTypes.object.isRequired
+ };
+
+ componentDidMount() {
+ this.context.headers.setHeaders(this.props.headers);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.headers.length !== this.props.headers.length) {
+ this.context.headers.setHeaders(prevProps.headers);
+ }
+ }
+
+ render() {
+ return null;
+ }
+}
diff --git a/server/sonar-docs/src/layouts/components/HeaderListProvider.js b/server/sonar-docs/src/layouts/components/HeaderListProvider.js
new file mode 100644
index 00000000000..1318b5c3854
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/HeaderListProvider.js
@@ -0,0 +1,47 @@
+/*
+ * 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 PropTypes from 'prop-types';
+
+export default class HeaderListProvider extends React.Component {
+ headers = [];
+
+ static childContextTypes = {
+ headers: PropTypes.object
+ };
+
+ state = { headers: [] };
+
+ getChildContext = () => {
+ return {
+ headers: {
+ setHeaders: this.setHeaders
+ }
+ };
+ };
+
+ setHeaders = headers => {
+ this.setState({ headers });
+ };
+
+ render() {
+ return this.props.children({ headers: this.state.headers });
+ }
+}
diff --git a/server/sonar-docs/src/layouts/components/HeadingsLink.js b/server/sonar-docs/src/layouts/components/HeadingsLink.js
new file mode 100644
index 00000000000..d7cf430ac01
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/HeadingsLink.js
@@ -0,0 +1,93 @@
+/*
+ * 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 class HeadingsLink extends React.Component {
+ componentDidMount() {
+ document.addEventListener('scroll', this.scrollHandler, true);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('scroll', this.scrollHandler, true);
+ }
+
+ highlightHeading = (index, scrollTo) => {
+ const previousNode = document.querySelector('.targetted-heading');
+ if (previousNode) {
+ previousNode.classList.remove('targetted-heading');
+ }
+
+ const node = document.querySelector('#header-' + index);
+ if (node) {
+ node.classList.add('targetted-heading');
+ if (scrollTo) {
+ window.scrollTo(0, node.offsetTop - 30);
+ }
+ }
+ };
+
+ scrollHandler = () => {
+ const headings = Array.from(document.querySelectorAll('.headings-container ul li a'));
+ const scrollTop = window.pageYOffset | document.body.scrollTop;
+ let headingIndex = 0;
+ for (let i = 0; i < headings.length; i++) {
+ if (document.querySelector('#header-' + (i + 1)).offsetTop > scrollTop + 40) {
+ break;
+ }
+ headingIndex = i;
+ }
+ headings.forEach(h => h.classList.remove('active'));
+ headings[headingIndex].classList.add('active');
+ this.highlightHeading(headingIndex + 1, false);
+ };
+
+ clickHandler = target => {
+ return event => {
+ event.stopPropagation();
+ event.preventDefault();
+ this.highlightHeading(target, true);
+ };
+ };
+
+ render() {
+ const headers = this.props.headers.filter(
+ h => h.depth === 2 && h.value.toLowerCase() !== 'table of contents'
+ );
+ if (headers.length < 1) {
+ return null;
+ }
+
+ return (
+ <div className="headings-container">
+ <ul>
+ {headers.map((header, index) => {
+ return (
+ <li key={index + 1}>
+ <a onClick={this.clickHandler(index + 1)} href={'#header-' + (index + 1)}>
+ {header.value}
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-docs/src/layouts/components/OutsideClickHandler.js b/server/sonar-docs/src/layouts/components/OutsideClickHandler.js
new file mode 100644
index 00000000000..b5c1e9657dd
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/OutsideClickHandler.js
@@ -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.
+ */
+import * as React from 'react';
+import { findDOMNode } from 'react-dom';
+
+export default class OutsideClickHandler extends React.Component {
+ element = null;
+
+ componentDidMount() {
+ setTimeout(() => {
+ this.addClickHandler();
+ }, 0);
+ }
+
+ componentWillUnmount() {
+ this.removeClickHandler();
+ }
+
+ addClickHandler = () => {
+ window.addEventListener('click', this.handleWindowClick);
+ };
+
+ removeClickHandler = () => {
+ window.removeEventListener('click', this.handleWindowClick);
+ };
+
+ handleWindowClick = event => {
+ const node = findDOMNode(this);
+ if (!node || !node.contains(event.target)) {
+ this.props.onClickOutside();
+ }
+ };
+
+ render() {
+ return this.props.children;
+ }
+}
diff --git a/server/sonar-docs/src/layouts/components/Search.js b/server/sonar-docs/src/layouts/components/Search.js
new file mode 100644
index 00000000000..569bf4c4eb9
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/Search.js
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React, { Component } from 'react';
+import lunr, { LunrIndex } from 'lunr';
+
+// Search component
+export default class Search extends Component {
+ index = null;
+
+ constructor(props) {
+ super(props);
+ this.index = lunr(function() {
+ this.ref('id');
+ this.field('title', { boost: 10 });
+ this.field('text');
+
+ this.metadataWhitelist = ['position'];
+
+ props.pages.forEach(page =>
+ this.add({
+ id: page.id,
+ title: page.frontmatter.title,
+ text: page.html.replace(/<(?:.|\n)*?>/gm, '')
+ })
+ );
+ });
+ }
+
+ getFormattedResults = (query, results) => {
+ return results.map(match => {
+ const page = this.props.pages.find(page => page.id === match.ref);
+ const highlights = {};
+ let longestTerm = '';
+
+ // remember the longest term that matches the query *exactly*
+ Object.keys(match.matchData.metadata).forEach(term => {
+ if (query.toLowerCase().includes(term.toLowerCase()) && longestTerm.length < term.length) {
+ longestTerm = term;
+ }
+
+ Object.keys(match.matchData.metadata[term]).forEach(fieldName => {
+ const { position: positions } = match.matchData.metadata[term][fieldName];
+ highlights[fieldName] = [...(highlights[fieldName] || []), ...positions];
+ });
+ });
+
+ return {
+ page: {
+ id: page.id,
+ slug: page.fields.slug,
+ title: page.frontmatter.title,
+ text: page.html.replace(/<(?:.|\n)*?>/gm, '')
+ },
+ highlights,
+ longestTerm
+ };
+ });
+ };
+
+ handleChange = event => {
+ const { value } = event.currentTarget;
+ if (value != '') {
+ const results = this.getFormattedResults(value, this.index.search(`${value}~1 ${value}*`));
+ this.props.onResultsChange(results);
+ } else {
+ this.props.onResultsChange([]);
+ }
+ };
+
+ render() {
+ return (
+ <input
+ aria-label="Search"
+ className="search-input"
+ onChange={this.handleChange}
+ placeholder="Search..."
+ type="search"
+ />
+ );
+ }
+}
diff --git a/server/sonar-docs/src/layouts/components/SearchEntryResult.js b/server/sonar-docs/src/layouts/components/SearchEntryResult.js
new file mode 100644
index 00000000000..bfd0daba0ca
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/SearchEntryResult.js
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Link from 'gatsby-link';
+import { highlightMarks, cutWords } from '../utils';
+
+export default function SearchResultEntry({ active, result }) {
+ return (
+ <Link className={active ? 'active search-result' : 'search-result'} to={result.page.slug}>
+ <SearchResultTitle result={result} />
+ <SearchResultText result={result} />
+ </Link>
+ );
+}
+
+export function SearchResultTitle({ result }) {
+ let titleWithMarks;
+
+ const titleHighlights = result.highlights.title;
+ if (titleHighlights && titleHighlights.length > 0) {
+ const { title } = result.page;
+ const tokens = highlightMarks(
+ title,
+ titleHighlights.map(h => ({ from: h[0], to: h[0] + h[1] }))
+ );
+ titleWithMarks = <SearchResultTokens tokens={tokens} />;
+ } else {
+ titleWithMarks = result.page.title;
+ }
+
+ return <div className="search-result">{titleWithMarks}</div>;
+}
+
+export function SearchResultText({ result }) {
+ const textHighlights = result.highlights.text;
+ if (textHighlights && textHighlights.length > 0) {
+ const { text } = result.page;
+ const tokens = highlightMarks(text, textHighlights.map(h => ({ from: h[0], to: h[0] + h[1] })));
+ return (
+ <div className="note">
+ <SearchResultTokens tokens={cutWords(tokens)} />
+ </div>
+ );
+ } else {
+ return null;
+ }
+}
+
+export function SearchResultTokens({ tokens }) {
+ return (
+ <span>
+ {tokens.map((token, index) => (
+ <span key={index}>{token.marked ? <mark key={index}>{token.text}</mark> : token.text}</span>
+ ))}
+ </span>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/Sidebar.js b/server/sonar-docs/src/layouts/components/Sidebar.js
new file mode 100644
index 00000000000..cf68d376f42
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/Sidebar.js
@@ -0,0 +1,128 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import Link from 'gatsby-link';
+import { fromPairs } from 'lodash';
+import { sortNodes } from '../utils';
+import CategoryLink from './CategoryLink';
+import VersionSelect from './VersionSelect';
+import Search from './Search';
+import SearchEntryResult from './SearchEntryResult';
+
+export default class Sidebar extends React.PureComponent {
+ state = { loaded: false, results: [], versions: [] };
+
+ componentDidMount() {
+ this.loadVersions();
+ }
+
+ loadVersions() {
+ fetch('/DocsVersions.json').then(response =>
+ response.json().then(json => {
+ this.setState({ loaded: true, versions: json });
+ })
+ );
+ }
+
+ getPagesHierarchy() {
+ const categories = sortNodes(
+ this.props.pages.filter(p => p.fields.slug.split('/').length === 3)
+ );
+ const pages = this.props.pages.filter(p => p.fields.slug.split('/').length > 3);
+ const categoriesObject = fromPairs(categories.map(c => [c.fields.slug, { ...c, pages: [] }]));
+ pages.forEach(page => {
+ const parentSlug = page.fields.slug
+ .split('/')
+ .slice(0, 2)
+ .join('/');
+ categoriesObject[parentSlug + '/'].pages.push(page);
+ });
+ return categoriesObject;
+ }
+
+ renderResults = () => {
+ return (
+ <div>
+ {this.state.results.map(result => (
+ <SearchEntryResult
+ active={
+ (this.props.location.pathname === result.page.slug && result.page.slug === '/') ||
+ (result.page.slug !== '/' && this.props.location.pathname.endsWith(result.page.slug))
+ }
+ key={result.page.id}
+ result={result}
+ />
+ ))}
+ </div>
+ );
+ };
+
+ handleSearch = results => {
+ this.setState({ results });
+ };
+
+ render() {
+ const nodes = this.getPagesHierarchy();
+ const isOnCurrentVersion =
+ this.state.versions.find(v => v.value === this.props.version) !== undefined;
+ return (
+ <div className="page-sidebar">
+ <div className="sidebar-header">
+ <Link to="/">
+ <img
+ alt="Continuous Code Quality"
+ css={{ verticalAlign: 'top', margin: 0 }}
+ width="160"
+ src="/images/SonarQubeIcon.svg"
+ title="Continuous Code Quality"
+ />
+ </Link>
+ <VersionSelect
+ location={this.props.location}
+ version={this.props.version}
+ versions={this.state.versions}
+ />
+
+ {this.state.loaded &&
+ !isOnCurrentVersion && (
+ <div className="alert alert-warning">
+ This is an archived version of the doc for{' '}
+ <b>SonarQube version {this.props.version}</b>. <a href="/">See Documentation</a> for
+ current functionnality.
+ </div>
+ )}
+ </div>
+ <div className="page-indexes">
+ <Search pages={this.props.pages} onResultsChange={this.handleSearch} />
+ {this.state.results.length > 0 && this.renderResults()}
+ {this.state.results.length === 0 &&
+ Object.keys(nodes).map(key => (
+ <CategoryLink
+ key={key}
+ headers={this.props.headers}
+ node={nodes[key]}
+ location={this.props.location}
+ />
+ ))}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-docs/src/layouts/components/SubpageLink.js b/server/sonar-docs/src/layouts/components/SubpageLink.js
new file mode 100644
index 00000000000..1d4746fb73f
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/SubpageLink.js
@@ -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 Link from 'gatsby-link';
+import HeadingsLink from './HeadingsLink';
+
+export default function SubpageLink({ node, headers, displayHeading }) {
+ return (
+ <div>
+ <h3>
+ <Link className={displayHeading ? 'active' : ''} to={node.fields.slug}>
+ {node.frontmatter.title}
+ </Link>
+ </h3>
+ {displayHeading && <HeadingsLink headers={headers} />}
+ </div>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/VersionSelect.js b/server/sonar-docs/src/layouts/components/VersionSelect.js
new file mode 100644
index 00000000000..3028c9dcc39
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/VersionSelect.js
@@ -0,0 +1,68 @@
+/*
+ * 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 fetch from 'isomorphic-fetch';
+import ChevronDownIcon from './icons/ChevronDownIcon';
+import ChevronUpIcon from './icons/ChevronUpIcon';
+import OutsideClickHandler from './OutsideClickHandler';
+
+export default class VersionSelect extends React.PureComponent {
+ state = { open: false };
+
+ handleClick = () => {
+ this.setState(state => ({ open: !state.open }));
+ };
+
+ handleClickOutside = () => {
+ this.setState({ open: false });
+ };
+
+ render() {
+ const { versions } = this.props;
+ const hasVersions = versions.length > 1;
+ const isOnCurrentVersion =
+ !hasVersions || versions.find(v => v.value === this.props.version) !== undefined;
+ return (
+ <div className="version-select">
+ <button onClick={this.handleClick}>
+ Docs <span className={isOnCurrentVersion ? 'current' : ''}>{this.props.version}</span>
+ {hasVersions && !this.state.open && <ChevronDownIcon size={10} />}
+ {hasVersions && this.state.open && <ChevronUpIcon size={10} />}
+ </button>
+ {this.state.open &&
+ hasVersions && (
+ <OutsideClickHandler onClickOutside={this.handleClickOutside}>
+ <ul>
+ {versions.map(version => {
+ return (
+ <li key={version.value}>
+ <a href={version.current ? '/' : '/' + version.value}>
+ <span className={version.current ? 'current' : ''}>{version.value}</span>
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </OutsideClickHandler>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-docs/src/layouts/components/icons/AlertWarnIcon.js b/server/sonar-docs/src/layouts/components/icons/AlertWarnIcon.js
new file mode 100644
index 00000000000..02633abcdde
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/icons/AlertWarnIcon.js
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon from './Icon';
+
+export default function AlertWarnIcon({ className, fill = '#ed7d20', size }) {
+ return (
+ <Icon className={className} size={size}>
+ <path
+ d="M8 1.143q1.866 0 3.442.92t2.496 2.496.92 3.442-.92 3.442-2.496 2.496-3.442.92-3.442-.92-2.496-2.496-.92-3.442.92-3.442 2.496-2.496T8 1.143zm1.143 11.134v-1.696q0-.125-.08-.21t-.196-.085H7.153q-.116 0-.205.089t-.089.205v1.696q0 .116.089.205t.205.089h1.714q.116 0 .196-.085t.08-.21zm-.018-3.072l.161-5.545q0-.107-.089-.161-.089-.071-.214-.071H7.019q-.125 0-.214.071-.089.054-.089.161l.152 5.545q0 .089.089.156t.214.067h1.652q.125 0 .21-.067t.094-.156z"
+ style={{ fill }}
+ />
+ </Icon>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/icons/ChevronDownIcon.js b/server/sonar-docs/src/layouts/components/icons/ChevronDownIcon.js
new file mode 100644
index 00000000000..e401334a24b
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/icons/ChevronDownIcon.js
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon from './Icon';
+
+export default function ChevronDownIcon({ className, fill = 'currentColor', size }) {
+ return (
+ <Icon className={className} size={size}>
+ <path
+ d="M3.2,5.6c0-0.1,0-0.2,0.1-0.3c0.2-0.2,0.5-0.2,0.6,0l4.1,4.1l4.1-4.1c0.2-0.2,0.5-0.2,0.6,0 c0.2,0.2,0.2,0.5,0,0.6c0,0,0,0,0,0l0,0l-4.5,4.5c-0.2,0.2-0.5,0.2-0.6,0l0,0L3.3,5.9C3.2,5.9,3.2,5.7,3.2,5.6z"
+ style={{ fill }}
+ />
+ </Icon>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/icons/ChevronUpIcon.js b/server/sonar-docs/src/layouts/components/icons/ChevronUpIcon.js
new file mode 100644
index 00000000000..26393e8a35c
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/icons/ChevronUpIcon.js
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon from './Icon';
+
+export default function ChevronUpIcon({ className, fill = 'currentColor', size }) {
+ return (
+ <Icon className={className} size={size}>
+ <path
+ d="M13,10c0,0.1,0,0.2-0.1,0.3c-0.2,0.2-0.5,0.2-0.6,0L8.1,6.2L4,10.3c-0.2,0.2-0.5,0.2-0.6,0 c-0.2-0.2-0.2-0.5,0-0.6c0,0,0,0,0,0l0,0l4.5-4.5c0.2-0.2,0.5-0.2,0.6,0l0,0l4.4,4.4C13,9.7,13,9.8,13,10z"
+ style={{ fill }}
+ />
+ </Icon>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/icons/DownloadIcon.js b/server/sonar-docs/src/layouts/components/icons/DownloadIcon.js
new file mode 100644
index 00000000000..afd2d92009c
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/icons/DownloadIcon.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon from './Icon';
+
+export default function DownloadIcon({ className, fill = 'currentColor', size }) {
+ return (
+ <Icon className={className} size={size} viewBox="0 0 48 48">
+ <path
+ style={{ fill }}
+ d="M45.68 22.86a1.31 1.31 0 0 0-1.32 1.32v12a5.91 5.91 0 0 1-5.9 5.91H9.54a5.91 5.91 0 0 1-5.9-5.91V24A1.32 1.32 0 0 0 1 24v12.16a8.56 8.56 0 0 0 8.54 8.55h28.92A8.56 8.56 0 0 0 47 36.16v-12a1.32 1.32 0 0 0-1.32-1.3z"
+ />
+ <path
+ d="M23.07 34.24a1.36 1.36 0 0 0 .93.39 1.32 1.32 0 0 0 .93-.39l8.37-8.38A1.32 1.32 0 0 0 31.44 24l-6.12 6.13V3.39a1.32 1.32 0 0 0-2.64 0v26.74L16.55 24a1.32 1.32 0 0 0-1.86 1.86z"
+ style={{ fill }}
+ />
+ </Icon>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/components/icons/Icon.js b/server/sonar-docs/src/layouts/components/icons/Icon.js
new file mode 100644
index 00000000000..3e7673e48fa
--- /dev/null
+++ b/server/sonar-docs/src/layouts/components/icons/Icon.js
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+
+export default function Icon({
+ children,
+ className,
+ size = 16,
+ style,
+ height = size,
+ width = size,
+ viewBox = '0 0 16 16',
+ ...other
+}) {
+ return (
+ <svg
+ className={className}
+ height={height}
+ style={{
+ fillRule: 'evenodd',
+ clipRule: 'evenodd',
+ strokeLinejoin: 'round',
+ strokeMiterlimit: 1.41421,
+ ...style
+ }}
+ version="1.1"
+ viewBox={viewBox}
+ width={width}
+ xmlSpace="preserve"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ {...other}>
+ {children}
+ </svg>
+ );
+}
diff --git a/server/sonar-docs/src/layouts/index.js b/server/sonar-docs/src/layouts/index.js
index 200d4210c8c..2fc755141ce 100644
--- a/server/sonar-docs/src/layouts/index.js
+++ b/server/sonar-docs/src/layouts/index.js
@@ -1,42 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
import React from 'react';
+import Sidebar from './components/Sidebar';
+import DownloadIcon from './components/icons/DownloadIcon';
+import Footer from './components/Footer';
+import HeaderListProvider from './components/HeaderListProvider';
-const headerHeight = 48;
-
-const containerCss = {
- minWidth: 320,
- maxWidth: 800,
- marginLeft: 'auto',
- marginRight: 'auto',
- paddingLeft: 16,
- paddingRight: 16
-};
+const version = process.env.GATSBY_DOCS_VERSION || '1.0';
export default function Layout(props) {
return (
- <div>
- <header css={{ height: headerHeight, backgroundColor: '#262626' }}>
- <div
- css={{
- display: 'flex',
- alignItems: 'center',
- alignContent: 'center',
- height: headerHeight,
- ...containerCss
- }}>
- <a href="/">
- <img
- alt="Continuous Code Quality"
- css={{ verticalAlign: 'top', margin: 0 }}
- height="30"
- src="https://next.sonarqube.com/sonarqube/images/logo.svg?v=6.6"
- title="Continuous Code Quality"
- width="83"
- />
- </a>
- </div>
- </header>
-
- <div css={containerCss}>{props.children()}</div>
+ <div className="main-container">
+ <div className="blue-bar" />
+ <HeaderListProvider>
+ {({ headers }) => (
+ <div className="layout-page">
+ <div className="page-sidebar-inner">
+ <Sidebar
+ headers={headers}
+ location={props.location}
+ pages={props.data.allMarkdownRemark.edges
+ .map(e => e.node)
+ .filter(n => !n.fields.slug.startsWith('/tooltips'))
+ .filter(
+ n =>
+ !n.frontmatter.scope ||
+ n.frontmatter.scope === 'sonarqube' ||
+ n.frontmatter.scope === 'static'
+ )}
+ searchIndex={props.data.siteSearchIndex}
+ version={version}
+ />
+ </div>
+ <div className="page-main">
+ <div className="useful-links-block">
+ <div className="useful-link-title">Download</div>
+ <a href="https://www.sonarqube.org/" rel="noopener noreferrer" target="_blank">
+ <DownloadIcon /> SonarQube
+ </a>
+ <div className="useful-link-title">Get Help</div>
+ <a
+ href="https://community.sonarsource.com/"
+ rel="noopener noreferrer"
+ target="_blank">
+ <img src="/images/community-icon.svg" alt="Community" /> Community
+ </a>
+ <div className="useful-link-title">Stay Connected</div>
+ <a href="https://twitter.com/SonarQube" rel="noopener noreferrer" target="_blank">
+ <img src="/images/tw-icon-small.svg" alt="Twitter" /> Twitter
+ </a>
+ <a
+ href="https://www.sonarsource.com/resources/product-news/"
+ rel="noopener noreferrer"
+ target="_blank">
+ <img src="/images/sq-icon-small.svg" alt="Product News" /> Product News
+ </a>
+ </div>
+ <div className="page-container">{props.children()}</div>
+ <Footer />
+ </div>
+ </div>
+ )}
+ </HeaderListProvider>
</div>
);
}
+
+export const query = graphql`
+ query IndexQuery {
+ allMarkdownRemark {
+ edges {
+ node {
+ id
+ headings {
+ depth
+ value
+ }
+ frontmatter {
+ title
+ order
+ scope
+ }
+ fields {
+ slug
+ }
+ html
+ }
+ }
+ }
+ }
+`;
diff --git a/server/sonar-docs/src/layouts/utils.js b/server/sonar-docs/src/layouts/utils.js
new file mode 100644
index 00000000000..9fdae25b2fd
--- /dev/null
+++ b/server/sonar-docs/src/layouts/utils.js
@@ -0,0 +1,103 @@
+import { sortBy } from 'lodash';
+
+export function sortNodes(nodes) {
+ return nodes.sort((a, b) => {
+ if (a.frontmatter.order) {
+ return b.frontmatter.order ? a.frontmatter.order - b.frontmatter.order : 1;
+ }
+ return a.frontmatter.title < b.frontmatter.title ? -1 : 1;
+ });
+}
+
+const WORDS = 6;
+
+function cutLeadingWords(str) {
+ let words = 0;
+ for (let i = str.length - 1; i >= 0; i--) {
+ if (/\s/.test(str[i])) {
+ words++;
+ }
+ if (words === WORDS) {
+ return i > 0 ? `...${str.substring(i + 1)}` : str;
+ }
+ }
+ return str;
+}
+
+function cutTrailingWords(str) {
+ let words = 0;
+ for (let i = 0; i < str.length; i++) {
+ if (/\s/.test(str[i])) {
+ words++;
+ }
+ if (words === WORDS) {
+ return i < str.length - 1 ? `${str.substring(0, i)}...` : str;
+ }
+ }
+ return str;
+}
+
+export function cutWords(tokens) {
+ const result = [];
+ let length = 0;
+
+ const highlightPos = tokens.findIndex(token => token.marked);
+ if (highlightPos > 0) {
+ const text = cutLeadingWords(tokens[highlightPos - 1].text);
+ result.push({ text, marked: false });
+ length += text.length;
+ }
+
+ result.push(tokens[highlightPos]);
+ length += tokens[highlightPos].text.length;
+
+ for (let i = highlightPos + 1; i < tokens.length; i++) {
+ if (length + tokens[i].text.length > 100) {
+ const text = cutTrailingWords(tokens[i].text);
+ result.push({ text, marked: false });
+ return result;
+ } else {
+ result.push(tokens[i]);
+ length += tokens[i].text.length;
+ }
+ }
+
+ return result;
+}
+
+export function highlightMarks(str, marks) {
+ const sortedMarks = sortBy(
+ [
+ ...marks.map(mark => ({ pos: mark.from, start: true })),
+ ...marks.map(mark => ({ pos: mark.to, start: false }))
+ ],
+ mark => mark.pos,
+ mark => Number(!mark.start)
+ );
+
+ const cuts = [];
+ let start = 0;
+ let balance = 0;
+
+ for (const mark of sortedMarks) {
+ if (mark.start) {
+ if (balance === 0 && start !== mark.pos) {
+ cuts.push({ text: str.substring(start, mark.pos), marked: false });
+ start = mark.pos;
+ }
+ balance++;
+ } else {
+ balance--;
+ if (balance === 0 && start !== mark.pos) {
+ cuts.push({ text: str.substring(start, mark.pos), marked: true });
+ start = mark.pos;
+ }
+ }
+ }
+
+ if (start < str.length - 1) {
+ cuts.push({ text: str.substr(start), marked: false });
+ }
+
+ return cuts;
+}