diff options
author | Pascal Mugnier <pascal.mugnier@sonarsource.com> | 2018-09-10 11:32:33 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2018-09-19 10:50:57 +0200 |
commit | 55822b5c264747a28161902119482ef7f000ab66 (patch) | |
tree | a5a984280429502ffe41219b21502bece97727b1 /server/sonar-docs/src/layouts | |
parent | 80418249fe5b1f8cf3ee10f4f80d538351fd106c (diff) | |
download | sonarqube-55822b5c264747a28161902119482ef7f000ab66.tar.gz sonarqube-55822b5c264747a28161902119482ef7f000ab66.zip |
MMF-1377 Finalize the static documentation site (#673)
Diffstat (limited to 'server/sonar-docs/src/layouts')
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; +} |