diff options
Diffstat (limited to 'server/sonar-web/src/main/js')
50 files changed, 1003 insertions, 497 deletions
diff --git a/server/sonar-web/src/main/js/@types/remark-react.d.ts b/server/sonar-web/src/main/js/@types/remark-react.d.ts new file mode 100644 index 00000000000..0e944118732 --- /dev/null +++ b/server/sonar-web/src/main/js/@types/remark-react.d.ts @@ -0,0 +1,33 @@ +/* + * 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 'remark-react' { + interface Options { + /** `h()` */ + createElement?: (...args: any[]) => JSX.Element; + /** Key prefix. */ + prefix?: string; + /** Components. */ + remarkReactComponents?: any; + /** Sanitation schema. */ + sanitize?: any; + } + + export default function remarkReact(options?: Options): JSX.Element; +} diff --git a/server/sonar-web/src/main/js/@types/remark.d.ts b/server/sonar-web/src/main/js/@types/remark.d.ts new file mode 100644 index 00000000000..60cc5c56b85 --- /dev/null +++ b/server/sonar-web/src/main/js/@types/remark.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 'remark' { + export default function remark(): any; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css index 2362ba94a4e..8be68d6447c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css @@ -21,6 +21,7 @@ .navbar-context-branches { display: inline-flex; justify-content: center; + align-items: center; flex-shrink: 1 !important; min-width: 0; line-height: calc(2 * var(--gridSize)); 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 a369fd1412a..5569d2a7844 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 @@ -34,8 +34,8 @@ import { isPullRequest } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; -import HelpIcon from '../../../../components/icons-components/HelpIcon'; -import BubblePopupHelper from '../../../../components/common/BubblePopupHelper'; +import PlusCircleIcon from '../../../../components/icons-components/PlusCircleIcon'; +import Popup from '../../../../components/controls/Popup'; import Tooltip from '../../../../components/controls/Tooltip'; interface Props { @@ -47,8 +47,6 @@ interface Props { interface State { dropdownOpen: boolean; - noBranchSupportPopupOpen: boolean; - singleBranchPopupOpen: boolean; } export default class ComponentNavBranch extends React.PureComponent<Props, State> { @@ -59,14 +57,9 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State onSonarCloud: PropTypes.bool }; - constructor(props: Props) { - super(props); - this.state = { - dropdownOpen: false, - noBranchSupportPopupOpen: false, - singleBranchPopupOpen: false - }; - } + state: State = { + dropdownOpen: false + }; componentDidMount() { this.mounted = true; @@ -78,7 +71,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State !isSameBranchLike(nextProps.currentBranchLike, this.props.currentBranchLike) || nextProps.location !== this.props.location ) { - this.setState({ dropdownOpen: false, singleBranchPopupOpen: false }); + this.setState({ dropdownOpen: false }); } } @@ -99,34 +92,6 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State } }; - toggleSingleBranchPopup = (show?: boolean) => { - if (show !== undefined) { - this.setState({ singleBranchPopupOpen: show }); - } else { - this.setState(state => ({ singleBranchPopupOpen: !state.singleBranchPopupOpen })); - } - }; - - toggleNoBranchSupportPopup = (show?: boolean) => { - if (show !== undefined) { - this.setState({ noBranchSupportPopupOpen: show }); - } else { - this.setState(state => ({ noBranchSupportPopupOpen: !state.noBranchSupportPopupOpen })); - } - }; - - handleSingleBranchClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.stopPropagation(); - this.toggleSingleBranchPopup(); - }; - - handleNoBranchSupportClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.stopPropagation(); - this.toggleNoBranchSupportPopup(); - }; - renderDropdown = () => { const { configuration } = this.props.component; return this.state.dropdownOpen ? ( @@ -174,31 +139,23 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State }; renderSingleBranchPopup = () => ( - <div className="display-inline-block spacer-left"> - <a className="link-no-underline" href="#" onClick={this.handleSingleBranchClick}> - <HelpIcon fill={theme.blue} /> - </a> - <BubblePopupHelper - isOpen={this.state.singleBranchPopupOpen} - popup={<SingleBranchHelperPopup />} - position="bottomleft" - togglePopup={this.toggleSingleBranchPopup} - /> - </div> + <Popup overlay={<SingleBranchHelperPopup />}> + {({ onClick }) => ( + <a className="display-flex-center spacer-left link-no-underline" href="#" onClick={onClick}> + <PlusCircleIcon fill={theme.blue} size={12} /> + </a> + )} + </Popup> ); renderNoBranchSupportPopup = () => ( - <div className="display-inline-block spacer-left"> - <a className="link-no-underline" href="#" onClick={this.handleNoBranchSupportClick}> - <HelpIcon fill={theme.gray80} /> - </a> - <BubblePopupHelper - isOpen={this.state.noBranchSupportPopupOpen} - popup={<NoBranchSupportPopup />} - position="bottomleft" - togglePopup={this.toggleNoBranchSupportPopup} - /> - </div> + <Popup overlay={<NoBranchSupportPopup />}> + {({ onClick }) => ( + <a className="display-flex-center spacer-left link-no-underline" href="#" onClick={onClick}> + <PlusCircleIcon fill={theme.gray80} size={12} /> + </a> + )} + </Popup> ); render() { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/NoBranchSupportPopup.tsx b/server/sonar-web/src/main/js/app/components/nav/component/NoBranchSupportPopup.tsx index d6a15f81671..5493c37d39c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/NoBranchSupportPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/NoBranchSupportPopup.tsx @@ -18,25 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import BubblePopup from '../../../../components/common/BubblePopup'; import { translate } from '../../../../helpers/l10n'; -interface Props { - popupPosition?: any; -} - -export default function NoBranchSupportPopup(props: Props) { +export default function NoBranchSupportPopup() { return ( - <BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom"> - <div className="abs-width-400"> - <h6 className="spacer-bottom">{translate('branches.no_support.header')}</h6> - <p className="big-spacer-bottom markdown">{translate('branches.no_support.header.text')}</p> - <p> - <a href="https://redirect.sonarsource.com/editions/developer.html" target="_blank"> - {translate('learn_more')} - </a> - </p> - </div> - </BubblePopup> + <> + <h6 className="spacer-bottom">{translate('branches.no_support.header')}</h6> + <p className="big-spacer-bottom markdown">{translate('branches.no_support.header.text')}</p> + <p> + <a + href="https://redirect.sonarsource.com/editions/developer.html" + rel="noopener noreferrer" + target="_blank"> + {translate('learn_more')} + </a> + </p> + </> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/SingleBranchHelperPopup.tsx b/server/sonar-web/src/main/js/app/components/nav/component/SingleBranchHelperPopup.tsx index 399aff45650..b569b999505 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/SingleBranchHelperPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/SingleBranchHelperPopup.tsx @@ -18,28 +18,22 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import BubblePopup from '../../../../components/common/BubblePopup'; import { translate } from '../../../../helpers/l10n'; -interface Props { - popupPosition?: any; -} - -export default function SingleBranchHelperPopup(props: Props) { +export default function SingleBranchHelperPopup() { return ( - <BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom"> - <div className="abs-width-400"> - <h6 className="spacer-bottom">{translate('branches.learn_how_to_analyze')}</h6> - <p className="big-spacer-bottom markdown"> - {translate('branches.learn_how_to_analyze.text')} - </p> - <a - className="button" - href="https://redirect.sonarsource.com/doc/branches.html" - target="_blank"> - {translate('about_page.read_documentation')} - </a> - </div> - </BubblePopup> + <> + <h6 className="spacer-bottom">{translate('branches.learn_how_to_analyze')}</h6> + <p className="big-spacer-bottom markdown"> + {translate('branches.learn_how_to_analyze.text')} + </p> + <a + className="button" + href="https://redirect.sonarsource.com/doc/branches.html" + rel="noopener noreferrer" + target="_blank"> + {translate('about_page.read_documentation')} + </a> + </> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx index 5258daf383f..2c2651988af 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx @@ -114,10 +114,7 @@ it('renders single branch popup', () => { />, { context: { branchesEnabled: true } } ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(false); - click(wrapper.find('a')); - expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(true); + expect(wrapper.find('Popup')).toMatchSnapshot(); }); it('renders no branch support popup', () => { @@ -130,10 +127,7 @@ it('renders no branch support popup', () => { />, { context: { branchesEnabled: false } } ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(false); - click(wrapper.find('a')); - expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(true); + expect(wrapper.find('Popup')).toMatchSnapshot(); }); it('renders nothing on SonarCloud without branch support', () => { 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 42bc367a274..c3042574729 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 @@ -36,44 +36,9 @@ exports[`renders main branch 1`] = ` `; exports[`renders no branch support popup 1`] = ` -<div - className="navbar-context-branches" -> - <BranchIcon - branchLike={ - Object { - "isMain": true, - "name": "master", - } - } - className="little-spacer-right" - fill="#cdcdcd" - /> - <span - className="note" - > - master - </span> - <div - className="display-inline-block spacer-left" - > - <a - className="link-no-underline" - href="#" - onClick={[Function]} - > - <HelpIcon - fill="#cdcdcd" - /> - </a> - <BubblePopupHelper - isOpen={false} - popup={<NoBranchSupportPopup />} - position="bottomleft" - togglePopup={[Function]} - /> - </div> -</div> +<Popup + overlay={<NoBranchSupportPopup />} +/> `; exports[`renders pull request 1`] = ` @@ -185,41 +150,7 @@ exports[`renders short-living branch 1`] = ` `; exports[`renders single branch popup 1`] = ` -<div - className="navbar-context-branches" -> - <BranchIcon - branchLike={ - Object { - "isMain": true, - "name": "master", - } - } - className="little-spacer-right" - /> - <span - className="note" - > - master - </span> - <div - className="display-inline-block spacer-left" - > - <a - className="link-no-underline" - href="#" - onClick={[Function]} - > - <HelpIcon - fill="#4b9fd5" - /> - </a> - <BubblePopupHelper - isOpen={false} - popup={<SingleBranchHelperPopup />} - position="bottomleft" - togglePopup={[Function]} - /> - </div> -</div> +<Popup + overlay={<SingleBranchHelperPopup />} +/> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/NoBranchSupportPopup-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/NoBranchSupportPopup-test.tsx.snap index cdc9d4cb965..964fc40a823 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/NoBranchSupportPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/NoBranchSupportPopup-test.tsx.snap @@ -1,30 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom" -> - <div - className="abs-width-400" +<React.Fragment> + <h6 + className="spacer-bottom" > - <h6 - className="spacer-bottom" - > - branches.no_support.header - </h6> - <p - className="big-spacer-bottom markdown" + branches.no_support.header + </h6> + <p + className="big-spacer-bottom markdown" + > + branches.no_support.header.text + </p> + <p> + <a + href="https://redirect.sonarsource.com/editions/developer.html" + rel="noopener noreferrer" + target="_blank" > - branches.no_support.header.text - </p> - <p> - <a - href="https://redirect.sonarsource.com/editions/developer.html" - target="_blank" - > - learn_more - </a> - </p> - </div> -</BubblePopup> + learn_more + </a> + </p> +</React.Fragment> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/SingleBranchHelperPopup-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/SingleBranchHelperPopup-test.tsx.snap index 459630aab47..a27c56302eb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/SingleBranchHelperPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/SingleBranchHelperPopup-test.tsx.snap @@ -1,29 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders 1`] = ` -<BubblePopup - customClass="bubble-popup-bottom" -> - <div - className="abs-width-400" +<React.Fragment> + <h6 + className="spacer-bottom" > - <h6 - className="spacer-bottom" - > - branches.learn_how_to_analyze - </h6> - <p - className="big-spacer-bottom markdown" - > - branches.learn_how_to_analyze.text - </p> - <a - className="button" - href="https://redirect.sonarsource.com/doc/branches.html" - target="_blank" - > - about_page.read_documentation - </a> - </div> -</BubblePopup> + branches.learn_how_to_analyze + </h6> + <p + className="big-spacer-bottom markdown" + > + branches.learn_how_to_analyze.text + </p> + <a + className="button" + href="https://redirect.sonarsource.com/doc/branches.html" + rel="noopener noreferrer" + target="_blank" + > + about_page.read_documentation + </a> +</React.Fragment> `; diff --git a/server/sonar-web/src/main/js/app/styles/components/badges.css b/server/sonar-web/src/main/js/app/styles/components/badges.css index c7c42cc6d59..9d0e5a5a2d9 100644 --- a/server/sonar-web/src/main/js/app/styles/components/badges.css +++ b/server/sonar-web/src/main/js/app/styles/components/badges.css @@ -143,6 +143,7 @@ a.badge-focus:active { color: var(--secondFontColor); font-size: var(--smallFontSize); font-weight: 400; + white-space: nowrap; } .outline-badge.active { diff --git a/server/sonar-web/src/main/js/app/styles/components/search-navigator.css b/server/sonar-web/src/main/js/app/styles/components/search-navigator.css index 6c9d545ce78..b661a707b43 100644 --- a/server/sonar-web/src/main/js/app/styles/components/search-navigator.css +++ b/server/sonar-web/src/main/js/app/styles/components/search-navigator.css @@ -464,7 +464,6 @@ a.search-navigator-facet:focus .facet-stat { .search-navigator-facet-header-value { display: block; - padding: 8px 0; overflow: hidden; } diff --git a/server/sonar-web/src/main/js/app/styles/components/tooltips.css b/server/sonar-web/src/main/js/app/styles/components/tooltips.css deleted file mode 100644 index ca2109a4cd4..00000000000 --- a/server/sonar-web/src/main/js/app/styles/components/tooltips.css +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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. - */ -.tooltip, -.rc-tooltip { - position: absolute; - z-index: var(--tooltipZIndex); - display: block; - height: auto; - font-size: var(--smallFontSize); - font-weight: 300; - line-height: 1.5; - animation: fadeIn 0.3s forwards; -} - -.rc-tooltip-hidden { - display: none; -} - -.tooltip.top, -.rc-tooltip-placement-top { - padding: 5px 0; - margin-top: -3px; -} - -.tooltip.right, -.rc-tooltip-placement-right { - padding: 0 5px; - margin-left: 3px; -} - -.tooltip.bottom, -.rc-tooltip-placement-bottom { - padding: 5px 0; - margin-top: 3px; -} - -.tooltip.left, -.rc-tooltip-placement-left { - padding: 0 5px; - margin-left: -3px; -} - -.tooltip-inner, -.rc-tooltip-inner { - max-width: 300px; - padding: 3px 8px; - color: #fff; - text-align: left; - text-decoration: none; - background-color: #475760; - border-radius: 4px; - letter-spacing: 0.04em; - overflow: hidden; - word-break: break-word; -} - -.tooltip-inner .alert, -.rc-tooltip-inner .alert { - margin-bottom: 5px; - border-radius: 4px; -} - -.tooltip-arrow, -.rc-tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border: solid transparent; -} - -.tooltip.top .tooltip-arrow, -.rc-tooltip-placement-top .rc-tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-width: 5px 5px 0; - border-top-color: #475760; -} - -.tooltip.top-left .tooltip-arrow, -.rc-tooltip-placement-topLeft .rc-tooltip-arrow { - right: 5px; - bottom: 0; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #475760; -} - -.tooltip.top-right .tooltip-arrow, -.rc-tooltip-placement-topRight .rc-tooltip-arrow { - bottom: 0; - left: 5px; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #475760; -} - -.tooltip.right .tooltip-arrow, -.rc-tooltip-placement-right .rc-tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: #475760; -} - -.tooltip.left .tooltip-arrow, -.rc-tooltip-placement-left .rc-tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: #475760; -} - -.tooltip.bottom .tooltip-arrow, -.rc-tooltip-placement-bottom .rc-tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-width: 0 5px 5px; - border-bottom-color: #475760; -} - -.tooltip.bottom-left .tooltip-arrow, -.rc-tooltip-placement-bottomLeft .rc-tooltip-arrow { - top: 0; - right: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #475760; -} - -.tooltip.bottom-right .tooltip-arrow, -.rc-tooltip-placement-bottomRight .rc-tooltip-arrow { - top: 0; - left: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #475760; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} diff --git a/server/sonar-web/src/main/js/app/styles/init/links.css b/server/sonar-web/src/main/js/app/styles/init/links.css index c370f7bb0e8..bcf268567d2 100644 --- a/server/sonar-web/src/main/js/app/styles/init/links.css +++ b/server/sonar-web/src/main/js/app/styles/init/links.css @@ -52,10 +52,6 @@ a:focus { border-bottom-color: var(--lightBlue); } -.tooltip a { - color: var(--lightBlue); -} - .link-no-underline { border-bottom: none; } diff --git a/server/sonar-web/src/main/js/app/styles/init/tables.css b/server/sonar-web/src/main/js/app/styles/init/tables.css index bdc3568bf8d..2a2a29366bb 100644 --- a/server/sonar-web/src/main/js/app/styles/init/tables.css +++ b/server/sonar-web/src/main/js/app/styles/init/tables.css @@ -76,6 +76,7 @@ table.data > thead:after { } table.data > thead > tr > th { + position: relative; vertical-align: top; line-height: 18px; padding: 8px 10px; @@ -96,6 +97,7 @@ table.data > tfoot > tr > td { } table.data > tbody > tr > td { + position: relative; padding: 8px 10px; vertical-align: text-top; line-height: 16px; @@ -263,3 +265,11 @@ table.form td img { table#project-history tr > td { vertical-align: top; } + +.table-cell-doc { + position: absolute; + z-index: var(--aboveNormalZIndex); + right: -8px; + top: 50%; + margin-top: -6px; +} diff --git a/server/sonar-web/src/main/js/app/styles/style.css b/server/sonar-web/src/main/js/app/styles/style.css index f5bcff13ca5..4fc6c561948 100644 --- a/server/sonar-web/src/main/js/app/styles/style.css +++ b/server/sonar-web/src/main/js/app/styles/style.css @@ -65,6 +65,14 @@ line-height: 1.5; } +.markdown.cut-margins > *:first-child { + margin-top: 0 !important; +} + +.markdown.cut-margins > *:last-child { + margin-bottom: 0 !important; +} + .rule-desc p, .markdown p, .rule-desc ul, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx index dd0a053202e..7e844278970 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { orderBy, without, sortBy } from 'lodash'; import * as classNames from 'classnames'; import { FacetKey } from '../query'; +import Tooltip from '../../../components/controls/Tooltip'; import FacetBox from '../../../components/facet/FacetBox'; import FacetHeader from '../../../components/facet/FacetHeader'; import FacetItem from '../../../components/facet/FacetItem'; @@ -37,6 +38,7 @@ export interface BasicProps { } interface Props extends BasicProps { + children?: React.ReactNode; disabled?: boolean; disabledHelper?: string; halfWidth?: boolean; @@ -101,13 +103,17 @@ export default class Facet extends React.PureComponent<Props> { className={classNames({ 'search-navigator-facet-box-forbidden': this.props.disabled })} property={this.props.property}> <FacetHeader - helper={this.props.disabled ? this.props.disabledHelper : undefined} - name={translate('coding_rules.facet', this.props.property)} + name={ + <Tooltip overlay={this.props.disabled ? this.props.disabledHelper : undefined}> + <span>{translate('coding_rules.facet', this.props.property)}</span> + </Tooltip> + } onClear={this.handleClear} onClick={this.props.disabled ? undefined : this.handleHeaderClick} open={this.props.open && !this.props.disabled} - values={values} - /> + values={values}> + {this.props.children} + </FacetHeader> {this.props.open && items !== undefined && <FacetItemsList>{items.map(this.renderItem)}</FacetItemsList>} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx index 402b971dcaa..68b6f9ae1ca 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx @@ -22,6 +22,7 @@ import { sortBy } from 'lodash'; import * as classNames from 'classnames'; import { Query, FacetKey } from '../query'; import { Profile } from '../../../api/quality-profiles'; +import DocTooltip from '../../../components/docs/DocTooltip'; import FacetBox from '../../../components/facet/FacetBox'; import FacetHeader from '../../../components/facet/FacetHeader'; import FacetItem from '../../../components/facet/FacetItem'; @@ -161,8 +162,9 @@ export default class ProfileFacet extends React.PureComponent<Props> { onClear={this.handleClear} onClick={this.handleHeaderClick} open={this.props.open} - values={this.getTextValue()} - /> + values={this.getTextValue()}> + <DocTooltip className="spacer-left" doc="rules/rules-quality-profiles" /> + </FacetHeader> {this.props.open && <FacetItemsList>{profiles.map(this.renderItem)}</FacetItemsList>} </FacetBox> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx index e951e3b9d44..21481a1f240 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -31,6 +31,7 @@ import { getRuleDetails, deleteRule, updateRule } from '../../../api/rules'; import { RuleActivation, RuleDetails as IRuleDetails } from '../../../app/types'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import ConfirmButton from '../../../components/controls/ConfirmButton'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { Button } from '../../../components/ui/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -175,7 +176,7 @@ export default class RuleDetails extends React.PureComponent<Props, State> { {params.length > 0 && <RuleDetailsParameters params={params} />} {isEditable && ( - <div className="coding-rules-detail-description"> + <div className="coding-rules-detail-description display-flex-center"> {/* `templateRule` is used to get rule meta data, `customRule` is used to get parameter values */} {/* it's expected to pass the same rule to both parameters */} <CustomRuleButton @@ -202,12 +203,15 @@ export default class RuleDetails extends React.PureComponent<Props, State> { modalHeader={translate('coding_rules.delete_rule')} onConfirm={this.handleDelete}> {({ onClick }) => ( - <Button - className="button-red spacer-left js-delete" - id="coding-rules-detail-rule-delete" - onClick={onClick}> - {translate('delete')} - </Button> + <> + <Button + className="button-red spacer-left js-delete" + id="coding-rules-detail-rule-delete" + onClick={onClick}> + {translate('delete')} + </Button> + <DocTooltip className="spacer-left" doc="rules/custom-rule-removal" /> + </> )} </ConfirmButton> </div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx index 5d5bc25cdae..16e9f0d6f65 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx @@ -27,6 +27,7 @@ import { getRuleUrl } from '../../../helpers/urls'; import LinkIcon from '../../../components/icons-components/LinkIcon'; import RuleScopeIcon from '../../../components/icons-components/RuleScopeIcon'; import Tooltip from '../../../components/controls/Tooltip'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { translate } from '../../../helpers/l10n'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import SeverityHelper from '../../../components/shared/SeverityHelper'; @@ -174,16 +175,15 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> { return null; } return ( - <Tooltip overlay={translate('coding_rules.custom_rule.title')}> - <li className="coding-rules-detail-property"> - {translate('coding_rules.custom_rule')} - {' ('} - <Link to={getRuleUrl(ruleDetails.templateKey, this.props.organization)}> - {translate('coding_rules.show_template')} - </Link> - {')'} - </li> - </Tooltip> + <li className="coding-rules-detail-property"> + {translate('coding_rules.custom_rule')} + {' ('} + <Link to={getRuleUrl(ruleDetails.templateKey, this.props.organization)}> + {translate('coding_rules.show_template')} + </Link> + {')'} + <DocTooltip className="little-spacer-left" doc="rules/custom-rules" /> + </li> ); }; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx index 4a5c87d4774..1f9c3f803ab 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import Facet, { BasicProps } from './Facet'; import { Omit } from '../../../app/types'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { translate } from '../../../helpers/l10n'; interface Props extends Omit<BasicProps, 'onChange' | 'values'> { @@ -55,8 +56,9 @@ export default class TemplateFacet extends React.PureComponent<Props> { renderName={this.renderName} renderTextName={this.renderName} singleSelection={true} - values={value !== undefined ? [String(value)] : []} - /> + values={value !== undefined ? [String(value)] : []}> + <DocTooltip className="spacer-left" doc="rules/rule-templates" /> + </Facet> ); } } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js index 20ab7885146..8c647e71b56 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js @@ -25,6 +25,7 @@ import * as theme from '../../../app/theme'; import { translate } from '../../../helpers/l10n'; import Level from '../../../components/ui/Level'; import Tooltip from '../../../components/controls/Tooltip'; +import DocTooltip from '../../../components/docs/DocTooltip'; import HelpIcon from '../../../components/icons-components/HelpIcon'; /*:: import type { Component, MeasuresList } from '../types'; */ @@ -64,10 +65,11 @@ export default function QualityGate({ branchLike, component, measures } /*: Prop return ( <div className="overview-quality-gate" id="overview-quality-gate"> - <h2 className="overview-title"> - {translate('overview.quality_gate')} - <Level level={level} /> - </h2> + <div className="display-flex-center"> + <h2 className="overview-title">{translate('overview.quality_gate')}</h2> + <DocTooltip className="spacer-left" doc="quality-gates/project-homepage-quality-gate" /> + <Level className="big-spacer-left" level={level} /> + </div> {ignoredConditions && ( <div className="alert alert-info display-inline-block big-spacer-top"> diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap index 2b9f86b6062..2387806ada6 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGate-test.js.snap @@ -5,14 +5,23 @@ exports[`renders message about ignored conditions 1`] = ` className="overview-quality-gate" id="overview-quality-gate" > - <h2 - className="overview-title" + <div + className="display-flex-center" > - overview.quality_gate + <h2 + className="overview-title" + > + overview.quality_gate + </h2> + <DocTooltip + className="spacer-left" + doc="quality-gates/project-homepage-quality-gate" + /> <Level + className="big-spacer-left" level="OK" /> - </h2> + </div> <div className="alert alert-info display-inline-block big-spacer-top" > diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index ac492720ff5..0d0dc6dbee9 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -45,11 +45,6 @@ font-weight: 400; } -.overview-title > .level { - vertical-align: top; - margin-left: 15px; -} - /* * Quality Gate */ diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx index 5adc7becf55..de42184f96d 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx @@ -18,12 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import DocTooltip from '../../components/docs/DocTooltip'; import { translate } from '../../helpers/l10n'; export default function Header() { return ( <header className="page-header"> - <h1 className="page-title">{translate('project_quality_gate.page')}</h1> + <div className="page-title display-flex-center"> + <h1>{translate('project_quality_gate.page')}</h1> + <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-projects" /> + </div> <div className="page-description">{translate('project_quality_gate.page.description')}</div> </header> ); diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap index eaade8b5468..ccf8e090d57 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap @@ -4,11 +4,17 @@ exports[`renders 1`] = ` <header className="page-header" > - <h1 - className="page-title" + <div + className="page-title display-flex-center" > - project_quality_gate.page - </h1> + <h1> + project_quality_gate.page + </h1> + <DocTooltip + className="spacer-left" + doc="quality-gates/quality-gate-projects" + /> + </div> <div className="page-description" > diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx index b1347b6ff2e..cf31adcb621 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx @@ -18,12 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import DocTooltip from '../../components/docs/DocTooltip'; import { translate } from '../../helpers/l10n'; export default function Header() { return ( <header className="page-header"> - <h1 className="page-title">{translate('project_quality_profiles.page')}</h1> + <div className="page-title display-flex-center"> + <h1>{translate('project_quality_profiles.page')}</h1> + <DocTooltip className="spacer-left" doc="quality-profiles/quality-profile-projects" /> + </div> <div className="page-description"> {translate('project_quality_profiles.page.description')} </div> diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap index 4f0e35f4a2f..03ed0f32b01 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap @@ -4,11 +4,17 @@ exports[`renders 1`] = ` <header className="page-header" > - <h1 - className="page-title" + <div + className="page-title display-flex-center" > - project_quality_profiles.page - </h1> + <h1> + project_quality_profiles.page + </h1> + <DocTooltip + className="spacer-left" + doc="quality-profiles/quality-profile-projects" + /> + </div> <div className="page-description" > diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js index f3fb35559e7..982d6d9f2ab 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js @@ -21,6 +21,7 @@ import React from 'react'; import { sortBy, uniqBy } from 'lodash'; import AddConditionForm from './AddConditionForm'; import Condition from './Condition'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { translate, getLocalizedMetricName } from '../../../helpers/l10n'; function getKey(condition, index) { @@ -89,7 +90,10 @@ export default class Conditions extends React.PureComponent { })); return ( <div className="quality-gate-section" id="quality-gate-conditions"> - <h3 className="spacer-bottom">{translate('quality_gates.conditions')}</h3> + <header className="display-flex-center spacer-bottom"> + <h3>{translate('quality_gates.conditions')}</h3> + <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-conditions" /> + </header> <div className="big-spacer-bottom">{translate('quality_gates.introduction')}</div> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js index ebe73fe1e8a..35b37f12bae 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js @@ -20,6 +20,7 @@ import React from 'react'; import Conditions from './Conditions'; import Projects from './Projects'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { translate } from '../../../helpers/l10n'; export default class DetailsContent extends React.PureComponent { @@ -47,7 +48,10 @@ export default class DetailsContent extends React.PureComponent { /> <div id="quality-gate-projects" className="quality-gate-section"> - <h3 className="spacer-bottom">{translate('quality_gates.projects')}</h3> + <header className="display-flex-center spacer-bottom"> + <h3>{translate('quality_gates.projects')}</h3> + <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-projects" /> + </header> {gate.isDefault ? ( defaultMessage ) : ( diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx index a661becfdef..8e2b1aa14ba 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx @@ -23,6 +23,7 @@ import RenameQualityGateForm from './RenameQualityGateForm'; import CopyQualityGateForm from './CopyQualityGateForm'; import DeleteQualityGateForm from './DeleteQualityGateForm'; import { fetchQualityGate, QualityGate, setQualityGateAsDefault } from '../../../api/quality-gates'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; @@ -73,10 +74,15 @@ export default class DetailsHeader extends React.PureComponent<Props, State> { <div className="layout-page-header-panel layout-page-main-header issues-main-header"> <div className="layout-page-header-panel-inner layout-page-main-header-inner"> <div className="layout-page-main-inner"> - <h2 className="pull-left"> - {qualityGate.name} - {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />} - </h2> + <div className="pull-left display-flex-center"> + <h2>{qualityGate.name}</h2> + {qualityGate.isBuiltIn && ( + <> + <BuiltInQualityGateBadge className="spacer-left" /> + <DocTooltip className="spacer-left" doc="quality-gates/built-in-quality-gate" /> + </> + )} + </div> <div className="pull-right"> {actions.rename && ( diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx index 4a267e581fb..da61b1c5623 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import CreateQualityGateForm from '../components/CreateQualityGateForm'; import { QualityGate } from '../../../api/quality-gates'; import { Organization } from '../../../app/types'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; @@ -50,7 +51,6 @@ export default class ListHeader extends React.PureComponent<Props, State> { return ( <header className="page-header"> - <h1 className="page-title">{translate('quality_gates.page')}</h1> {this.props.canCreate && ( <div className="page-actions"> <Button id="quality-gate-add" onClick={this.openCreateQualityGateForm}> @@ -58,6 +58,10 @@ export default class ListHeader extends React.PureComponent<Props, State> { </Button> </div> )} + <div className="display-flex-center"> + <h1 className="page-title">{translate('quality_gates.page')}</h1> + <DocTooltip className="spacer-left" doc="quality-gates/quality-gate" /> + </div> {this.state.createQualityGateOpen && ( <CreateQualityGateForm onClose={this.closeCreateQualityGateForm} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx index c30eff07980..af274592121 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx @@ -32,8 +32,8 @@ interface Props { export default function ProfileLink({ name, language, organization, children, ...other }: Props) { return ( <Link - to={getProfilePath(name, language, organization)} activeClassName="link-no-underline" + to={getProfilePath(name, language, organization)} {...other}> {children} </Link> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx index 812d6110047..ae1e57854f9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { groupBy, pick, sortBy } from 'lodash'; import ProfilesListRow from './ProfilesListRow'; import ProfilesListHeader from './ProfilesListHeader'; +import DocTooltip from '../../../components/docs/DocTooltip'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Profile } from '../types'; @@ -61,7 +62,13 @@ export default class ProfilesList extends React.PureComponent<Props> { {', '} {translateWithParameters('quality_profiles.x_profiles', profilesCount)} </th> - <th className="text-right nowrap">{translate('quality_profiles.list.projects')}</th> + <th className="text-right nowrap"> + {translate('quality_profiles.list.projects')} + <DocTooltip + className="table-cell-doc" + doc="quality-profiles/quality-profile-projects" + /> + </th> <th className="text-right nowrap">{translate('quality_profiles.list.rules')}</th> <th className="text-right nowrap">{translate('quality_profiles.list.updated')}</th> <th className="text-right nowrap">{translate('quality_profiles.list.used')}</th> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx index 677ce226b99..a0b3302ff48 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx @@ -28,6 +28,7 @@ import { getRulesUrl } from '../../../helpers/urls'; import { isStagnant } from '../utils'; import { Profile } from '../types'; import Tooltip from '../../../components/controls/Tooltip'; +import DocTooltip from '../../../components/docs/DocTooltip'; interface Props { onRequestFail: (reason: any) => void; @@ -41,14 +42,21 @@ export default class ProfilesListRow extends React.PureComponent<Props> { const { profile } = this.props; const offset = 25 * (profile.depth - 1); return ( - <div style={{ paddingLeft: offset }}> - <ProfileLink - language={profile.language} - name={profile.name} - organization={this.props.organization}> - {profile.name} - </ProfileLink> - {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />} + <div className="display-flex-center" style={{ paddingLeft: offset }}> + <div> + <ProfileLink + language={profile.language} + name={profile.name} + organization={this.props.organization}> + {profile.name} + </ProfileLink> + </div> + {profile.isBuiltIn && ( + <> + <BuiltInQualityProfileBadge className="spacer-left" /> + <DocTooltip className="spacer-left" doc="quality-profiles/built-in-quality-profile" /> + </> + )} </div> ); } @@ -57,7 +65,12 @@ export default class ProfilesListRow extends React.PureComponent<Props> { const { profile } = this.props; if (profile.isDefault) { - return <span className="badge">{translate('default')}</span>; + return ( + <> + <span className="badge">{translate('default')}</span> + <DocTooltip className="table-cell-doc" doc="quality-profiles/default-quality-profile" /> + </> + ); } return <span>{profile.projectCount}</span>; @@ -122,23 +135,23 @@ export default class ProfilesListRow extends React.PureComponent<Props> { render() { return ( <tr - className="quality-profiles-table-row" + className="quality-profiles-table-row text-middle" data-key={this.props.profile.key} data-name={this.props.profile.name}> - <td className="quality-profiles-table-name">{this.renderName()}</td> - <td className="quality-profiles-table-projects thin nowrap text-right"> + <td className="quality-profiles-table-name text-middle">{this.renderName()}</td> + <td className="quality-profiles-table-projects thin nowrap text-middle text-right"> {this.renderProjects()} </td> - <td className="quality-profiles-table-rules thin nowrap text-right"> + <td className="quality-profiles-table-rules thin nowrap text-middle text-right"> {this.renderRules()} </td> - <td className="quality-profiles-table-date thin nowrap text-right"> + <td className="quality-profiles-table-date thin nowrap text-middle text-right"> {this.renderUpdateDate()} </td> - <td className="quality-profiles-table-date thin nowrap text-right"> + <td className="quality-profiles-table-date thin nowrap text-middle text-right"> {this.renderUsageDate()} </td> - <td className="quality-profiles-table-actions thin nowrap text-right"> + <td className="quality-profiles-table-actions thin nowrap text-middle text-right"> <ProfileActions fromList={true} onRequestFail={this.props.onRequestFail} diff --git a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx index 38ebd834989..7020ee57490 100644 --- a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx +++ b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx @@ -32,6 +32,10 @@ interface Props { position: BubblePopupPosition; } +/** + * Deprecated. + * Use <Popup /> instead. + */ export default function BubblePopup(props: Props) { const popupClass = classNames('bubble-popup', props.customClass); const popupStyle = { ...props.position }; diff --git a/server/sonar-web/src/main/js/components/controls/Popup.tsx b/server/sonar-web/src/main/js/components/controls/Popup.tsx new file mode 100644 index 00000000000..68f8415f4e5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/Popup.tsx @@ -0,0 +1,73 @@ +/* + * 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 OutsideClickHandler from './OutsideClickHandler'; +import Tooltip from './Tooltip'; + +interface Props { + children: (props: { onClick: () => void }) => React.ReactElement<any>; + overlay: React.ReactNode; +} + +interface State { + visible: boolean; +} + +export default class Popup extends React.Component<Props, State> { + state: State = { visible: false }; + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.overlay !== this.props.overlay) { + this.setState({ visible: false }); + } + } + + handleClick = (event?: React.MouseEvent<HTMLElement>) => { + if (event) { + event.preventDefault(); + event.currentTarget.blur(); + } + + // defer opening to not trigger OutsideClickHandler.onClickOutside callback + setTimeout(() => { + this.setState({ visible: true }); + }, 0); + }; + + handleClickOutside = () => { + this.setState({ visible: false }); + }; + + renderOverlay() { + return ( + <OutsideClickHandler onClickOutside={this.handleClickOutside}> + {({ ref }) => <div ref={ref}>{this.props.overlay}</div>} + </OutsideClickHandler> + ); + } + + render() { + return ( + <Tooltip classNameSpace="popup" overlay={this.renderOverlay()} visible={this.state.visible}> + {this.props.children({ onClick: this.handleClick })} + </Tooltip> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.css b/server/sonar-web/src/main/js/components/controls/Tooltip.css index c2529a06b75..bbb8ae5c942 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.css +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.css @@ -17,72 +17,120 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -.tooltip { +.tooltip, +.popup { position: absolute; z-index: var(--tooltipZIndex); display: block; height: auto; box-sizing: border-box; +} + +.tooltip { font-size: var(--smallFontSize); font-weight: 300; line-height: 1.5; animation: fadeIn 0.3s forwards; } -.tooltip.top { +.popup { + font-size: var(--baseFontSize); + font-weight: normal; +} + +.tooltip.top, +.popup.top { padding: 5px 0; margin-top: -3px; } -.tooltip.right { +.tooltip.right, +.popup.right { padding: 0 5px; margin-left: 3px; } -.tooltip.bottom { +.tooltip.bottom, +.popup.bottom { padding: 5px 0; margin-top: 3px; } -.tooltip.left { +.tooltip.left, +.popup.left { padding: 0 5px; margin-left: -3px; } -.tooltip-inner { +.tooltip-inner, +.popup-inner { max-width: 300px; - padding: 3px 8px; - color: #fff; text-align: left; text-decoration: none; - background-color: #475760; border-radius: 4px; - letter-spacing: 0.04em; overflow: hidden; word-break: break-word; } +.tooltip-inner { + padding: 3px 8px; + color: #fff; + background-color: #475760; + letter-spacing: 0.04em; +} + +.popup-inner { + padding: calc(2 * var(--gridSize)); + border: 1px solid var(--barBorderColor); + color: var(--baseFontColor); + background-color: #fff; + box-shadow: var(--defaultShadow); +} + .tooltip-inner .alert { margin-bottom: 5px; border-radius: 4px; } -.tooltip-arrow { +.tooltip-inner a { + color: var(--lightBlue); +} + +.tooltip-arrow, +.popup-arrow, +.popup-arrow::after { position: absolute; width: 0; height: 0; border: solid transparent; } -.tooltip.top .tooltip-arrow { +.tooltip.top .tooltip-arrow, +.popup.top .popup-arrow, +.popup.top .popup-arrow::after { bottom: 0; left: 50%; border-width: 5px 5px 0; - border-top-color: #475760; transform: translateX(-5px); } -.tooltip.right .tooltip-arrow { +.tooltip.top .tooltip-arrow { + border-top-color: #475760; +} + +.popup.top .popup-arrow { + border-top-color: var(--barBorderColor); +} + +.popup.top .popup-arrow::after { + content: ''; + border-top-color: #fff; + transform: translateX(-5px) translateY(-1px); +} + +.tooltip.right .tooltip-arrow, +.popup.right .popup-arrow, +.popup.right .popup-arrow::after { top: 50%; left: 0; transform: translateY(-5px); @@ -90,7 +138,23 @@ border-right-color: #475760; } -.tooltip.left .tooltip-arrow { +.tooltip.right .tooltip-arrow { + border-right-color: #475760; +} + +.popup.right .popup-arrow { + border-right-color: var(--barBorderColor); +} + +.popup.right .popup-arrow::after { + content: ''; + border-right-color: #fff; + transform: translateY(-5px) translateX(1px); +} + +.tooltip.left .tooltip-arrow, +.popup.left .popup-arrow, +.popup.left .popup-arrow::after { top: 50%; right: 0; transform: translateY(-5px); @@ -98,7 +162,23 @@ border-left-color: #475760; } -.tooltip.bottom .tooltip-arrow { +.tooltip.left .tooltip-arrow { + border-left-color: #475760; +} + +.popup.left .popup-arrow { + border-left-color: var(--barBorderColor); +} + +.popup.left .popup-arrow::after { + content: ''; + border-left-color: #fff; + transform: translateY(-5px) translateX(-1px); +} + +.tooltip.bottom .tooltip-arrow, +.popup.bottom .popup-arrow, +.popup.bottom .popup-arrow::after { top: 0; left: 50%; transform: translateX(-5px); @@ -106,6 +186,20 @@ border-bottom-color: #475760; } +.tooltip.bottom .tooltip-arrow { + border-bottom-color: #475760; +} + +.popup.bottom .popup-arrow { + border-bottom-color: var(--barBorderColor); +} + +.popup.bottom .popup-arrow::after { + content: ''; + border-bottom-color: #fff; + transform: translateX(-5px) translateY(1px); +} + @keyframes fadeIn { from { opacity: 0; diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx index a16c9c691db..adc29a6270e 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx @@ -25,6 +25,7 @@ import './Tooltip.css'; export type Placement = 'bottom' | 'right' | 'left' | 'top'; interface Props { + classNameSpace?: string; children: React.ReactElement<{}>; mouseEnterDelay?: number; onShow?: () => void; @@ -244,6 +245,8 @@ export class TooltipInner extends React.Component<Props, State> { }; render() { + const { classNameSpace = 'tooltip' } = this.props; + return ( <> {React.cloneElement(this.props.children, { @@ -253,7 +256,7 @@ export class TooltipInner extends React.Component<Props, State> { {this.isVisible() && ( <TooltipPortal> <div - className={`tooltip ${this.getPlacement()}`} + className={`${classNameSpace} ${this.getPlacement()}`} ref={this.tooltipNodeRef} style={ isMeasured(this.state) @@ -265,9 +268,9 @@ export class TooltipInner extends React.Component<Props, State> { } : undefined }> - <div className="tooltip-inner">{this.props.overlay}</div> + <div className={`${classNameSpace}-inner`}>{this.props.overlay}</div> <div - className="tooltip-arrow" + className={`${classNameSpace}-arrow`} style={ isMeasured(this.state) ? { marginLeft: -this.state.leftFix, marginTop: -this.state.topFix } 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> +`; diff --git a/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx b/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx index 00b6803c124..cc1ae72047a 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx +++ b/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx @@ -25,8 +25,9 @@ import { Button } from '../ui/buttons'; import { translate, translateWithParameters } from '../../helpers/l10n'; interface Props { + children?: React.ReactNode; helper?: string; - name: string; + name: React.ReactNode; onClear?: () => void; onClick?: () => void; open: boolean; @@ -90,6 +91,8 @@ export default class FacetHeader extends React.PureComponent<Props> { </span> )} + {this.props.children} + <span className="search-navigator-facet-header-value spacer-left spacer-right "> {this.renderValueIndicator()} </span> diff --git a/server/sonar-web/src/main/js/components/icons-components/PlusCircleIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/PlusCircleIcon.tsx new file mode 100644 index 00000000000..eb529b71d77 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/PlusCircleIcon.tsx @@ -0,0 +1,39 @@ +/* + * 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 { IconProps } from './types'; + +export default function PlusCircleIcon({ className, fill = 'currentColor', size = 16 }: IconProps) { + return ( + <svg + className={className} + height={size} + version="1.1" + viewBox="0 0 16 16" + width={size} + xmlSpace="preserve" + xmlnsXlink="http://www.w3.org/1999/xlink"> + <path + d="M8 1c3.863 0 7 3.137 7 7s-3.137 7-7 7-7-3.137-7-7 3.137-7 7-7zm3.726 7.985A.274.274 0 0 0 12 8.711V7.289a.274.274 0 0 0-.274-.274H8.985V4.274A.274.274 0 0 0 8.711 4H7.289a.274.274 0 0 0-.274.274v2.741H4.274A.274.274 0 0 0 4 7.289v1.422c0 .152.123.274.274.274h2.741v2.741c0 .151.122.274.274.274h1.422a.274.274 0 0 0 .274-.274V8.985h2.741z" + style={{ fill }} + />; + </svg> + ); +} |