diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-04-25 13:54:16 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-05-03 20:20:50 +0200 |
commit | 4c2edb7abdb283c6bed56ce6be32304f67529045 (patch) | |
tree | 45835a2040670502953df94725fbee4768440e89 /server/sonar-web/src/main/js/components/docs | |
parent | e43f918b5fc85a8b13cf966a9c23bd7d9f344fb5 (diff) | |
download | sonarqube-4c2edb7abdb283c6bed56ce6be32304f67529045.tar.gz sonarqube-4c2edb7abdb283c6bed56ce6be32304f67529045.zip |
SONAR-10611 Display inline documentation tooltips (#180)
Diffstat (limited to 'server/sonar-web/src/main/js/components/docs')
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> +`; |