aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/docs
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-04-25 13:54:16 +0200
committerSonarTech <sonartech@sonarsource.com>2018-05-03 20:20:50 +0200
commit4c2edb7abdb283c6bed56ce6be32304f67529045 (patch)
tree45835a2040670502953df94725fbee4768440e89 /server/sonar-web/src/main/js/components/docs
parente43f918b5fc85a8b13cf966a9c23bd7d9f344fb5 (diff)
downloadsonarqube-4c2edb7abdb283c6bed56ce6be32304f67529045.tar.gz
sonarqube-4c2edb7abdb283c6bed56ce6be32304f67529045.zip
SONAR-10611 Display inline documentation tooltips (#180)
Diffstat (limited to 'server/sonar-web/src/main/js/components/docs')
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocLink.tsx45
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx48
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocTooltip.tsx134
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx30
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx43
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap7
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap32
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap61
9 files changed, 448 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/docs/DocLink.tsx b/server/sonar-web/src/main/js/components/docs/DocLink.tsx
new file mode 100644
index 00000000000..fd19f91cd94
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/DocLink.tsx
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+
+export default function DocLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
+ const { children, href, ...other } = props;
+
+ if (process.env.NODE_ENV === 'development') {
+ if (href && href.startsWith('#')) {
+ return (
+ <>
+ {/* TODO implement after SONAR-10612 Create documentation space in the web app */}
+ <Link to="" {...other}>
+ {children}
+ </Link>
+ <strong className="little-spacer-left text-danger">[TODO]</strong>
+ </>
+ );
+ }
+ }
+
+ return (
+ <a href={href} {...other}>
+ {children}
+ </a>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
new file mode 100644
index 00000000000..7a74a9f4b10
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.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 * as classNames from 'classnames';
+import remark from 'remark';
+import reactRenderer from 'remark-react';
+import DocLink from './DocLink';
+
+interface Props {
+ className?: string;
+ content: string | undefined;
+}
+
+export default function DocMarkdownBlock({ className, content }: Props) {
+ return (
+ <div className={classNames('markdown', className)}>
+ {
+ remark()
+ .use(reactRenderer, {
+ remarkReactComponents: {
+ // do not render outer <div />
+ div: React.Fragment,
+ // use custom link to render documentation anchors
+ a: DocLink
+ }
+ })
+ .processSync(content).contents
+ }
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
new file mode 100644
index 00000000000..0dc27dd9954
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 DocMarkdownBlock from './DocMarkdownBlock';
+import HelpIcon from '../icons-components/HelpIcon';
+import Tooltip from '../controls/Tooltip';
+import OutsideClickHandler from '../controls/OutsideClickHandler';
+import * as theme from '../../app/theme';
+
+interface Props {
+ className?: string;
+ /** Key of the documentation chunk */
+ doc: string;
+}
+
+interface State {
+ content?: string;
+ loading: boolean;
+ open: boolean;
+}
+
+export default class DocTooltip extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: false, open: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ document.addEventListener('scroll', this.close, true);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.doc !== this.props.doc) {
+ this.setState({ content: undefined, loading: false, open: false });
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ document.removeEventListener('scroll', this.close, true);
+ }
+
+ fetchContent = () => {
+ this.setState({ loading: true });
+ import(`Docs/${this.props.doc}.md`).then(
+ ({ default: content }) => {
+ if (this.mounted) {
+ this.setState({ content, loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ close = () => {
+ this.setState({ open: false });
+ };
+
+ handleHelpClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ if (!this.state.open && !this.state.loading && this.state.content === undefined) {
+ this.fetchContent();
+ }
+
+ if (this.state.open) {
+ this.setState({ open: false });
+ } else {
+ // defer opening to not trigger OutsideClickHandler.onClickOutside callback
+ setTimeout(() => {
+ this.setState({ open: true });
+ }, 0);
+ }
+ };
+
+ renderOverlay() {
+ if (this.state.loading) {
+ return (
+ <div className="abs-width-300">
+ <i className="spinner" />
+ </div>
+ );
+ }
+
+ return (
+ <OutsideClickHandler onClickOutside={this.close}>
+ {({ ref }) => (
+ <div ref={ref}>
+ <DocMarkdownBlock className="cut-margins abs-width-300" content={this.state.content} />
+ </div>
+ )}
+ </OutsideClickHandler>
+ );
+ }
+
+ render() {
+ return (
+ <div className={classNames('display-flex-center', this.props.className)}>
+ <Tooltip
+ classNameSpace="popup"
+ overlay={this.renderOverlay()}
+ visible={this.state.content !== undefined && this.state.open}>
+ <a
+ className="display-flex-center link-no-underline"
+ href="#"
+ onClick={this.handleHelpClick}>
+ <HelpIcon fill={theme.gray80} size={12} />
+ </a>
+ </Tooltip>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx
new file mode 100644
index 00000000000..ad740c2d833
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import DocLink from '../DocLink';
+
+it('should render simple link', () => {
+ expect(shallow(<DocLink href="http://sample.com" />)).toMatchSnapshot();
+});
+
+it.skip('should render documentation anchor', () => {
+ expect(shallow(<DocLink href="#quality-profiles" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
new file mode 100644
index 00000000000..587343d7c0b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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 DocMarkdownBlock from '../DocMarkdownBlock';
+
+// mock `remark` and `remark-react` to work around the issue with cjs imports
+jest.mock('remark', () => {
+ const remark = require.requireActual('remark');
+ return { default: remark };
+});
+
+jest.mock('remark-react', () => {
+ const remarkReact = require.requireActual('remark-react');
+ return { default: remarkReact };
+});
+
+it('should render simple markdown', () => {
+ expect(shallow(<DocMarkdownBlock content="this is *bold* text" />)).toMatchSnapshot();
+});
+
+it('should render use custom component for links', () => {
+ expect(
+ shallow(<DocMarkdownBlock content="some [link](#quality-profiles)" />).find('DocLink')
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-test.tsx
new file mode 100644
index 00000000000..bc5b4998cc7
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltip-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 DocTooltip from '../DocTooltip';
+import { click } from '../../../helpers/testUtils';
+
+jest.useFakeTimers();
+
+it('should render', () => {
+ const wrapper = shallow(<DocTooltip doc="foo/bar" />);
+ wrapper.setState({ content: 'this is *bold* text', open: true, loading: true });
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ loading: false });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should reset state when receiving new doc', () => {
+ const wrapper = shallow(<DocTooltip doc="foo/bar" />);
+ wrapper.setState({ content: 'this is *bold* text', open: true });
+ wrapper.setProps({ doc: 'baz' });
+ expect(wrapper.state()).toEqual({ content: undefined, loading: false, open: false });
+});
+
+it('should toggle', () => {
+ const wrapper = shallow(<DocTooltip doc="foo/bar" />);
+ expect(wrapper.state('open')).toBe(false);
+ click(wrapper.find('a'));
+ jest.runAllTimers();
+ expect(wrapper.state('open')).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap
new file mode 100644
index 00000000000..97a6d1d24bc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render simple link 1`] = `
+<a
+ href="http://sample.com"
+/>
+`;
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
new file mode 100644
index 00000000000..28a2441e530
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render simple markdown 1`] = `
+<div
+ className="markdown"
+>
+ <React.Fragment
+ key="h-1"
+ >
+ <p
+ key="h-2"
+ >
+ this is
+ <em
+ key="h-3"
+ >
+ bold
+ </em>
+ text
+ </p>
+ </React.Fragment>
+</div>
+`;
+
+exports[`should render use custom component for links 1`] = `
+<DocLink
+ href="#quality-profiles"
+ key="h-3"
+>
+ link
+</DocLink>
+`;
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap
new file mode 100644
index 00000000000..38e7789c044
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltip-test.tsx.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="display-flex-center"
+>
+ <Tooltip
+ classNameSpace="popup"
+ overlay={
+ <div
+ className="abs-width-300"
+ >
+ <i
+ className="spinner"
+ />
+ </div>
+ }
+ visible={true}
+ >
+ <a
+ className="display-flex-center link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <HelpIcon
+ fill="#cdcdcd"
+ size={12}
+ />
+ </a>
+ </Tooltip>
+</div>
+`;
+
+exports[`should render 2`] = `
+<div
+ className="display-flex-center"
+>
+ <Tooltip
+ classNameSpace="popup"
+ overlay={
+ <OutsideClickHandler
+ onClickOutside={[Function]}
+ >
+ [Function]
+ </OutsideClickHandler>
+ }
+ visible={true}
+ >
+ <a
+ className="display-flex-center link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <HelpIcon
+ fill="#cdcdcd"
+ size={12}
+ />
+ </a>
+ </Tooltip>
+</div>
+`;