From b998b44aafeff3726122bbc491e2c84aed284b61 Mon Sep 17 00:00:00 2001
From: Stas Vilchik
Date: Fri, 13 Jul 2018 12:27:02 +0200
Subject: SONAR-11013 Add search capabilities to the embedded documentation
(#513)
---
server/sonar-web/src/main/js/@types/lunr.d.ts | 48 ++++++++
server/sonar-web/src/main/js/@types/md.d.ts | 23 ++++
.../src/main/js/@types/strip-markdown.d.ts | 22 ++++
.../nav/component/ComponentNavBranch.tsx | 8 +-
.../__snapshots__/ComponentNavBranch-test.tsx.snap | 4 +-
.../sonar-web/src/main/js/app/styles/init/type.css | 1 +
.../organizations/CreateOrganizationForm.tsx | 5 +-
.../apps/coding-rules/components/ProfileFacet.tsx | 5 +-
.../apps/coding-rules/components/RuleDetails.tsx | 5 +-
.../coding-rules/components/RuleDetailsMeta.tsx | 5 +-
.../apps/coding-rules/components/TemplateFacet.tsx | 5 +-
.../main/js/apps/documentation/components/App.tsx | 93 +++++----------
.../main/js/apps/documentation/components/Menu.tsx | 53 ++++-----
.../documentation/components/SearchResultEntry.tsx | 97 ++++++++++++++++
.../documentation/components/SearchResults.tsx | 78 +++++++++++++
.../js/apps/documentation/components/Sidebar.tsx | 68 +++++++++++
.../components/__tests__/Menu-test.tsx | 48 ++++++++
.../__tests__/SearchResultEntry-test.tsx | 85 ++++++++++++++
.../components/__tests__/SearchResults-test.tsx | 66 +++++++++++
.../components/__tests__/Sidebar-test.tsx | 42 +++++++
.../__tests__/__snapshots__/Menu-test.tsx.snap | 70 ++++++++++++
.../__snapshots__/SearchResultEntry-test.tsx.snap | 125 +++++++++++++++++++++
.../__snapshots__/SearchResults-test.tsx.snap | 58 ++++++++++
.../__tests__/__snapshots__/Sidebar-test.tsx.snap | 84 ++++++++++++++
.../src/main/js/apps/documentation/pages.ts | 50 +++++++++
.../src/main/js/apps/documentation/routes.ts | 12 +-
.../src/main/js/apps/documentation/utils.ts | 101 ++++++++++++++++-
.../js/apps/marketplace/components/EditionBox.tsx | 11 +-
.../__snapshots__/EditionBox-test.tsx.snap | 4 +-
.../components/OrganizationMembers.tsx | 5 +-
.../OrganizationMembers-test.tsx.snap | 2 +-
.../navigation/OrganizationNavigationMeta.tsx | 4 +-
.../qualityGate/ApplicationQualityGate.tsx | 5 +-
.../js/apps/overview/qualityGate/QualityGate.js | 5 +-
.../ApplicationQualityGate-test.tsx.snap | 4 +-
.../__snapshots__/QualityGate-test.js.snap | 2 +-
.../src/main/js/apps/projectQualityGate/Header.tsx | 5 +-
.../__tests__/__snapshots__/Header-test.tsx.snap | 2 +-
.../main/js/apps/projectQualityProfiles/Header.tsx | 5 +-
.../__tests__/__snapshots__/Header-test.tsx.snap | 2 +-
.../components/BuiltInQualityGateBadge.tsx | 7 +-
.../apps/quality-gates/components/Conditions.tsx | 5 +-
.../quality-gates/components/DetailsContent.tsx | 5 +-
.../apps/quality-gates/components/ListHeader.tsx | 5 +-
.../components/BuiltInQualityProfileBadge.tsx | 7 +-
.../js/apps/quality-profiles/home/ProfilesList.tsx | 2 +-
.../apps/quality-profiles/home/ProfilesListRow.tsx | 3 +-
.../js/apps/securityReports/components/App.tsx | 5 +-
.../__tests__/__snapshots__/App-test.tsx.snap | 10 +-
.../projectOnboarding/OrganizationStep.tsx | 5 +-
.../__snapshots__/OrganizationStep-test.tsx.snap | 101 +----------------
.../js/components/common/PrivacyBadgeContainer.tsx | 30 +++--
.../PrivacyBadgeContainer-test.tsx.snap | 4 +-
.../src/main/js/components/docs/DocInclude.tsx | 75 -------------
.../main/js/components/docs/DocMarkdownBlock.tsx | 23 +---
.../src/main/js/components/docs/DocParagraph.tsx | 35 ------
.../src/main/js/components/docs/DocTooltip.tsx | 71 ++++--------
.../components/docs/__tests__/DocTooltip-test.tsx | 17 +--
.../__snapshots__/DocMarkdownBlock-test.tsx.snap | 36 +++---
.../__snapshots__/DocTooltip-test.tsx.snap | 16 +--
server/sonar-web/src/main/js/helpers/markdown.d.ts | 3 +
server/sonar-web/src/main/js/helpers/markdown.js | 19 +++-
62 files changed, 1315 insertions(+), 486 deletions(-)
create mode 100644 server/sonar-web/src/main/js/@types/lunr.d.ts
create mode 100644 server/sonar-web/src/main/js/@types/md.d.ts
create mode 100644 server/sonar-web/src/main/js/@types/strip-markdown.d.ts
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/documentation/pages.ts
delete mode 100644 server/sonar-web/src/main/js/components/docs/DocInclude.tsx
delete mode 100644 server/sonar-web/src/main/js/components/docs/DocParagraph.tsx
(limited to 'server/sonar-web/src/main')
diff --git a/server/sonar-web/src/main/js/@types/lunr.d.ts b/server/sonar-web/src/main/js/@types/lunr.d.ts
new file mode 100644
index 00000000000..1fd03395605
--- /dev/null
+++ b/server/sonar-web/src/main/js/@types/lunr.d.ts
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+declare module 'lunr' {
+ export interface Lunr {
+ add(doc: any): void;
+
+ field(field: string, options?: { boost?: number }): void;
+
+ ref(field: string): void;
+
+ metadataWhitelist?: string[];
+ }
+
+ export interface LunrInit {
+ (this: Lunr): void;
+ }
+
+ export interface LunrMatch {
+ ref: string;
+ score: number;
+ matchData: { metadata: any };
+ }
+
+ export interface LunrIndex {
+ search(query: string): LunrMatch[];
+ }
+
+ function lunr(initializer: LunrInit): LunrIndex;
+
+ export default lunr;
+}
diff --git a/server/sonar-web/src/main/js/@types/md.d.ts b/server/sonar-web/src/main/js/@types/md.d.ts
new file mode 100644
index 00000000000..e036188c5c2
--- /dev/null
+++ b/server/sonar-web/src/main/js/@types/md.d.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+declare module '*.md' {
+ const value: string;
+ export default value;
+}
diff --git a/server/sonar-web/src/main/js/@types/strip-markdown.d.ts b/server/sonar-web/src/main/js/@types/strip-markdown.d.ts
new file mode 100644
index 00000000000..80d69fb86bf
--- /dev/null
+++ b/server/sonar-web/src/main/js/@types/strip-markdown.d.ts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+declare module 'strip-markdown' {
+ export default function stripMarkdown(): any;
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
index d02389be63d..069472c96e5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
@@ -181,7 +181,9 @@ export default class ComponentNavBranch extends React.PureComponent{displayName}
-
+
@@ -193,7 +195,9 @@ export default class ComponentNavBranch extends React.PureComponent{displayName}
-
+
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
index 72ce3a7917c..86a8372f886 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
@@ -68,7 +68,7 @@ exports[`renders main branch 1`] = `
exports[`renders no branch support popup 1`] = `
{
- );
+ return <>{this.getMenuEntriesHierarchy().map(entry => this.renderEntry(entry, 0))}>;
}
}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx
new file mode 100644
index 00000000000..613c390d788
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 classNames from 'classnames';
+import { Link } from 'react-router';
+import { highlightMarks, cutWords, DocumentationEntry } from '../utils';
+
+export interface SearchResult {
+ page: DocumentationEntry;
+ highlights: { [field: string]: [number, number][] };
+}
+
+interface Props {
+ active: boolean;
+ result: SearchResult;
+}
+
+export default function SearchResultEntry({ active, result }: Props) {
+ return (
+
+
+
+
+ );
+}
+
+export function SearchResultTitle({ result }: { result: SearchResult }) {
+ let titleWithMarks: React.ReactNode;
+
+ 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 = ;
+ } else {
+ titleWithMarks = result.page.title;
+ }
+
+ return (
+
+ );
+ } else {
+ return null;
+ }
+}
+
+export function SearchResultTokens({
+ tokens
+}: {
+ tokens: Array<{ text: string; marked: boolean }>;
+}) {
+ return (
+ <>
+ {tokens.map((token, index) => (
+
+ {token.marked ? {token.text} : token.text}
+
+ ))}
+ >
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx
new file mode 100644
index 00000000000..30cfdc9fd94
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 lunr, { LunrIndex } from 'lunr';
+import SearchResultEntry, { SearchResult } from './SearchResultEntry';
+import { DocumentationEntry } from '../utils';
+
+interface Props {
+ pages: DocumentationEntry[];
+ query: string;
+ splat: string;
+}
+
+export default class SearchResults extends React.PureComponent {
+ index: LunrIndex;
+
+ constructor(props: Props) {
+ super(props);
+ this.index = lunr(function() {
+ this.ref('relativeName');
+ this.field('title', { boost: 10 });
+ this.field('text');
+
+ this.metadataWhitelist = ['position'];
+
+ props.pages.forEach(page => this.add(page));
+ });
+ }
+
+ render() {
+ const { query } = this.props;
+ const results = this.index
+ .search(`${query}~1 ${query}*`)
+ .map(match => {
+ const page = this.props.pages.find(page => page.relativeName === match.ref);
+ const highlights: { [field: string]: [number, number][] } = {};
+
+ Object.keys(match.matchData.metadata).forEach(term => {
+ Object.keys(match.matchData.metadata[term]).forEach(fieldName => {
+ const { position: positions } = match.matchData.metadata[term][fieldName];
+ highlights[fieldName] = [...(highlights[fieldName] || []), ...positions];
+ });
+ });
+
+ return { page, highlights };
+ })
+ .filter(result => result.page) as SearchResult[];
+
+ return (
+ <>
+ {results.map(result => (
+
+ ))}
+ >
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx
new file mode 100644
index 00000000000..24a9a6feb8b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx
@@ -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 Menu from './Menu';
+import SearchResults from './SearchResults';
+import { DocumentationEntry } from '../utils';
+import SearchBox from '../../../components/controls/SearchBox';
+
+interface Props {
+ pages: DocumentationEntry[];
+ splat: string;
+}
+
+interface State {
+ query: string;
+}
+
+export default class Sidebar extends React.PureComponent {
+ state: State = { query: '' };
+
+ handleSearch = (query: string) => {
+ this.setState({ query });
+ };
+
+ render() {
+ return (
+ <>
+
+
+
+ {this.state.query ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx
new file mode 100644
index 00000000000..2f71253a2d3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import Menu from '../Menu';
+
+function createPage(title: string, relativeName: string, text = '') {
+ return { relativeName, title, order: -1, text, content: text };
+}
+
+const pages = [
+ createPage(
+ 'Lorem Ipsum',
+ 'lorem/index',
+ "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
+ ),
+ createPage(
+ 'Where does it come from?',
+ 'lorem/origin',
+ 'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.'
+ ),
+ createPage(
+ 'Where does Foobar come from?',
+ 'foobar',
+ 'Foobar is a universal variable understood to represent whatever is being discussed.'
+ )
+];
+
+it('should render hierarchical menu', () => {
+ expect(shallow()).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx
new file mode 100644
index 00000000000..370c39b376f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import SearchResultEntry, {
+ SearchResultText,
+ SearchResultTitle,
+ SearchResultTokens
+} from '../SearchResultEntry';
+
+const page = {
+ content: '',
+ order: -1,
+ relativeName: 'foo/bar',
+ text: 'Foobar is a universal variable understood to represent whatever is being discussed.',
+ title: 'Foobar'
+};
+
+describe('SearchResultEntry', () => {
+ it('should render', () => {
+ expect(
+ shallow()
+ ).toMatchSnapshot();
+ });
+});
+
+describe('SearchResultText', () => {
+ it('should render with highlights', () => {
+ expect(
+ shallow()
+ ).toMatchSnapshot();
+ });
+
+ it('should render without highlights', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+});
+
+describe('SearchResultTitle', () => {
+ it('should render with highlights', () => {
+ expect(
+ shallow()
+ ).toMatchSnapshot();
+ });
+
+ it('should render not without highlights', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+});
+
+describe('SearchResultTokens', () => {
+ it('should render', () => {
+ expect(
+ shallow(
+
+ )
+ ).toMatchSnapshot();
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx
new file mode 100644
index 00000000000..d4151e45220
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx
@@ -0,0 +1,66 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import lunr from 'lunr';
+import SearchResults from '../SearchResults';
+
+jest.mock('lunr', () => ({
+ default: jest.fn(() => ({
+ search: jest.fn(() => [
+ {
+ ref: 'lorem/origin',
+ matchData: {
+ metadata: { from: { title: { position: [[19, 5]] }, text: { position: [[121, 4]] } } }
+ }
+ },
+ { ref: 'foobar', matchData: { metadata: { from: { title: { position: [[23, 4]] } } } } }
+ ])
+ }))
+}));
+
+function createPage(title: string, relativeName: string, text = '') {
+ return { relativeName, title, order: -1, text, content: text };
+}
+
+const pages = [
+ createPage(
+ 'Lorem Ipsum',
+ 'lorem/index',
+ "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
+ ),
+ createPage(
+ 'Where does it come from?',
+ 'lorem/origin',
+ 'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.'
+ ),
+ createPage(
+ 'Where does Foobar come from?',
+ 'foobar',
+ 'Foobar is a universal variable understood to represent whatever is being discussed.'
+ )
+];
+
+it('should search', () => {
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ expect(lunr).toBeCalled();
+ expect((wrapper.instance() as SearchResults).index.search).toBeCalledWith('from~1 from*');
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx
new file mode 100644
index 00000000000..6fc596b466f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import Sidebar from '../Sidebar';
+
+function createPage(title: string, relativeName: string, text = '') {
+ return { relativeName, title, order: -1, text, content: text };
+}
+
+const pages = [
+ createPage('Lorem Ipsum', 'lorem/index'),
+ createPage('Where does Foobar come from?', 'foobar')
+];
+
+it('should render menu', () => {
+ expect(shallow()).toMatchSnapshot();
+});
+
+it('should search', () => {
+ const wrapper = shallow();
+ wrapper.find('SearchBox').prop('onChange')('foo');
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap
new file mode 100644
index 00000000000..dbc7ad1e5a0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render hierarchical menu 1`] = `
+
+
+
+
+
+ Lorem Ipsum
+
+
+
+
+
+ Where does it come from?
+
+
+
+
+
+
+
+ Where does Foobar come from?
+
+
+
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap
new file mode 100644
index 00000000000..7a2b86c3590
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SearchResultEntry should render 1`] = `
+
+
+
+
+`;
+
+exports[`SearchResultText should render with highlights 1`] = `
+
+
+
+`;
+
+exports[`SearchResultText should render without highlights 1`] = `""`;
+
+exports[`SearchResultTitle should render not without highlights 1`] = `
+
+ Foobar
+
+`;
+
+exports[`SearchResultTitle should render with highlights 1`] = `
+
+
+
+`;
+
+exports[`SearchResultTokens should render 1`] = `
+
+
+ Foobar is a
+
+
+
+ universal
+
+
+
+ variable understood to represent whatever is being discussed.
+
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap
new file mode 100644
index 00000000000..69473caa244
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should search 1`] = `
+
+
+
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
new file mode 100644
index 00000000000..c1527eb3f9c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render menu 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`should search 1`] = `
+
+
+
+
+
+
+
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/documentation/pages.ts b/server/sonar-web/src/main/js/apps/documentation/pages.ts
new file mode 100644
index 00000000000..cb4eb61c9c1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/documentation/pages.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 remark from 'remark';
+import strip from 'strip-markdown';
+import { DocumentationEntry } from './utils';
+import * as Docs from './documentation.directory-loader';
+import { separateFrontMatter, filterContent } from '../../helpers/markdown';
+import { isSonarCloud } from '../../helpers/system';
+
+export default function getPages(): DocumentationEntry[] {
+ return Docs.map((file: any) => {
+ const parsed = separateFrontMatter(file.content);
+ const content = filterContent(parsed.content);
+
+ const text = remark()
+ .use(strip)
+ .processSync(content)
+ .contents.replace(/\n+/, ' ')
+ .trim();
+
+ return {
+ relativeName: file.path,
+ title: parsed.frontmatter.title,
+ order: Number(parsed.frontmatter.order || -1),
+ scope:
+ parsed.frontmatter.scope && parsed.frontmatter.scope.toLowerCase() === 'sonarcloud'
+ ? ('sonarcloud' as 'sonarcloud')
+ : undefined,
+ text,
+ content: file.content
+ };
+ }).filter((page: DocumentationEntry) => isSonarCloud() || page.scope !== 'sonarcloud');
+}
diff --git a/server/sonar-web/src/main/js/apps/documentation/routes.ts b/server/sonar-web/src/main/js/apps/documentation/routes.ts
index 704e4739073..617422a5ce5 100644
--- a/server/sonar-web/src/main/js/apps/documentation/routes.ts
+++ b/server/sonar-web/src/main/js/apps/documentation/routes.ts
@@ -19,14 +19,8 @@
*/
import { lazyLoad } from '../../components/lazyLoad';
-const routes = [
- {
- indexRoute: { component: lazyLoad(() => import('./components/App')) }
- },
- {
- path: '**',
- indexRoute: { component: lazyLoad(() => import('./components/App')) }
- }
-];
+const App = lazyLoad(() => import('./components/App'));
+
+const routes = [{ indexRoute: { component: App } }, { path: '**', indexRoute: { component: 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
index 114c670ee6e..854f09bce68 100644
--- a/server/sonar-web/src/main/js/apps/documentation/utils.ts
+++ b/server/sonar-web/src/main/js/apps/documentation/utils.ts
@@ -17,12 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { sortBy } from 'lodash';
+
export interface DocumentationEntry {
- children: DocumentationEntry[];
- name: string;
- order: string;
+ content: string;
+ order: number;
relativeName: string;
scope?: 'sonarcloud';
+ text: string;
title: string;
}
@@ -50,3 +52,96 @@ export function getEntryChildren(entries: DocumentationEntry[], root?: string) {
);
});
}
+
+const WORDS = 6;
+
+function cutLeadingWords(str: string) {
+ 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: string) {
+ 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: Array<{ text: string; marked: boolean }>) {
+ const result: Array<{ text: string; marked: boolean }> = [];
+ 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: string, marks: Array<{ from: number; to: number }>) {
+ 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: Array<{ text: string; marked: boolean }> = [];
+ 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;
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
index 699d99a114c..1d59ca8d8e6 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
@@ -18,9 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import tooltipDCE from 'Docs/tooltips/editions/datacenter.md';
+import tooltipDE from 'Docs/tooltips/editions/developer.md';
+import tooltipEE from 'Docs/tooltips/editions/enterprise.md';
import { Edition, getEditionUrl, EditionKey } from '../utils';
-import DocInclude from '../../../components/docs/DocInclude';
import { translate } from '../../../helpers/l10n';
+import { lazyLoad } from '../../../components/lazyLoad';
+
+const DocMarkdownBlock = lazyLoad(() => import('../../../components/docs/DocMarkdownBlock'));
interface Props {
currentEdition?: EditionKey;
@@ -32,7 +37,9 @@ interface Props {
export default function EditionBox({ edition, ncloc, serverId, currentEdition }: Props) {
return (