diff options
Diffstat (limited to 'server/sonar-web/src/main')
147 files changed, 775 insertions, 5689 deletions
diff --git a/server/sonar-web/src/main/js/@types/rehype-raw.d.ts b/server/sonar-web/src/main/js/@types/rehype-raw.d.ts deleted file mode 100644 index 3fad41c4560..00000000000 --- a/server/sonar-web/src/main/js/@types/rehype-raw.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 'rehype-raw' { - export default function rehypeRaw(): any; -} diff --git a/server/sonar-web/src/main/js/@types/rehype-react.d.ts b/server/sonar-web/src/main/js/@types/rehype-react.d.ts deleted file mode 100644 index 50a919d9677..00000000000 --- a/server/sonar-web/src/main/js/@types/rehype-react.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 'rehype-react' { - export default function rehypeReact(): any; -} diff --git a/server/sonar-web/src/main/js/@types/rehype-slug.d.ts b/server/sonar-web/src/main/js/@types/rehype-slug.d.ts deleted file mode 100644 index 857e1bbcbd0..00000000000 --- a/server/sonar-web/src/main/js/@types/rehype-slug.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 'rehype-slug' { - export default function rehypeSlug(): any; -} diff --git a/server/sonar-web/src/main/js/@types/remark-custom-blocks.d.ts b/server/sonar-web/src/main/js/@types/remark-custom-blocks.d.ts deleted file mode 100644 index 759096d37b8..00000000000 --- a/server/sonar-web/src/main/js/@types/remark-custom-blocks.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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-custom-blocks' { - export default function customBlock(): any; -} 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 deleted file mode 100644 index 780409f9aae..00000000000 --- a/server/sonar-web/src/main/js/@types/remark-react.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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-rehype.d.ts b/server/sonar-web/src/main/js/@types/remark-rehype.d.ts deleted file mode 100644 index 85a2bb14e97..00000000000 --- a/server/sonar-web/src/main/js/@types/remark-rehype.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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-rehype' { - export default function remarkRehype(): any; -} diff --git a/server/sonar-web/src/main/js/@types/remark.d.ts b/server/sonar-web/src/main/js/@types/remark.d.ts deleted file mode 100644 index f7beee505c3..00000000000 --- a/server/sonar-web/src/main/js/@types/remark.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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/@types/unist-util-visit.d.ts b/server/sonar-web/src/main/js/@types/unist-util-visit.d.ts deleted file mode 100644 index 9002a105615..00000000000 --- a/server/sonar-web/src/main/js/@types/unist-util-visit.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 'unist-util-visit' { - export default function visit( - ast: any, - visitor: (node: { type: string; value: string }) => void - ): void; -} diff --git a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts index 9f02909bc9f..4ac206dbf5d 100644 --- a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts @@ -19,34 +19,46 @@ */ import { cloneDeep } from 'lodash'; import { HousekeepingPolicy } from '../../apps/audit-logs/utils'; +import { BranchParameters } from '../../types/branch-like'; import { SettingsKey, SettingValue } from '../../types/settings'; import { getValue } from '../settings'; export default class SettingsServiceMock { - settingValue?: SettingValue; - defaultValues: SettingValue = { - key: SettingsKey.AuditHouseKeeping, - value: HousekeepingPolicy.Weekly - }; + settingValues: SettingValue[]; + defaultValues: SettingValue[] = [ + { + key: SettingsKey.AuditHouseKeeping, + value: HousekeepingPolicy.Weekly + } + ]; constructor() { - this.settingValue = cloneDeep(this.defaultValues); - (getValue as jest.Mock).mockImplementation(this.getValuesHandler); + this.settingValues = cloneDeep(this.defaultValues); + (getValue as jest.Mock).mockImplementation(this.handleGetValues); } - getValuesHandler = () => { - return Promise.resolve(this.settingValue); + handleGetValues = (data: { key: string; component?: string } & BranchParameters) => { + const setting = this.settingValues.find(s => s.key === data.key); + + return this.reply(setting); }; - unsetHousekeepingPolicy() { - this.settingValue = undefined; + emptySettings() { + this.settingValues = []; } setYearlyHousekeepingPolicy() { - this.settingValue = { key: 'test', value: HousekeepingPolicy.Yearly }; + const auditSetting = this.settingValues.find(s => s.key === SettingsKey.AuditHouseKeeping); + if (auditSetting) { + auditSetting.value = HousekeepingPolicy.Yearly; + } } resetSettingvalues = () => { - this.settingValue = cloneDeep(this.defaultValues); + this.settingValues = cloneDeep(this.defaultValues); }; + + reply<T>(response: T): Promise<T> { + return Promise.resolve(cloneDeep(response)); + } } diff --git a/server/sonar-web/src/main/js/app/components/DocumentationRedirect.tsx b/server/sonar-web/src/main/js/app/components/DocumentationRedirect.tsx new file mode 100644 index 00000000000..3f4206d868a --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/DocumentationRedirect.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 { Helmet } from 'react-helmet-async'; +import { useLocation } from 'react-router-dom'; +import Link from '../../components/common/Link'; +import { getUrlForDoc } from '../../helpers/docs'; +import withAppStateContext, { WithAppStateContextProps } from './app-state/withAppStateContext'; + +const PAUSE_REDIRECT = 1; + +function DocumentationRedirect({ appState }: WithAppStateContextProps) { + const location = useLocation(); + const url = getUrlForDoc(appState.version, location.pathname.replace(/^\/documentation/, '')); + + return ( + <> + <Helmet> + <meta httpEquiv="refresh" content={`${PAUSE_REDIRECT}; url='${url}'`} /> + </Helmet> + <div className="global-loading"> + <div className="display-flex-center"> + <i className="spinner global-loading-spinner" /> + <span className="spacer-left global-loading-text">Redirecting...</span> + </div> + <div className="display-flex-justify-content spacer-top large"> + <Link to={url}>Click here if you're not being redirected automatically</Link> + </div> + </div> + </> + ); +} + +export default withAppStateContext(DocumentationRedirect); diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx index 7631e5345a6..3ed1bec60b8 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import DocLink from '../../components/common/DocLink'; import InstanceMessage from '../../components/common/InstanceMessage'; import Link from '../../components/common/Link'; import { Alert } from '../../components/ui/Alert'; @@ -74,7 +75,7 @@ export function GlobalFooter({ hideLoggedInInfo, appState }: GlobalFooterProps) </a> </li> <li className="page-footer-menu-item"> - <Link to="/documentation">{translate('footer.documentation')}</Link> + <DocLink to="/">{translate('footer.documentation')}</DocLink> </li> <li className="page-footer-menu-item"> <a diff --git a/server/sonar-web/src/main/js/app/components/__tests__/DocumentationRedirect-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/DocumentationRedirect-test.tsx new file mode 100644 index 00000000000..9053428f7d2 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/DocumentationRedirect-test.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react'; +import * as React from 'react'; +import { Route } from 'react-router-dom'; +import { mockAppState } from '../../../helpers/testMocks'; +import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; +import DocumentationRedirect from '../DocumentationRedirect'; + +it('should redirect to static doc for specific version', async () => { + renderDocumentationRedirect('land', '9.7.1234'); + + expect(await screen.findByRole('link')).toHaveAttribute( + 'href', + 'https://docs.sonarqube.org/9.7/land' + ); +}); + +it('should redirect to static doc for latest version', async () => { + renderDocumentationRedirect('land', '9.7-SNAPSHOT'); + + expect(await screen.findByRole('link')).toHaveAttribute( + 'href', + 'https://docs.sonarqube.org/latest/land' + ); +}); + +function renderDocumentationRedirect(navigatge: string, version?: string) { + renderAppRoutes( + `documentation/${navigatge}`, + () => <Route path="/documentation/*" element={<DocumentationRedirect />} />, + { appState: mockAppState({ version }) } + ); +} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx index 4d47866e21e..79daabad3ac 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx @@ -90,14 +90,6 @@ it('should render only the children', async () => { getWrapper({ appState: mockAppState({ canAdmin: false }), currentUser: { ...LOGGED_IN_USER }, - location: mockLocation({ pathname: '/documentation/' }) - }) - ); - - await shouldNotHaveModals( - getWrapper({ - appState: mockAppState({ canAdmin: false }), - currentUser: { ...LOGGED_IN_USER }, location: mockLocation({ pathname: '/create-organization' }) }) ); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap index 93db6c4ba6f..d9f37661358 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap @@ -44,11 +44,11 @@ exports[`should display the sq version 1`] = ` <li className="page-footer-menu-item" > - <ForwardRef(Link) - to="/documentation" + <withAppStateContext(DocLink) + to="/" > footer.documentation - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </li> <li className="page-footer-menu-item" @@ -108,11 +108,11 @@ exports[`should not render the only logged in information 1`] = ` <li className="page-footer-menu-item" > - <ForwardRef(Link) - to="/documentation" + <withAppStateContext(DocLink) + to="/" > footer.documentation - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </li> <li className="page-footer-menu-item" @@ -173,11 +173,11 @@ exports[`should render the only logged in information 1`] = ` <li className="page-footer-menu-item" > - <ForwardRef(Link) - to="/documentation" + <withAppStateContext(DocLink) + to="/" > footer.documentation - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </li> <li className="page-footer-menu-item" diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx index 7c51dd97fa7..3cbee80e9e5 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx @@ -96,7 +96,8 @@ export function CurrentBranchLike(props: CurrentBranchLikeProps) { links={[ { href: 'https://redirect.sonarsource.com/editions/developer.html', - label: translate('learn_more') + label: translate('learn_more'), + doc: false } ]} title={ @@ -119,17 +120,18 @@ export function CurrentBranchLike(props: CurrentBranchLikeProps) { data-test="only-one-branch-like" links={[ { - href: '/documentation/branches/overview/', + href: '/branches/overview/', label: translate('branch_like_navigation.only_one_branch.documentation') }, { - href: '/documentation/analysis/pull-request/', + href: '/analysis/pull-request/', label: translate('branch_like_navigation.only_one_branch.pr_analysis') }, { href: `/tutorials?id=${component.key}`, label: translate('branch_like_navigation.tutorial_for_ci'), - inPlace: true + inPlace: true, + doc: false } ]} title={translate('branch_like_navigation.only_one_branch.title')}> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap index eb009ecfeee..513f07e8cfb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap @@ -127,6 +127,7 @@ exports[`projects should render correctly when branches support is disabled: alm links={ Array [ Object { + "doc": false, "href": "https://redirect.sonarsource.com/editions/developer.html", "label": "learn_more", }, @@ -169,6 +170,7 @@ exports[`projects should render correctly when branches support is disabled: alm links={ Array [ Object { + "doc": false, "href": "https://redirect.sonarsource.com/editions/developer.html", "label": "learn_more", }, @@ -211,6 +213,7 @@ exports[`projects should render correctly when branches support is disabled: def links={ Array [ Object { + "doc": false, "href": "https://redirect.sonarsource.com/editions/developer.html", "label": "learn_more", }, @@ -278,14 +281,15 @@ exports[`projects should render correctly when there is only one branchlike 1`] links={ Array [ Object { - "href": "/documentation/branches/overview/", + "href": "/branches/overview/", "label": "branch_like_navigation.only_one_branch.documentation", }, Object { - "href": "/documentation/analysis/pull-request/", + "href": "/analysis/pull-request/", "label": "branch_like_navigation.only_one_branch.pr_analysis", }, Object { + "doc": false, "href": "/tutorials?id=my-project", "inPlace": true, "label": "branch_like_navigation.tutorial_for_ci", diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 93cc1be5009..44f1ac28530 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -29,7 +29,6 @@ import ChangeAdminPasswordApp from '../../apps/change-admin-password/ChangeAdmin import codeRoutes from '../../apps/code/routes'; import codingRulesRoutes from '../../apps/coding-rules/routes'; import componentMeasuresRoutes from '../../apps/component-measures/routes'; -import documentationRoutes from '../../apps/documentation/routes'; import groupsRoutes from '../../apps/groups/routes'; import { globalIssuesRoutes, projectIssuesRoutes } from '../../apps/issues/routes'; import maintenanceRoutes from '../../apps/maintenance/routes'; @@ -73,6 +72,7 @@ import { } from '../components/available-features/AvailableFeaturesContext'; import ComponentContainer from '../components/ComponentContainer'; import CurrentUserContextProvider from '../components/current-user/CurrentUserContextProvider'; +import DocumentationRedirect from '../components/DocumentationRedirect'; import GlobalAdminPageExtension from '../components/extensions/GlobalAdminPageExtension'; import GlobalPageExtension from '../components/extensions/GlobalPageExtension'; import PortfolioPage from '../components/extensions/PortfolioPage'; @@ -142,10 +142,6 @@ function renderRedirects() { {renderRedirect({ from: '/admin', to: '/admin/settings' })} {renderRedirect({ from: '/background_tasks', to: '/admin/background_tasks' })} - {renderRedirect({ - from: '/documentation/analysis/languages/vb', - to: '/documentation/analysis/languages/vbnet/' - })} {renderRedirect({ from: '/groups', to: '/admin/groups' })} {renderRedirect({ from: '/extension/governance/portfolios', to: '/portfolios' })} {renderRedirect({ from: '/permission_templates', to: '/admin/permission_templates' })} @@ -163,6 +159,7 @@ function renderRedirects() { {renderRedirect({ from: '/users', to: '/admin/users' })} {renderRedirect({ from: '/onboarding', to: '/projects/create' })} {renderRedirect({ from: '/markdown/help', to: '/formatting/help' })} + <Route path="/documentation/*" element={<DocumentationRedirect />} /> </> ); } @@ -265,8 +262,6 @@ export default function startReactApp( {codingRulesRoutes()} - {documentationRoutes()} - <Route path="extension/:pluginKey/:extensionKey" element={<GlobalPageExtension />} diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx index 80128101cbe..813ede23e2d 100644 --- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditApp-it.tsx @@ -141,7 +141,7 @@ it('should not render if governance is not enable', () => { }); it('should show right option when keeping log for month', async () => { - handler.unsetHousekeepingPolicy(); + handler.emptySettings(); renderAuditLogs(); expect(await ui.pageTitle.find()).toBeInTheDocument(); expect(ui.todayRadio.get()).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx index dd4cbe6f060..020395e1918 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { translate } from '../../../helpers/l10n'; import Workers from './Workers'; @@ -37,12 +37,9 @@ export default function Header(props: Props) { )} <p className="page-description"> {translate('background_tasks.page.description')} - <Link - className="spacer-left" - target="_blank" - to={{ pathname: '/documentation/analysis/background-tasks/' }}> + <DocLink className="spacer-left" to="/analysis/background-tasks/"> {translate('learn_more')} - </Link> + </DocLink> </p> </header> ); 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 76a12c9d80f..a78cbf07a81 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 @@ -175,7 +175,7 @@ export default class ProfileFacet extends React.PureComponent<Props> { content={translate('coding_rules.facet.qprofile.help')} links={[ { - href: '/documentation/instance-administration/quality-profiles/', + href: '/instance-administration/quality-profiles/', label: translate('coding_rules.facet.qprofile.link') } ]} diff --git a/server/sonar-web/src/main/js/apps/documentation/__tests__/pages-test.ts b/server/sonar-web/src/main/js/apps/documentation/__tests__/pages-test.ts deleted file mode 100644 index 263144f12bc..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/__tests__/pages-test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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. - */ -/* eslint-disable no-console */ -import { filterContent, ParsedContent } from '../../../helpers/markdown'; -import { mockDocumentationMarkdown } from '../../../helpers/testMocks'; -import { Dict } from '../../../types/types'; - -jest.mock('remark', () => () => ({ - parse: jest.fn().mockReturnValue({}) -})); - -jest.mock( - 'unist-util-visit', - () => (_: any, cb: (node: { type: string; value: string }) => void) => { - cb({ type: 'text', value: 'Text content' }); - } -); - -jest.mock('../../../helpers/markdown', () => { - const markdown = jest.requireActual('../../../helpers/markdown'); - return { ...markdown, filterContent: jest.fn().mockImplementation(markdown.filterContent) }; -}); - -const lorem = { - url: 'analysis/languages/lorem', - title: 'toto doc', - key: 'toto', - content: 'TOTO CONTENT' -}; -const foo = { - url: `analysis/languages/foo`, - title: 'foo doc', - key: 'foo' -}; -const loremDoc = mockDocumentationMarkdown(lorem); -const fooDoc = mockDocumentationMarkdown(foo); - -jest.mock('../documentation.directory-loader', () => [ - { content: loremDoc, path: lorem.url }, - { content: fooDoc, path: foo.url } -]); - -it('should correctly parse files', () => { - const pages = getPages(); - expect(pages.length).toBe(2); - - expect(pages[0]).toMatchObject({ - relativeName: lorem.url, - url: `/${lorem.url}/`, - title: lorem.title, - navTitle: undefined, - order: -1, - scope: undefined, - content: lorem.content - }); - - expect(pages[1]).toMatchObject({ - relativeName: foo.url, - url: `/${foo.url}/`, - title: foo.title, - navTitle: undefined, - order: -1, - scope: undefined - }); -}); - -it('should correctly handle overrides (replace & add)', () => { - const overrideFooDoc = { - content: 'OVERRIDE_FOO', - title: 'OVERRIDE_TITLE_FOO', - key: foo.key - }; - const newDoc = { - content: 'TATA', - title: 'TATA_TITLE', - key: 'tata' - }; - - const overrides: Dict<ParsedContent> = {}; - overrides[foo.url] = { frontmatter: overrideFooDoc, content: overrideFooDoc.content }; - overrides[`analysis/languages/${newDoc.key}`] = { frontmatter: newDoc, content: newDoc.content }; - const pages = getPages(overrides); - - expect(pages.length).toBe(3); - expect(pages[1].content).toBe(overrideFooDoc.content); - expect(pages[1].title).toBe(overrideFooDoc.title); - expect(pages[2].content).toBe(newDoc.content); - expect(pages[2].title).toBe(newDoc.title); -}); - -it('should not break the whole doc when one page cannot be parsed', () => { - const originalConsoleError = console.error; - console.error = jest.fn(); - - (filterContent as jest.Mock).mockImplementationOnce(() => { - throw Error('Parse page error'); - }); - const pages = getPages(); - expect(pages.length).toBe(2); - expect(pages[0].content).toBe(''); - expect(console.error).toHaveBeenCalledTimes(1); - - console.error = originalConsoleError; -}); - -function getPages(overrides: Dict<ParsedContent> = {}) { - // This allows the use of out-of-scope data inside jest.mock - // Usually, it is impossible as jest.mock'ed module is hoisted on the top of the file - return jest.requireActual('../pages').default(overrides); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx b/server/sonar-web/src/main/js/apps/documentation/components/App.tsx deleted file mode 100644 index 7834ae8612a..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/App.tsx +++ /dev/null @@ -1,237 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 navigationTreeSonarQube from 'Docs/../static/SonarQubeNavigationTree.json'; -import { DocNavigationItem } from 'Docs/@types/types'; -import * as React from 'react'; -import { Helmet } from 'react-helmet-async'; -import { useLocation, useParams } from 'react-router-dom'; -import { getInstalledPlugins } from '../../../api/plugins'; -import { getPluginStaticFileContent } from '../../../api/static'; -import NotFound from '../../../app/components/NotFound'; -import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; -import Link from '../../../components/common/Link'; -import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; -import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; -import { translate } from '../../../helpers/l10n'; -import { ParsedContent, separateFrontMatter } from '../../../helpers/markdown'; -import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; -import { isDefined } from '../../../helpers/types'; -import { InstalledPlugin, PluginType } from '../../../types/plugins'; -import { Dict } from '../../../types/types'; -import { getUrlsList } from '../navTreeUtils'; -import getPages from '../pages'; -import '../styles.css'; -import { DocumentationEntry } from '../utils'; -import Sidebar from './Sidebar'; - -interface Props { - params: { splat?: string }; - location: { hash: string }; -} - -interface State { - loading: boolean; - pages: DocumentationEntry[]; - tree: DocNavigationItem[]; -} - -const LANGUAGES_BASE_URL = 'analysis/languages'; - -export class App extends React.PureComponent<Props, State> { - mounted = false; - state: State = { - loading: false, - pages: [], - tree: [] - }; - - componentDidMount() { - this.mounted = true; - addSideBarClass(); - - this.setState({ loading: true }); - - const tree = (navigationTreeSonarQube as any).default as DocNavigationItem[]; - - this.getLanguagePluginsDocumentation(tree).then( - overrides => { - if (this.mounted) { - this.setState({ - loading: false, - pages: getPages(overrides), - tree - }); - } - }, - () => { - if (this.mounted) { - this.setState({ - loading: false - }); - } - } - ); - } - - componentWillUnmount() { - this.mounted = false; - removeSideBarClass(); - } - - getLanguagePluginsDocumentation = async (tree: DocNavigationItem[]) => { - const plugins = await getInstalledPlugins(PluginType.Bundled).catch( - () => [] as InstalledPlugin[] - ); - - const pluginsWithDoc = await Promise.all( - plugins.map(plugin => { - if (plugin.documentationPath) { - const matchArray = /^static\/(.*)/.exec(plugin.documentationPath); - - if (matchArray && matchArray.length > 1) { - return getPluginStaticFileContent(plugin.key, matchArray[1]).then( - content => ({ ...plugin, content }), - () => undefined - ); - } - } - - return undefined; - }) - ); - - const regex = new RegExp(`/${LANGUAGES_BASE_URL}/\\w+/$`); - const overridablePaths = getUrlsList(tree).filter( - path => regex.test(path) && path !== `/${LANGUAGES_BASE_URL}/overview/` - ); - - const parsedContent: Dict<ParsedContent> = {}; - - pluginsWithDoc.filter(isDefined).forEach(plugin => { - const parsed = separateFrontMatter(plugin.content); - - if (plugin.issueTrackerUrl) { - // Inject issue tracker link - let issueTrackerLink = '## Issue Tracker'; - issueTrackerLink += '\r\n'; - issueTrackerLink += `Check the [issue tracker](${plugin.issueTrackerUrl}) for this language.`; - parsed.content = `${parsed.content}\r\n${issueTrackerLink}`; - } - - if ( - parsed?.frontmatter?.key && - overridablePaths.includes(`/${LANGUAGES_BASE_URL}/${parsed.frontmatter.key}/`) - ) { - parsedContent[`${LANGUAGES_BASE_URL}/${parsed.frontmatter.key}`] = parsed; - } - }); - - return parsedContent; - }; - - render() { - const { loading, pages, tree } = this.state; - const { - params: { splat = '' }, - location: { hash } - } = this.props; - - if (loading) { - return ( - <div className="page page-limited"> - <DeferredSpinner /> - </div> - ); - } - - const page = pages.find(p => p.url === `/${splat}`); - const mainTitle = translate('documentation.page_title.sonarqube'); - const isIndex = splat === 'index'; - - if (!page) { - return ( - <> - <Helmet title={mainTitle}> - <meta content="noindex nofollow" name="robots" /> - </Helmet> - <A11ySkipTarget anchor="documentation_main" /> - <NotFound withContainer={false} /> - </> - ); - } - - return ( - <div className="layout-page"> - <Helmet - defer={false} - title={isIndex || !page.title ? mainTitle : `${page.title} | ${mainTitle}`}> - <meta content="noindex nofollow" name="robots" /> - </Helmet> - - <ScreenPositionHelper className="layout-page-side-outer"> - {({ top }) => ( - <div className="layout-page-side" style={{ top }}> - <div className="layout-page-side-inner"> - <div className="layout-page-filters"> - <div className="documentation-page-header"> - <A11ySkipTarget - anchor="documentation_menu" - label={translate('documentation.skip_to_nav')} - weight={10} - /> - - <Link to="/documentation/"> - <h1>{translate('documentation.page')}</h1> - </Link> - </div> - <Sidebar navigation={tree} pages={pages} splat={splat} /> - </div> - </div> - </div> - )} - </ScreenPositionHelper> - - <div className="layout-page-main"> - <div className="layout-page-main-inner"> - <div className="boxed-group"> - <A11ySkipTarget anchor="documentation_main" /> - - <DocMarkdownBlock - className="documentation-content cut-margins boxed-group-inner" - content={page.content} - stickyToc={true} - title={page.title} - scrollToHref={hash} - /> - </div> - </div> - </div> - </div> - ); - } -} - -export default function AppWrapper() { - const params = useParams(); - const location = useLocation(); - - return <App params={{ splat: params['*'] }} location={location} />; -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx deleted file mode 100644 index 87b432afa9c..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { DocNavigationItem } from 'Docs/@types/types'; -import * as React from 'react'; -import { - getOpenChainFromPath, - isDocsNavigationBlock, - isDocsNavigationExternalLink -} from '../navTreeUtils'; -import { DocumentationEntry, getNodeFromUrl } from '../utils'; -import MenuBlock from './MenuBlock'; -import { MenuExternalLink } from './MenuExternalLink'; -import { MenuItem } from './MenuItem'; - -interface Props { - navigation: DocNavigationItem[]; - pages: DocumentationEntry[]; - splat: string; -} - -interface State { - openChain: DocNavigationItem[]; -} - -export default class Menu extends React.PureComponent<Props, State> { - constructor(props: Props) { - super(props); - this.state = { - openChain: getOpenChainFromPath(this.props.splat, this.props.navigation) - }; - } - - componentWillReceiveProps(nextProps: Props) { - if (this.props.splat !== nextProps.splat) { - this.setState({ openChain: getOpenChainFromPath(nextProps.splat, nextProps.navigation) }); - } - } - - render() { - const { openChain } = this.state; - return ( - <> - {this.props.navigation.map(item => { - if (isDocsNavigationBlock(item)) { - return ( - <MenuBlock - block={item} - key={item.title} - openByDefault={openChain.includes(item)} - openChain={openChain} - pages={this.props.pages} - splat={this.props.splat} - title={item.title} - /> - ); - } - if (isDocsNavigationExternalLink(item)) { - return <MenuExternalLink key={item.title} title={item.title} url={item.url} />; - } - return ( - <MenuItem - key={item} - node={getNodeFromUrl(this.props.pages, item)} - splat={this.props.splat} - /> - ); - })} - </> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/MenuBlock.tsx b/server/sonar-web/src/main/js/apps/documentation/components/MenuBlock.tsx deleted file mode 100644 index d1687c28859..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/MenuBlock.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 classNames from 'classnames'; -import { DocNavigationItem, DocsNavigationBlock } from 'Docs/@types/types'; -import * as React from 'react'; -import { ButtonLink } from '../../../components/controls/buttons'; -import OpenCloseIcon from '../../../components/icons/OpenCloseIcon'; -import { isDocsNavigationBlock } from '../navTreeUtils'; -import { DocumentationEntry, getNodeFromUrl } from '../utils'; -import { MenuItem } from './MenuItem'; - -interface Props { - block: DocsNavigationBlock; - depth?: number; - openByDefault: boolean; - openChain: DocNavigationItem[]; - pages: DocumentationEntry[]; - splat: string; - title: string; -} - -interface State { - open: boolean; -} - -export default class MenuBlock extends React.PureComponent<Props, State> { - state: State; - - constructor(props: Props) { - super(props); - this.state = { - open: props.openByDefault !== undefined ? props.openByDefault : false - }; - } - - handleClick = () => { - this.setState(prevState => ({ - open: !prevState.open - })); - }; - - renderMenuItems = (block: DocsNavigationBlock): React.ReactNode => { - const { depth = 0, openChain, pages, splat } = this.props; - return block.children.map(item => { - if (typeof item === 'string') { - return ( - <MenuItem depth={depth + 1} key={item} node={getNodeFromUrl(pages, item)} splat={splat} /> - ); - } else if (isDocsNavigationBlock(item)) { - return ( - <MenuBlock - block={item} - depth={depth + 1} - key={item.title} - openByDefault={openChain.includes(item)} - openChain={openChain} - pages={pages} - splat={splat} - title={item.title} - /> - ); - } else { - return null; - } - }); - }; - - render() { - const { block, depth = 0, title } = this.props; - const { open } = this.state; - const maxDepth = Math.min(depth, 3); - return ( - <> - <ButtonLink - className={classNames('list-group-item', { [`depth-${maxDepth}`]: depth > 0 })} - onClick={this.handleClick}> - <h3 className="list-group-item-heading"> - <OpenCloseIcon className="little-spacer-right" open={open} /> - {title} - </h3> - </ButtonLink> - {open && this.renderMenuItems(block)} - </> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/MenuExternalLink.tsx b/server/sonar-web/src/main/js/apps/documentation/components/MenuExternalLink.tsx deleted file mode 100644 index 3d2bef9ac06..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/MenuExternalLink.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 DetachIcon from '../../../components/icons/DetachIcon'; - -interface Props { - title: string; - url: string; -} - -export function MenuExternalLink({ title, url }: Props) { - return ( - <a href={url} key={title} target="_blank" rel="noopener noreferrer"> - <h3 className="list-group-item-heading"> - <DetachIcon className="spacer-right" /> - {title} - </h3> - </a> - ); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx b/server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx deleted file mode 100644 index e3fcf7f5498..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 classNames from 'classnames'; -import * as React from 'react'; -import Link from '../../../components/common/Link'; -import { testPathAgainstUrl } from '../navTreeUtils'; -import { DocumentationEntry } from '../utils'; - -interface Props { - depth?: number; - node: DocumentationEntry | undefined; - splat: string; -} - -export function MenuItem({ depth = 0, node, splat }: Props) { - if (!node) { - return null; - } - - const active = testPathAgainstUrl(node.url, splat); - const maxDepth = Math.min(depth, 3); - const title = node.navTitle || node.title; - - return ( - <Link - className={classNames('list-group-item', { active, [`depth-${maxDepth}`]: depth > 0 })} - key={node.url} - to={'/documentation' + node.url}> - <h3 className="list-group-item-heading" title={title}> - {title} - </h3> - </Link> - ); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx deleted file mode 100644 index f563d5300aa..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 classNames from 'classnames'; -import * as React from 'react'; -import Link from '../../../components/common/Link'; -import { Dict } from '../../../types/types'; -import { cutWords, DocumentationEntry, highlightMarks } from '../utils'; - -export interface SearchResult { - exactMatch?: boolean; - highlights: Dict<[number, number][]>; - longestTerm: string; - page: DocumentationEntry; - query: string; -} - -interface Props { - active: boolean; - result: SearchResult; -} - -export default function SearchResultEntry({ active, result }: Props) { - return ( - <Link - className={classNames('list-group-item', { active })} - to={'/documentation' + result.page.url}> - <SearchResultTitle result={result} /> - <SearchResultText result={result} /> - </Link> - ); -} - -export function SearchResultTitle({ result }: { result: SearchResult }) { - let titleWithMarks: React.ReactNode; - - const titleHighlights = result.highlights.title; - if (titleHighlights && titleHighlights.length > 0) { - const { title } = result.page; - const tokens = highlightMarks( - title, - titleHighlights.map(h => ({ from: h[0], to: h[0] + h[1] })) - ); - titleWithMarks = <SearchResultTokens tokens={tokens} />; - } else { - titleWithMarks = result.page.title; - } - - return ( - <h3 className="list-group-item-heading" style={{ fontWeight: 'normal' }}> - {titleWithMarks} - </h3> - ); -} - -export function SearchResultText({ result }: { result: SearchResult }) { - const textHighlights = result.highlights.text; - const { text } = result.page; - let tokens: { - text: string; - marked: boolean; - }[] = []; - - if (result.exactMatch) { - const pageText = result.page.text.toLowerCase(); - const highlights: { from: number; to: number }[] = []; - let start = 0; - let index = pageText.indexOf(result.query, start); - let loopCount = 0; - - while (index > -1 && loopCount < 10) { - loopCount++; - highlights.push({ from: index, to: index + result.query.length }); - start = index + 1; - index = pageText.indexOf(result.query, start); - } - - if (highlights.length) { - tokens = highlightMarks(text, highlights); - } - } - - if (tokens.length === 0 && textHighlights && textHighlights.length > 0) { - tokens = highlightMarks( - text, - textHighlights.map(h => ({ from: h[0], to: h[0] + h[1] })) - ); - } - - if (tokens.length) { - return ( - <div className="note"> - <SearchResultTokens tokens={cutWords(tokens)} /> - </div> - ); - } else { - return null; - } -} - -export function SearchResultTokens({ - tokens -}: { - tokens: Array<{ text: string; marked: boolean }>; -}) { - return ( - <> - {tokens.map((token, index) => ( - <React.Fragment key={index}> - {token.marked ? <mark key={index}>{token.text}</mark> : token.text} - </React.Fragment> - ))} - </> - ); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx deleted file mode 100644 index 784909c3e40..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { Dict, DocNavigationItem } from 'Docs/@types/types'; -import { sortBy } from 'lodash'; -import lunr, { LunrBuilder, LunrIndex, LunrToken } from 'lunr'; -import * as React from 'react'; -import { isDefined } from '../../../helpers/types'; -import { getUrlsList } from '../navTreeUtils'; -import { DocumentationEntry } from '../utils'; -import SearchResultEntry from './SearchResultEntry'; - -interface Props { - navigation: DocNavigationItem[]; - pages: DocumentationEntry[]; - query?: string; - splat: string; -} - -export default class SearchResults extends React.PureComponent<Props> { - index: LunrIndex; - - constructor(props: Props) { - super(props); - this.index = lunr(function() { - this.use(tokenContextPlugin); - this.ref('relativeName'); - this.field('title', { boost: 10 }); - this.field('text'); - - this.metadataWhitelist = ['position', 'tokenContext']; - - const urlsList = getUrlsList(props.navigation); - props.pages.filter(page => urlsList.includes(page.url)).forEach(page => this.add(page)); - }); - } - - search(query: string) { - return this.index - .search( - query - .replace(/[\^\-+:~*]/g, '') - .split(/\s+/) - .map(s => `${s}~1 ${s}*`) - .join(' ') - ) - .map(match => { - const page = this.props.pages.find(p => p.relativeName === match.ref); - - // This should never happen, but provide this check for type safety. - if (!page) { - return undefined; - } - - const highlights: Dict<[number, number][]> = {}; - let longestTerm = ''; - let exactMatch = false; - - // Loop over all matching terms/tokens. - Object.keys(match.matchData.metadata).forEach(term => { - // Remember the longest term that matches the query as close as possible. - if (query.includes(term.toLowerCase()) && longestTerm.length < term.length) { - longestTerm = term; - } - - Object.keys(match.matchData.metadata[term]).forEach(fieldName => { - const { position: positions, tokenContext: tokenContexts } = match.matchData.metadata[ - term - ][fieldName]; - - highlights[fieldName] = [...(highlights[fieldName] || []), ...positions]; - - // Check if we have an *exact match*. - if (!exactMatch && tokenContexts) { - tokenContexts.forEach((tokenContext: string) => { - if (!exactMatch && tokenContext.includes(query)) { - exactMatch = true; - } - }); - } - }); - }); - - return { exactMatch, highlights, longestTerm, page, query }; - }) - .filter(isDefined); - } - - render() { - const query = this.props.query?.toLowerCase(); - - if (!query) { - return null; - } - - const results = this.search(query); - - // Re-order results by the length of the longest matched term and by exact - // match (if applicable). The longer the matched term is, the higher the - // chance the result is more relevant. - const sortedResults = sortBy( - // Sort by longest term. - sortBy(results, result => -result.longestTerm.length), - // Sort by exact match. - result => result.exactMatch && -1 - ); - - return ( - <> - {sortedResults.map(result => ( - <SearchResultEntry - active={result.page.relativeName === this.props.splat} - key={result.page.relativeName} - result={result} - /> - ))} - </> - ); - } -} - -// Lunr doesn't support exact multiple-term matching. Meaning "foo bar" will not -// boost a sentence like "Foo bar baz" more than "Baz bar foo". In order to -// provide more accurate results, we store the token context, to see if we can -// perform an "exact match". Unfortunately, we cannot extend the search logic, -// only the tokenizer at *index time*. This is why we store the context as -// meta-data, and post-process the matches before rendering (see above). For -// performance reasons, we only add 2 extra tokens, one in front, one after. -// This means we support "exact macthing" for up to 3 terms. More search terms -// would fallback to the regular matching algorithm, which is OK: the more terms -// searched for, the better the standard algorithm will perform anyway. In the -// end, the best would be for Lunr to support multi-term matching, as extending -// the search algorithm for this would be way too complicated. -export function tokenContextPluginCallback(token: LunrToken, index: number, tokens: LunrToken[]) { - const prevToken = tokens[index - 1] || ''; - const nextToken = tokens[index + 1] || ''; - token.metadata['tokenContext'] = [prevToken.toString(), token.toString(), nextToken.toString()] - .filter(s => s.length) - .join(' ') - .toLowerCase(); - return token; -} - -let tokenContextPluginRegistered = false; - -export function tokenContextPlugin(builder: LunrBuilder) { - if (!tokenContextPluginRegistered) { - (lunr as any).Pipeline.registerFunction(tokenContextPluginCallback, 'tokenContext'); - tokenContextPluginRegistered = true; - } - - builder.pipeline.before((lunr as any).stemmer, tokenContextPluginCallback); - builder.metadataWhitelist.push('tokenContext'); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx b/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx deleted file mode 100644 index b581b04f3e2..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { DocNavigationItem } from 'Docs/@types/types'; -import * as React from 'react'; -import SearchBox from '../../../components/controls/SearchBox'; -import { DocumentationEntry } from '../utils'; -import Menu from './Menu'; -import SearchResults from './SearchResults'; - -interface Props { - navigation: DocNavigationItem[]; - pages: DocumentationEntry[]; - splat: string; -} - -interface State { - query: string; -} - -export default class Sidebar extends React.PureComponent<Props, State> { - state: State = { query: '' }; - - handleSearch = (query: string) => { - this.setState({ query: query.trim() }); - }; - - render() { - return ( - <> - <SearchBox - className="big-spacer-top spacer-bottom" - minLength={2} - onChange={this.handleSearch} - placeholder="Search for pages or keywords" - value={this.state.query} - /> - <div className="documentation-results panel"> - <div className="list-group"> - <SearchResults - navigation={this.props.navigation} - pages={this.props.pages} - query={this.state.query} - splat={this.props.splat} - /> - - {!this.state.query && ( - <Menu - navigation={this.props.navigation} - pages={this.props.pages} - splat={this.props.splat} - /> - )} - </div> - </div> - </> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx deleted file mode 100644 index e19bb2e155e..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { addSideBarClass, removeSideBarClass } from '../../../../helpers/pages'; -import { request } from '../../../../helpers/request'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import { InstalledPlugin } from '../../../../types/plugins'; -import getPages from '../../pages'; -import { App } from '../App'; - -jest.mock('../../../../components/common/ScreenPositionHelper'); - -jest.mock('Docs/../static/SonarQubeNavigationTree.json', () => [ - { - title: 'SonarQube', - children: [ - '/lorem/ipsum/', - '/analysis/languages/csharp/', - { - title: 'Child category', - children: [ - '/lorem/ipsum/dolor', - { - title: 'Grandchild category', - children: ['/lorem/ipsum/sit'] - }, - '/lorem/ipsum/amet' - ] - } - ] - } -]); - -jest.mock('../../../../helpers/pages', () => ({ - addSideBarClass: jest.fn(), - removeSideBarClass: jest.fn() -})); - -jest.mock('../../../../helpers/request', () => { - const { mockDocumentationMarkdown } = jest.requireActual('../../../../helpers/testMocks'); - return { - request: jest.fn(() => ({ - submit: jest.fn().mockResolvedValue({ - status: 200, - text: jest.fn().mockResolvedValue(mockDocumentationMarkdown({ key: 'csharp' })) - }) - })) - }; -}); - -jest.mock('../../pages', () => { - const { mockDocumentationEntry } = jest.requireActual('../../../../helpers/testMocks'); - return jest - .fn() - .mockReturnValue([ - mockDocumentationEntry(), - mockDocumentationEntry({ url: '/analysis/languages/csharp/' }) - ]); -}); - -jest.mock('../../../../api/plugins', () => ({ - getInstalledPlugins: jest.fn().mockResolvedValue([ - { - key: 'csharp', - documentationPath: 'static/documentation.md', - issueTrackerUrl: 'csharp_plugin_issue_tracker_url' - }, - { key: 'vbnet', documentationPath: 'Sstatic/documentation.md' }, - { key: 'vbnett', documentationPath: undefined } - ] as InstalledPlugin[]) -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render correctly for SonarQube', async () => { - const wrapper = shallowRender(); - expect(wrapper.find('DeferredSpinner').exists()).toBe(true); - expect(addSideBarClass).toHaveBeenCalled(); - - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('ScreenPositionHelper').dive()).toMatchSnapshot(); - - wrapper.unmount(); - expect(removeSideBarClass).toHaveBeenCalled(); -}); - -it("should show a 404 if the page doesn't exist", async () => { - const wrapper = shallowRender({ params: { splat: 'unknown' } }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -it('should try to fetch language plugin documentation if documentationPath matches', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - expect(request).toHaveBeenCalledWith('/static/csharp/documentation.md'); - expect(request).not.toHaveBeenCalledWith('/static/vbnet/documentation.md'); - expect(request).not.toHaveBeenCalledWith('/static/vbnett/documentation.md'); - expect(getPages).toHaveBeenCalledWith( - expect.objectContaining({ - 'analysis/languages/csharp': expect.any(Object) - }) - ); -}); - -it('should display the issue tracker url of the plugin if it exists', async () => { - const wrapper = shallowRender({ params: { splat: 'analysis/languages/csharp/' } }); - await waitAndUpdate(wrapper); - - const { content } = (getPages as jest.Mock).mock.calls[0][0]['analysis/languages/csharp']; - - expect(content).toContain('csharp_plugin_issue_tracker_url'); -}); - -function shallowRender(props: Partial<App['props']> = {}) { - return shallow(<App params={{ splat: 'lorem/ipsum' }} location={{ hash: '#foo' }} {...props} />); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx deleted file mode 100644 index f5da4e84c22..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import Menu from '../Menu'; - -function createPage(title: string, relativeName: string, text = '') { - return { relativeName, url: '/' + relativeName, title, navTitle: undefined, text, content: text }; -} - -const pages = [ - createPage( - 'Lorem Ipsum', - 'lorem/index', - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book." - ), - createPage( - 'Where does it come from?', - 'lorem/origin', - 'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.' - ), - createPage( - 'Where does Foobar come from?', - 'foobar', - 'Foobar is a universal variable understood to represent whatever is being discussed.' - ) -]; - -it('should render hierarchical menu', () => { - const wrapper = shallow( - <Menu - navigation={[{ title: 'Block', children: ['/lorem/index', '/lorem/origin'] }, 'foobar']} - pages={pages} - splat="lorem/origin" - /> - ); - - expect(wrapper).toMatchSnapshot(); - wrapper.setProps({ splat: 'baz/bar' }); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuBlock-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuBlock-test.tsx deleted file mode 100644 index c2ba9a70683..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuBlock-test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; -import MenuBlock from '../MenuBlock'; - -it('should render a closed menu block', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render an opened menu block', () => { - expect(shallowRender({ openByDefault: true })).toMatchSnapshot(); -}); - -it('should not render a high depth differently than a depth of 3', () => { - expect( - shallowRender({ block: { title: 'Foo', children: ['/foo'] }, depth: 6 }) - ).toMatchSnapshot(); -}); - -it('can be opened and closed', () => { - const wrapper = shallowRender(); - expect(wrapper.state('open')).toBe(false); - click(wrapper.find('ButtonLink')); - expect(wrapper.state('open')).toBe(true); -}); - -function shallowRender(props: Partial<MenuBlock['props']> = {}) { - return shallow( - <MenuBlock - block={{ - title: 'Foo', - children: [ - '/bar/', - '/baz/', - { - title: 'Baz', - children: ['/baz/foo'] - }, - { - title: 'Bar', - url: 'http://example.com' - } - ] - }} - openByDefault={false} - openChain={[]} - pages={[ - { - content: 'bar', - relativeName: '/bar/', - text: 'bar', - title: 'Bar', - navTitle: undefined, - url: '/bar/' - }, - { - content: 'baz', - relativeName: '/baz/', - text: 'baz', - title: 'baz', - navTitle: 'baznav', - url: '/baz/' - } - ]} - splat="/foo/" - title="Foo" - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuItem-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuItem-test.tsx deleted file mode 100644 index bada3d99f90..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuItem-test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { DocumentationEntry } from '../../utils'; -import { MenuItem } from '../MenuItem'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render correctly if the current node matches the splat', () => { - expect(shallowRender({ splat: 'bar' })).toMatchSnapshot(); -}); - -it('should not render a high depth differently than a depth of 3', () => { - expect(shallowRender({ depth: 6 })).toMatchSnapshot(); -}); - -function shallowRender(props = {}) { - return shallow(<MenuItem node={{ url: '/bar' } as DocumentationEntry} splat="foo" {...props} />); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx deleted file mode 100644 index 34046888c4f..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import SearchResultEntry, { - SearchResult, - SearchResultText, - SearchResultTitle, - SearchResultTokens -} from '../SearchResultEntry'; - -describe('SearchResultEntry', () => { - it('should render', () => { - expect( - shallow(<SearchResultEntry active={true} result={mockSearchResult()} />) - ).toMatchSnapshot(); - }); -}); - -describe('SearchResultText', () => { - it('should render with highlights', () => { - expect( - shallow(<SearchResultText result={mockSearchResult({ highlights: { text: [[12, 9]] } })} />) - ).toMatchSnapshot(); - }); - - it('should correctly extract exact matches', () => { - expect( - shallow( - <SearchResultText - result={mockSearchResult({ exactMatch: true, query: 'variable understood' })} - /> - ) - ).toMatchSnapshot(); - }); - - it('should render without highlights', () => { - expect(shallow(<SearchResultText result={mockSearchResult()} />)).toMatchSnapshot(); - }); -}); - -describe('SearchResultTitle', () => { - it('should render with highlights', () => { - expect( - shallow(<SearchResultTitle result={mockSearchResult({ highlights: { title: [[0, 6]] } })} />) - ).toMatchSnapshot(); - }); - - it('should render not without highlights', () => { - expect(shallow(<SearchResultTitle result={mockSearchResult()} />)).toMatchSnapshot(); - }); -}); - -describe('SearchResultTokens', () => { - it('should render', () => { - expect( - shallow( - <SearchResultTokens - tokens={[ - { marked: false, text: 'Foobar is a ' }, - { marked: true, text: 'universal' }, - { - marked: false, - text: ' variable understood to represent whatever is being discussed.' - } - ]} - /> - ) - ).toMatchSnapshot(); - }); -}); - -function mockSearchResult(overrides: Partial<SearchResult> = {}) { - return { - page: { - content: '', - relativeName: 'foo/bar', - url: '/foo/bar', - text: 'Foobar is a universal variable understood to represent whatever is being discussed.', - title: 'Foobar', - navTitle: undefined - }, - highlights: {}, - longestTerm: '', - query: '', - ...overrides - }; -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx deleted file mode 100644 index 429d6b4ebaf..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResults-test.tsx +++ /dev/null @@ -1,262 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import lunr, { LunrBuilder, LunrToken } from 'lunr'; -import * as React from 'react'; -import { mockDocumentationEntry } from '../../../../helpers/testMocks'; -import { getUrlsList } from '../../navTreeUtils'; -import { DocumentationEntry } from '../../utils'; -import SearchResultEntry from '../SearchResultEntry'; -import SearchResults, { tokenContextPlugin, tokenContextPluginCallback } from '../SearchResults'; - -jest.mock('../../navTreeUtils', () => ({ - getUrlsList: jest.fn().mockReturnValue([]) -})); - -jest.mock('lunr', () => { - const lunr = jest.fn(() => ({ - search: jest.fn(() => [ - { - ref: 'lorem/origin', - matchData: { - metadata: { - simply: { - title: { position: [[19, 5]] }, - text: { - position: [ - [15, 6], - [28, 4] - ], - tokenContext: ['is simply dummy', 'simply dummy text'] - } - } - } - } - }, - { - ref: 'foobar', - matchData: { - metadata: { - simply: { - title: { position: [[23, 4]] }, - text: { - position: [ - [111, 6], - [118, 4] - ], - tokenContext: ['dummy simply text'] - } - } - } - } - } - ]) - })); - - (lunr as any).Pipeline = { - registerFunction: jest.fn() - }; - - return lunr; -}); - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ query: '' }).type()).toBeNull(); -}); - -describe('search engine', () => { - class LunrIndexMock { - plugins: Function[] = []; - fields: Array<{ field: string; args: any }> = []; - docs: DocumentationEntry[] = []; - metadataWhitelist: string[] = []; - - use(fn: Function) { - this.plugins.push(fn); - } - - ref(_ref: string) { - /* noop */ - } - - field(field: string, args = {}) { - this.fields.push({ field, args }); - } - - add(doc: DocumentationEntry) { - this.docs.push(doc); - } - } - - it('should correctly populate the index', () => { - (getUrlsList as jest.Mock).mockReturnValueOnce(['/lorem/index', '/lorem/origin']); - - shallowRender(); - - // Fetch the callback passed to lunr(), which serves as the index constructor. - const indexConstructor: Function = (lunr as jest.Mock).mock.calls[0][0]; - - // Apply it to our mock index. - const lunrMock = new LunrIndexMock(); - indexConstructor.apply(lunrMock); - - expect(lunrMock.docs.length).toBe(2); - expect(lunrMock.plugins).toContain(tokenContextPlugin); - expect(lunrMock.metadataWhitelist).toEqual(['position', 'tokenContext']); - expect(lunrMock.fields).toEqual([ - expect.objectContaining({ field: 'title', args: { boost: 10 } }), - expect.objectContaining({ field: 'text' }) - ]); - }); - - it('should correctly look for an exact match', () => { - // No exact match, should sort as the matches came in. - const wrapper = shallowRender({ query: 'text simply' }); - expect( - wrapper - .find(SearchResultEntry) - .at(0) - .props().result.page.relativeName - ).toBe('lorem/origin'); - - // Exact match, specific page should be at the top. - wrapper.setProps({ query: 'simply text' }); - expect( - wrapper - .find(SearchResultEntry) - .at(0) - .props().result.page.relativeName - ).toBe('foobar'); - }); - - it('should trigger a search if query is set', () => { - const wrapper = shallowRender({ query: undefined }); - expect(wrapper.instance().index.search).not.toHaveBeenCalled(); - wrapper.setProps({ query: 'si:+mply text' }); - expect(wrapper.instance().index.search).toHaveBeenCalledWith('simply~1 simply* text~1 text*'); - }); -}); - -describe('tokenContextPluginCallback', () => { - class LunrTokenMock { - str: string; - metadata: any; - - constructor(str: string) { - this.str = str; - this.metadata = {}; - } - - toString() { - return this.str; - } - } - - class LunrBuilderMock { - pipeline: { before: (stemmer: any, cb: Function) => void }; - metadataWhitelist: string[]; - - constructor() { - this.pipeline = { - before: () => { - /* noop */ - } - }; - this.metadataWhitelist = []; - } - } - - function mockLunrToken(str: string): LunrToken { - return new LunrTokenMock(str); - } - - function mockLunrBuilder(): LunrBuilder { - return new LunrBuilderMock(); - } - - it('should correctly provide token context for text', () => { - const tokens = [ - mockLunrToken('this'), - mockLunrToken('is'), - mockLunrToken('some'), - mockLunrToken('text') - ]; - - expect(tokenContextPluginCallback(mockLunrToken('this'), 0, tokens).metadata).toEqual( - expect.objectContaining({ tokenContext: 'this is' }) - ); - expect(tokenContextPluginCallback(mockLunrToken('is'), 1, tokens).metadata).toEqual( - expect.objectContaining({ tokenContext: 'this is some' }) - ); - expect(tokenContextPluginCallback(mockLunrToken('some'), 2, tokens).metadata).toEqual( - expect.objectContaining({ tokenContext: 'is some text' }) - ); - expect(tokenContextPluginCallback(mockLunrToken('text'), 3, tokens).metadata).toEqual( - expect.objectContaining({ tokenContext: 'some text' }) - ); - }); - - it('should only register the plugin once', () => { - tokenContextPlugin(mockLunrBuilder()); - tokenContextPlugin(mockLunrBuilder()); - expect((lunr as any).Pipeline.registerFunction).toHaveBeenCalledTimes(1); - }); -}); - -function shallowRender(props: Partial<SearchResults['props']> = {}) { - return shallow<SearchResults>( - <SearchResults - navigation={['lorem/index', 'lorem/origin', 'foobar']} - pages={[ - mockDocumentationEntry({ - title: 'Lorem Ipsum', - relativeName: 'lorem/index', - url: '/lorem/index', - content: - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", - text: - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book." - }), - mockDocumentationEntry({ - title: 'Where does it come from?', - relativeName: 'lorem/origin', - url: '/lorem/origin', - content: - 'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.', - text: - 'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.' - }), - mockDocumentationEntry({ - title: 'Where does Foobar come from?', - relativeName: 'foobar', - url: '/foobar', - content: - 'Foobar is a universal variable understood to represent whatever is being discussed. Now we need some keywords: simply text.', - text: - 'Foobar is a universal variable understood to represent whatever is being discussed. Now we need some keywords: simply text.' - }) - ]} - query="what is 42" - splat="foobar" - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx deleted file mode 100644 index 27540d3c1fa..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/Sidebar-test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import Sidebar from '../Sidebar'; - -function createPage(title: string, relativeName: string, text = '') { - return { relativeName, url: '/' + relativeName, title, navTitle: undefined, text, content: text }; -} - -const pages = [ - createPage('Lorem Ipsum', 'lorem/index'), - createPage('Where does Foobar come from?', 'foobar') -]; - -it('should render menu', () => { - expect( - shallow( - <Sidebar - navigation={[{ title: 'Block', children: ['/lorem/index'] }, 'foobar']} - pages={pages} - splat="foobar" - /> - ) - ).toMatchSnapshot(); -}); - -it('should search', () => { - const wrapper = shallow( - <Sidebar - navigation={[{ title: 'Block', children: ['/lorem/index'] }, 'foobar']} - pages={pages} - splat="foobar" - /> - ); - wrapper.find('SearchBox').prop<Function>('onChange')('foo'); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index 690037a28e2..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,151 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly for SonarQube 1`] = ` -<div - className="layout-page" -> - <Helmet - defer={false} - encodeSpecialCharacters={true} - prioritizeSeoTags={false} - title="Lorem | documentation.page_title.sonarqube" - > - <meta - content="noindex nofollow" - name="robots" - /> - </Helmet> - <ScreenPositionHelper - className="layout-page-side-outer" - > - <Component /> - </ScreenPositionHelper> - <div - className="layout-page-main" - > - <div - className="layout-page-main-inner" - > - <div - className="boxed-group" - > - <A11ySkipTarget - anchor="documentation_main" - /> - <DocMarkdownBlock - className="documentation-content cut-margins boxed-group-inner" - content="Lorem ipsum dolor sit amet fredum" - scrollToHref="#foo" - stickyToc={true} - title="Lorem" - /> - </div> - </div> - </div> -</div> -`; - -exports[`should render correctly for SonarQube 2`] = ` -<div - className="layout-page-side" - style={ - Object { - "top": 0, - } - } -> - <div - className="layout-page-side-inner" - > - <div - className="layout-page-filters" - > - <div - className="documentation-page-header" - > - <A11ySkipTarget - anchor="documentation_menu" - label="documentation.skip_to_nav" - weight={10} - /> - <ForwardRef(Link) - to="/documentation/" - > - <h1> - documentation.page - </h1> - </ForwardRef(Link)> - </div> - <Sidebar - navigation={ - Array [ - Object { - "children": Array [ - "/lorem/ipsum/", - "/analysis/languages/csharp/", - Object { - "children": Array [ - "/lorem/ipsum/dolor", - Object { - "children": Array [ - "/lorem/ipsum/sit", - ], - "title": "Grandchild category", - }, - "/lorem/ipsum/amet", - ], - "title": "Child category", - }, - ], - "title": "SonarQube", - }, - ] - } - pages={ - Array [ - Object { - "content": "Lorem ipsum dolor sit amet fredum", - "navTitle": undefined, - "relativeName": "Lorem", - "text": "Lorem ipsum dolor sit amet fredum", - "title": "Lorem", - "url": "/lorem/ipsum", - }, - Object { - "content": "Lorem ipsum dolor sit amet fredum", - "navTitle": undefined, - "relativeName": "Lorem", - "text": "Lorem ipsum dolor sit amet fredum", - "title": "Lorem", - "url": "/analysis/languages/csharp/", - }, - ] - } - splat="lorem/ipsum" - /> - </div> - </div> -</div> -`; - -exports[`should show a 404 if the page doesn't exist 1`] = ` -<Fragment> - <Helmet - defer={true} - encodeSpecialCharacters={true} - prioritizeSeoTags={false} - title="documentation.page_title.sonarqube" - > - <meta - content="noindex nofollow" - name="robots" - /> - </Helmet> - <A11ySkipTarget - anchor="documentation_main" - /> - <NotFound - withContainer={false} - /> -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap deleted file mode 100644 index 7e95338634f..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap +++ /dev/null @@ -1,118 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render hierarchical menu 1`] = ` -<Fragment> - <MenuBlock - block={ - Object { - "children": Array [ - "/lorem/index", - "/lorem/origin", - ], - "title": "Block", - } - } - key="Block" - openByDefault={true} - openChain={ - Array [ - Object { - "children": Array [ - "/lorem/index", - "/lorem/origin", - ], - "title": "Block", - }, - "/lorem/origin", - ] - } - pages={ - Array [ - Object { - "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", - "navTitle": undefined, - "relativeName": "lorem/index", - "text": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", - "title": "Lorem Ipsum", - "url": "/lorem/index", - }, - Object { - "content": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", - "navTitle": undefined, - "relativeName": "lorem/origin", - "text": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", - "title": "Where does it come from?", - "url": "/lorem/origin", - }, - Object { - "content": "Foobar is a universal variable understood to represent whatever is being discussed.", - "navTitle": undefined, - "relativeName": "foobar", - "text": "Foobar is a universal variable understood to represent whatever is being discussed.", - "title": "Where does Foobar come from?", - "url": "/foobar", - }, - ] - } - splat="lorem/origin" - title="Block" - /> - <MenuItem - key="foobar" - splat="lorem/origin" - /> -</Fragment> -`; - -exports[`should render hierarchical menu 2`] = ` -<Fragment> - <MenuBlock - block={ - Object { - "children": Array [ - "/lorem/index", - "/lorem/origin", - ], - "title": "Block", - } - } - key="Block" - openByDefault={false} - openChain={Array []} - pages={ - Array [ - Object { - "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", - "navTitle": undefined, - "relativeName": "lorem/index", - "text": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", - "title": "Lorem Ipsum", - "url": "/lorem/index", - }, - Object { - "content": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", - "navTitle": undefined, - "relativeName": "lorem/origin", - "text": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", - "title": "Where does it come from?", - "url": "/lorem/origin", - }, - Object { - "content": "Foobar is a universal variable understood to represent whatever is being discussed.", - "navTitle": undefined, - "relativeName": "foobar", - "text": "Foobar is a universal variable understood to represent whatever is being discussed.", - "title": "Where does Foobar come from?", - "url": "/foobar", - }, - ] - } - splat="baz/bar" - title="Block" - /> - <MenuItem - key="foobar" - splat="baz/bar" - /> -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuBlock-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuBlock-test.tsx.snap deleted file mode 100644 index 6d5600201c8..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuBlock-test.tsx.snap +++ /dev/null @@ -1,124 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should not render a high depth differently than a depth of 3 1`] = ` -<Fragment> - <ButtonLink - className="list-group-item depth-3" - onClick={[Function]} - > - <h3 - className="list-group-item-heading" - > - <OpenCloseIcon - className="little-spacer-right" - open={false} - /> - Foo - </h3> - </ButtonLink> -</Fragment> -`; - -exports[`should render a closed menu block 1`] = ` -<Fragment> - <ButtonLink - className="list-group-item" - onClick={[Function]} - > - <h3 - className="list-group-item-heading" - > - <OpenCloseIcon - className="little-spacer-right" - open={false} - /> - Foo - </h3> - </ButtonLink> -</Fragment> -`; - -exports[`should render an opened menu block 1`] = ` -<Fragment> - <ButtonLink - className="list-group-item" - onClick={[Function]} - > - <h3 - className="list-group-item-heading" - > - <OpenCloseIcon - className="little-spacer-right" - open={true} - /> - Foo - </h3> - </ButtonLink> - <MenuItem - depth={1} - key="/bar/" - node={ - Object { - "content": "bar", - "navTitle": undefined, - "relativeName": "/bar/", - "text": "bar", - "title": "Bar", - "url": "/bar/", - } - } - splat="/foo/" - /> - <MenuItem - depth={1} - key="/baz/" - node={ - Object { - "content": "baz", - "navTitle": "baznav", - "relativeName": "/baz/", - "text": "baz", - "title": "baz", - "url": "/baz/", - } - } - splat="/foo/" - /> - <MenuBlock - block={ - Object { - "children": Array [ - "/baz/foo", - ], - "title": "Baz", - } - } - depth={1} - key="Baz" - openByDefault={false} - openChain={Array []} - pages={ - Array [ - Object { - "content": "bar", - "navTitle": undefined, - "relativeName": "/bar/", - "text": "bar", - "title": "Bar", - "url": "/bar/", - }, - Object { - "content": "baz", - "navTitle": "baznav", - "relativeName": "/baz/", - "text": "baz", - "title": "baz", - "url": "/baz/", - }, - ] - } - splat="/foo/" - title="Baz" - /> -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap deleted file mode 100644 index 6eb967c4ed3..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should not render a high depth differently than a depth of 3 1`] = ` -<ForwardRef(Link) - className="list-group-item depth-3" - key="/bar" - to="/documentation/bar" -> - <h3 - className="list-group-item-heading" - /> -</ForwardRef(Link)> -`; - -exports[`should render correctly 1`] = ` -<ForwardRef(Link) - className="list-group-item" - key="/bar" - to="/documentation/bar" -> - <h3 - className="list-group-item-heading" - /> -</ForwardRef(Link)> -`; - -exports[`should render correctly if the current node matches the splat 1`] = ` -<ForwardRef(Link) - className="list-group-item active" - key="/bar" - to="/documentation/bar" -> - <h3 - className="list-group-item-heading" - /> -</ForwardRef(Link)> -`; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap deleted file mode 100644 index 9537e85112a..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap +++ /dev/null @@ -1,142 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SearchResultEntry should render 1`] = ` -<ForwardRef(Link) - className="list-group-item active" - to="/documentation/foo/bar" -> - <SearchResultTitle - result={ - Object { - "highlights": Object {}, - "longestTerm": "", - "page": Object { - "content": "", - "navTitle": undefined, - "relativeName": "foo/bar", - "text": "Foobar is a universal variable understood to represent whatever is being discussed.", - "title": "Foobar", - "url": "/foo/bar", - }, - "query": "", - } - } - /> - <SearchResultText - result={ - Object { - "highlights": Object {}, - "longestTerm": "", - "page": Object { - "content": "", - "navTitle": undefined, - "relativeName": "foo/bar", - "text": "Foobar is a universal variable understood to represent whatever is being discussed.", - "title": "Foobar", - "url": "/foo/bar", - }, - "query": "", - } - } - /> -</ForwardRef(Link)> -`; - -exports[`SearchResultText should correctly extract exact matches 1`] = ` -<div - className="note" -> - <SearchResultTokens - tokens={ - Array [ - Object { - "marked": false, - "text": "Foobar is a universal ", - }, - Object { - "marked": true, - "text": "variable understood", - }, - Object { - "marked": false, - "text": " to represent whatever is being discussed.", - }, - ] - } - /> -</div> -`; - -exports[`SearchResultText should render with highlights 1`] = ` -<div - className="note" -> - <SearchResultTokens - tokens={ - Array [ - Object { - "marked": false, - "text": "Foobar is a ", - }, - Object { - "marked": true, - "text": "universal", - }, - Object { - "marked": false, - "text": " variable understood to represent whatever is being discussed.", - }, - ] - } - /> -</div> -`; - -exports[`SearchResultText should render without highlights 1`] = `""`; - -exports[`SearchResultTitle should render not without highlights 1`] = ` -<h3 - className="list-group-item-heading" - style={ - Object { - "fontWeight": "normal", - } - } -> - Foobar -</h3> -`; - -exports[`SearchResultTitle should render with highlights 1`] = ` -<h3 - className="list-group-item-heading" - style={ - Object { - "fontWeight": "normal", - } - } -> - <SearchResultTokens - tokens={ - Array [ - Object { - "marked": true, - "text": "Foobar", - }, - ] - } - /> -</h3> -`; - -exports[`SearchResultTokens should render 1`] = ` -<Fragment> - Foobar is a - <mark - key="1" - > - universal - </mark> - variable understood to represent whatever is being discussed. -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap deleted file mode 100644 index ae487ab0528..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: default 1`] = ` -<Fragment> - <SearchResultEntry - active={false} - key="lorem/origin" - result={ - Object { - "exactMatch": false, - "highlights": Object { - "text": Array [ - Array [ - 15, - 6, - ], - Array [ - 28, - 4, - ], - ], - "title": Array [ - Array [ - 19, - 5, - ], - ], - }, - "longestTerm": "", - "page": Object { - "content": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", - "navTitle": undefined, - "relativeName": "lorem/origin", - "text": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", - "title": "Where does it come from?", - "url": "/lorem/origin", - }, - "query": "what is 42", - } - } - /> - <SearchResultEntry - active={true} - key="foobar" - result={ - Object { - "exactMatch": false, - "highlights": Object { - "text": Array [ - Array [ - 111, - 6, - ], - Array [ - 118, - 4, - ], - ], - "title": Array [ - Array [ - 23, - 4, - ], - ], - }, - "longestTerm": "", - "page": Object { - "content": "Foobar is a universal variable understood to represent whatever is being discussed. Now we need some keywords: simply text.", - "navTitle": undefined, - "relativeName": "foobar", - "text": "Foobar is a universal variable understood to represent whatever is being discussed. Now we need some keywords: simply text.", - "title": "Where does Foobar come from?", - "url": "/foobar", - }, - "query": "what is 42", - } - } - /> -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap deleted file mode 100644 index b83396b9873..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Sidebar-test.tsx.snap +++ /dev/null @@ -1,145 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render menu 1`] = ` -<Fragment> - <SearchBox - className="big-spacer-top spacer-bottom" - minLength={2} - onChange={[Function]} - placeholder="Search for pages or keywords" - value="" - /> - <div - className="documentation-results panel" - > - <div - className="list-group" - > - <SearchResults - navigation={ - Array [ - Object { - "children": Array [ - "/lorem/index", - ], - "title": "Block", - }, - "foobar", - ] - } - pages={ - Array [ - Object { - "content": "", - "navTitle": undefined, - "relativeName": "lorem/index", - "text": "", - "title": "Lorem Ipsum", - "url": "/lorem/index", - }, - Object { - "content": "", - "navTitle": undefined, - "relativeName": "foobar", - "text": "", - "title": "Where does Foobar come from?", - "url": "/foobar", - }, - ] - } - query="" - splat="foobar" - /> - <Menu - navigation={ - Array [ - Object { - "children": Array [ - "/lorem/index", - ], - "title": "Block", - }, - "foobar", - ] - } - pages={ - Array [ - Object { - "content": "", - "navTitle": undefined, - "relativeName": "lorem/index", - "text": "", - "title": "Lorem Ipsum", - "url": "/lorem/index", - }, - Object { - "content": "", - "navTitle": undefined, - "relativeName": "foobar", - "text": "", - "title": "Where does Foobar come from?", - "url": "/foobar", - }, - ] - } - splat="foobar" - /> - </div> - </div> -</Fragment> -`; - -exports[`should search 1`] = ` -<Fragment> - <SearchBox - className="big-spacer-top spacer-bottom" - minLength={2} - onChange={[Function]} - placeholder="Search for pages or keywords" - value="foo" - /> - <div - className="documentation-results panel" - > - <div - className="list-group" - > - <SearchResults - navigation={ - Array [ - Object { - "children": Array [ - "/lorem/index", - ], - "title": "Block", - }, - "foobar", - ] - } - pages={ - Array [ - Object { - "content": "", - "navTitle": undefined, - "relativeName": "lorem/index", - "text": "", - "title": "Lorem Ipsum", - "url": "/lorem/index", - }, - Object { - "content": "", - "navTitle": undefined, - "relativeName": "foobar", - "text": "", - "title": "Where does Foobar come from?", - "url": "/foobar", - }, - ] - } - query="foo" - splat="foobar" - /> - </div> - </div> -</Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js b/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js deleted file mode 100644 index 3092d16f949..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/documentation.directory-loader.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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. - */ -const path = require('path'); - -module.exports = { - placeholder: true // doesn't matter, this is replaced by esbuild -}; diff --git a/server/sonar-web/src/main/js/apps/documentation/navTreeUtils.ts b/server/sonar-web/src/main/js/apps/documentation/navTreeUtils.ts deleted file mode 100644 index 84b95eff83b..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/navTreeUtils.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 NavigationTree from 'Docs/../static/SonarQubeNavigationTree.json'; -import { - DocNavigationItem, - DocsNavigationBlock, - DocsNavigationExternalLink -} from 'Docs/@types/types'; -import { flatten } from 'lodash'; - -export function getNavTree() { - return NavigationTree as DocNavigationItem[]; -} - -export function getUrlsList(navTree: DocNavigationItem[]): string[] { - return flatten( - navTree.map(leaf => { - if (isDocsNavigationBlock(leaf)) { - return getUrlsList(leaf.children); - } - if (isDocsNavigationExternalLink(leaf)) { - return [leaf.url]; - } - return [leaf]; - }) - ); -} - -export function getOpenChainFromPath(pathname: string, navTree: DocNavigationItem[]) { - let chain: DocNavigationItem[] = []; - - let found = false; - const walk = (leaf: DocNavigationItem, parents: DocNavigationItem[] = []) => { - if (found) { - return; - } - - parents = parents.concat(leaf); - - if (isDocsNavigationBlock(leaf)) { - leaf.children.forEach(child => { - if (typeof child === 'string' && testPathAgainstUrl(child, pathname)) { - chain = parents.concat(child); - found = true; - } else { - walk(child, parents); - } - }); - } else if (typeof leaf === 'string' && testPathAgainstUrl(leaf, pathname)) { - chain = parents; - found = true; - } - }; - - navTree.forEach(leaf => walk(leaf)); - - return chain; -} - -export function isDocsNavigationBlock(leaf?: DocNavigationItem): leaf is DocsNavigationBlock { - return typeof leaf === 'object' && (leaf as DocsNavigationBlock).children !== undefined; -} - -export function isDocsNavigationExternalLink( - leaf?: DocNavigationItem -): leaf is DocsNavigationExternalLink { - return typeof leaf === 'object' && (leaf as DocsNavigationExternalLink).url !== undefined; -} - -export function testPathAgainstUrl(path: string, url: string) { - const leadingRegEx = /^\//; - const trailingRegEx = /\/$/; - return ( - path.replace(leadingRegEx, '').replace(trailingRegEx, '') === - url.replace(leadingRegEx, '').replace(trailingRegEx, '') - ); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/pages.ts b/server/sonar-web/src/main/js/apps/documentation/pages.ts deleted file mode 100644 index 11b8dca96a7..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/pages.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import remark from 'remark'; -import visit from 'unist-util-visit'; -import { filterContent, ParsedContent, separateFrontMatter } from '../../helpers/markdown'; -import { Dict } from '../../types/types'; -import Docs from './documentation.directory-loader'; -import { DocumentationEntry, DocumentationEntryScope } from './utils'; - -export default function getPages(parsedOverrides: Dict<ParsedContent> = {}): DocumentationEntry[] { - // Get entries, merge with overrides if applicable. - const pages = ((Docs as unknown) as Array<{ content: string; path: string }>).map(file => { - let parsed = separateFrontMatter(file.content); - - if (parsedOverrides[file.path]) { - const parsedOverride = parsedOverrides[file.path]; - parsed = { - content: parsedOverride.content, - frontmatter: { ...parsed.frontmatter, ...parsedOverride.frontmatter } - }; - delete parsedOverrides[file.path]; - } - - return { parsed, file }; - }); - - // Add new entries. - Object.keys(parsedOverrides).forEach(path => { - const parsed = parsedOverrides[path]; - pages.push({ - parsed, - file: { content: parsed.content, path } - }); - }); - - return pages.map(({ parsed, file }) => { - let content = ''; - let text = ''; - try { - content = filterContent(parsed.content); - text = getText(content); - } catch (e) { - /* eslint-disable-next-line no-console */ - console.error( - `Documentation - an error occured while parsing page "${parsed.frontmatter.url || - file.path}":`, - e - ); - } - - return { - relativeName: file.path, - url: parsed.frontmatter.url || `/${file.path}/`, - title: parsed.frontmatter.title, - navTitle: parsed.frontmatter.nav || undefined, - order: Number(parsed.frontmatter.order || -1), - scope: parsed.frontmatter.scope - ? (parsed.frontmatter.scope.toLowerCase() as DocumentationEntryScope) - : undefined, - text, - content - }; - }); -} - -function getText(content: string) { - const ast = remark().parse(content); - const texts: string[] = []; - visit(ast, node => { - if (node.type === `text` || node.type === `inlineCode`) { - texts.push(node.value); - } - }); - return texts.join(' ').replace(/\s+/g, ' '); -} diff --git a/server/sonar-web/src/main/js/apps/documentation/routes.tsx b/server/sonar-web/src/main/js/apps/documentation/routes.tsx deleted file mode 100644 index c7be80023d0..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/routes.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { Route } from 'react-router-dom'; -import App from './components/App'; - -const routes = () => ( - <Route path="documentation"> - <Route index={true} element={<App />} /> - <Route path="*" element={<App />} /> - </Route> -); - -export default routes; diff --git a/server/sonar-web/src/main/js/apps/documentation/styles.css b/server/sonar-web/src/main/js/apps/documentation/styles.css deleted file mode 100644 index 599e453ead6..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/styles.css +++ /dev/null @@ -1,201 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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. - */ -.documentation-page-header { - margin: 10px 20px; -} - -.documentation-footer div, -.documentation-footer .page-footer-menu { - max-width: 740px; -} - -.documentation-content { - padding: calc(4 * var(--gridSize)) calc(8 * var(--gridSize)); -} - -.documentation-content.markdown { - position: relative; - font-size: 16px; - line-height: 1.7; -} - -.documentation-content .markdown-content { - width: 100%; -} - -.documentation-content.markdown iframe { - width: 100%; - border: 0; - overflow-y: auto; - height: 65vh; -} - -.documentation-content.markdown .documentation-title { - font-size: 24px; - padding-top: var(--gridSize); - margin-bottom: 2em; -} - -.documentation-content.markdown h2 { - font-size: 18px; - font-weight: 800; - margin-top: 3em; -} - -.documentation-content.markdown h3 { - font-size: 16px; - margin-bottom: 0.8em; -} - -.documentation-content.markdown pre { - border: 1px solid #e6e6e6; - border-radius: 3px; - background-color: rgba(0, 0, 0, 0.06); -} - -.documentation-content.markdown .alert, -.documentation-content.markdown p, -.documentation-content.markdown pre, -.documentation-content.markdown table { - margin: 0.8em 0 2em; -} - -.documentation-content.markdown ul { - margin: 0 0 2em; -} - -.documentation-content.markdown ul > ul { - margin: 0; -} - -.documentation-content.markdown p + ul, -.documentation-content.markdown p + ol, -.documentation-content.markdown p + pre { - margin: -1em 0 2em; -} - -.documentation-content.markdown li > p, -.documentation-content.markdown li > p + pre { - margin: 0; -} - -.documentation-content.markdown li > p + ul, -.documentation-content.markdown li > p + ol { - margin: 0; -} - -.documentation-content.markdown img[src$='.svg'] { - vertical-align: text-bottom; -} - -.documentation-content.markdown .alert { - display: block; - padding: var(--gridSize) calc(2 * var(--gridSize)); -} - -.documentation-content.markdown .alert .custom-block-body { - padding-left: 24px; - background-position: left 6px; - background-repeat: no-repeat; -} - -.documentation-content.markdown .alert-success .custom-block-body { - background-image: url(/images/check.svg); -} - -.documentation-content.markdown .alert-info .custom-block-body { - background-image: url(/images/info.svg); -} - -.documentation-content.markdown .alert-warning .custom-block-body { - background-image: url(/images/exclamation.svg); -} - -.documentation-content.markdown .alert-error .custom-block-body, -.documentation-content.markdown .alert-danger .custom-block-body { - background-image: url(/images/cross.svg); -} - -.documentation-content.markdown .collapse-container { - border: 1px solid var(--barBorderColor); - border-radius: 2px; - background-color: var(--barBackgroundColor); - padding: 8px; - margin: 0.8em 0 2em; -} - -.documentation-content.markdown .collapse-container > a:first-child { - display: block; -} - -.documentation-content.markdown .collapse-container > a:first-child:focus { - color: var(--darkBlue); -} - -.documentation-content.markdown .collapse-container *:last-child { - margin-bottom: 0; -} - -.markdown.has-toc { - display: flex; -} - -.markdown.has-toc .markdown-content { - flex-shrink: 1; - overflow: hidden; - text-overflow: ellipsis; - overflow-x: auto; -} - -.markdown-toc { - flex: 0 0 240px; - margin-right: -40px; -} - -.markdown-toc-content { - margin-left: calc(4 * var(--gridSize)); - padding: 0 var(--gridSize); - font-size: var(--baseFontSize); - background: white; - position: sticky; - top: calc(20px + var(--globalNavHeight)); -} - -.markdown-toc-content h4 { - margin: 0 var(--gridSize) var(--gridSize) var(--gridSize); - font-size: var(--mediumFontSize); -} - -.markdown-toc-content a { - display: block; - color: var(--baseFontColor); - padding: calc(var(--gridSize) / 2) var(--gridSize); - border: 1px solid white; - line-height: 1.2; - transition: none; -} - -.markdown-toc a:hover { - border-color: var(--blue); -} - -.markdown-toc a.active { - font-weight: bold; -} diff --git a/server/sonar-web/src/main/js/apps/documentation/utils.ts b/server/sonar-web/src/main/js/apps/documentation/utils.ts deleted file mode 100644 index a6d8e2d9fc8..00000000000 --- a/server/sonar-web/src/main/js/apps/documentation/utils.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { sortBy } from 'lodash'; - -export type DocumentationEntryScope = 'sonarqube' | 'sonarcloud' | 'static'; - -export interface DocumentationEntry { - content: string; - relativeName: string; - navTitle: string | undefined; - text: string; - title: string; - url: string; -} - -export function getNodeFromUrl(pages: DocumentationEntry[], url: string) { - return pages.find(p => p.url === url); -} - -const WORDS = 6; - -function cutLeadingWords(str: string) { - let words = 0; - for (let i = str.length - 1; i >= 0; i--) { - if (/\s/.test(str[i])) { - words++; - } - if (words === WORDS) { - return i > 0 ? `...${str.substring(i + 1)}` : str; - } - } - return str; -} - -function cutTrailingWords(str: string) { - let words = 0; - for (let i = 0; i < str.length; i++) { - if (/\s/.test(str[i])) { - words++; - } - if (words === WORDS) { - return i < str.length - 1 ? `${str.substring(0, i)}...` : str; - } - } - return str; -} - -export function cutWords(tokens: Array<{ text: string; marked: boolean }>) { - const result: Array<{ text: string; marked: boolean }> = []; - let length = 0; - - const highlightPos = tokens.findIndex(token => token.marked); - if (highlightPos > 0) { - const text = cutLeadingWords(tokens[highlightPos - 1].text); - result.push({ text, marked: false }); - length += text.length; - } - - result.push(tokens[highlightPos]); - length += tokens[highlightPos].text.length; - - for (let i = highlightPos + 1; i < tokens.length; i++) { - if (length + tokens[i].text.length > 100) { - const text = cutTrailingWords(tokens[i].text); - result.push({ text, marked: false }); - return result; - } else { - result.push(tokens[i]); - length += tokens[i].text.length; - } - } - - return result; -} - -export function highlightMarks(str: string, marks: Array<{ from: number; to: number }>) { - const sortedMarks = sortBy( - [ - ...marks.map(mark => ({ pos: mark.from, start: true })), - ...marks.map(mark => ({ pos: mark.to, start: false })) - ], - mark => mark.pos, - mark => Number(!mark.start) - ); - - const cuts: Array<{ text: string; marked: boolean }> = []; - let start = 0; - let balance = 0; - - for (const mark of sortedMarks) { - if (mark.start) { - if (balance === 0 && start !== mark.pos) { - cuts.push({ text: str.substring(start, mark.pos), marked: false }); - start = mark.pos; - } - balance++; - } else { - balance--; - if (balance === 0 && start !== mark.pos) { - cuts.push({ text: str.substring(start, mark.pos), marked: true }); - start = mark.pos; - } - } - } - - if (start < str.length - 1) { - cuts.push({ text: str.substr(start), marked: false }); - } - - return cuts; -} diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx index 60d03b5b190..ece024efdae 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -28,7 +28,7 @@ import { getPluginUpdates } from '../../api/plugins'; import { getValue, setSimpleSettingValue } from '../../api/settings'; -import Link from '../../components/common/Link'; +import DocLink from '../../components/common/DocLink'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { Alert } from '../../components/ui/Alert'; @@ -172,11 +172,9 @@ export class App extends React.PureComponent<Props, State> { defaultMessage={translate('marketplace.page.plugins.description2')} values={{ link: ( - <Link - to="/documentation/instance-administration/marketplace/" - target="_blank"> + <DocLink to="/instance-administration/marketplace/"> {translate('marketplace.page.plugins.description2.link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap index dfe9f4507b0..95315b39ff7 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap @@ -43,12 +43,11 @@ exports[`should render correctly: loaded 1`] = ` id="marketplace.page.plugins.description2" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/instance-administration/marketplace/" + "link": <withAppStateContext(DocLink) + to="/instance-administration/marketplace/" > marketplace.page.plugins.description2.link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -141,12 +140,11 @@ exports[`should render correctly: loading 1`] = ` id="marketplace.page.plugins.description2" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/instance-administration/marketplace/" + "link": <withAppStateContext(DocLink) + to="/instance-administration/marketplace/" > marketplace.page.plugins.description2.link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx index ab37b6e8f59..20f2890ec2e 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx @@ -17,11 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import tooltipDCE from 'Docs/tooltips/editions/datacenter.md'; -import tooltipDE from 'Docs/tooltips/editions/developer.md'; -import tooltipEE from 'Docs/tooltips/editions/enterprise.md'; import * as React from 'react'; -import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock'; +import Link from '../../../components/common/Link'; import { getEditionUrl } from '../../../helpers/editions'; import { translate } from '../../../helpers/l10n'; import { Edition, EditionKey } from '../../../types/editions'; @@ -36,16 +33,116 @@ interface Props { export default function EditionBox({ edition, ncloc, serverId, currentEdition }: Props) { return ( <div className="boxed-group boxed-group-inner marketplace-edition"> - {edition.key === 'datacenter' && <DocMarkdownBlock content={tooltipDCE} />} - {edition.key === 'developer' && <DocMarkdownBlock content={tooltipDE} />} - {edition.key === 'enterprise' && <DocMarkdownBlock content={tooltipEE} />} + {edition.key === EditionKey.datacenter && ( + <div className="markdown"> + <div className="markdown-content"> + <div> + <h3 id="data-center-edition"> + <img + alt="SonarQube logo" + className="max-width-100 little-spacer-right" + src="/images/embed-doc/sq-icon.svg" + /> + Data Center Edition + </h3> + <p> + <em>Designed for High Availability and Scalability</em> + </p> + <p>Enterprise Edition functionality plus:</p> + <ul> + <li>Component redundancy</li> + <li>Data resiliency</li> + <li>Horizontal scalability</li> + </ul> + </div> + </div> + </div> + )} + {edition.key === EditionKey.developer && ( + <div className="markdown"> + <div className="markdown-content"> + <div> + <h3 id="developer-edition"> + <img + alt="SonarQube logo" + className="max-width-100 little-spacer-right" + src="/images/embed-doc/sq-icon.svg" + /> + Developer Edition + </h3> + <p> + <em>Built for Developers by Developers</em> + </p> + <p>Community Edition functionality plus:</p> + <ul> + <li> + PR / MR decoration & Quality Gate + <img + alt="GitHub" + className="little-spacer-left max-width-100" + src="/images/alm/github.svg" + /> + <img + alt="GitLab" + className="little-spacer-left max-width-100" + src="/images/alm/gitlab.svg" + /> + <img + alt="Azure DevOps" + className="little-spacer-left max-width-100" + src="/images/alm/azure.svg" + /> + <img + alt="Bitbucket" + className="little-spacer-left max-width-100" + src="/images/alm/bitbucket.svg" + /> + </li> + <li> + Taint analysis / Injection flaw detection for Java, C#, PHP, Python, JS & TS + </li> + <li>Branch analysis</li> + <li>Project aggregation</li> + <li>Additional languages: C, C++, Obj-C, PS/SQL, ABAP, TSQL & Swift</li> + </ul> + </div> + </div> + </div> + )} + {edition.key === EditionKey.enterprise && ( + <div className="markdown"> + <div className="markdown-content"> + <div> + <h3 id="enterprise-edition"> + <img + alt="SonarQube logo" + className="max-width-100 little-spacer-right" + src="/images/embed-doc/sq-icon.svg" + />{' '} + Enterprise Edition + </h3> + <p> + <em>Designed to Meet Enterprise Requirements</em> + </p> + <p>Developer Edition functionality plus:</p> + <ul> + <li>Faster analysis with parallel processing</li> + <li>OWASP/CWE security reports</li> + <li>Portfolio management</li> + <li>Executive reporting</li> + <li>Project transfer</li> + <li>Additional languages: Apex, COBOL, PL/I, RPG & VB6</li> + </ul> + </div> + </div> + </div> + )} <div className="marketplace-edition-action spacer-top"> - <a - href={getEditionUrl(edition, { ncloc, serverId, sourceEdition: currentEdition })} - rel="noopener noreferrer" + <Link + to={getEditionUrl(edition, { ncloc, serverId, sourceEdition: currentEdition })} target="_blank"> {translate('marketplace.request_free_trial')} - </a> + </Link> </div> </div> ); diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap index 51c52c7ea2a..544953e04d3 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap @@ -4,19 +4,80 @@ exports[`should display the edition 1`] = ` <div className="boxed-group boxed-group-inner marketplace-edition" > - <DocMarkdownBlock - content="test-file-stub" - /> + <div + className="markdown" + > + <div + className="markdown-content" + > + <div> + <h3 + id="developer-edition" + > + <img + alt="SonarQube logo" + className="max-width-100 little-spacer-right" + src="/images/embed-doc/sq-icon.svg" + /> + Developer Edition + </h3> + <p> + <em> + Built for Developers by Developers + </em> + </p> + <p> + Community Edition functionality plus: + </p> + <ul> + <li> + PR / MR decoration & Quality Gate + <img + alt="GitHub" + className="little-spacer-left max-width-100" + src="/images/alm/github.svg" + /> + <img + alt="GitLab" + className="little-spacer-left max-width-100" + src="/images/alm/gitlab.svg" + /> + <img + alt="Azure DevOps" + className="little-spacer-left max-width-100" + src="/images/alm/azure.svg" + /> + <img + alt="Bitbucket" + className="little-spacer-left max-width-100" + src="/images/alm/bitbucket.svg" + /> + </li> + <li> + Taint analysis / Injection flaw detection for Java, C#, PHP, Python, JS & TS + </li> + <li> + Branch analysis + </li> + <li> + Project aggregation + </li> + <li> + Additional languages: C, C++, Obj-C, PS/SQL, ABAP, TSQL & Swift + </li> + </ul> + </div> + </div> + </div> <div className="marketplace-edition-action spacer-top" > - <a - href="https://redirect.sonarsource.com/editions/developer.html?ncloc=1000&serverId=serverId&sourceEdition=community" - rel="noopener noreferrer" + <ForwardRef(Link) target="_blank" + to="https://redirect.sonarsource.com/editions/developer.html?ncloc=1000&serverId=serverId&sourceEdition=community" > marketplace.request_free_trial - </a> + </ForwardRef(Link)> </div> </div> `; diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx index b377671d51f..15cdb87860c 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../components/common/DocLink'; import Link from '../../../components/common/Link'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; @@ -94,9 +95,7 @@ export default function MeasuresPanelNoNewCode(props: MeasuresPanelNoNewCodeProp id="overview.measures.empty_link" values={{ learn_more_link: ( - <Link to="/documentation/user-guide/clean-as-you-code/"> - {translate('learn_more')} - </Link> + <DocLink to="/user-guide/clean-as-you-code/">{translate('learn_more')}</DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx index 76e9ea22aa1..49def02a09b 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx @@ -60,11 +60,11 @@ it('should render the default message', () => { id="overview.measures.empty_link" values={ Object { - "learn_more_link": <ForwardRef(Link) - to="/documentation/user-guide/clean-as-you-code/" + "learn_more_link": <withAppStateContext(DocLink) + to="/user-guide/clean-as-you-code/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap index 7929a930024..7bfe4d72fb9 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap @@ -34,11 +34,11 @@ exports[`should render "bad code setting" explanation: no link 1`] = ` id="overview.measures.empty_link" values={ Object { - "learn_more_link": <ForwardRef(Link) - to="/documentation/user-guide/clean-as-you-code/" + "learn_more_link": <withAppStateContext(DocLink) + to="/user-guide/clean-as-you-code/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -81,11 +81,11 @@ exports[`should render "bad code setting" explanation: with link 1`] = ` id="overview.measures.empty_link" values={ Object { - "learn_more_link": <ForwardRef(Link) - to="/documentation/user-guide/clean-as-you-code/" + "learn_more_link": <withAppStateContext(DocLink) + to="/user-guide/clean-as-you-code/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -160,11 +160,11 @@ exports[`should render the default message 6`] = ` id="overview.measures.empty_link" values={ Object { - "learn_more_link": <ForwardRef(Link) - to="/documentation/user-guide/clean-as-you-code/" + "learn_more_link": <withAppStateContext(DocLink) + to="/user-guide/clean-as-you-code/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx index 634306b49fb..b1ca7a8665b 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../components/common/DocLink'; import Link from '../../../components/common/Link'; import { translate } from '../../../helpers/l10n'; @@ -38,9 +39,9 @@ export default function AppHeader(props: AppHeaderProps) { id="project_baseline.page.description" values={{ link: ( - <Link to="/documentation/project-administration/new-code-period/"> + <DocLink to="/project-administration/new-code-period/"> {translate('project_baseline.page.description.link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap index 8ff79641274..b9d9688d93d 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap @@ -17,11 +17,11 @@ exports[`should render correctly: can admin 1`] = ` id="project_baseline.page.description" values={ Object { - "link": <ForwardRef(Link) - to="/documentation/project-administration/new-code-period/" + "link": <withAppStateContext(DocLink) + to="/project-administration/new-code-period/" > project_baseline.page.description.link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -60,11 +60,11 @@ exports[`should render correctly: cannot admin 1`] = ` id="project_baseline.page.description" values={ Object { - "link": <ForwardRef(Link) - to="/documentation/project-administration/new-code-period/" + "link": <withAppStateContext(DocLink) + to="/project-administration/new-code-period/" > project_baseline.page.description.link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index 1df8b307482..4a09610dcbd 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -184,7 +184,7 @@ export class Conditions extends React.PureComponent<Props> { content={translate('quality_gates.conditions.help')} links={[ { - href: '/documentation/user-guide/clean-as-you-code/', + href: '/user-guide/clean-as-you-code/', label: translate('quality_gates.conditions.help.link') } ]} 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 d0a3a4cd550..8704f5bdaad 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 @@ -54,7 +54,7 @@ export default function ListHeader({ canCreate, refreshQualityGates }: Props) { content={translate('quality_gates.help')} links={[ { - href: '/documentation/user-guide/quality-gates/', + href: '/user-guide/quality-gates/', label: translate('learn_more') } ]} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx index 0280b5b11d2..d9811232f4c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { Actions } from '../../../api/quality-profiles'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { Button } from '../../../components/controls/buttons'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { Alert } from '../../../components/ui/Alert'; @@ -106,14 +106,9 @@ export class PageHeader extends React.PureComponent<Props, State> { {translate('quality_profiles.intro1')} <br /> {translate('quality_profiles.intro2')} - <Link - className="spacer-left" - target="_blank" - to={{ - pathname: '/documentation/instance-administration/quality-profiles/' - }}> + <DocLink className="spacer-left" to="/instance-administration/quality-profiles/"> {translate('learn_more')} - </Link> + </DocLink> </div> {this.state.restoreFormOpen && ( diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap index b56ff86f837..3b2de8d69bd 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -15,17 +15,12 @@ exports[`should render correctly 1`] = ` quality_profiles.intro1 <br /> quality_profiles.intro2 - <ForwardRef(Link) + <withAppStateContext(DocLink) className="spacer-left" - target="_blank" - to={ - Object { - "pathname": "/documentation/instance-administration/quality-profiles/", - } - } + to="/instance-administration/quality-profiles/" > learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> </header> `; @@ -63,17 +58,12 @@ exports[`should render correctly 2`] = ` quality_profiles.intro1 <br /> quality_profiles.intro2 - <ForwardRef(Link) + <withAppStateContext(DocLink) className="spacer-left" - target="_blank" - to={ - Object { - "pathname": "/documentation/instance-administration/quality-profiles/", - } - } + to="/instance-administration/quality-profiles/" > learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> </header> `; @@ -117,17 +107,12 @@ exports[`should render correctly 3`] = ` quality_profiles.intro1 <br /> quality_profiles.intro2 - <ForwardRef(Link) + <withAppStateContext(DocLink) className="spacer-left" - target="_blank" - to={ - Object { - "pathname": "/documentation/instance-administration/quality-profiles/", - } - } + to="/instance-administration/quality-profiles/" > learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> </header> `; @@ -165,17 +150,12 @@ exports[`should show a create form 1`] = ` quality_profiles.intro1 <br /> quality_profiles.intro2 - <ForwardRef(Link) + <withAppStateContext(DocLink) className="spacer-left" - target="_blank" - to={ - Object { - "pathname": "/documentation/instance-administration/quality-profiles/", - } - } + to="/instance-administration/quality-profiles/" > learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> <CreateProfileForm languages={ @@ -253,17 +233,12 @@ exports[`should show a restore form 1`] = ` quality_profiles.intro1 <br /> quality_profiles.intro2 - <ForwardRef(Link) + <withAppStateContext(DocLink) className="spacer-left" - target="_blank" - to={ - Object { - "pathname": "/documentation/instance-administration/quality-profiles/", - } - } + to="/instance-administration/quality-profiles/" > learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> <RestoreProfileForm onClose={[Function]} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx index 05096cc74a4..89599d1fe80 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; @@ -57,12 +57,9 @@ export default function EmptyHotspotsPage(props: EmptyHotspotsPageProps) { {translate(`hotspots.${translationRoot}.description`)} </div> {!(filtered || isStaticListOfHotspots) && ( - <Link - className="big-spacer-top" - target="_blank" - to={{ pathname: '/documentation/user-guide/security-hotspots/' }}> + <DocLink className="big-spacer-top" to="/user-guide/security-hotspots/"> {translate('hotspots.learn_more')} - </Link> + </DocLink> )} </div> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap index 440d4a6e8ae..0a01522c9a7 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap @@ -20,17 +20,12 @@ exports[`should render correctly 1`] = ` > hotspots.no_hotspots.description </div> - <ForwardRef(Link) + <withAppStateContext(DocLink) className="big-spacer-top" - target="_blank" - to={ - Object { - "pathname": "/documentation/user-guide/security-hotspots/", - } - } + to="/user-guide/security-hotspots/" > hotspots.learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> `; @@ -54,17 +49,12 @@ exports[`should render correctly: file 1`] = ` > hotspots.no_hotspots_for_file.description </div> - <ForwardRef(Link) + <withAppStateContext(DocLink) className="big-spacer-top" - target="_blank" - to={ - Object { - "pathname": "/documentation/user-guide/security-hotspots/", - } - } + to="/user-guide/security-hotspots/" > hotspots.learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> `; diff --git a/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx b/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx index 0421c0a8528..78b71f912cd 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { translate } from '../../../helpers/l10n'; import { AdditionalCategoryComponentProps } from './AdditionalCategories'; import CategoryDefinitionsList from './CategoryDefinitionsList'; @@ -30,11 +30,9 @@ export function AnalysisScope(props: AdditionalCategoryComponentProps) { <> <p className="spacer-bottom"> {translate('settings.analysis_scope.wildcards.introduction')} - <Link - className="spacer-left" - to="/documentation/project-administration/narrowing-the-focus/"> + <DocLink className="spacer-left" to="/project-administration/narrowing-the-focus/"> {translate('learn_more')} - </Link> + </DocLink> </p> <table className="data spacer-bottom"> diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx index 1fc628761ea..c0cb640c133 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { getNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -160,9 +160,9 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { id="settings.new_code_period.description" values={{ link: ( - <Link to="/documentation/project-administration/new-code-period/"> + <DocLink to="/project-administration/new-code-period/"> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap index 8f3c70f5740..0b50c3ff5b8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap @@ -6,12 +6,12 @@ exports[`should render correctly 1`] = ` className="spacer-bottom" > settings.analysis_scope.wildcards.introduction - <ForwardRef(Link) + <withAppStateContext(DocLink) className="spacer-left" - to="/documentation/project-administration/narrowing-the-focus/" + to="/project-administration/narrowing-the-focus/" > learn_more - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </p> <table className="data spacer-bottom" diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap index 80f68b5aa3e..d51fc151cfd 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-test.tsx.snap @@ -29,11 +29,11 @@ exports[`should render correctly 1`] = ` id="settings.new_code_period.description" values={ Object { - "link": <ForwardRef(Link) - to="/documentation/project-administration/new-code-period/" + "link": <withAppStateContext(DocLink) + to="/project-administration/new-code-period/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx index e13e9a0a946..0924eedf763 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import Link from '../../../../components/common/Link'; +import DocLink from '../../../../components/common/DocLink'; import { Button } from '../../../../components/controls/buttons'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; import Tooltip from '../../../../components/controls/Tooltip'; @@ -230,9 +230,9 @@ export default function AlmBindingDefinitionBox(props: AlmBindingDefinitionBoxPr )} values={{ link: ( - <Link target="_blank" to={ALM_DOCUMENTATION_PATHS[AlmKeys.GitHub]}> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.GitHub]}> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx index be83d609cbe..9ebfd7a8d53 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import Link from '../../../../components/common/Link'; +import DocLink from '../../../../components/common/DocLink'; import { ButtonLink } from '../../../../components/controls/buttons'; import ValidationInput, { ValidationInputErrorPlacement @@ -127,14 +127,9 @@ export function AlmBindingDefinitionFormField<B extends AlmBindingDefinitionBase defaultMessage={translate('settings.almintegration.form.secret.can_encrypt')} values={{ learn_more: ( - <Link - target="_blank" - to={{ - pathname: - '/documentation/instance-administration/security/#settings-encryption' - }}> + <DocLink to="/instance-administration/security/#settings-encryption"> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.tsx index 8cee4e30d84..3b454eb03fe 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AzureForm.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../../components/common/DocLink'; import Link from '../../../../components/common/Link'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; @@ -80,9 +81,9 @@ export default function AzureForm(props: AzureFormProps) { ), permission: <strong>{'Code > Read & Write'}</strong>, doc_link: ( - <Link target="_blank" to={ALM_DOCUMENTATION_PATHS[AlmKeys.Azure]}> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.Azure]}> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx index 075775bc4c0..449ecdf5b17 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../../components/common/DocLink'; import Link from '../../../../components/common/Link'; import { Alert } from '../../../../components/ui/Alert'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; @@ -91,9 +92,9 @@ export default function BitbucketCloudForm(props: BitbucketCloudFormProps) { ), permission: <strong>Pull Requests: Read</strong>, doc_link: ( - <Link target="_blank" to={ALM_DOCUMENTATION_PATHS[AlmKeys.BitbucketCloud]}> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.BitbucketCloud]}> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.tsx index 2920a0d03d6..3b60f43a85c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketServerForm.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../../components/common/DocLink'; import Link from '../../../../components/common/Link'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; @@ -78,9 +79,9 @@ export default function BitbucketServerForm(props: BitbucketServerFormProps) { ), permission: <strong>Read</strong>, doc_link: ( - <Link target="_blank" to={ALM_DOCUMENTATION_PATHS[AlmKeys.BitbucketServer]}> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.BitbucketServer]}> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx index 80709f0a8cb..cda048ea286 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import Link from '../../../../components/common/Link'; +import DocLink from '../../../../components/common/DocLink'; import { Alert } from '../../../../components/ui/Alert'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; @@ -70,9 +70,9 @@ export default function GithubForm(props: GithubFormProps) { id="settings.almintegration.github.info" values={{ link: ( - <Link target="_blank" to={ALM_DOCUMENTATION_PATHS[AlmKeys.GitHub]}> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.GitHub]}> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.tsx index e0e8f4b5b11..58686ea35a8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GitlabForm.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../../components/common/DocLink'; import Link from '../../../../components/common/Link'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; @@ -76,9 +77,9 @@ export default function GitlabForm(props: GitlabFormProps) { permission: <strong>Reporter</strong>, scope: <strong>api</strong>, doc_link: ( - <Link target="_blank" to={ALM_DOCUMENTATION_PATHS[AlmKeys.GitLab]}> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.GitLab]}> {translate('learn_more')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap index 188b67245c7..4afcf1ec133 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap @@ -522,12 +522,11 @@ exports[`should render correctly: success with alert 1`] = ` id="settings.almintegration.github.additional_permission" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/github-integration/" + "link": <withAppStateContext(DocLink) + to="/analysis/github-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -649,12 +648,11 @@ exports[`should render correctly: success with branches disabled 1`] = ` id="settings.almintegration.github.additional_permission" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/github-integration/" + "link": <withAppStateContext(DocLink) + to="/analysis/github-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap index 8928255a0de..f878e875d5e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap @@ -81,16 +81,11 @@ exports[`should render correctly: encryptable 1`] = ` id="settings.almintegration.form.secret.can_encrypt" values={ Object { - "learn_more": <ForwardRef(Link) - target="_blank" - to={ - Object { - "pathname": "/documentation/instance-administration/security/#settings-encryption", - } - } + "learn_more": <withAppStateContext(DocLink) + to="/instance-administration/security/#settings-encryption" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap index fb5e0271cef..e7e4fed3d74 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AzureForm-test.tsx.snap @@ -41,12 +41,11 @@ exports[`should render correctly: create 1`] = ` id="settings.almintegration.form.personal_access_token.azure.help" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/azuredevops-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/azuredevops-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, "pat": <ForwardRef(Link) target="_blank" to="https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate" @@ -113,12 +112,11 @@ exports[`should render correctly: edit 1`] = ` id="settings.almintegration.form.personal_access_token.azure.help" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/azuredevops-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/azuredevops-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, "pat": <ForwardRef(Link) target="_blank" to="https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate" diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap index 62a4d83d38f..23f4867930a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap @@ -45,12 +45,11 @@ exports[`should render correctly: default 1`] = ` id="settings.almintegration.bitbucketcloud.info" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/bitbucket-cloud-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/bitbucket-cloud-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, "oauth": <ForwardRef(Link) target="_blank" to="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/" @@ -131,12 +130,11 @@ exports[`should render correctly: invalid workspace ID 1`] = ` id="settings.almintegration.bitbucketcloud.info" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/bitbucket-cloud-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/bitbucket-cloud-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, "oauth": <ForwardRef(Link) target="_blank" to="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/" diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap index f428bc72620..f580668efb4 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketServerForm-test.tsx.snap @@ -36,12 +36,11 @@ exports[`should render correctly 1`] = ` id="settings.almintegration.form.personal_access_token.bitbucket.help" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/bitbucket-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/bitbucket-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, "pat": <ForwardRef(Link) target="_blank" to="https://confluence.atlassian.com/bitbucketserver0515/personal-access-tokens-961275199.html" diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap index d6125804a6f..71a84ff7946 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap @@ -43,12 +43,11 @@ exports[`should render correctly 1`] = ` id="settings.almintegration.github.info" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/github-integration/" + "link": <withAppStateContext(DocLink) + to="/analysis/github-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -147,12 +146,11 @@ exports[`should render correctly 2`] = ` id="settings.almintegration.github.info" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/github-integration/" + "link": <withAppStateContext(DocLink) + to="/analysis/github-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap index 3b034da6c5a..7af8b68c428 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GitlabForm-test.tsx.snap @@ -34,12 +34,11 @@ exports[`should render correctly 1`] = ` id="settings.almintegration.form.personal_access_token.gitlab.help" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/gitlab-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/gitlab-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, "pat": <ForwardRef(Link) target="_blank" to="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html" @@ -102,12 +101,11 @@ exports[`should render correctly 2`] = ` id="settings.almintegration.form.personal_access_token.gitlab.help" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/gitlab-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/gitlab-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, "pat": <ForwardRef(Link) target="_blank" to="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html" diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx index d8ce34a0b2d..4653c2a69fb 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; -import Link from '../../../../components/common/Link'; +import DocLink from '../../../../components/common/DocLink'; import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper'; import BoxedTabs, { getTabId, getTabPanelId } from '../../../../components/controls/BoxedTabs'; import { Alert } from '../../../../components/ui/Alert'; @@ -142,12 +142,10 @@ export default function Authentication(props: Props) { defaultMessage={translate('settings.authentication.help')} values={{ link: ( - <Link - to={`/documentation/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[currentTab]}/`} - rel="noopener noreferrer" - target="_blank"> + <DocLink + to={`/instance-administration/authentication/${DOCUMENTATION_LINK_SUFFIXES[currentTab]}/`}> {translate('settings.authentication.help.link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx index ad01fa26e18..5544186be4c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx @@ -22,7 +22,7 @@ import { FormattedMessage } from 'react-intl'; import withAvailableFeatures, { WithAvailableFeaturesProps } from '../../../../app/components/available-features/withAvailableFeatures'; -import Link from '../../../../components/common/Link'; +import DocLink from '../../../../components/common/DocLink'; import Toggle from '../../../../components/controls/Toggle'; import { Alert } from '../../../../components/ui/Alert'; import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker'; @@ -286,11 +286,7 @@ export function AlmSpecificForm(props: AlmSpecificFormProps) { renderBooleanField({ help: true, helpParams: { - doc_link: ( - <Link to={ALM_DOCUMENTATION_PATHS[alm]} target="_blank"> - {translate('learn_more')} - </Link> - ) + doc_link: <DocLink to={ALM_DOCUMENTATION_PATHS[alm]}>{translate('learn_more')}</DocLink> }, id: 'monorepo', onFieldChange: props.onFieldChange, diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap index b7413c7b625..26c43916050 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap @@ -802,12 +802,11 @@ exports[`should render the monorepo field when the feature is supported 1`] = ` id="settings.pr_decoration.binding.form.monorepo.help" values={ Object { - "doc_link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/azuredevops-integration/" + "doc_link": <withAppStateContext(DocLink) + to="/analysis/azuredevops-integration/" > learn_more - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.tsx b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.tsx index af24bb91804..5307a9bd840 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionForm.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { encryptValue } from '../../../api/settings'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { SubmitButton } from '../../../components/controls/buttons'; import { ClipboardButton } from '../../../components/controls/clipboard'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -129,9 +129,9 @@ export default class EncryptionForm extends React.PureComponent<Props, State> { id="encryption.form_note" values={{ moreInformationLink: ( - <Link to="/documentation/instance-administration/security/" target="_blank"> + <DocLink to="/instance-administration/security/"> {translate('more_information')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.tsx b/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.tsx index 66c6ecfb3fd..6fc39716548 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/encryption/GenerateSecretKeyForm.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { SubmitButton } from '../../../components/controls/buttons'; import { ClipboardButton } from '../../../components/controls/clipboard'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -112,9 +112,9 @@ export default class GenerateSecretKeyForm extends React.PureComponent<Props, St id="encryption.secret_key_description" values={{ moreInformationLink: ( - <Link to="/documentation/instance-administration/security/" target="_blank"> + <DocLink to="/instance-administration/security/"> {translate('more_information')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap index 4f79df0e425..c8771a86fd7 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/EncryptionForm-test.tsx.snap @@ -50,12 +50,11 @@ exports[`should render correctly 1`] = ` id="encryption.form_note" values={ Object { - "moreInformationLink": <ForwardRef(Link) - target="_blank" - to="/documentation/instance-administration/security/" + "moreInformationLink": <withAppStateContext(DocLink) + to="/instance-administration/security/" > more_information - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap index 6073e39503a..82f4f112d07 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/__snapshots__/GenerateSecretKeyForm-test.tsx.snap @@ -16,12 +16,11 @@ exports[`should render correctly 1`] = ` id="encryption.secret_key_description" values={ Object { - "moreInformationLink": <ForwardRef(Link) - target="_blank" - to="/documentation/instance-administration/security/" + "moreInformationLink": <withAppStateContext(DocLink) + to="/instance-administration/security/" > more_information - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx index f0705457e0b..bffd08731c0 100644 --- a/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { deactivateUser } from '../../../api/users'; +import DocLink from '../../../components/common/DocLink'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import Checkbox from '../../../components/controls/Checkbox'; import Modal from '../../../components/controls/Modal'; @@ -99,12 +100,9 @@ export default class DeactivateForm extends React.PureComponent<Props, State> { id="delete-user-warning" values={{ link: ( - <a - href="/documentation/instance-administration/authentication/overview/" - rel="noopener noreferrer" - target="_blank"> + <DocLink to="/instance-administration/authentication/overview/"> {translate('users.delete_user.help.link')} - </a> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx index e581653e527..19088eb99ea 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import Link from '../../../components/common/Link'; +import DocLink from '../../../components/common/DocLink'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -41,9 +41,9 @@ export default function PageHeader({ children, loading }: Props) { id="webhooks.description" values={{ url: ( - <Link to="/documentation/project-administration/webhooks/"> + <DocLink to="/project-administration/webhooks/"> {translate('webhooks.documentation_link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap index b1cbc315ff3..d6cdf4408c1 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -21,11 +21,11 @@ exports[`should render correctly 1`] = ` id="webhooks.description" values={ Object { - "url": <ForwardRef(Link) - to="/documentation/project-administration/webhooks/" + "url": <withAppStateContext(DocLink) + to="/project-administration/webhooks/" > webhooks.documentation_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/docs/DocImg.tsx b/server/sonar-web/src/main/js/components/common/DocLink.tsx index b5cd154b2bf..53d9c8c4e78 100644 --- a/server/sonar-web/src/main/js/components/docs/DocImg.tsx +++ b/server/sonar-web/src/main/js/components/common/DocLink.tsx @@ -18,21 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { getBaseUrl } from '../../helpers/system'; +import withAppStateContext, { + WithAppStateContextProps +} from '../../app/components/app-state/withAppStateContext'; +import { getUrlForDoc } from '../../helpers/docs'; +import Link, { LinkProps } from './Link'; -export default function DocImg(props: React.ImgHTMLAttributes<HTMLImageElement>) { - const { alt, src, ...other } = props; +type Props = WithAppStateContextProps & + Omit<LinkProps, 'to'> & { to: string; innerRef?: React.Ref<HTMLAnchorElement> }; - if (process.env.NODE_ENV === 'development') { - return <img alt={alt} className="max-width-100" src={getBaseUrl() + src} {...other} />; - } - - return ( - <img - alt={alt} - className="max-width-100" - src={getBaseUrl() + '/images/embed-doc' + src} - {...other} - /> - ); +export function DocLink({ appState, to, innerRef, ...props }: Props) { + const toStatic = getUrlForDoc(appState.version, to); + return <Link ref={innerRef} to={toStatic} target="_blank" {...props} />; } + +export default withAppStateContext(DocLink); diff --git a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx index 953f5fa6b04..c44ca0efd50 100644 --- a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx +++ b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx @@ -19,13 +19,14 @@ */ import * as React from 'react'; import HelpTooltip from '../../components/controls/HelpTooltip'; +import DocLink from './DocLink'; import Link from './Link'; export interface DocumentationTooltipProps { children?: React.ReactNode; className?: string; content?: React.ReactNode; - links?: Array<{ href: string; label: string; inPlace?: boolean }>; + links?: Array<{ href: string; label: string; inPlace?: boolean; doc?: boolean }>; title?: string; } @@ -49,7 +50,7 @@ export default function DocumentationTooltip(props: DocumentationTooltipProps) { <> <hr className="big-spacer-top big-spacer-bottom" /> - {links.map(({ href, label, inPlace }) => ( + {links.map(({ href, label, inPlace, doc = true }) => ( <div className="little-spacer-bottom" key={label} @@ -58,9 +59,13 @@ export default function DocumentationTooltip(props: DocumentationTooltipProps) { // they won't be "clickable"), we hide the whole links section. // See https://sarahmhigley.com/writing/tooltips-in-wcag-21/ aria-hidden={true}> - <Link to={href} target={inPlace ? undefined : '_blank'}> - {label} - </Link> + {doc ? ( + <DocLink to={href}>{label}</DocLink> + ) : ( + <Link to={href} target={inPlace ? undefined : '_blank'}> + {label} + </Link> + )} </div> ))} </> diff --git a/server/sonar-web/src/main/js/components/common/__tests__/DocumentationTooltip-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/DocumentationTooltip-test.tsx index 16cca514122..67821c47ea9 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/DocumentationTooltip-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/DocumentationTooltip-test.tsx @@ -27,8 +27,8 @@ it('renders correctly', () => { shallowRender({ links: [ { href: 'http://link.tosome.place', label: 'external link' }, - { href: '/documentation/guide', label: 'internal link' }, - { href: '/projects', label: 'in place', inPlace: true } + { href: '/guide', label: 'internal link' }, + { href: '/projects', label: 'in place', inPlace: true, doc: false } ] }) ).toMatchSnapshot('with links'); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap index 88d7984d8e3..b1ed2aba980 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DocumentationTooltip-test.tsx.snap @@ -77,23 +77,21 @@ exports[`renders correctly: with links 1`] = ` aria-hidden={true} className="little-spacer-bottom" > - <ForwardRef(Link) - target="_blank" + <withAppStateContext(DocLink) to="http://link.tosome.place" > external link - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> <div aria-hidden={true} className="little-spacer-bottom" > - <ForwardRef(Link) - target="_blank" - to="/documentation/guide" + <withAppStateContext(DocLink) + to="/guide" > internal link - </ForwardRef(Link)> + </withAppStateContext(DocLink)> </div> <div aria-hidden={true} diff --git a/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx deleted file mode 100644 index 4a3eb1d604a..00000000000 --- a/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 OpenCloseIcon from '../../components/icons/OpenCloseIcon'; - -interface State { - open: boolean; -} - -export default class DocCollapsibleBlock extends React.PureComponent<{}, State> { - state = { open: false }; - - handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { - this.setState(state => ({ open: !state.open })); - event.stopPropagation(); - event.preventDefault(); - }; - - renderTitle(children: any) { - return ( - <a - aria-expanded={this.state.open} - aria-haspopup={true} - role="button" - className="link-no-underline" - href="#" - onClick={this.handleClick}> - <OpenCloseIcon className="text-middle little-spacer-right" open={this.state.open} /> - {children.props ? children.props.children : children} - </a> - ); - } - - render() { - const childrenAsArray = React.Children.toArray(this.props.children); - if (childrenAsArray.length < 1) { - return null; - } - - const firstChildChildren = React.Children.toArray( - (childrenAsArray[0] as React.ReactElement<any>).props.children - ); - if (firstChildChildren.length < 2) { - return null; - } - - return ( - <div className="collapse-container"> - {this.renderTitle(firstChildChildren[0])} - {this.state.open && firstChildChildren.slice(1)} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/docs/DocLink.tsx b/server/sonar-web/src/main/js/components/docs/DocLink.tsx deleted file mode 100644 index 78c3228bb92..00000000000 --- a/server/sonar-web/src/main/js/components/docs/DocLink.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 withAppStateContext from '../../app/components/app-state/withAppStateContext'; -import { AppState } from '../../types/appstate'; -import Link from '../common/Link'; - -interface OwnProps { - appState: AppState; - customProps?: { - [k: string]: any; - }; -} - -type Props = OwnProps & React.AnchorHTMLAttributes<HTMLAnchorElement>; - -const SONARQUBE_LINK = '/#sonarqube#/'; -const SONARQUBE_ADMIN_LINK = '/#sonarqube-admin#/'; - -export class DocLink extends React.PureComponent<Props> { - handleClickOnAnchor = (event: React.MouseEvent<HTMLAnchorElement>) => { - const { customProps, href = '#' } = this.props; - if (customProps && customProps.onAnchorClick) { - customProps.onAnchorClick(href, event); - } - }; - - render() { - const { appState, children, href, customProps, ...other } = this.props; - if (href && href.startsWith('#')) { - return ( - <a href="#" onClick={this.handleClickOnAnchor}> - {children} - </a> - ); - } - - if (href && href.startsWith('/')) { - if (href.startsWith(SONARQUBE_LINK)) { - return <SonarQubeLink url={href}>{children}</SonarQubeLink>; - } else if (href.startsWith(SONARQUBE_ADMIN_LINK)) { - return ( - <SonarQubeAdminLink canAdmin={appState.canAdmin} url={href}> - {children} - </SonarQubeAdminLink> - ); - } - const url = '/documentation' + href; - return ( - <Link to={url} {...other}> - {children} - </Link> - ); - } - - return href ? ( - <Link to={href} target="_blank" size={12} {...other}> - {children} - </Link> - ) : null; - } -} - -export default withAppStateContext(DocLink); - -interface SonarQubeLinkProps { - children: React.ReactNode; - url: string; -} - -function SonarQubeLink({ children, url }: SonarQubeLinkProps) { - const to = `/${url.substr(SONARQUBE_LINK.length)}`; - return ( - <Link target="_blank" to={to}> - {children} - </Link> - ); -} - -interface SonarQubeAdminLinkProps { - canAdmin?: boolean; - children: React.ReactNode; - url: string; -} - -function SonarQubeAdminLink({ canAdmin, children, url }: SonarQubeAdminLinkProps) { - if (!canAdmin) { - return <>{children}</>; - } - const to = `/${url.substr(SONARQUBE_ADMIN_LINK.length)}`; - return ( - <Link target="_blank" to={to}> - {children} - </Link> - ); -} diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.css b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.css deleted file mode 100644 index 840ceebe78f..00000000000 --- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.css +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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. - */ -.markdown-content .alert { - margin-bottom: var(--gridSize); - border: 1px solid; - border-radius: 2px; -} - -.markdown-content .alert.is-inline { - display: inline-flex; -} - -.markdown-content .alert:empty { - display: none; -} - -.markdown-content .alert-error, -.markdown-content .alert-danger { - border-color: var(--alertBorderError); - background-color: var(--alertBackgroundError); - color: var(--alertTextError); -} - -.markdown-content .alert-error .alert-icon, -.markdown-content .alert-danger .alert-icon { - border-color: var(--alertBorderError); -} - -.markdown-content .alert-warning { - border-color: var(--alertBorderWarning); - background-color: var(--alertBackgroundWarning); - color: var(--alertTextWarning); -} - -.markdown-content .alert-warning .alert-icon { - border-color: var(--alertBorderWarning); -} - -.markdown-content .alert-info { - border-color: var(--alertBorderInfo); - background-color: var(--alertBackgroundInfo); - color: var(--alertTextInfo); -} - -.markdown-content .alert-info .alert-icon { - border-color: var(--alertBorderInfo); -} - -.markdown-content .alert-success { - border-color: var(--alertBorderSuccess); - background-color: var(--alertBackgroundSuccess); - color: var(--alertTextSuccess); -} - -.markdown-content .alert-success .alert-icon { - border-color: var(--alertBorderSuccess); -} diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx deleted file mode 100644 index 2e9a7770578..00000000000 --- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 classNames from 'classnames'; -import * as React from 'react'; -import rehypeRaw from 'rehype-raw'; -import rehypeReact from 'rehype-react'; -import rehypeSlug from 'rehype-slug'; -import remark from 'remark'; -import remarkCustomBlocks from 'remark-custom-blocks'; -import remarkRehype from 'remark-rehype'; -import { scrollToElement } from '../../helpers/scrolling'; -import { Dict } from '../../types/types'; -import DocCollapsibleBlock from './DocCollapsibleBlock'; -import DocImg from './DocImg'; -import DocLink from './DocLink'; -import './DocMarkdownBlock.css'; -import DocToc from './DocToc'; -import DocTooltipLink from './DocTooltipLink'; - -interface Props { - childProps?: Dict<string>; - className?: string; - content: string; - isTooltip?: boolean; - scrollToHref?: string; - stickyToc?: boolean; - title?: string; -} - -const WAIT_TIMEOUT = 500; - -export default class DocMarkdownBlock extends React.PureComponent<Props> { - node: HTMLElement | null = null; - - componentDidMount() { - const { scrollToHref } = this.props; - if (scrollToHref) { - setTimeout(() => { - this.handleAnchorClick(scrollToHref); - }, WAIT_TIMEOUT); - } - } - - handleAnchorClick = (href: string, event?: React.MouseEvent<HTMLAnchorElement>) => { - if (this.node) { - const element = this.node.querySelector(href); - if (element) { - if (event) { - event.preventDefault(); - } - scrollToElement(element, { bottomOffset: window.innerHeight - 80 }); - - // We cannot use React Router here, because we cannot simply replace a hash. - if (history.pushState) { - history.pushState(null, '', href); - } - } - } - }; - - render() { - const { childProps, content, className, title, stickyToc, isTooltip } = this.props; - - const md = remark(); - - // TODO find a way to replace these custom blocks with real Alert components - md.use(remarkCustomBlocks, { - danger: { classes: 'alert alert-danger' }, - warning: { classes: 'alert alert-warning' }, - info: { classes: 'alert alert-info' }, - success: { classes: 'alert alert-success' }, - collapse: { classes: 'collapse' } - }) - .use(remarkRehype, { allowDangerousHTML: true }) - .use(rehypeSlug) - .use(rehypeRaw) - .use(rehypeReact, { - createElement: React.createElement, - components: { - div: Block, - // use custom link to render documentation anchors - a: isTooltip - ? withChildProps(DocTooltipLink, childProps) - : withChildProps(DocLink, { onAnchorClick: this.handleAnchorClick }), - // use custom img tag to render documentation images - img: DocImg - } - }); - - return ( - <div - className={classNames('markdown', className, { 'has-toc': stickyToc })} - ref={ref => (this.node = ref)}> - <div className="markdown-content"> - {title !== undefined && <h1 className="documentation-title">{title}</h1>} - {md.processSync(content).contents} - </div> - {stickyToc && <DocToc content={content} onAnchorClick={this.handleAnchorClick} />} - </div> - ); - } -} - -function withChildProps<P>( - WrappedComponent: React.ComponentType<P & { customProps?: Dict<any> }>, - childProps?: Dict<any> -) { - return function withChildProps(props: P) { - return <WrappedComponent customProps={childProps} {...props} />; - }; -} - -function Block(props: React.HtmlHTMLAttributes<HTMLDivElement>) { - if (props.className) { - if (props.className.includes('collapse')) { - return <DocCollapsibleBlock>{props.children}</DocCollapsibleBlock>; - } else { - return <div className={classNames('cut-margins', props.className)}>{props.children}</div>; - } - } else { - return props.children; - } -} diff --git a/server/sonar-web/src/main/js/components/docs/DocToc.tsx b/server/sonar-web/src/main/js/components/docs/DocToc.tsx deleted file mode 100644 index 2332fd17337..00000000000 --- a/server/sonar-web/src/main/js/components/docs/DocToc.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 classNames from 'classnames'; -import { debounce, memoize } from 'lodash'; -import * as React from 'react'; -import { findDOMNode } from 'react-dom'; -import remark from 'remark'; -import reactRenderer from 'remark-react'; -import { translate } from '../../helpers/l10n'; -import onlyToc from './plugins/remark-only-toc'; - -interface Props { - content: string; - onAnchorClick: (href: string, event: React.MouseEvent<HTMLAnchorElement>) => void; -} - -interface State { - anchors: AnchorObject[]; - highlightAnchor?: string; -} - -interface AnchorObject { - href: string; - title: string; -} - -export default class DocToc extends React.PureComponent<Props, State> { - debouncedScrollHandler: () => void; - - node: HTMLDivElement | null = null; - - state: State = { anchors: [] }; - - constructor(props: Props) { - super(props); - this.debouncedScrollHandler = debounce(this.scrollHandler); - } - - static getDerivedStateFromProps(props: Props) { - const { content } = props; - return { anchors: DocToc.getAnchors(content) }; - } - - componentDidMount() { - window.addEventListener('scroll', this.debouncedScrollHandler, true); - this.scrollHandler(); - } - - componentWillUnmount() { - window.removeEventListener('scroll', this.debouncedScrollHandler, true); - } - - static getAnchors = memoize((content: string) => { - const file: { contents: JSX.Element } = remark() - .use(reactRenderer) - .use(onlyToc) - .processSync('\n## doctoc\n' + content); - - if (file && file.contents.props.children) { - let list = file.contents; - let limit = 10; - while (limit && list.props.children.length && list.type !== 'ul') { - list = list.props.children[0]; - limit--; - } - - if (list.type === 'ul' && list.props.children.length) { - return list.props.children - .map((li: JSX.Element | string) => { - if (typeof li === 'string') { - return null; - } - - const anchor = li.props.children[0]; - return { - href: anchor.props.href, - title: anchor.props.children[0] - } as AnchorObject; - }) - .filter((item: AnchorObject | null) => item); - } - } - return []; - }); - - scrollHandler = () => { - // eslint-disable-next-line react/no-find-dom-node - const node = findDOMNode(this) as HTMLElement; - - if (!node || !node.parentNode) { - return; - } - - const headings: NodeListOf<HTMLHeadingElement> = node.parentNode.querySelectorAll('h2[id]'); - const scrollTop = window.pageYOffset || document.body.scrollTop; - let highlightAnchor; - - for (let i = 0, len = headings.length; i < len; i++) { - if (headings.item(i).offsetTop > scrollTop + 120) { - break; - } - highlightAnchor = `#${headings.item(i).id}`; - } - - this.setState({ - highlightAnchor - }); - }; - - render() { - const { anchors, highlightAnchor } = this.state; - - if (anchors.length === 0) { - return null; - } - - return ( - <div className="markdown-toc"> - <div className="markdown-toc-content"> - <h4>{translate('documentation.on_this_page')}</h4> - {anchors.map(anchor => { - return ( - <a - className={classNames({ active: highlightAnchor === anchor.href })} - href={anchor.href} - key={anchor.title} - onClick={(event: React.MouseEvent<HTMLAnchorElement>) => { - this.props.onAnchorClick(anchor.href, event); - }}> - {anchor.title} - </a> - ); - })} - </div> - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx deleted file mode 100644 index daf1c8c4662..00000000000 --- a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { forEach } from 'lodash'; -import * as React from 'react'; -import { Dict } from '../../types/types'; -import Link from '../common/Link'; - -interface OwnProps { - customProps?: Dict<string>; -} - -type Props = OwnProps & React.AnchorHTMLAttributes<HTMLAnchorElement>; - -export default function DocTooltipLink({ children, customProps, href, ...other }: Props) { - if (customProps) { - forEach(customProps, (value, key) => { - if (href) { - href = href.replace(`#${key}#`, encodeURIComponent(value)); - } - }); - } - - if (href && href.startsWith('/')) { - href = `/documentation/${href.substr(1)}`; - - return ( - <Link target="_blank" to={href} {...other}> - {children} - </Link> - ); - } - - return href ? ( - <Link size={12} to={href} target="_blank" {...other}> - {children} - </Link> - ) : null; -} diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx deleted file mode 100644 index b0eba13816e..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../helpers/testUtils'; -import DocCollapsibleBlock from '../DocCollapsibleBlock'; - -const children = ( - <div> - <h2>Foo</h2> - <p>Bar</p> - </div> -); - -it('should render a collapsible block', () => { - const wrapper = shallow(<DocCollapsibleBlock>{children}</DocCollapsibleBlock>); - expect(wrapper).toMatchSnapshot(); - - click(wrapper.find('a')); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should not render if not at least 2 children', () => { - const wrapper = shallow( - <DocCollapsibleBlock> - <div>foobar</div> - </DocCollapsibleBlock> - ); - expect(wrapper).toMatchSnapshot(); -}); 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 deleted file mode 100644 index fbc71124d70..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockAppState } from '../../../helpers/testMocks'; -import { DocLink } from '../DocLink'; - -it('should render simple link', () => { - expect( - shallow( - <DocLink appState={mockAppState({ canAdmin: false })} href="http://sample.com"> - link text - </DocLink> - ) - ).toMatchSnapshot(); -}); - -it('should render documentation link', () => { - expect( - shallow( - <DocLink appState={mockAppState({ canAdmin: false })} href="/foo/bar"> - link text - </DocLink> - ) - ).toMatchSnapshot(); -}); - -it('should render sonarqube link on sonarqube', () => { - const wrapper = shallow( - <DocLink appState={mockAppState({ canAdmin: false })} href="/#sonarqube#/foo/bar"> - link text - </DocLink> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('SonarQubeLink').dive()).toMatchSnapshot(); -}); - -it('should render sonarqube admin link on sonarqube for admin', () => { - const wrapper = shallow( - <DocLink appState={mockAppState({ canAdmin: true })} href="/#sonarqube-admin#/foo/bar"> - link text - </DocLink> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); -}); - -it('should not render sonarqube admin link on sonarqube for non-admin', () => { - const wrapper = shallow( - <DocLink appState={mockAppState({ canAdmin: false })} href="/#sonarqube-admin#/foo/bar"> - link text - </DocLink> - ); - expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); -}); - -it('should render documentation anchor', () => { - expect( - shallow( - <DocLink appState={mockAppState({ canAdmin: false })} href="#quality-profiles"> - link text - </DocLink> - ) - ).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 deleted file mode 100644 index 1df772fbe6f..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx +++ /dev/null @@ -1,153 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { scrollToElement } from '../../../helpers/scrolling'; -import { mockEvent } from '../../../helpers/testUtils'; -import DocMarkdownBlock from '../DocMarkdownBlock'; - -const CONTENT = ` -## Lorem ipsum - -Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. - -## Sit amet - -### Maecenas diam - -Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. - -### Integer - -At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. - -## Nam blandit - -Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. -`; - -// mock `remark` & co to work around the issue with cjs imports -jest.mock('remark', () => jest.requireActual('remark')); -jest.mock('remark-rehype', () => jest.requireActual('remark-rehype')); -jest.mock('rehype-raw', () => jest.requireActual('rehype-raw')); -jest.mock('rehype-react', () => jest.requireActual('rehype-react')); -jest.mock('rehype-slug', () => jest.requireActual('rehype-slug')); - -jest.mock('../../../helpers/scrolling', () => ({ - scrollToElement: jest.fn() -})); - -const WINDOW_HEIGHT = 800; -const originalWindowHeight = window.innerHeight; - -const historyPushState = jest.fn(); -const originalHistoryPushState = history.pushState; - -beforeEach(jest.clearAllMocks); - -beforeAll(() => { - Object.defineProperty(window, 'innerHeight', { - writable: true, - configurable: true, - value: WINDOW_HEIGHT - }); - Object.defineProperty(history, 'pushState', { - writable: true, - configurable: true, - value: historyPushState - }); -}); - -afterAll(() => { - Object.defineProperty(window, 'innerHeight', { - writable: true, - configurable: true, - value: originalWindowHeight - }); - Object.defineProperty(history, 'pushState', { - writable: true, - configurable: true, - value: originalHistoryPushState - }); -}); - -it('should render correctly', () => { - expect(shallowRender({ content: 'this is *bold* text' })).toMatchSnapshot('default'); - expect( - shallowRender({ content: 'some [link](/quality-profiles)' }).find('withChildProps') - ).toMatchSnapshot('custom component for links'); - expect( - shallowRender({ - childProps: { foo: 'bar' }, - content: 'some [link](#quality-profiles)', - isTooltip: true - }).find('withChildProps') - ).toMatchSnapshot('custom props for links'); - expect(shallowRender({ content: CONTENT, stickyToc: true })).toMatchSnapshot('sticky TOC'); -}); - -it('should correctly scroll to clicked headings', () => { - const element = {} as Element; - const querySelector: (selector: string) => Element | null = jest.fn((selector: string) => - selector === '#id' ? element : null - ); - const preventDefault = jest.fn(); - const wrapper = shallowRender(); - const instance = wrapper.instance(); - - // Node Ref isn't set yet. - instance.handleAnchorClick('#unknown', mockEvent()); - expect(scrollToElement).not.toHaveBeenCalled(); - - // Set node Ref. - instance.node = { querySelector } as HTMLElement; - - // Unknown element. - instance.handleAnchorClick('#unknown', mockEvent()); - expect(scrollToElement).not.toHaveBeenCalled(); - - // Known element, should scroll. - instance.handleAnchorClick('#id', mockEvent({ preventDefault })); - expect(scrollToElement).toHaveBeenCalledWith(element, { bottomOffset: 720 }); - expect(preventDefault).toHaveBeenCalled(); - expect(historyPushState).toHaveBeenCalledWith(null, '', '#id'); -}); - -it('should correctly scroll to a specific heading if passed as a prop', () => { - jest.useFakeTimers(); - - const element = {} as Element; - const querySelector: (_: string) => Element | null = jest.fn(() => element); - const wrapper = shallowRender({ scrollToHref: '#id' }); - const instance = wrapper.instance(); - instance.node = { querySelector } as HTMLElement; - - expect(scrollToElement).not.toHaveBeenCalled(); - - jest.runAllTimers(); - - expect(scrollToElement).toHaveBeenCalledWith(element, { bottomOffset: 720 }); - jest.runOnlyPendingTimers(); - jest.useRealTimers(); -}); - -function shallowRender(props: Partial<DocMarkdownBlock['props']> = {}) { - return shallow<DocMarkdownBlock>(<DocMarkdownBlock content="" {...props} />); -} diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocToc-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocToc-test.tsx deleted file mode 100644 index 1a837603275..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocToc-test.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { mount } from 'enzyme'; -import * as React from 'react'; -import { click, scrollTo } from '../../../helpers/testUtils'; -import DocToc from '../DocToc'; - -const OFFSET = 300; - -const CONTENT = ` -## Lorem ipsum - -Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. - -## Sit amet - -### Maecenas diam - -Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. - -### Integer - -At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. - -## Nam blandit - -Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. -`; - -jest.mock('remark', () => { - const remark = jest.requireActual('remark'); - return remark; -}); - -jest.mock('remark-react', () => { - const remarkReact = jest.requireActual('remark-react'); - return remarkReact; -}); - -jest.mock('lodash', () => { - const lodash = jest.requireActual('lodash'); - lodash.debounce = (fn: any) => fn; - return lodash; -}); - -jest.mock('react-dom', () => ({ - findDOMNode: jest.fn() -})); - -it('should render correctly', () => { - const wrapper = renderComponent(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should trigger the handler when an anchor is clicked', () => { - const onAnchorClick = jest.fn(); - const wrapper = renderComponent({ onAnchorClick }); - click(wrapper.find('a[href="#sit-amet"]')); - expect(onAnchorClick).toHaveBeenCalled(); -}); - -it('should highlight anchors when scrolling', () => { - mockDomEnv(); - const wrapper = renderComponent(); - - scrollTo({ top: OFFSET }); - expect(wrapper.state('highlightAnchor')).toEqual('#lorem-ipsum'); - - scrollTo({ top: OFFSET * 3 }); - expect(wrapper.state('highlightAnchor')).toEqual('#nam-blandit'); -}); - -function renderComponent(props: Partial<DocToc['props']> = {}) { - return mount(<DocToc content={CONTENT} onAnchorClick={jest.fn()} {...props} />); -} - -function mockDomEnv() { - const findDOMNode = require('react-dom').findDOMNode as jest.Mock<any>; - const parent = document.createElement('div'); - const element = document.createElement('div'); - parent.appendChild(element); - - let offset = OFFSET; - (CONTENT.match(/^## .+$/gm) as Array<string>).forEach(match => { - const slug = match - .replace(/^#+ */, '') - .replace(' ', '-') - .toLowerCase() - .trim(); - const heading = document.createElement('h2'); - heading.id = slug; - Object.defineProperty(heading, 'offsetTop', { value: offset }); - offset += OFFSET; - - parent.appendChild(heading); - }); - - findDOMNode.mockReturnValue(element); -} diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx deleted file mode 100644 index 632d4d31af6..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import DocTooltipLink from '../DocTooltipLink'; - -it('should render simple link', () => { - expect(shallow(<DocTooltipLink href="http://sample.com" />)).toMatchSnapshot(); -}); - -it('should render internal link', () => { - expect(shallow(<DocTooltipLink href="/foo/bar" />)).toMatchSnapshot(); -}); - -it('should render links with custom props', () => { - expect( - shallow(<DocTooltipLink customProps={{ bar: 'baz' }} href="/foo/#bar#" />) - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap deleted file mode 100644 index 7d1afc88f86..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should not render if not at least 2 children 1`] = `""`; - -exports[`should render a collapsible block 1`] = ` -<div - className="collapse-container" -> - <a - aria-expanded={false} - aria-haspopup={true} - className="link-no-underline" - href="#" - onClick={[Function]} - role="button" - > - <OpenCloseIcon - className="text-middle little-spacer-right" - open={false} - /> - Foo - </a> -</div> -`; - -exports[`should render a collapsible block 2`] = ` -<div - className="collapse-container" -> - <a - aria-expanded={true} - aria-haspopup={true} - className="link-no-underline" - href="#" - onClick={[Function]} - role="button" - > - <OpenCloseIcon - className="text-middle little-spacer-right" - open={true} - /> - Foo - </a> - <p - key=".1" - > - Bar - </p> -</div> -`; 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 deleted file mode 100644 index 48b6223cb29..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap +++ /dev/null @@ -1,69 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should not render sonarqube admin link on sonarqube for non-admin 1`] = ` -<Fragment> - link text -</Fragment> -`; - -exports[`should render documentation anchor 1`] = ` -<a - href="#" - onClick={[Function]} -> - link text -</a> -`; - -exports[`should render documentation link 1`] = ` -<ForwardRef(Link) - to="/documentation/foo/bar" -> - link text -</ForwardRef(Link)> -`; - -exports[`should render simple link 1`] = ` -<ForwardRef(Link) - size={12} - target="_blank" - to="http://sample.com" -> - link text -</ForwardRef(Link)> -`; - -exports[`should render sonarqube admin link on sonarqube for admin 1`] = ` -<SonarQubeAdminLink - canAdmin={true} - url="/#sonarqube-admin#/foo/bar" -> - link text -</SonarQubeAdminLink> -`; - -exports[`should render sonarqube admin link on sonarqube for admin 2`] = ` -<ForwardRef(Link) - target="_blank" - to="/foo/bar" -> - link text -</ForwardRef(Link)> -`; - -exports[`should render sonarqube link on sonarqube 1`] = ` -<SonarQubeLink - url="/#sonarqube#/foo/bar" -> - link text -</SonarQubeLink> -`; - -exports[`should render sonarqube link on sonarqube 2`] = ` -<ForwardRef(Link) - target="_blank" - to="/foo/bar" -> - link text -</ForwardRef(Link)> -`; 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 deleted file mode 100644 index e15b023ec86..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap +++ /dev/null @@ -1,148 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: custom component for links 1`] = ` -<withChildProps - href="/quality-profiles" - key="h-2" -> - link -</withChildProps> -`; - -exports[`should render correctly: custom props for links 1`] = ` -<withChildProps - href="#quality-profiles" - key="h-2" -> - link -</withChildProps> -`; - -exports[`should render correctly: default 1`] = ` -<div - className="markdown" -> - <div - className="markdown-content" - > - <div> - <p - key="h-1" - > - this is - <em - key="h-2" - > - bold - </em> - text - </p> - </div> - </div> -</div> -`; - -exports[`should render correctly: sticky TOC 1`] = ` -<div - className="markdown has-toc" -> - <div - className="markdown-content" - > - <div> - <Block - key="h-1" - > - <h2 - id="lorem-ipsum" - key="h-2" - > - Lorem ipsum - </h2> - - - <p - key="h-3" - > - Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. - </p> - - - <h2 - id="sit-amet" - key="h-4" - > - Sit amet - </h2> - - - <h3 - id="maecenas-diam" - key="h-5" - > - Maecenas diam - </h3> - - - <p - key="h-6" - > - Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. - </p> - - - <h3 - id="integer" - key="h-7" - > - Integer - </h3> - - - <p - key="h-8" - > - At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. - </p> - - - <h2 - id="nam-blandit" - key="h-9" - > - Nam blandit - </h2> - - - <p - key="h-10" - > - Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. - </p> - </Block> - </div> - </div> - <DocToc - content=" -## Lorem ipsum - -Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. - -## Sit amet - -### Maecenas diam - -Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. - -### Integer - -At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. - -## Nam blandit - -Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. -" - onAnchorClick={[Function]} - /> -</div> -`; diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocToc-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocToc-test.tsx.snap deleted file mode 100644 index ea33e896b40..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocToc-test.tsx.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<DocToc - content=" -## Lorem ipsum - -Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. - -## Sit amet - -### Maecenas diam - -Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. - -### Integer - -At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. - -## Nam blandit - -Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. -" - onAnchorClick={[MockFunction]} -> - <div - className="markdown-toc" - > - <div - className="markdown-toc-content" - > - <h4> - documentation.on_this_page - </h4> - <a - className="" - href="#lorem-ipsum" - key="Lorem ipsum" - onClick={[Function]} - > - Lorem ipsum - </a> - <a - className="" - href="#sit-amet" - key="Sit amet" - onClick={[Function]} - > - Sit amet - </a> - <a - className="" - href="#nam-blandit" - key="Nam blandit" - onClick={[Function]} - > - Nam blandit - </a> - </div> - </div> -</DocToc> -`; diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap deleted file mode 100644 index 43cac601766..00000000000 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render internal link 1`] = ` -<ForwardRef(Link) - target="_blank" - to="/documentation/foo/bar" -/> -`; - -exports[`should render links with custom props 1`] = ` -<ForwardRef(Link) - target="_blank" - to="/documentation/foo/baz" -/> -`; - -exports[`should render simple link 1`] = ` -<ForwardRef(Link) - size={12} - target="_blank" - to="http://sample.com" -/> -`; diff --git a/server/sonar-web/src/main/js/components/docs/plugins/__tests__/remark-only-toc-test.ts b/server/sonar-web/src/main/js/components/docs/plugins/__tests__/remark-only-toc-test.ts deleted file mode 100644 index e3c6a7cde19..00000000000 --- a/server/sonar-web/src/main/js/components/docs/plugins/__tests__/remark-only-toc-test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 util from 'mdast-util-toc'; -import onlyToc from '../remark-only-toc'; - -jest.mock('mdast-util-toc', () => ({ - __esModule: true, - default: jest.fn().mockReturnValue({}) -})); - -it('should only render toc', () => { - const node = { type: 'test', children: ['a'] }; - onlyToc()(node); - expect(node.children).toHaveLength(0); - - (util as jest.Mock).mockReturnValue({ index: -1 }); - node.children.push('a'); - - onlyToc()(node); - expect(node.children).toHaveLength(0); - - (util as jest.Mock).mockReturnValue({ index: 0 }); - node.children.push('a'); - - onlyToc()(node); - expect(node.children).toHaveLength(0); - - (util as jest.Mock).mockReturnValue({ index: 0, map: 'a' }); - node.children.push('a'); - - onlyToc()(node); - expect(node.children).toHaveLength(1); -}); diff --git a/server/sonar-web/src/main/js/components/docs/plugins/remark-only-toc.ts b/server/sonar-web/src/main/js/components/docs/plugins/remark-only-toc.ts deleted file mode 100644 index 06d95a4ead9..00000000000 --- a/server/sonar-web/src/main/js/components/docs/plugins/remark-only-toc.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 util from 'mdast-util-toc'; -import { Node } from 'unist'; - -/** - * This is a simplified version of the remark-toc plugin: https://github.com/remarkjs/remark-toc - * It *only* renders the TOC, and leaves all the rest out. - */ -export default function onlyToc() { - return transformer; - - function transformer(node: Node) { - const result = util(node, { heading: 'doctoc', maxDepth: 2 }); - - if (result.index === null || result.index === -1 || !result.map) { - node.children = []; - } else { - node.children = [result.map]; - } - } -} diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx index 256af65b302..d1d5c932654 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { translate } from '../../helpers/l10n'; import { getBaseUrl } from '../../helpers/system'; import { SuggestionLink } from '../../types/types'; +import DocLink from '../common/DocLink'; import Link from '../common/Link'; import { DropdownOverlay } from '../controls/Dropdown'; import { SuggestionsContext } from './SuggestionsContext'; @@ -38,7 +39,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { * If we have at least 1 suggestion, it will make the call first, and prevent 'documentation' from * getting the focus. */ - focusFirstItem = (node: HTMLAnchorElement | null) => { + focusFirstItem: React.Ref<HTMLAnchorElement> = (node: HTMLAnchorElement | null) => { if (node && !this.firstItem) { this.firstItem = node; this.firstItem.focus(); @@ -59,16 +60,15 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { } return ( <ul className="menu abs-width-240" role="group"> - {this.renderTitle(translate('embed_docs.suggestion'))} + {this.renderTitle(translate('docs.suggestion'))} {suggestions.map((suggestion, i) => ( <li key={suggestion.link}> - <Link - ref={i === 0 ? this.focusFirstItem : undefined} + <DocLink + innerRef={i === 0 ? this.focusFirstItem : undefined} onClick={this.props.onClose} - target="_blank" to={suggestion.link}> {suggestion.text} - </Link> + </DocLink> </li> ))} </ul> @@ -96,13 +96,9 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { <SuggestionsContext.Consumer>{this.renderSuggestions}</SuggestionsContext.Consumer> <ul className="menu abs-width-240" role="group"> <li> - <Link - ref={this.focusFirstItem} - onClick={this.props.onClose} - target="_blank" - to="/documentation"> - {translate('embed_docs.documentation')} - </Link> + <DocLink innerRef={this.focusFirstItem} onClick={this.props.onClose} to="/"> + {translate('docs.documentation')} + </DocLink> </li> <li> <Link onClick={this.props.onClose} to="/web_api"> @@ -116,17 +112,17 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { className="display-flex-center" to="https://community.sonarsource.com/" target="_blank"> - {translate('embed_docs.get_help')} + {translate('docs.get_help')} </Link> </li> </ul> <ul className="menu abs-width-240" role="group"> - {this.renderTitle(translate('embed_docs.stay_connected'))} + {this.renderTitle(translate('docs.stay_connected'))} <li> {this.renderIconLink( 'https://www.sonarqube.org/whats-new/?referrer=sonarqube', 'embed-doc/sq-icon.svg', - translate('embed_docs.news') + translate('docs.news') )} </li> <li> diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsSuggestions.json b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsSuggestions.json new file mode 100644 index 00000000000..af4a5bd79df --- /dev/null +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsSuggestions.json @@ -0,0 +1,118 @@ +{ + "account": [], + "api_documentation": [], + "background_tasks": [ + { + "link": "/analysis/background-tasks/", + "text": "About Background Tasks" + } + ], + "code": [], + "coding_rules": [ + { + "link": "/instance-administration/quality-profiles/", + "text": "Quality Profiles" + } + ], + "component_measures": [ + { + "link": "/user-guide/clean-as-you-code/", + "text": "Clean as You Code" + }, + { + "link": "/user-guide/metric-definitions/", + "text": "Metric Definitions" + } + ], + "global_permissions": [], + "issues": [], + "marketplace": [], + "overview": [ + { + "link": "/analysis/pull-request/", + "text": "Enable Pull Request Decoration" + }, + { + "link": "/analysis/ci-integration-overview/", + "text": "Set up CI analysis" + }, + { + "link": "/user-guide/clean-as-you-code/", + "text": "Clean as You Code" + }, + { + "link": "/user-guide/connected-mode/", + "text": "SonarLint Connected Mode" + } + ], + "permission_templates": [], + "profiles": [ + { + "link": "/instance-administration/quality-profiles/", + "text": "Quality Profiles" + } + ], + "project_activity": [], + "project_baseline": [ + { + "link": "/project-administration/new-code-period/", + "text": "Defining New Code" + } + ], + "project_quality_gate": [ + { + "link": "/user-guide/clean-as-you-code/", + "text": "Clean as You Code" + } + ], + "project_quality_profiles": [ + { + "link": "/instance-administration/quality-profiles/", + "text": "About Quality Profiles" + } + ], + "projects_management": [], + "projects": [], + "pull_requests": [ + { + "link": "/user-guide/clean-as-you-code/", + "text": "Clean as You Code" + }, + { + "link": "/analysis/pull-request/", + "text": "Analyzing Pull Requests" + }, + { + "link": "/user-guide/connected-mode/", + "text": "SonarLint connected mode" + } + ], + "quality_gates": [ + { + "link": "/user-guide/clean-as-you-code/", + "text": "Clean as You Code" + } + ], + "quality_profiles": [ + { + "link": "/instance-administration/quality-profiles/", + "text": "Quality Profiles" + } + ], + "security_reports": [ + { + "link": "/user-guide/security-reports/", + "text": "About Security Reports" + } + ], + "settings": [], + "system_info": [], + "user_groups": [], + "users": [], + "webhooks": [ + { + "link": "/project-administration/webhooks/", + "text": "About Webhooks" + } + ] +} diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/SuggestionsProvider.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/SuggestionsProvider.tsx index 89481d2ab81..70ec0da9d51 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/SuggestionsProvider.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/SuggestionsProvider.tsx @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import suggestionsJson from 'Docs/EmbedDocsSuggestions.json'; import * as React from 'react'; import { Dict, SuggestionLink } from '../../types/types'; +import suggestionsJson from './EmbedDocsSuggestions.json'; import { SuggestionsContext } from './SuggestionsContext'; type SuggestionsJson = Dict<SuggestionLink[]>; @@ -41,8 +41,6 @@ export default class SuggestionsProvider extends React.Component<{}, State> { } }); - suggestions = suggestions.filter(suggestion => suggestion.scope !== 'sonarcloud'); - this.setState({ suggestions }); }; diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx index 87f7315b3b2..c410eea978d 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx @@ -28,7 +28,7 @@ it('should render with no suggestions', () => { renderEmbedDocsPopup(); expect(screen.getAllByRole('link')).toHaveLength(5); - expect(screen.getByText('embed_docs.documentation')).toHaveFocus(); + expect(screen.getByText('docs.documentation')).toHaveFocus(); }); it('should render with suggestions', () => { diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx index 4cff54080c4..28a914dcee1 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx @@ -22,12 +22,9 @@ import * as React from 'react'; import SuggestionsProvider from '../SuggestionsProvider'; jest.mock( - 'Docs/EmbedDocsSuggestions.json', + '../EmbedDocsSuggestions.json', () => ({ - pageA: [ - { link: '/foo', text: 'Foo' }, - { link: '/bar', text: 'Bar', scope: 'sonarcloud' } - ], + pageA: [{ link: '/foo', text: 'Foo' }], pageB: [{ link: '/qux', text: 'Qux' }] }), { virtual: true } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx index b4dc65e4647..77e20ad10a7 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx @@ -72,7 +72,7 @@ export default function IssueMessageTags(props: IssueMessageTagsProps) { content={translate('rules.status', ruleStatus, 'help')} links={[ { - href: '/documentation/user-guide/rules/', + href: '/user-guide/rules/', label: translateWithParameters('see_x', translate('rules')) } ]}> diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.tsx index 1aa48b199d7..b334f5373cd 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/AlertClassicEditor.tsx @@ -23,7 +23,7 @@ import { Alert } from '../../../../components/ui/Alert'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys } from '../../../../types/alm-settings'; -import Link from '../../../common/Link'; +import DocLink from '../../../common/DocLink'; export default function AlertClassicEditor() { return ( @@ -33,9 +33,9 @@ export default function AlertClassicEditor() { defaultMessage={translate('onboarding.tutorial.with.azure_pipelines.BranchAnalysis.info')} values={{ doc_link: ( - <Link to={ALM_DOCUMENTATION_PATHS[AlmKeys.Azure]} target="_blank"> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.Azure]}> {translate('onboarding.tutorial.with.azure_pipelines.BranchAnalysis.info.doc_link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx index ea1ed9c725f..20852c0405a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/commands/PublishSteps.tsx @@ -27,7 +27,7 @@ import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys } from '../../../../types/alm-settings'; import { Feature } from '../../../../types/features'; -import Link from '../../../common/Link'; +import DocLink from '../../../common/DocLink'; import SentenceWithHighlights from '../../components/SentenceWithHighlights'; export interface PublishStepsProps extends WithAvailableFeaturesProps {} @@ -67,11 +67,11 @@ export function PublishSteps(props: PublishStepsProps) { )} values={{ link: ( - <Link to={ALM_DOCUMENTATION_PATHS[AlmKeys.Azure]} target="_blank"> + <DocLink to={ALM_DOCUMENTATION_PATHS[AlmKeys.Azure]}> {translate( 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection.link' )} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/components/CompilationInfo.tsx b/server/sonar-web/src/main/js/components/tutorials/components/CompilationInfo.tsx index cd7fa1e8db5..0753cec1c97 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/CompilationInfo.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/CompilationInfo.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; +import DocLink from '../../common/DocLink'; export interface CompilationInfoProps { className?: string; @@ -35,12 +36,9 @@ export function CompilationInfo({ className = 'spacer-top spacer-bottom' }: Comp defaultMessage={translate('onboarding.tutorial.cfamilly.compilation_database_info')} values={{ link: ( - <a - href="/documentation/analysis/languages/cfamily/" - rel="noopener noreferrer" - target="_blank"> + <DocLink to="/analysis/languages/cfamily/"> {translate('onboarding.tutorial.cfamilly.compilation_database_info.link')} - </a> + </DocLink> ) }} /> @@ -51,12 +49,9 @@ export function CompilationInfo({ className = 'spacer-top spacer-bottom' }: Comp defaultMessage={translate('onboarding.tutorial.cfamilly.speed_caching')} values={{ link: ( - <a - href="/documentation/analysis/languages/cfamily/#analysis-cache" - rel="noopener noreferrer" - target="_blank"> + <DocLink to="/analysis/languages/cfamily/#analysis-cache"> {translate('onboarding.tutorial.cfamilly.speed_caching.link')} - </a> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/components/ProjectTokenScopeInfo.tsx b/server/sonar-web/src/main/js/components/tutorials/components/ProjectTokenScopeInfo.tsx index 202d297397c..d7cd473c8c8 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/ProjectTokenScopeInfo.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/ProjectTokenScopeInfo.tsx @@ -21,6 +21,7 @@ import classNames from 'classnames'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../helpers/l10n'; +import DocLink from '../../common/DocLink'; import Link from '../../common/Link'; import { Alert } from '../../ui/Alert'; @@ -40,11 +41,7 @@ export default function ProjectTokenScopeInfo({ className }: ProjectTokenScopeIn {translate('onboarding.token.text.user_account')} </Link> ), - doc_link: ( - <Link target="_blank" to="/documentation/user-guide/user-token/"> - {translate('documentation')} - </Link> - ) + doc_link: <DocLink to="/user-guide/user-token/">{translate('documentation')}</DocLink> }} /> </Alert> diff --git a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/CompilationInfo-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/CompilationInfo-test.tsx.snap index 93595e2e15d..10463c95d30 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/CompilationInfo-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/__snapshots__/CompilationInfo-test.tsx.snap @@ -13,13 +13,11 @@ exports[`should render correctly 1`] = ` id="onboarding.tutorial.cfamilly.compilation_database_info" values={ Object { - "link": <a - href="/documentation/analysis/languages/cfamily/" - rel="noopener noreferrer" - target="_blank" + "link": <withAppStateContext(DocLink) + to="/analysis/languages/cfamily/" > onboarding.tutorial.cfamilly.compilation_database_info.link - </a>, + </withAppStateContext(DocLink)>, } } /> @@ -30,13 +28,11 @@ exports[`should render correctly 1`] = ` id="onboarding.tutorial.cfamilly.speed_caching" values={ Object { - "link": <a - href="/documentation/analysis/languages/cfamily/#analysis-cache" - rel="noopener noreferrer" - target="_blank" + "link": <withAppStateContext(DocLink) + to="/analysis/languages/cfamily/#analysis-cache" > onboarding.tutorial.cfamilly.speed_caching.link - </a>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.tsx index f2c4a4d2339..4705527a94f 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/PreRequisitesStep.tsx @@ -25,7 +25,7 @@ import ChevronRightIcon from '../../../components/icons/ChevronRightIcon'; import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; import { AlmKeys } from '../../../types/alm-settings'; -import Link from '../../common/Link'; +import DocLink from '../../common/DocLink'; import SentenceWithHighlights from '../components/SentenceWithHighlights'; import Step from '../components/Step'; @@ -72,9 +72,9 @@ export default function PreRequisitesStep(props: PreRequisitesStepProps) { id="onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide" values={{ link: ( - <Link target="_blank" to="/documentation/analysis/jenkins/"> + <DocLink to="/analysis/jenkins/"> {translate('onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide.link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap index eba200ef8ff..474ed5565f6 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/PreRequisitesStep-test.tsx.snap @@ -46,12 +46,11 @@ exports[`should render correctly: content 1`] = ` id="onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/jenkins/" + "link": <withAppStateContext(DocLink) + to="/analysis/jenkins/" > onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide.link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -106,12 +105,11 @@ exports[`should render correctly: content for branches disabled 1`] = ` id="onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/jenkins/" + "link": <withAppStateContext(DocLink) + to="/analysis/jenkins/" > onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide.link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -169,12 +167,11 @@ exports[`should render correctly: content for branches disabled, gitlab 1`] = ` id="onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/jenkins/" + "link": <withAppStateContext(DocLink) + to="/analysis/jenkins/" > onboarding.tutorial.with.jenkins.prereqs.step_by_step_guide.link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/DoneNextSteps.tsx b/server/sonar-web/src/main/js/components/tutorials/other/DoneNextSteps.tsx index 292bdc4387e..920b2695ee5 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/DoneNextSteps.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/other/DoneNextSteps.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../helpers/l10n'; import { Component } from '../../../types/types'; -import Link from '../../common/Link'; +import DocLink from '../../common/DocLink'; export interface DoneNextStepsProps { component: Component; @@ -51,24 +51,18 @@ export default function DoneNextSteps({ component }: DoneNextStepsProps) { id="onboarding.analysis.auto_refresh_after_analysis.check_these_links" values={{ link_branches: ( - <Link - to="/documentation/branches/overview/" - target="_blank" - rel="noopener noreferrer"> + <DocLink to="/branches/overview/"> {translate( 'onboarding.analysis.auto_refresh_after_analysis.check_these_links.branches' )} - </Link> + </DocLink> ), link_pr_analysis: ( - <Link - to="/documentation/analysis/pull-request/" - target="_blank" - rel="noopener noreferrer"> + <DocLink to="/analysis/pull-request/"> {translate( 'onboarding.analysis.auto_refresh_after_analysis.check_these_links.pr_analysis' )} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/TokenStep.tsx b/server/sonar-web/src/main/js/components/tutorials/other/TokenStep.tsx index d4ae7963b23..f28be6ea39f 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/TokenStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/other/TokenStep.tsx @@ -214,7 +214,7 @@ export default class TokenStep extends React.PureComponent<Props, State> { content={translate('onboarding.token.name.help')} links={[ { - href: '/documentation/user-guide/user-token/', + href: '/user-guide/user-token/', label: translate('learn_more') } ]} @@ -282,7 +282,7 @@ export default class TokenStep extends React.PureComponent<Props, State> { content={translate('onboarding.token.use_existing_token.help')} links={[ { - href: '/documentation/user-guide/user-token/', + href: '/user-guide/user-token/', label: translate('learn_more') } ]} diff --git a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap index c5c553d1340..61cea6b709b 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/DoneNextSteps-test.tsx.snap @@ -25,20 +25,16 @@ exports[`should render correctly: default 1`] = ` id="onboarding.analysis.auto_refresh_after_analysis.check_these_links" values={ Object { - "link_branches": <ForwardRef(Link) - rel="noopener noreferrer" - target="_blank" - to="/documentation/branches/overview/" + "link_branches": <withAppStateContext(DocLink) + to="/branches/overview/" > onboarding.analysis.auto_refresh_after_analysis.check_these_links.branches - </ForwardRef(Link)>, - "link_pr_analysis": <ForwardRef(Link) - rel="noopener noreferrer" - target="_blank" - to="/documentation/analysis/pull-request/" + </withAppStateContext(DocLink)>, + "link_pr_analysis": <withAppStateContext(DocLink) + to="/analysis/pull-request/" > onboarding.analysis.auto_refresh_after_analysis.check_these_links.pr_analysis - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -71,20 +67,16 @@ exports[`should render correctly: project admin 1`] = ` id="onboarding.analysis.auto_refresh_after_analysis.check_these_links" values={ Object { - "link_branches": <ForwardRef(Link) - rel="noopener noreferrer" - target="_blank" - to="/documentation/branches/overview/" + "link_branches": <withAppStateContext(DocLink) + to="/branches/overview/" > onboarding.analysis.auto_refresh_after_analysis.check_these_links.branches - </ForwardRef(Link)>, - "link_pr_analysis": <ForwardRef(Link) - rel="noopener noreferrer" - target="_blank" - to="/documentation/analysis/pull-request/" + </withAppStateContext(DocLink)>, + "link_pr_analysis": <withAppStateContext(DocLink) + to="/analysis/pull-request/" > onboarding.analysis.auto_refresh_after_analysis.check_these_links.pr_analysis - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/TokenStep-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/TokenStep-test.tsx.snap index e0696eebd28..ff1c2346c08 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/TokenStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/__snapshots__/TokenStep-test.tsx.snap @@ -50,7 +50,7 @@ exports[`generates token 1`] = ` links={ Array [ Object { - "href": "/documentation/user-guide/user-token/", + "href": "/user-guide/user-token/", "label": "learn_more", }, ] @@ -210,7 +210,7 @@ exports[`generates token 2`] = ` links={ Array [ Object { - "href": "/documentation/user-guide/user-token/", + "href": "/user-guide/user-token/", "label": "learn_more", }, ] @@ -582,7 +582,7 @@ exports[`revokes token 3`] = ` links={ Array [ Object { - "href": "/documentation/user-guide/user-token/", + "href": "/user-guide/user-token/", "label": "learn_more", }, ] diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/DotNetExecute.tsx b/server/sonar-web/src/main/js/components/tutorials/other/commands/DotNetExecute.tsx index 2a97d7cb8b2..958f2ca6aaa 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/DotNetExecute.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/DotNetExecute.tsx @@ -22,8 +22,8 @@ import { FormattedMessage } from 'react-intl'; import { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; +import DocLink from '../../../common/DocLink'; import InstanceMessage from '../../../common/InstanceMessage'; -import Link from '../../../common/Link'; import DoneNextSteps from '../DoneNextSteps'; export interface DotNetExecuteProps { @@ -50,9 +50,9 @@ export default function DotNetExecute({ commands, component }: DotNetExecuteProp id="onboarding.analysis.docs" values={{ link: ( - <Link to="/documentation/analysis/scan/sonarscanner-for-msbuild/" target="_blank"> + <DocLink to="/analysis/scan/sonarscanner-for-msbuild/"> {translate('onboarding.analysis.msbuild.docs_link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecBuildWrapper.tsx b/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecBuildWrapper.tsx index 8d752b03e12..9bc2bfe920c 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecBuildWrapper.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecBuildWrapper.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../../helpers/l10n'; import CodeSnippet from '../../../common/CodeSnippet'; -import Link from '../../../common/Link'; +import DocLink from '../../../common/DocLink'; import { OSs } from '../../types'; export interface ExecBuildWrapperProps { @@ -56,9 +56,9 @@ export default function ExecBuildWrapper(props: ExecBuildWrapperProps) { id="onboarding.analysis.build_wrapper.docs" values={{ link: ( - <Link to="/documentation/analysis/languages/cfamily/" target="_blank"> + <DocLink to="/analysis/languages/cfamily/"> {translate('onboarding.analysis.build_wrapper.docs_link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecScanner.tsx b/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecScanner.tsx index c1d2c035f18..067af232fd2 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecScanner.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/ExecScanner.tsx @@ -22,8 +22,8 @@ import { FormattedMessage } from 'react-intl'; import { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; +import DocLink from '../../../common/DocLink'; import InstanceMessage from '../../../common/InstanceMessage'; -import Link from '../../../common/Link'; import { OSs } from '../../types'; import { quote } from '../../utils'; import DoneNextSteps from '../DoneNextSteps'; @@ -65,9 +65,9 @@ export default function ExecScanner(props: ExecScannerProps) { id="onboarding.analysis.sq_scanner.docs" values={{ link: ( - <Link to="/documentation/analysis/scan/sonarscanner/" target="_blank"> + <DocLink to="/analysis/scan/sonarscanner/"> {translate('onboarding.analysis.sq_scanner.docs_link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaGradle.tsx b/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaGradle.tsx index afe3ed0030d..85a191cf74a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaGradle.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaGradle.tsx @@ -22,8 +22,8 @@ import { FormattedMessage } from 'react-intl'; import { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; +import DocLink from '../../../common/DocLink'; import InstanceMessage from '../../../common/InstanceMessage'; -import Link from '../../../common/Link'; import DoneNextSteps from '../DoneNextSteps'; export interface JavaGradleProps { @@ -68,9 +68,7 @@ export default function JavaGradle(props: JavaGradleProps) { id="onboarding.analysis.java.gradle.latest_version" values={{ link: ( - <Link to="/documentation/analysis/scan/sonarscanner-for-gradle/" target="_blank"> - {translate('here')} - </Link> + <DocLink to="/analysis/scan/sonarscanner-for-gradle/">{translate('here')}</DocLink> ) }} /> @@ -86,9 +84,9 @@ export default function JavaGradle(props: JavaGradleProps) { id="onboarding.analysis.docs" values={{ link: ( - <Link to="/documentation/analysis/scan/sonarscanner-for-gradle/" target="_blank"> + <DocLink to="/analysis/scan/sonarscanner-for-gradle/"> {translate('onboarding.analysis.java.gradle.docs_link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaMaven.tsx b/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaMaven.tsx index 5a939f2a36c..473f99d17ac 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaMaven.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/JavaMaven.tsx @@ -22,8 +22,8 @@ import { FormattedMessage } from 'react-intl'; import { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import CodeSnippet from '../../../common/CodeSnippet'; +import DocLink from '../../../common/DocLink'; import InstanceMessage from '../../../common/InstanceMessage'; -import Link from '../../../common/Link'; import DoneNextSteps from '../DoneNextSteps'; export interface JavaMavenProps { @@ -54,9 +54,9 @@ export default function JavaMaven(props: JavaMavenProps) { id="onboarding.analysis.docs" values={{ link: ( - <Link to="/documentation/analysis/scan/sonarscanner-for-maven/" target="_blank"> + <DocLink to="/analysis/scan/sonarscanner-for-maven/"> {translate('onboarding.analysis.java.maven.docs_link')} - </Link> + </DocLink> ) }} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap index 8c2722936e3..03cd7a29122 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/DotNetExecute-test.tsx.snap @@ -28,12 +28,11 @@ exports[`should render correctly 1`] = ` id="onboarding.analysis.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner-for-msbuild/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner-for-msbuild/" > onboarding.analysis.msbuild.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap index 4060e8cf706..378ca7b16c0 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecBuildWrapper-test.tsx.snap @@ -23,12 +23,11 @@ exports[`Shoud renders for "linux" correctly 1`] = ` id="onboarding.analysis.build_wrapper.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/languages/cfamily/" + "link": <withAppStateContext(DocLink) + to="/analysis/languages/cfamily/" > onboarding.analysis.build_wrapper.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -59,12 +58,11 @@ exports[`Shoud renders for "mac" correctly 1`] = ` id="onboarding.analysis.build_wrapper.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/languages/cfamily/" + "link": <withAppStateContext(DocLink) + to="/analysis/languages/cfamily/" > onboarding.analysis.build_wrapper.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -95,12 +93,11 @@ exports[`Shoud renders for "win" correctly 1`] = ` id="onboarding.analysis.build_wrapper.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/languages/cfamily/" + "link": <withAppStateContext(DocLink) + to="/analysis/languages/cfamily/" > onboarding.analysis.build_wrapper.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap index b4b780f819f..ef9958ca68a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/ExecScanner-test.tsx.snap @@ -33,12 +33,11 @@ exports[`should render correctly for "linux" 1`] = ` id="onboarding.analysis.sq_scanner.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner/" > onboarding.analysis.sq_scanner.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -103,12 +102,11 @@ exports[`should render correctly for "mac" 1`] = ` id="onboarding.analysis.sq_scanner.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner/" > onboarding.analysis.sq_scanner.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -173,12 +171,11 @@ exports[`should render correctly for "win" 1`] = ` id="onboarding.analysis.sq_scanner.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner/" > onboarding.analysis.sq_scanner.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -243,12 +240,11 @@ exports[`should render correctly for cfamily 1`] = ` id="onboarding.analysis.sq_scanner.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner/" > onboarding.analysis.sq_scanner.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -313,12 +309,11 @@ exports[`should render correctly for remote execution 1`] = ` id="onboarding.analysis.sq_scanner.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner/" > onboarding.analysis.sq_scanner.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap index a980bc7303e..3356e83af9e 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap @@ -28,12 +28,11 @@ exports[`renders correctly 1`] = ` id="onboarding.analysis.java.gradle.latest_version" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner-for-gradle/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner-for-gradle/" > here - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> @@ -62,12 +61,11 @@ exports[`renders correctly 1`] = ` id="onboarding.analysis.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner-for-gradle/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner-for-gradle/" > onboarding.analysis.java.gradle.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap index f91c2cc29cc..c004deea92a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/tutorials/other/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap @@ -32,12 +32,11 @@ exports[`renders correctly 1`] = ` id="onboarding.analysis.docs" values={ Object { - "link": <ForwardRef(Link) - target="_blank" - to="/documentation/analysis/scan/sonarscanner-for-maven/" + "link": <withAppStateContext(DocLink) + to="/analysis/scan/sonarscanner-for-maven/" > onboarding.analysis.java.maven.docs_link - </ForwardRef(Link)>, + </withAppStateContext(DocLink)>, } } /> diff --git a/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/markdown-test.ts.snap b/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/markdown-test.ts.snap deleted file mode 100644 index cdc1f9a74e0..00000000000 --- a/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/markdown-test.ts.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should cut sonarqube/sonarcloud/static content 1`] = ` -" -This text has inline text for SonarQube. Donec sed nulla magna. - -This is text for SonarQube, multi-line. Consectetur adipiscing elit. Duis dignissim nulla at massa iaculis interdum. -Aenean sit amet lacus a tortor ullamcorper interdum. Donec sed nulla magna. - - - - - -This is text for SonarQube, single line. - -* In hac habitasse -* Duis sagittis semper sapien nec tempor -* This is a bullet point for SonarQube - -* Platea dictumst - -Duis sagittis semper sapien nec tempor. Nullam vehicula nisi vitae nisi interdum aliquam. - -| Parameter Name | Description | -| --------------------- | ------------------ | -| sonar.pullrequest.github.repository | SLUG of the GitHub Repo | -| sonar.pullrequest.github.endpoint | The API url for your GitHub instance. | -" -`; - -exports[`should not break when conditional tags are misused 1`] = ` -"Random SC text - Break - Bad SQ conditional formatting - Break - SC text - Break - Bad SQ conditional formatting - Break - Static stuff" -`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/markdown-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/markdown-test.ts deleted file mode 100644 index 5c74c9f20ab..00000000000 --- a/server/sonar-web/src/main/js/helpers/__tests__/markdown-test.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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. - */ -/* eslint-disable no-console */ -import { filterContent, getFrontMatter, separateFrontMatter } from '../markdown'; - -jest.mock('../system', () => ({ - getInstance: () => 'SonarQube' -})); - -it('returns parsed frontmatter of one item', () => { - expect( - getFrontMatter(` - --- - title: Foo - --- - - some content here - `) - ).toEqual({ title: 'Foo' }); -}); - -it('returns parsed frontmatter of two items', () => { - expect( - getFrontMatter(` - --- - title: Foo - scope: sonarcloud - --- - - some content here - `) - ).toEqual({ title: 'Foo', scope: 'sonarcloud' }); -}); - -it('returns empty object when frontmatter is missing', () => { - expect( - getFrontMatter(` - some content here - `) - ).toEqual({}); -}); - -it('returns empty object when frontmatter is unfinished', () => { - expect( - getFrontMatter(` - --- - title: Foo - - some content here - `) - ).toEqual({}); -}); - -it('ignores frontmatter in wrong format', () => { - expect( - getFrontMatter(` - --- - title: Foo - scope: sonarcloud: sonarqube - --- - - some content here - `) - ).toEqual({ title: 'Foo' }); -}); - -it('returns parsed frontmatter and the rest of the content', () => { - expect( - separateFrontMatter(` ---- -title: Foo ---- - -some content here`) - ).toEqual({ content: '\nsome content here', frontmatter: { title: 'Foo' } }); -}); - -it('returns empty object and content when frontmatter is missing', () => { - expect(separateFrontMatter('some content here')).toEqual({ - content: 'some content here', - frontmatter: {} - }); -}); - -it('returns full content when frontmatter has bad formatting', () => { - const content = ` - ---- - title: Foo - scope: sonarcloud - --- - - some content here`; - - expect(separateFrontMatter(content)).toEqual({ content, frontmatter: {} }); -}); - -it('replaces {instance}', () => { - expect( - filterContent('This is {instance} content. It replaces all {instance}{instance} messages') - ).toBe('This is SonarQube content. It replaces all SonarQubeSonarQube messages'); -}); - -it('should cut sonarqube/sonarcloud/static content', () => { - const content = ` -This text has inline text for <!-- sonarqube -->SonarQube<!-- /sonarqube --><!-- sonarcloud -->SonarCloud<!-- /sonarcloud -->. Donec sed nulla magna. - -<!-- sonarqube --> -This is text for SonarQube, multi-line. Consectetur adipiscing elit. Duis dignissim nulla at massa iaculis interdum. -Aenean sit amet lacus a tortor ullamcorper interdum. Donec sed nulla magna. -<!-- /sonarqube --> - -<!-- sonarcloud --> -This is text for SonarCloud, multi-line. In hac habitasse platea dictumst. Duis sagittis semper sapien nec tempor. Nullam vehicula nisi vitae nisi interdum aliquam. Mauris volutpat nunc non fermentum rhoncus. Aenean laoreet, orci vitae tempor bibendum, -metus nisl euismod neque, vitae euismod nibh nisl eu velit. Vivamus luctus suscipit elit vel semper. -<!-- /sonarcloud --> - -<!-- static --> -This is static text. -<!-- /static --> - -<!-- sonarqube --> -This is text for SonarQube, single line. -<!-- /sonarqube --> - -* In hac habitasse -* Duis sagittis semper sapien nec tempor -<!-- sonarqube -->* This is a bullet point for SonarQube<!-- /sonarqube --> -<!-- sonarcloud -->* This is a bullet point for SonarCloud<!-- /sonarcloud --> -* Platea dictumst - -Duis sagittis semper sapien nec tempor. Nullam vehicula nisi vitae nisi interdum aliquam. - -| Parameter Name | Description | -| --------------------- | ------------------ | -| sonar.pullrequest.github.repository | SLUG of the GitHub Repo | -<!-- sonarqube --> -| sonar.pullrequest.github.endpoint | The API url for your GitHub instance. | -<!-- /sonarqube --> -`; - - expect(filterContent(content)).toMatchSnapshot(); -}); - -it('should not break when conditional tags are misused', () => { - const originalConsoleError = console.error; - console.error = jest.fn(); - - const content = `Random <!-- /sonarqube -->SC <!-- sonarqube -->text - Break - Bad <!-- /sonarcloud -->SQ conditional <!-- sonarcloud -->formatting - Break - <!-- sonarqube -->SC<!-- /sonarqube --><!-- sonarcloud -->SQ<!-- /sonarcloud --> text - Break - Bad <!-- /sonarcloud -->SQ conditional <!-- sonarcloud -->formatting - Break - <!-- static -->Static <!-- /sonarcloud -->stuff`; - expect(filterContent(content)).toMatchSnapshot(); - expect(console.error).toHaveBeenCalledTimes(2); - - console.error = originalConsoleError; -}); diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index 9da9a03c8e9..0f73991916f 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -57,11 +57,11 @@ export const RATING_COLORS = [ export const PROJECT_KEY_MAX_LEN = 400; export const ALM_DOCUMENTATION_PATHS = { - [AlmKeys.Azure]: '/documentation/analysis/azuredevops-integration/', - [AlmKeys.BitbucketServer]: '/documentation/analysis/bitbucket-integration/', - [AlmKeys.BitbucketCloud]: '/documentation/analysis/bitbucket-cloud-integration/', - [AlmKeys.GitHub]: '/documentation/analysis/github-integration/', - [AlmKeys.GitLab]: '/documentation/analysis/gitlab-integration/' + [AlmKeys.Azure]: '/analysis/azuredevops-integration/', + [AlmKeys.BitbucketServer]: '/analysis/bitbucket-integration/', + [AlmKeys.BitbucketCloud]: '/analysis/bitbucket-cloud-integration/', + [AlmKeys.GitHub]: '/analysis/github-integration/', + [AlmKeys.GitLab]: '/analysis/gitlab-integration/' }; export const IMPORT_COMPATIBLE_ALMS = [ diff --git a/server/sonar-web/src/main/js/helpers/markdown.d.ts b/server/sonar-web/src/main/js/helpers/docs.ts index 629a797df71..6bae9f6f56b 100644 --- a/server/sonar-web/src/main/js/helpers/markdown.d.ts +++ b/server/sonar-web/src/main/js/helpers/docs.ts @@ -17,18 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -interface FrontMatter { - [x: string]: string; -} - -interface ParsedContent { - content: string; - frontmatter: FrontMatter; -} -export function getFrontMatter(content: string): FrontMatter; +const VERSION_PARSER = /^(\d+\.\d+).*$/; -export function separateFrontMatter(content: string): ParsedContent; +export function getUrlForDoc(version: string, to: string) { + const versionPrefix = VERSION_PARSER.exec(version); + const isSnapshot = version.indexOf('SNAPSHOT') !== -1; + const docPrefix = + versionPrefix && versionPrefix.length === 2 && !isSnapshot ? versionPrefix[1] : 'latest'; -/** Removes SonarQube/SonarCloud only content */ -export function filterContent(content: string): string; + return `https://docs.sonarqube.org/${docPrefix}${to}`; +} diff --git a/server/sonar-web/src/main/js/helpers/markdown.js b/server/sonar-web/src/main/js/helpers/markdown.js deleted file mode 100644 index 6701d972642..00000000000 --- a/server/sonar-web/src/main/js/helpers/markdown.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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. - */ -// keep this file in JavaScript, because it is used by a webpack loader -module.exports = { getFrontMatter, separateFrontMatter, filterContent }; - -function getFrontMatter(content) { - const lines = content.split('\n'); - const position = getFrontMatterPosition(lines); - return position ? parseFrontMatter(lines.slice(position.firstLine + 1, position.lastLine)) : {}; -} - -function separateFrontMatter(content) { - const lines = content.split('\n'); - const position = getFrontMatterPosition(lines); - if (position) { - const frontmatter = parseFrontMatter(lines.slice(position.firstLine + 1, position.lastLine)); - const content = lines.slice(position.lastLine + 1).join('\n'); - return { frontmatter, content }; - } - return { frontmatter: {}, content }; -} - -function getFrontMatterPosition(lines) { - let firstLine; - let lastLine; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.trim() === '---') { - if (firstLine === undefined) { - firstLine = i; - } else { - lastLine = i; - break; - } - } - } - return lastLine !== undefined ? { firstLine, lastLine } : undefined; -} - -function parseFrontMatter(lines) { - const data = {}; - for (const element of lines) { - const tokens = element.split(':').map(x => x.trim()); - if (tokens.length === 2) { - data[tokens[0]] = tokens[1]; - } - } - return data; -} - -/** - * @param {string} content - * @returns {string} - */ -function filterContent(content) { - const regexBase = '<!-- \\/?(sonarqube|sonarcloud|static) -->'; - const { getInstance } = require('./system'); - const contentWithInstance = content.replace(/{instance}/gi, getInstance()); - const contentWithoutStatic = cutConditionalContent(contentWithInstance, 'static'); - const filteredContent = cutConditionalContent(contentWithoutStatic, 'sonarcloud'); - return filteredContent - .replace(new RegExp(`^${regexBase}(\n|\r|\r\n|$)`, 'gm'), '') // First, remove single-line ones, including ending carriage-returns. - .replace(new RegExp(`${regexBase}`, 'g'), ''); // Now remove all remaining ones. -} - -/** - * @param {string} content - * @param {string} tag - * @returns {string} - */ -function cutConditionalContent(content, tag) { - const beginning = `<!-- ${tag} -->`; - const ending = `<!-- /${tag} -->`; - - let newContent = content; - let start = newContent.indexOf(beginning); - let end = newContent.indexOf(ending); - while (start !== -1 && end !== -1) { - if (start < end) { - newContent = newContent.substring(0, start) + newContent.substring(end + ending.length); - } else { - // When conditional tags are incorrectly used we log an error, strip them out and pass to the next pair of tags - // eslint-disable-next-line no-console - console.error( - new Error( - `Documentation - incorrect usage of conditional formatting tags here: "${newContent.substring( - end, - start + beginning.length - )}"` - ) - ); - newContent = - newContent.substring(0, end) + - newContent.substring(end + ending.length, start) + - newContent.substring(start + beginning.length); - } - start = newContent.indexOf(beginning); - end = newContent.indexOf(ending); - } - - return newContent; -} diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 0f89329fe9f..d089cfcd86a 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -19,7 +19,6 @@ */ import { To } from 'react-router-dom'; import { RuleDescriptionSections } from '../apps/coding-rules/rule'; -import { DocumentationEntry } from '../apps/documentation/utils'; import { Exporter, Profile } from '../apps/quality-profiles/types'; import { Location, Router } from '../components/hoc/withRouter'; import { AppState } from '../types/appstate'; @@ -652,20 +651,6 @@ ${overrides.key ? 'key: ' + overrides.key : ''} ${content}`; } -export function mockDocumentationEntry( - overrides: Partial<DocumentationEntry> = {} -): DocumentationEntry { - return { - content: 'Lorem ipsum dolor sit amet fredum', - relativeName: 'Lorem', - navTitle: undefined, - text: 'Lorem ipsum dolor sit amet fredum', - title: 'Lorem', - url: '/lorem/ipsum', - ...overrides - }; -} - export function mockLanguage(overrides: Partial<Language> = {}): Language { return { key: 'css', |