aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>2023-06-22 17:04:33 +0200
committersonartech <sonartech@sonarsource.com>2023-06-23 20:03:17 +0000
commitbb2fa45b4e8d9e1f1e3d0842e3663ee41a109624 (patch)
tree7cd44852ccb485cbb70040635d4696fc86ed163c /server/sonar-web
parent92a86d4caf583a8778d2355d4bc11080f9edb093 (diff)
downloadsonarqube-bb2fa45b4e8d9e1f1e3d0842e3663ee41a109624.tar.gz
sonarqube-bb2fa45b4e8d9e1f1e3d0842e3663ee41a109624.zip
SONAR-19638 Add syntax highlighting to code snippets in rule details
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/design-system/package.json3
-rw-r--r--server/sonar-web/design-system/src/@types/highlightjs-apex.d.ts (renamed from server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx)34
-rw-r--r--server/sonar-web/design-system/src/@types/highlightjs-sap-abap.d.ts26
-rw-r--r--server/sonar-web/design-system/src/components/CodeSnippet.tsx120
-rw-r--r--server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx153
-rw-r--r--server/sonar-web/design-system/src/components/Highlighter.tsx182
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx46
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/CodeSyntaxHighlighter-test.tsx53
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap473
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap463
-rw-r--r--server/sonar-web/design-system/src/components/index.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx42
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap216
-rw-r--r--server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/rules/RuleDescription.tsx39
-rw-r--r--server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx66
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/tasks.ts4
-rw-r--r--server/sonar-web/yarn.lock34
23 files changed, 491 insertions, 1664 deletions
diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json
index 2ca10017669..3d8cf39ac87 100644
--- a/server/sonar-web/design-system/package.json
+++ b/server/sonar-web/design-system/package.json
@@ -35,6 +35,9 @@
"eslint-plugin-local-rules": "1.3.2",
"eslint-plugin-typescript-sort-keys": "2.3.0",
"highlight.js": "11.7.0",
+ "highlightjs-apex": "1.2.0",
+ "highlightjs-cobol": "0.3.3",
+ "highlightjs-sap-abap": "0.2.0",
"history": "5.3.0",
"jest": "29.5.0",
"postcss": "8.4.21",
diff --git a/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx b/server/sonar-web/design-system/src/@types/highlightjs-apex.d.ts
index c02ca7f6e51..efcfd753359 100644
--- a/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx
+++ b/server/sonar-web/design-system/src/@types/highlightjs-apex.d.ts
@@ -17,34 +17,10 @@
* 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 'highlightjs-apex' {
+ import { LanguageFn } from 'highlight.js';
-import { renderWithContext } from '../../helpers/testUtils';
-import { FCProps } from '../../types/misc';
-import { Highlighter } from '../Highlighter';
-
-it('renders correctly', () => {
- expect(setupWithProps().container).toMatchSnapshot();
-});
-
-it('should handle multiple lines of code', () => {
- expect(
- setupWithProps({
- code: `foo: bar
- pleh: help
- stuff:
- foo: bar
- bar: foo`,
- language: 'yaml',
- }).container
- ).toMatchSnapshot();
-});
-
-it('should display edit functions', () => {
- expect(
- setupWithProps({ code: 'One line command', toggleEdit: jest.fn() }).container
- ).toMatchSnapshot();
-});
-
-function setupWithProps(props: Partial<FCProps<typeof Highlighter>> = {}) {
- return renderWithContext(<Highlighter code="foo\nbar" {...props} />);
+ const defineLanguage: LanguageFn;
+ // eslint-disable-next-line import/no-default-export
+ export default defineLanguage;
}
diff --git a/server/sonar-web/design-system/src/@types/highlightjs-sap-abap.d.ts b/server/sonar-web/design-system/src/@types/highlightjs-sap-abap.d.ts
new file mode 100644
index 00000000000..d133b6e7f71
--- /dev/null
+++ b/server/sonar-web/design-system/src/@types/highlightjs-sap-abap.d.ts
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 'highlightjs-sap-abap' {
+ import { LanguageFn } from 'highlight.js';
+
+ const defineLanguage: LanguageFn;
+ // eslint-disable-next-line import/no-default-export
+ export default defineLanguage;
+}
diff --git a/server/sonar-web/design-system/src/components/CodeSnippet.tsx b/server/sonar-web/design-system/src/components/CodeSnippet.tsx
deleted file mode 100644
index e1fed4f8808..00000000000
--- a/server/sonar-web/design-system/src/components/CodeSnippet.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
-import classNames from 'classnames';
-import tw from 'twin.macro';
-import { themeBorder, themeColor } from '../helpers/theme';
-import { isDefined } from '../helpers/types';
-import { Highlighter, RegisteredLanguages } from './Highlighter';
-import { ClipboardButton } from './clipboard';
-
-interface Props {
- className?: string;
- highlight?: boolean;
- isOneLine?: boolean;
- join?: string;
- language?: RegisteredLanguages;
- noCopy?: boolean;
- render?: string;
- snippet: string | Array<string | undefined>;
- toggleEdit?: VoidFunction;
- wrap?: boolean;
-}
-
-// keep this "useless" concatenation for the readability reason
-// eslint-disable-next-line no-useless-concat
-const s = ' \\' + '\n ';
-
-export function CodeSnippet(props: Props) {
- const {
- className,
- isOneLine,
- highlight,
- join = s,
- language,
- noCopy,
- render,
- snippet,
- toggleEdit,
- wrap,
- } = props;
- const snippetArray = Array.isArray(snippet) ? snippet.filter(isDefined) : [snippet];
- const finalSnippet = isOneLine ? snippetArray.join(' ') : snippetArray.join(join);
-
- const isSimpleOneLine = isOneLine && noCopy;
-
- const copyButton = isOneLine ? (
- <StyledSingleLineClipboardButton copyValue={finalSnippet} />
- ) : (
- <StyledClipboardButton copyValue={finalSnippet} />
- );
-
- return (
- <Wrapper
- className={classNames(
- {
- 'code-snippet-highlighted-oneline': isOneLine,
- 'code-snippet-simple-oneline': isSimpleOneLine,
- },
- className,
- 'fs-mask'
- )}
- >
- {!noCopy && copyButton}
- <Highlighter
- code={render ?? finalSnippet}
- highlight={highlight}
- isSimpleOneLine={isSimpleOneLine}
- language={language}
- toggleEdit={isOneLine ? toggleEdit : undefined}
- wrap={wrap}
- />
- </Wrapper>
- );
-}
-
-const Wrapper = styled.div`
- background-color: ${themeColor('codeSnippetBackground')};
- border: ${themeBorder('default', 'codeSnippetBorder')};
-
- ${tw`sw-rounded-2`}
- ${tw`sw-relative`}
- ${tw`sw-my-2`}
-
- &.code-snippet-simple-oneline {
- ${tw`sw-my-0`}
- ${tw`sw-rounded-1`}
- }
-`;
-
-const StyledClipboardButton = styled(ClipboardButton)`
- ${tw`sw-select-none`}
- ${tw`sw-body-sm`}
- ${tw`sw-top-6 sw-right-6`}
- ${tw`sw-absolute`}
-
- .code-snippet-highlighted-oneline & {
- ${tw`sw-bottom-2`}
- }
-`;
-
-const StyledSingleLineClipboardButton = styled(StyledClipboardButton)`
- ${tw`sw-top-6 sw-bottom-6`}
-`;
diff --git a/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
new file mode 100644
index 00000000000..487b2223be6
--- /dev/null
+++ b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
+import hljs from 'highlight.js';
+import apex from 'highlightjs-apex';
+import cobol from 'highlightjs-cobol';
+import abap from 'highlightjs-sap-abap';
+import tw from 'twin.macro';
+import { themeColor, themeContrast } from '../helpers/theme';
+
+hljs.registerLanguage('abap', abap);
+hljs.registerLanguage('apex', apex);
+hljs.registerLanguage('cobol', cobol);
+
+hljs.registerAliases('azureresourcemanager', { languageName: 'json' });
+hljs.registerAliases('flex', { languageName: 'actionscript' });
+hljs.registerAliases('objc', { languageName: 'objectivec' });
+hljs.registerAliases('plsql', { languageName: 'pgsql' });
+hljs.registerAliases('secrets', { languageName: 'markdown' });
+hljs.registerAliases('web', { languageName: 'xml' });
+hljs.registerAliases(['cloudformation', 'kubernetes'], { languageName: 'yaml' });
+
+interface Props {
+ className?: string;
+ htmlAsString: string;
+ language?: string;
+}
+
+const CODE_REGEXP = '<(code|pre)\\b([^>]*?)>(.+?)<\\/\\1>';
+const GLOBAL_REGEXP = new RegExp(CODE_REGEXP, 'gs');
+const SINGLE_REGEXP = new RegExp(CODE_REGEXP, 's');
+
+const htmlDecode = (escapedCode: string) => {
+ const doc = new DOMParser().parseFromString(escapedCode, 'text/html');
+
+ return doc.documentElement.textContent ?? '';
+};
+
+export function CodeSyntaxHighlighter({ className, htmlAsString, language }: Props) {
+ let highlightedHtmlAsString = htmlAsString;
+
+ htmlAsString.match(GLOBAL_REGEXP)?.forEach((codeBlock) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const [, tag, attributes, code] = SINGLE_REGEXP.exec(codeBlock)!;
+
+ const unescapedCode = htmlDecode(code);
+
+ let highlightedCode;
+
+ try {
+ highlightedCode = hljs.highlight(unescapedCode, {
+ ignoreIllegals: true,
+ language: language ?? 'plaintext',
+ });
+ } catch {
+ highlightedCode = hljs.highlight(unescapedCode, {
+ ignoreIllegals: true,
+ language: 'plaintext',
+ });
+ }
+
+ highlightedHtmlAsString = highlightedHtmlAsString.replace(
+ codeBlock,
+ `<${tag}${attributes}>${highlightedCode.value}</${tag}>`
+ );
+ });
+
+ return (
+ <StyledSpan
+ className={`hljs ${className ?? ''}`}
+ // Safe: value is escaped by highlight.js
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: highlightedHtmlAsString }}
+ />
+ );
+}
+
+const StyledSpan = styled.span`
+ code {
+ ${tw`sw-code`};
+
+ background: ${themeColor('codeSnippetBackground')};
+ color: ${themeColor('codeSnippetBody')};
+
+ &.hljs {
+ padding: unset;
+ }
+ }
+
+ .hljs-meta,
+ .hljs-variable {
+ color: ${themeColor('codeSnippetBody')};
+ }
+
+ .hljs-doctag,
+ .hljs-title,
+ .hljs-title.class_,
+ .hljs-title.function_ {
+ color: ${themeColor('codeSnippetAnnotations')};
+ }
+
+ .hljs-comment {
+ ${tw`sw-code-comment`}
+
+ color: ${themeColor('codeSnippetComments')};
+ }
+
+ .hljs-keyword,
+ .hljs-tag,
+ .hljs-type {
+ color: ${themeColor('codeSnippetKeyword')};
+ }
+
+ .hljs-literal,
+ .hljs-number {
+ color: ${themeColor('codeSnippetConstants')};
+ }
+
+ .hljs-string {
+ color: ${themeColor('codeSnippetString')};
+ }
+
+ .hljs-meta .hljs-keyword {
+ color: ${themeColor('codeSnippetPreprocessingDirective')};
+ }
+
+ mark {
+ ${tw`sw-font-regular`}
+ ${tw`sw-p-1`}
+ ${tw`sw-rounded-1`}
+
+ background-color: ${themeColor('codeSnippetHighlight')};
+ color: ${themeContrast('codeSnippetHighlight')};
+ }
+`;
diff --git a/server/sonar-web/design-system/src/components/Highlighter.tsx b/server/sonar-web/design-system/src/components/Highlighter.tsx
deleted file mode 100644
index 73690313334..00000000000
--- a/server/sonar-web/design-system/src/components/Highlighter.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
-import classNames from 'classnames';
-import hljs from 'highlight.js/lib/core';
-import bash from 'highlight.js/lib/languages/bash';
-import gradle from 'highlight.js/lib/languages/gradle';
-import plaintext from 'highlight.js/lib/languages/plaintext';
-import powershell from 'highlight.js/lib/languages/powershell';
-import properties from 'highlight.js/lib/languages/properties';
-import shell from 'highlight.js/lib/languages/shell';
-import xml from 'highlight.js/lib/languages/xml';
-import yaml from 'highlight.js/lib/languages/yaml';
-import { useMemo } from 'react';
-import tw from 'twin.macro';
-import { translate } from '../helpers/l10n';
-import { themeColor, themeContrast } from '../helpers/theme';
-import { InteractiveIcon } from './InteractiveIcon';
-import { PencilIcon } from './icons';
-
-hljs.registerLanguage('yaml', yaml);
-hljs.registerLanguage('gradle', gradle);
-hljs.registerLanguage('properties', properties);
-hljs.registerLanguage('xml', xml);
-hljs.registerLanguage('bash', bash);
-hljs.registerLanguage('powershell', powershell);
-hljs.registerLanguage('shell', shell);
-hljs.registerLanguage('plaintext', plaintext);
-
-hljs.addPlugin({
- 'after:highlight': (data) => {
- data.value = data.value
- .replace(/&lt;mark&gt;/g, '<mark>')
- .replace(/&lt;\/mark&gt;/g, '</mark>');
- },
-});
-
-export type RegisteredLanguages =
- | 'bash'
- | 'gradle'
- | 'plaintext'
- | 'powershell'
- | 'properties'
- | 'shell'
- | 'xml'
- | 'yaml';
-
-interface Props {
- className?: string;
- code: string;
- highlight?: boolean;
- isSimpleOneLine?: boolean;
- language?: RegisteredLanguages;
- toggleEdit?: VoidFunction;
- wrap?: boolean;
-}
-
-export function Highlighter({
- className,
- code,
- highlight = true,
- isSimpleOneLine = false,
- language = 'yaml',
- toggleEdit,
- wrap,
-}: Props) {
- const highlighted = useMemo(
- () => hljs.highlight(code, { language: highlight ? language : 'plaintext' }),
- [code, highlight, language]
- );
-
- return (
- <StyledPre
- className={classNames({ 'code-wrap': wrap, 'simple-one-line': isSimpleOneLine }, className)}
- >
- <code
- className={classNames('hljs', { 'sw-inline': toggleEdit })}
- // Safe: value is escaped by highlight.js
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: highlighted.value }}
- />
- {toggleEdit && (
- <InteractiveIcon
- Icon={PencilIcon}
- aria-label={translate('edit')}
- className="sw-ml-2"
- onClick={toggleEdit}
- />
- )}
- </StyledPre>
- );
-}
-
-const StyledPre = styled.pre`
- ${tw`sw-flex sw-items-center`}
- ${tw`sw-overflow-x-auto`}
- ${tw`sw-p-6`}
-
- code {
- color: ${themeColor('codeSnippetBody')};
- background: ${themeColor('codeSnippetBackground')};
- ${tw`sw-code`};
-
- &.hljs {
- padding: unset;
- }
- }
-
- .hljs-variable,
- .hljs-meta {
- color: ${themeColor('codeSnippetBody')};
- }
-
- .hljs-doctag,
- .hljs-title,
- .hljs-title.class_,
- .hljs-title.function_ {
- color: ${themeColor('codeSnippetAnnotations')};
- }
-
- .hljs-comment {
- color: ${themeColor('codeSnippetComments')};
-
- ${tw`sw-code-comment`}
- }
-
- .hljs-tag,
- .hljs-type,
- .hljs-keyword {
- color: ${themeColor('codeSnippetKeyword')};
-
- ${tw`sw-code-highlight`}
- }
-
- .hljs-literal,
- .hljs-number {
- color: ${themeColor('codeSnippetConstants')};
- }
-
- .hljs-string {
- color: ${themeColor('codeSnippetString')};
- }
-
- .hljs-meta .hljs-keyword {
- color: ${themeColor('codeSnippetPreprocessingDirective')};
- }
-
- &.code-wrap {
- ${tw`sw-whitespace-pre-wrap`}
- ${tw`sw-break-all`}
- }
-
- mark {
- color: ${themeContrast('codeSnippetHighlight')};
- background-color: ${themeColor('codeSnippetHighlight')};
- ${tw`sw-font-regular`}
- ${tw`sw-rounded-1`}
- ${tw`sw-p-1`}
- }
-
- &.simple-one-line {
- ${tw`sw-min-h-[1.25rem]`}
- ${tw`sw-py-0 sw-px-1`}
- }
-`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx b/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx
deleted file mode 100644
index 87bca8a7bdc..00000000000
--- a/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { HelmetProvider } from 'react-helmet-async';
-import { renderWithContext } from '../../helpers/testUtils';
-import { FCProps } from '../../types/misc';
-import { CodeSnippet } from '../CodeSnippet';
-
-it('should show full size when multiline with no editting', () => {
- const { container } = setupWithProps();
- const copyButton = screen.getByRole('button', { name: 'Copy' });
- expect(copyButton).toHaveStyle('top: 1.5rem');
- expect(container).toMatchSnapshot();
-});
-
-it('should show reduced size when single line with no editting', () => {
- const { container } = setupWithProps({ isOneLine: true, snippet: 'foobar' });
- const copyButton = screen.getByRole('button', { name: 'Copy' });
- expect(copyButton).toHaveStyle('top: 1.5rem');
- expect(container).toMatchSnapshot();
-});
-
-function setupWithProps(props: Partial<FCProps<typeof CodeSnippet>> = {}) {
- return renderWithContext(
- <HelmetProvider>
- <CodeSnippet snippet={'foo\nbar'} {...props} />
- </HelmetProvider>
- );
-}
diff --git a/server/sonar-web/design-system/src/components/__tests__/CodeSyntaxHighlighter-test.tsx b/server/sonar-web/design-system/src/components/__tests__/CodeSyntaxHighlighter-test.tsx
new file mode 100644
index 00000000000..a55e079b885
--- /dev/null
+++ b/server/sonar-web/design-system/src/components/__tests__/CodeSyntaxHighlighter-test.tsx
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { render } from '../../helpers/testUtils';
+import { CodeSyntaxHighlighter } from '../CodeSyntaxHighlighter';
+
+it('renders correctly with no code', () => {
+ const { container } = render(
+ <CodeSyntaxHighlighter
+ htmlAsString={`
+ <p>Hello there!</p>
+
+ <p>There's no code here.</p>
+ `}
+ />
+ );
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(container.getElementsByClassName('hljs-string').length).toBe(0);
+});
+
+it('renders correctly with code', () => {
+ const { container } = render(
+ <CodeSyntaxHighlighter
+ htmlAsString={`
+ <p>Hello there!</p>
+
+ <p>There's some <code>"code"</code> here.</p>
+ `}
+ language="typescript"
+ />
+ );
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(container.getElementsByClassName('hljs-string').length).toBe(1);
+});
diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
deleted file mode 100644
index ec00317fed2..00000000000
--- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
+++ /dev/null
@@ -1,473 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should show full size when multiline with no editting 1`] = `
-.emotion-0 {
- background-color: rgb(252,252,253);
- border: 1px solid rgb(225,230,243);
- border-radius: 0.5rem;
- position: relative;
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
-}
-
-.emotion-0.code-snippet-simple-oneline {
- margin-top: 0;
- margin-bottom: 0;
- border-radius: 0.25rem;
-}
-
-.emotion-4 {
- box-sizing: border-box;
- -webkit-text-decoration: none;
- text-decoration: none;
- outline: none;
- border: var(--border);
- color: var(--color);
- background-color: var(--background);
- -webkit-transition: background-color 0.2s ease,outline 0.2s ease;
- transition: background-color 0.2s ease,outline 0.2s ease;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- height: 2.25rem;
- font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1.25rem;
- font-weight: 600;
- padding-left: 1rem;
- padding-right: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- border-radius: 0.5rem;
- cursor: pointer;
- --background: rgb(255,255,255);
- --backgroundHover: rgb(239,242,249);
- --color: rgb(62,67,87);
- --focus: rgba(197,205,223,0.2);
- --border: 1px solid rgb(197,205,223);
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1.25rem;
- font-weight: 400;
- right: 1.5rem;
- top: 1.5rem;
- position: absolute;
-}
-
-.emotion-4:hover {
- color: var(--color);
- background-color: var(--backgroundHover);
-}
-
-.emotion-4:focus,
-.emotion-4:active {
- color: var(--color);
- outline: 4px solid var(--focus);
-}
-
-.emotion-4:disabled,
-.emotion-4:disabled:hover {
- color: rgb(166,173,194);
- background-color: rgb(239,242,249);
- border: 1px solid rgb(197,205,223);
- cursor: not-allowed;
-}
-
-.emotion-4>svg {
- margin-right: 0.25rem;
-}
-
-.emotion-4 [disabled] {
- pointer-events: none;
-}
-
-.code-snippet-highlighted-oneline .emotion-4 {
- bottom: 0.5rem;
-}
-
-.emotion-6 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- overflow-x: auto;
- padding: 1.5rem;
-}
-
-.emotion-6 code {
- color: rgb(51,53,60);
- background: rgb(252,252,253);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 400;
-}
-
-.emotion-6 code.hljs {
- padding: unset;
-}
-
-.emotion-6 .hljs-variable,
-.emotion-6 .hljs-meta {
- color: rgb(51,53,60);
-}
-
-.emotion-6 .hljs-doctag,
-.emotion-6 .hljs-title,
-.emotion-6 .hljs-title.class_,
-.emotion-6 .hljs-title.function_ {
- color: rgb(34,84,192);
-}
-
-.emotion-6 .hljs-comment {
- color: rgb(109,111,119);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-style: italic;
-}
-
-.emotion-6 .hljs-tag,
-.emotion-6 .hljs-type,
-.emotion-6 .hljs-keyword {
- color: rgb(152,29,150);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 700;
-}
-
-.emotion-6 .hljs-literal,
-.emotion-6 .hljs-number {
- color: rgb(126,83,5);
-}
-
-.emotion-6 .hljs-string {
- color: rgb(32,105,31);
-}
-
-.emotion-6 .hljs-meta .hljs-keyword {
- color: rgb(47,103,48);
-}
-
-.emotion-6.code-wrap {
- white-space: pre-wrap;
- word-break: break-all;
-}
-
-.emotion-6 mark {
- color: rgb(217,45,32);
- background-color: rgb(197,205,223);
- font-weight: 400;
- border-radius: 0.25rem;
- padding: 0.25rem;
-}
-
-.emotion-6.simple-one-line {
- min-height: 1.25rem;
- padding-left: 0.25rem;
- padding-right: 0.25rem;
- padding-top: 0;
- padding-bottom: 0;
-}
-
-<div>
- <div
- class="fs-mask emotion-0 emotion-1"
- >
- <button
- aria-describedby="tooltip-1"
- class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
- data-clipboard-text="foo
-bar"
- type="button"
- >
- <svg
- aria-hidden="true"
- class="octicon octicon-copy"
- fill="currentColor"
- focusable="false"
- height="16"
- role="img"
- style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
- viewBox="0 0 16 16"
- width="16"
- >
- <path
- d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
- />
- <path
- d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
- />
- </svg>
- Copy
- </button>
- <pre
- class=" emotion-6 emotion-7"
- >
- <code
- class="hljs"
- >
- <span
- class="hljs-string"
- >
- foo
- </span>
-
-
- <span
- class="hljs-string"
- >
- bar
- </span>
- </code>
- </pre>
- </div>
-</div>
-`;
-
-exports[`should show reduced size when single line with no editting 1`] = `
-.emotion-0 {
- background-color: rgb(252,252,253);
- border: 1px solid rgb(225,230,243);
- border-radius: 0.5rem;
- position: relative;
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
-}
-
-.emotion-0.code-snippet-simple-oneline {
- margin-top: 0;
- margin-bottom: 0;
- border-radius: 0.25rem;
-}
-
-.emotion-4 {
- box-sizing: border-box;
- -webkit-text-decoration: none;
- text-decoration: none;
- outline: none;
- border: var(--border);
- color: var(--color);
- background-color: var(--background);
- -webkit-transition: background-color 0.2s ease,outline 0.2s ease;
- transition: background-color 0.2s ease,outline 0.2s ease;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- height: 2.25rem;
- font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1.25rem;
- font-weight: 600;
- padding-left: 1rem;
- padding-right: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- border-radius: 0.5rem;
- cursor: pointer;
- --background: rgb(255,255,255);
- --backgroundHover: rgb(239,242,249);
- --color: rgb(62,67,87);
- --focus: rgba(197,205,223,0.2);
- --border: 1px solid rgb(197,205,223);
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1.25rem;
- font-weight: 400;
- right: 1.5rem;
- top: 1.5rem;
- position: absolute;
- bottom: 1.5rem;
- top: 1.5rem;
-}
-
-.emotion-4:hover {
- color: var(--color);
- background-color: var(--backgroundHover);
-}
-
-.emotion-4:focus,
-.emotion-4:active {
- color: var(--color);
- outline: 4px solid var(--focus);
-}
-
-.emotion-4:disabled,
-.emotion-4:disabled:hover {
- color: rgb(166,173,194);
- background-color: rgb(239,242,249);
- border: 1px solid rgb(197,205,223);
- cursor: not-allowed;
-}
-
-.emotion-4>svg {
- margin-right: 0.25rem;
-}
-
-.emotion-4 [disabled] {
- pointer-events: none;
-}
-
-.code-snippet-highlighted-oneline .emotion-4 {
- bottom: 0.5rem;
-}
-
-.emotion-6 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- overflow-x: auto;
- padding: 1.5rem;
-}
-
-.emotion-6 code {
- color: rgb(51,53,60);
- background: rgb(252,252,253);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 400;
-}
-
-.emotion-6 code.hljs {
- padding: unset;
-}
-
-.emotion-6 .hljs-variable,
-.emotion-6 .hljs-meta {
- color: rgb(51,53,60);
-}
-
-.emotion-6 .hljs-doctag,
-.emotion-6 .hljs-title,
-.emotion-6 .hljs-title.class_,
-.emotion-6 .hljs-title.function_ {
- color: rgb(34,84,192);
-}
-
-.emotion-6 .hljs-comment {
- color: rgb(109,111,119);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-style: italic;
-}
-
-.emotion-6 .hljs-tag,
-.emotion-6 .hljs-type,
-.emotion-6 .hljs-keyword {
- color: rgb(152,29,150);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 700;
-}
-
-.emotion-6 .hljs-literal,
-.emotion-6 .hljs-number {
- color: rgb(126,83,5);
-}
-
-.emotion-6 .hljs-string {
- color: rgb(32,105,31);
-}
-
-.emotion-6 .hljs-meta .hljs-keyword {
- color: rgb(47,103,48);
-}
-
-.emotion-6.code-wrap {
- white-space: pre-wrap;
- word-break: break-all;
-}
-
-.emotion-6 mark {
- color: rgb(217,45,32);
- background-color: rgb(197,205,223);
- font-weight: 400;
- border-radius: 0.25rem;
- padding: 0.25rem;
-}
-
-.emotion-6.simple-one-line {
- min-height: 1.25rem;
- padding-left: 0.25rem;
- padding-right: 0.25rem;
- padding-top: 0;
- padding-bottom: 0;
-}
-
-<div>
- <div
- class="code-snippet-highlighted-oneline fs-mask emotion-0 emotion-1"
- >
- <button
- aria-describedby="tooltip-2"
- class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
- data-clipboard-text="foobar"
- type="button"
- >
- <svg
- aria-hidden="true"
- class="octicon octicon-copy"
- fill="currentColor"
- focusable="false"
- height="16"
- role="img"
- style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
- viewBox="0 0 16 16"
- width="16"
- >
- <path
- d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
- />
- <path
- d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
- />
- </svg>
- Copy
- </button>
- <pre
- class=" emotion-6 emotion-7"
- >
- <code
- class="hljs"
- >
- <span
- class="hljs-string"
- >
- foobar
- </span>
- </code>
- </pre>
- </div>
-</div>
-`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap
deleted file mode 100644
index d7fed058225..00000000000
--- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap
+++ /dev/null
@@ -1,463 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders correctly 1`] = `
-.emotion-0 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- overflow-x: auto;
- padding: 1.5rem;
-}
-
-.emotion-0 code {
- color: rgb(51,53,60);
- background: rgb(252,252,253);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 400;
-}
-
-.emotion-0 code.hljs {
- padding: unset;
-}
-
-.emotion-0 .hljs-variable,
-.emotion-0 .hljs-meta {
- color: rgb(51,53,60);
-}
-
-.emotion-0 .hljs-doctag,
-.emotion-0 .hljs-title,
-.emotion-0 .hljs-title.class_,
-.emotion-0 .hljs-title.function_ {
- color: rgb(34,84,192);
-}
-
-.emotion-0 .hljs-comment {
- color: rgb(109,111,119);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-style: italic;
-}
-
-.emotion-0 .hljs-tag,
-.emotion-0 .hljs-type,
-.emotion-0 .hljs-keyword {
- color: rgb(152,29,150);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 700;
-}
-
-.emotion-0 .hljs-literal,
-.emotion-0 .hljs-number {
- color: rgb(126,83,5);
-}
-
-.emotion-0 .hljs-string {
- color: rgb(32,105,31);
-}
-
-.emotion-0 .hljs-meta .hljs-keyword {
- color: rgb(47,103,48);
-}
-
-.emotion-0.code-wrap {
- white-space: pre-wrap;
- word-break: break-all;
-}
-
-.emotion-0 mark {
- color: rgb(217,45,32);
- background-color: rgb(197,205,223);
- font-weight: 400;
- border-radius: 0.25rem;
- padding: 0.25rem;
-}
-
-.emotion-0.simple-one-line {
- min-height: 1.25rem;
- padding-left: 0.25rem;
- padding-right: 0.25rem;
- padding-top: 0;
- padding-bottom: 0;
-}
-
-<div>
- <pre
- class=" emotion-0 emotion-1"
- >
- <code
- class="hljs"
- >
- <span
- class="hljs-string"
- >
- foo\\nbar
- </span>
- </code>
- </pre>
-</div>
-`;
-
-exports[`should display edit functions 1`] = `
-.emotion-0 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- overflow-x: auto;
- padding: 1.5rem;
-}
-
-.emotion-0 code {
- color: rgb(51,53,60);
- background: rgb(252,252,253);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 400;
-}
-
-.emotion-0 code.hljs {
- padding: unset;
-}
-
-.emotion-0 .hljs-variable,
-.emotion-0 .hljs-meta {
- color: rgb(51,53,60);
-}
-
-.emotion-0 .hljs-doctag,
-.emotion-0 .hljs-title,
-.emotion-0 .hljs-title.class_,
-.emotion-0 .hljs-title.function_ {
- color: rgb(34,84,192);
-}
-
-.emotion-0 .hljs-comment {
- color: rgb(109,111,119);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-style: italic;
-}
-
-.emotion-0 .hljs-tag,
-.emotion-0 .hljs-type,
-.emotion-0 .hljs-keyword {
- color: rgb(152,29,150);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 700;
-}
-
-.emotion-0 .hljs-literal,
-.emotion-0 .hljs-number {
- color: rgb(126,83,5);
-}
-
-.emotion-0 .hljs-string {
- color: rgb(32,105,31);
-}
-
-.emotion-0 .hljs-meta .hljs-keyword {
- color: rgb(47,103,48);
-}
-
-.emotion-0.code-wrap {
- white-space: pre-wrap;
- word-break: break-all;
-}
-
-.emotion-0 mark {
- color: rgb(217,45,32);
- background-color: rgb(197,205,223);
- font-weight: 400;
- border-radius: 0.25rem;
- padding: 0.25rem;
-}
-
-.emotion-0.simple-one-line {
- min-height: 1.25rem;
- padding-left: 0.25rem;
- padding-right: 0.25rem;
- padding-top: 0;
- padding-bottom: 0;
-}
-
-.emotion-3 {
- box-sizing: border-box;
- border: none;
- outline: none;
- -webkit-text-decoration: none;
- text-decoration: none;
- color: var(--color);
- background-color: var(--background);
- -webkit-transition: background-color 0.2s ease,outline 0.2s ease,color 0.2s ease;
- transition: background-color 0.2s ease,outline 0.2s ease,color 0.2s ease;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- -webkit-box-pack: center;
- -ms-flex-pack: center;
- -webkit-justify-content: center;
- justify-content: center;
- cursor: pointer;
- height: 2.25rem;
- border-radius: 0.5rem;
- padding-left: 0.625rem;
- padding-right: 0.625rem;
- --background: transparent;
- --backgroundHover: rgb(232,235,255);
- --color: rgb(75,86,187);
- --colorHover: rgb(43,51,104);
- --focus: rgba(93,108,208,0.2);
-}
-
-.emotion-3:hover,
-.emotion-3:focus,
-.emotion-3:active {
- color: var(--colorHover);
- background-color: var(--backgroundHover);
-}
-
-.emotion-3:focus,
-.emotion-3:active {
- outline: 4px solid var(--focus);
-}
-
-.emotion-3:disabled,
-.emotion-3:disabled:hover {
- color: rgb(166,173,194);
- background-color: var(--background);
- cursor: not-allowed;
-}
-
-<div>
- <pre
- class=" emotion-0 emotion-1"
- >
- <code
- class="hljs sw-inline"
- >
- <span
- class="hljs-string"
- >
- One
- </span>
-
- <span
- class="hljs-string"
- >
- line
- </span>
-
- <span
- class="hljs-string"
- >
- command
- </span>
- </code>
- <button
- aria-label="edit"
- class="sw-ml-2 emotion-2 emotion-3 emotion-4"
- type="button"
- >
- <svg
- aria-hidden="true"
- class=""
- fill="currentColor"
- focusable="false"
- height="16"
- role="img"
- style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
- viewBox="0 0 16 16"
- width="16"
- >
- <path
- d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"
- />
- </svg>
- </button>
- </pre>
-</div>
-`;
-
-exports[`should handle multiple lines of code 1`] = `
-.emotion-0 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- overflow-x: auto;
- padding: 1.5rem;
-}
-
-.emotion-0 code {
- color: rgb(51,53,60);
- background: rgb(252,252,253);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 400;
-}
-
-.emotion-0 code.hljs {
- padding: unset;
-}
-
-.emotion-0 .hljs-variable,
-.emotion-0 .hljs-meta {
- color: rgb(51,53,60);
-}
-
-.emotion-0 .hljs-doctag,
-.emotion-0 .hljs-title,
-.emotion-0 .hljs-title.class_,
-.emotion-0 .hljs-title.function_ {
- color: rgb(34,84,192);
-}
-
-.emotion-0 .hljs-comment {
- color: rgb(109,111,119);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-style: italic;
-}
-
-.emotion-0 .hljs-tag,
-.emotion-0 .hljs-type,
-.emotion-0 .hljs-keyword {
- color: rgb(152,29,150);
- font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
- font-size: 0.875rem;
- line-height: 1.125rem;
- font-weight: 700;
-}
-
-.emotion-0 .hljs-literal,
-.emotion-0 .hljs-number {
- color: rgb(126,83,5);
-}
-
-.emotion-0 .hljs-string {
- color: rgb(32,105,31);
-}
-
-.emotion-0 .hljs-meta .hljs-keyword {
- color: rgb(47,103,48);
-}
-
-.emotion-0.code-wrap {
- white-space: pre-wrap;
- word-break: break-all;
-}
-
-.emotion-0 mark {
- color: rgb(217,45,32);
- background-color: rgb(197,205,223);
- font-weight: 400;
- border-radius: 0.25rem;
- padding: 0.25rem;
-}
-
-.emotion-0.simple-one-line {
- min-height: 1.25rem;
- padding-left: 0.25rem;
- padding-right: 0.25rem;
- padding-top: 0;
- padding-bottom: 0;
-}
-
-<div>
- <pre
- class=" emotion-0 emotion-1"
- >
- <code
- class="hljs"
- >
- <span
- class="hljs-attr"
- >
- foo:
- </span>
-
- <span
- class="hljs-string"
- >
- bar
- </span>
-
-
- <span
- class="hljs-attr"
- >
- pleh:
- </span>
-
- <span
- class="hljs-string"
- >
- help
- </span>
-
-
- <span
- class="hljs-attr"
- >
- stuff:
- </span>
-
-
- <span
- class="hljs-attr"
- >
- foo:
- </span>
-
- <span
- class="hljs-string"
- >
- bar
- </span>
-
-
- <span
- class="hljs-attr"
- >
- bar:
- </span>
-
- <span
- class="hljs-string"
- >
- foo
- </span>
- </code>
- </pre>
-</div>
-`;
diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts
index 2724f1199d8..dbe05f21f67 100644
--- a/server/sonar-web/design-system/src/components/index.ts
+++ b/server/sonar-web/design-system/src/components/index.ts
@@ -26,7 +26,7 @@ export { Breadcrumbs } from './Breadcrumbs';
export * from './BubbleChart';
export * from './Card';
export * from './Checkbox';
-export * from './CodeSnippet';
+export * from './CodeSyntaxHighlighter';
export * from './ColorsLegend';
export * from './CoverageIndicator';
export * from './DatePicker';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
index 27c79da9e56..3a66d28122c 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
@@ -17,6 +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 { CodeSyntaxHighlighter } from 'design-system';
import * as React from 'react';
import { updateRule } from '../../../api/rules';
import FormattingTips from '../../../components/common/FormattingTips';
@@ -46,8 +48,8 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
state: State = {
description: '',
descriptionForm: false,
- submitting: false,
removeDescriptionModal: false,
+ submitting: false,
};
componentDidMount() {
@@ -89,6 +91,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
}).then(
(ruleDetails) => {
this.props.onChange(ruleDetails);
+
if (this.mounted) {
this.setState({ submitting: false, descriptionForm: false });
}
@@ -104,7 +107,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
handleExtendDescriptionClick = () => {
this.setState({
// set description` to the current `mdNote` each time the form is open
- description: this.props.ruleDetails.mdNote || '',
+ description: this.props.ruleDetails.mdNote ?? '',
descriptionForm: true,
});
};
@@ -112,14 +115,13 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
renderExtendedDescription = () => (
<div id="coding-rules-detail-description-extra">
{this.props.ruleDetails.htmlNote !== undefined && (
- <div
- className="rule-desc spacer-bottom markdown"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: sanitizeUserInput(this.props.ruleDetails.htmlNote),
- }}
+ <CodeSyntaxHighlighter
+ className="rule-desc markdown sw-mb-2"
+ htmlAsString={sanitizeUserInput(this.props.ruleDetails.htmlNote)}
+ language={this.props.ruleDetails.lang}
/>
)}
+
{this.props.canWrite && (
<Button
id="coding-rules-detail-extend-description"
@@ -147,6 +149,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
/>
</td>
</tr>
+
<tr>
<td>
<Button
@@ -156,6 +159,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
>
{translate('save')}
</Button>
+
{this.props.ruleDetails.mdNote !== undefined && (
<>
<Button
@@ -174,6 +178,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
)}
</>
)}
+
<ResetButtonLink
className="spacer-left"
disabled={this.state.submitting}
@@ -184,6 +189,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
</ResetButtonLink>
{this.state.submitting && <i className="spinner spacer-left" />}
</td>
+
<td className="text-right">
<FormattingTips />
</td>
@@ -216,23 +222,24 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
return (
<div className="js-rule-description">
{defaultSection && (
- <section
+ <CodeSyntaxHighlighter
className="coding-rules-detail-description markdown"
key={defaultSection.key}
- /* eslint-disable-next-line react/no-danger */
- dangerouslySetInnerHTML={{ __html: sanitizeString(defaultSection.content) }}
+ htmlAsString={sanitizeString(defaultSection.content)}
+ language={ruleDetails.lang}
/>
)}
{hasDescriptionSection && !defaultSection && (
<>
{introductionSection && (
- <div
+ <CodeSyntaxHighlighter
className="rule-desc"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(introductionSection) }}
+ htmlAsString={sanitizeString(introductionSection)}
+ language={ruleDetails.lang}
/>
)}
+
<RuleTabViewer ruleDetails={ruleDetails} />
</>
)}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
index 0a9e334bd08..e62c665ea42 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
@@ -17,6 +17,7 @@
* 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 { getRuleDetails } from '../../../api/rules';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
@@ -36,19 +37,20 @@ import HotspotViewerRenderer from './HotspotViewerRenderer';
interface Props {
component: Component;
hotspotKey: string;
- onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
hotspotsReviewedMeasure?: string;
- onUpdateHotspot: (hotspotKey: string) => Promise<void>;
onLocationClick: (index: number) => void;
+ onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
+ onUpdateHotspot: (hotspotKey: string) => Promise<void>;
selectedHotspotLocation?: number;
standards?: Standards;
}
interface State {
hotspot?: Hotspot;
- ruleDescriptionSections?: RuleDescriptionSection[];
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
+ ruleDescriptionSections?: RuleDescriptionSection[];
+ ruleLanguage?: string;
showStatusUpdateSuccessModal: boolean;
}
@@ -78,6 +80,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
fetchHotspot = async () => {
this.setState({ loading: true });
+
try {
const hotspot = await getSecurityHotspotDetails(this.props.hotspotKey);
const ruleDetails = await getRuleDetails({ key: hotspot.rule.key }).then((r) => r.rule);
@@ -86,6 +89,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
this.setState({
hotspot,
loading: false,
+ ruleLanguage: ruleDetails.lang,
ruleDescriptionSections: ruleDetails.descriptionSections,
});
}
@@ -112,6 +116,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
handleSwitchFilterToStatusOfUpdatedHotspot = () => {
const { lastStatusChangedTo } = this.state;
+
if (lastStatusChangedTo) {
this.props.onSwitchStatusFilter(getStatusFilterFromStatusOption(lastStatusChangedTo));
}
@@ -122,10 +127,12 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
};
render() {
- const { component, selectedHotspotLocation, standards, hotspotsReviewedMeasure } = this.props;
+ const { component, hotspotsReviewedMeasure, selectedHotspotLocation, standards } = this.props;
+
const {
hotspot,
ruleDescriptionSections,
+ ruleLanguage,
loading,
showStatusUpdateSuccessModal,
lastStatusChangedTo,
@@ -133,19 +140,20 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
return (
<HotspotViewerRenderer
+ component={component}
+ hotspot={hotspot}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
lastStatusChangedTo={lastStatusChangedTo}
+ loading={loading}
onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal}
+ onLocationClick={this.props.onLocationClick}
onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot}
- showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
- standards={standards}
- component={component}
- hotspot={hotspot}
- ruleDescriptionSections={ruleDescriptionSections}
- loading={loading}
onUpdateHotspot={this.handleHotspotUpdate}
- onLocationClick={this.props.onLocationClick}
+ ruleDescriptionSections={ruleDescriptionSections}
+ ruleLanguage={ruleLanguage}
selectedHotspotLocation={selectedHotspotLocation}
+ showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
+ standards={standards}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
index 6e4ae9e6798..8aedbfe45d0 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
@@ -17,6 +17,7 @@
* 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 withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
@@ -36,17 +37,17 @@ export interface HotspotViewerRendererProps {
component: Component;
currentUser: CurrentUser;
hotspot?: Hotspot;
- ruleDescriptionSections?: RuleDescriptionSection[];
hotspotsReviewedMeasure?: string;
- onSwitchFilterToStatusOfUpdatedHotspot: () => void;
lastStatusChangedTo?: HotspotStatusOption;
- onCloseStatusUpdateSuccessModal: () => void;
- showStatusUpdateSuccessModal: boolean;
-
loading: boolean;
- onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
+ onCloseStatusUpdateSuccessModal: () => void;
onLocationClick: (index: number) => void;
+ onSwitchFilterToStatusOfUpdatedHotspot: () => void;
+ onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
+ ruleDescriptionSections?: RuleDescriptionSection[];
+ ruleLanguage?: string;
selectedHotspotLocation?: number;
+ showStatusUpdateSuccessModal: boolean;
standards?: Standards;
}
@@ -55,12 +56,13 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
component,
currentUser,
hotspot,
+ hotspotsReviewedMeasure,
+ lastStatusChangedTo,
loading,
- selectedHotspotLocation,
ruleDescriptionSections,
+ ruleLanguage,
+ selectedHotspotLocation,
showStatusUpdateSuccessModal,
- hotspotsReviewedMeasure,
- lastStatusChangedTo,
standards,
} = props;
@@ -89,6 +91,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
onCommentUpdate={props.onUpdateHotspot}
/>
}
+ branchLike={branchLike}
codeTabContent={
<HotspotSnippetContainer
branchLike={branchLike}
@@ -99,11 +102,11 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
/>
}
component={component}
- standards={standards}
- onUpdateHotspot={props.onUpdateHotspot}
- branchLike={branchLike}
hotspot={hotspot}
+ onUpdateHotspot={props.onUpdateHotspot}
ruleDescriptionSections={ruleDescriptionSections}
+ ruleLanguage={ruleLanguage}
+ standards={standards}
/>
</div>
)}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
index 23e62039ee8..2401739e699 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { ToggleButton, getTabId, getTabPanelId } from 'design-system';
import { groupBy, omit } from 'lodash';
import * as React from 'react';
@@ -34,18 +35,20 @@ import { HotspotHeader } from './HotspotHeader';
interface Props {
activityTabContent: React.ReactNode;
+ branchLike?: BranchLike;
codeTabContent: React.ReactNode;
- hotspot: Hotspot;
- ruleDescriptionSections?: RuleDescriptionSection[];
component: Component;
- branchLike?: BranchLike;
+ hotspot: Hotspot;
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
+ ruleDescriptionSections?: RuleDescriptionSection[];
+ ruleLanguage?: string;
standards?: Standards;
}
+
interface Tab {
- value: TabKeys;
- label: string;
counter?: number;
+ label: string;
+ value: TabKeys;
}
export enum TabKeys {
@@ -61,13 +64,14 @@ const STICKY_HEADER_COMPRESS_THRESHOLD = 200;
export default function HotspotViewerTabs(props: Props) {
const {
- ruleDescriptionSections,
- codeTabContent,
activityTabContent,
- hotspot,
+ branchLike,
+ codeTabContent,
component,
+ hotspot,
+ ruleDescriptionSections,
+ ruleLanguage,
standards,
- branchLike,
} = props;
const { isScrolled, isCompressed, resetScrollDownCompress } = useScrollDownCompress(
@@ -119,6 +123,7 @@ export default function HotspotViewerTabs(props: Props) {
if (isInput(event) || isShortcut(event)) {
return true;
}
+
if (event.key === KeyboardKeys.LeftArrow) {
event.preventDefault();
selectNeighboringTab(-1);
@@ -143,6 +148,7 @@ export default function HotspotViewerTabs(props: Props) {
const handleSelectTabs = (tabKey: TabKeys) => {
const currentTab = tabs.find((tab) => tab.value === tabKey);
+
if (currentTab) {
setCurrentTab(currentTab);
}
@@ -152,10 +158,12 @@ export default function HotspotViewerTabs(props: Props) {
document.addEventListener('keydown', handleKeyboardNavigation);
return () => document.removeEventListener('keydown', handleKeyboardNavigation);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
setCurrentTab(tabs[0]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hotspot.key]);
React.useEffect(() => {
@@ -163,9 +171,11 @@ export default function HotspotViewerTabs(props: Props) {
window.scrollTo({ top: 0 });
}
resetScrollDownCompress();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTab]);
const descriptionSectionsByKey = groupBy(ruleDescriptionSections, (section) => section.key);
+
const rootCauseDescriptionSections =
descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
@@ -173,13 +183,13 @@ export default function HotspotViewerTabs(props: Props) {
return (
<>
<HotspotHeader
- hotspot={hotspot}
- component={component}
- standards={standards}
- onUpdateHotspot={props.onUpdateHotspot}
branchLike={branchLike}
- isScrolled={isScrolled}
+ component={component}
+ hotspot={hotspot}
isCompressed={isCompressed}
+ isScrolled={isScrolled}
+ onUpdateHotspot={props.onUpdateHotspot}
+ standards={standards}
tabs={
<ToggleButton
role="tablist"
@@ -198,12 +208,13 @@ export default function HotspotViewerTabs(props: Props) {
{currentTab.value === TabKeys.Code && codeTabContent}
{currentTab.value === TabKeys.RiskDescription && rootCauseDescriptionSections && (
- <RuleDescription sections={rootCauseDescriptionSections} />
+ <RuleDescription language={ruleLanguage} sections={rootCauseDescriptionSections} />
)}
{currentTab.value === TabKeys.VulnerabilityDescription &&
descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
<RuleDescription
+ language={ruleLanguage}
sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
/>
)}
@@ -211,6 +222,7 @@ export default function HotspotViewerTabs(props: Props) {
{currentTab.value === TabKeys.FixRecommendation &&
descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
<RuleDescription
+ language={ruleLanguage}
sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
/>
)}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
index ca8be50d77f..28bb2f1ddf8 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
+++ b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
@@ -17,12 +17,13 @@
* 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 { screen, waitFor } from '@testing-library/react';
import * as React from 'react';
import { getTask } from '../../../api/ce';
import { mockTaskWarning } from '../../../helpers/mocks/tasks';
import { mockCurrentUser } from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { AnalysisWarningsModal } from '../AnalysisWarningsModal';
jest.mock('../../../api/ce', () => ({
@@ -34,48 +35,50 @@ jest.mock('../../../api/ce', () => ({
beforeEach(jest.clearAllMocks);
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot('default');
- expect(shallowRender({ warnings: [mockTaskWarning({ dismissable: true })] })).toMatchSnapshot(
- 'with dismissable warnings'
- );
- expect(
- shallowRender({
+describe('should render correctly', () => {
+ it('should not show dismiss buttons for non-dismissable warnings', () => {
+ renderAnalysisWarningsModal();
+
+ expect(screen.getByText('warning 1')).toBeInTheDocument();
+ expect(screen.getByText('warning 2')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
+ });
+
+ it('should show a dismiss button for dismissable warnings', () => {
+ renderAnalysisWarningsModal({ warnings: [mockTaskWarning({ dismissable: true })] });
+
+ expect(screen.getByRole('button', { name: 'dismiss_permanently' })).toBeInTheDocument();
+ });
+
+ it('should not show dismiss buttons if not logged in', () => {
+ renderAnalysisWarningsModal({
currentUser: mockCurrentUser({ isLoggedIn: false }),
warnings: [mockTaskWarning({ dismissable: true })],
- })
- ).toMatchSnapshot('do not show dismissable links for anonymous');
+ });
+
+ expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
+ });
});
it('should not fetch task warnings if it does not have to', () => {
- shallowRender();
+ renderAnalysisWarningsModal();
+
expect(getTask).not.toHaveBeenCalled();
});
it('should fetch task warnings if it has to', async () => {
- const wrapper = shallowRender({ taskId: 'abcd1234', warnings: undefined });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
- expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']);
-});
+ renderAnalysisWarningsModal({ taskId: 'abcd1234', warnings: undefined });
-it('should correctly handle updates', async () => {
- const wrapper = shallowRender();
-
- await waitAndUpdate(wrapper);
- expect(getTask).not.toHaveBeenCalled();
-
- wrapper.setProps({ taskId: '1', warnings: undefined });
- await waitAndUpdate(wrapper);
- expect(getTask).toHaveBeenCalled();
+ expect(screen.queryByText('message foo')).not.toBeInTheDocument();
+ expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']);
- (getTask as jest.Mock).mockClear();
- wrapper.setProps({ taskId: undefined, warnings: [mockTaskWarning()] });
- expect(getTask).not.toHaveBeenCalled();
+ await waitFor(() => {
+ expect(screen.getByText('message foo')).toBeInTheDocument();
+ });
});
-function shallowRender(props: Partial<AnalysisWarningsModal['props']> = {}) {
- return shallow<AnalysisWarningsModal>(
+function renderAnalysisWarningsModal(props: Partial<AnalysisWarningsModal['props']> = {}) {
+ return renderComponent(
<AnalysisWarningsModal
currentUser={mockCurrentUser({ isLoggedIn: true })}
onClose={jest.fn()}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap
deleted file mode 100644
index d7393372849..00000000000
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap
+++ /dev/null
@@ -1,216 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should fetch task warnings if it has to 1`] = `
-<Gl
- body={
- <DeferredSpinner
- loading={false}
- >
- <React.Fragment>
- <div
- className="sw-flex sw-items-center sw-mt-2"
- >
- <FlagMessage
- variant="warning"
- >
- <HtmlFormatter>
- <span
- dangerouslySetInnerHTML={
- {
- "__html": "message foo",
- }
- }
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div />
- </React.Fragment>
- <React.Fragment>
- <div
- className="sw-flex sw-items-center sw-mt-2"
- >
- <FlagMessage
- variant="warning"
- >
- <HtmlFormatter>
- <span
- dangerouslySetInnerHTML={
- {
- "__html": "message-bar",
- }
- }
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div />
- </React.Fragment>
- <React.Fragment>
- <div
- className="sw-flex sw-items-center sw-mt-2"
- >
- <FlagMessage
- variant="warning"
- >
- <HtmlFormatter>
- <span
- dangerouslySetInnerHTML={
- {
- "__html": "multiline message<br>secondline<br> third line",
- }
- }
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div />
- </React.Fragment>
- </DeferredSpinner>
- }
- headerTitle="warnings"
- onClose={[MockFunction]}
- primaryButton={null}
- secondaryButtonLabel="close"
-/>
-`;
-
-exports[`should render correctly: default 1`] = `
-<Gl
- body={
- <DeferredSpinner
- loading={false}
- >
- <React.Fragment>
- <div
- className="sw-flex sw-items-center sw-mt-2"
- >
- <FlagMessage
- variant="warning"
- >
- <HtmlFormatter>
- <span
- dangerouslySetInnerHTML={
- {
- "__html": "warning 1",
- }
- }
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div />
- </React.Fragment>
- <React.Fragment>
- <div
- className="sw-flex sw-items-center sw-mt-2"
- >
- <FlagMessage
- variant="warning"
- >
- <HtmlFormatter>
- <span
- dangerouslySetInnerHTML={
- {
- "__html": "warning 2",
- }
- }
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div />
- </React.Fragment>
- </DeferredSpinner>
- }
- headerTitle="warnings"
- onClose={[MockFunction]}
- primaryButton={null}
- secondaryButtonLabel="close"
-/>
-`;
-
-exports[`should render correctly: do not show dismissable links for anonymous 1`] = `
-<Gl
- body={
- <DeferredSpinner
- loading={false}
- >
- <React.Fragment>
- <div
- className="sw-flex sw-items-center sw-mt-2"
- >
- <FlagMessage
- variant="warning"
- >
- <HtmlFormatter>
- <span
- dangerouslySetInnerHTML={
- {
- "__html": "Lorem ipsum",
- }
- }
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div />
- </React.Fragment>
- </DeferredSpinner>
- }
- headerTitle="warnings"
- onClose={[MockFunction]}
- primaryButton={null}
- secondaryButtonLabel="close"
-/>
-`;
-
-exports[`should render correctly: with dismissable warnings 1`] = `
-<Gl
- body={
- <DeferredSpinner
- loading={false}
- >
- <React.Fragment>
- <div
- className="sw-flex sw-items-center sw-mt-2"
- >
- <FlagMessage
- variant="warning"
- >
- <HtmlFormatter>
- <span
- dangerouslySetInnerHTML={
- {
- "__html": "Lorem ipsum",
- }
- }
- />
- </HtmlFormatter>
- </FlagMessage>
- </div>
- <div>
- <div
- className="sw-mt-4"
- >
- <DangerButtonSecondary
- disabled={false}
- onClick={[Function]}
- >
- dismiss_permanently
- </DangerButtonSecondary>
- <DeferredSpinner
- className="sw-ml-2"
- loading={false}
- />
- </div>
- </div>
- </React.Fragment>
- </DeferredSpinner>
- }
- headerTitle="warnings"
- onClose={[MockFunction]}
- primaryButton={null}
- secondaryButtonLabel="close"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx
index 952979cba94..b9b7e58c7bd 100644
--- a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx
+++ b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx
@@ -17,6 +17,7 @@
* 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 { ToggleButton } from 'design-system';
import { cloneDeep, debounce, groupBy } from 'lodash';
@@ -84,7 +85,9 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
constructor(props: IssueTabViewerProps) {
super(props);
+
this.educationPrinciplesRef = React.createRef();
+
this.checkIfEducationPrinciplesAreVisible = debounce(
this.checkIfEducationPrinciplesAreVisible,
DEBOUNCE_FOR_SCROLL
@@ -98,6 +101,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
const tabs = this.computeTabs(Boolean(this.state.displayEducationalPrinciplesNotification));
const query = new URLSearchParams(this.props.location.search);
+
if (query.has('why')) {
this.setState({
selectedTab: tabs.find((tab) => tab.key === TabKeys.WhyIsThisAnIssue) || tabs[0],
@@ -114,6 +118,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
selectedFlowIndex,
selectedLocationIndex,
} = this.props;
+
const { selectedTab } = this.state;
if (
@@ -163,6 +168,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
ruleDetails.educationPrinciples.length > 0 &&
isLoggedIn &&
!dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
+
const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
return {
@@ -175,7 +181,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
const {
codeTabContent,
- ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
+ ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType },
ruleDescriptionContextKey,
extendedDescription,
activityTabContent,
@@ -214,11 +220,12 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
<RuleDescription
+ defaultContextKey={ruleDescriptionContextKey}
+ language={ruleLanguage}
sections={
descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
}
- defaultContextKey={ruleDescriptionContextKey}
/>
),
},
@@ -228,6 +235,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
<RuleDescription
+ language={ruleLanguage}
sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
/>
),
@@ -238,8 +246,9 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
<RuleDescription
- sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
defaultContextKey={ruleDescriptionContextKey}
+ language={ruleLanguage}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
/>
),
},
@@ -257,10 +266,11 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
content: ((educationPrinciples && educationPrinciples.length > 0) ||
descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
<MoreInfoRuleDescription
- educationPrinciples={educationPrinciples}
- sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
+ educationPrinciples={educationPrinciples}
educationPrinciplesRef={this.educationPrinciplesRef}
+ language={ruleLanguage}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
/>
),
counter: displayEducationalPrinciplesNotification ? 1 : undefined,
@@ -346,7 +356,8 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
{({ top }) => (
<div
style={{
- // We substract the footer height with padding (80) and the main layout padding (20) and the tabs padding (20)
+ // We substract the footer height with padding (80) and the main layout padding (20)
+ // and the tabs padding (20)
maxHeight: scrollInTab ? `calc(100vh - ${top + 120}px)` : 'initial',
}}
className="sw-flex sw-flex-col"
diff --git a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
index d134501fb5c..1b5f3e25215 100644
--- a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
@@ -17,31 +17,35 @@
* 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 { RuleDescriptionSection } from '../../apps/coding-rules/rule';
import { translate } from '../../helpers/l10n';
import { Dict } from '../../types/types';
import { ButtonLink } from '../controls/buttons';
import { Alert } from '../ui/Alert';
+import RuleDescription from './RuleDescription';
import DefenseInDepth from './educationPrinciples/DefenseInDepth';
import NeverTrustUserInput from './educationPrinciples/NeverTrustUserInput';
-import RuleDescription from './RuleDescription';
import './style.css';
interface Props {
- sections?: RuleDescriptionSection[];
- educationPrinciples?: string[];
displayEducationalPrinciplesNotification?: boolean;
+ educationPrinciples?: string[];
educationPrinciplesRef?: React.RefObject<HTMLDivElement>;
+ language?: string;
+ sections?: RuleDescriptionSection[];
}
const EDUCATION_PRINCIPLES_MAP: Dict<React.ComponentType> = {
defense_in_depth: DefenseInDepth,
never_trust_user_input: NeverTrustUserInput,
};
+
export default class MoreInfoRuleDescription extends React.PureComponent<Props, {}> {
handleNotificationScroll = () => {
const element = this.props.educationPrinciplesRef?.current;
+
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
}
@@ -50,10 +54,12 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
render() {
const {
displayEducationalPrinciplesNotification,
+ language,
sections = [],
educationPrinciples = [],
educationPrinciplesRef,
} = this.props;
+
return (
<div className="padded rule-desc">
{displayEducationalPrinciplesNotification && (
@@ -61,6 +67,7 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
<p className="little-spacer-bottom little-spacer-top">
{translate('coding_rules.more_info.notification_message')}
</p>
+
<ButtonLink
onClick={() => {
this.handleNotificationScroll();
@@ -70,10 +77,11 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
</ButtonLink>
</Alert>
)}
+
{sections.length > 0 && (
<>
<h2>{translate('coding_rules.more_info.resources.title')}</h2>
- <RuleDescription sections={sections} />
+ <RuleDescription language={language} sections={sections} />
</>
)}
@@ -82,11 +90,14 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
<h2 ref={educationPrinciplesRef}>
{translate('coding_rules.more_info.education_principles.title')}
</h2>
+
{educationPrinciples.map((key) => {
const Concept = EDUCATION_PRINCIPLES_MAP[key];
+
if (Concept === undefined) {
return null;
}
+
return (
<div key={key} className="education-principles big-spacer-top big-padded">
<Concept />
diff --git a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
index 9f8a6d41a41..7cd177c3fa1 100644
--- a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
@@ -17,8 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import styled from '@emotion/styled';
-import { FlagMessage, HtmlFormatter, themeBorder, themeColor, ToggleButton } from 'design-system';
+import {
+ CodeSyntaxHighlighter,
+ FlagMessage,
+ HtmlFormatter,
+ ToggleButton,
+ themeBorder,
+ themeColor,
+} from 'design-system';
import * as React from 'react';
import { RuleDescriptionSection } from '../../apps/coding-rules/rule';
import applyCodeDifferences from '../../helpers/code-difference';
@@ -29,9 +37,10 @@ import OtherContextOption from './OtherContextOption';
const OTHERS_KEY = 'others';
interface Props {
- sections: RuleDescriptionSection[];
- defaultContextKey?: string;
className?: string;
+ defaultContextKey?: string;
+ language?: string;
+ sections: RuleDescriptionSection[];
}
interface State {
@@ -102,13 +111,14 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
const { contexts } = this.state;
const selected = contexts.find((ctxt) => ctxt.displayName === value);
+
if (selected) {
this.setState({ selectedContext: selected });
}
};
render() {
- const { className, sections } = this.props;
+ const { className, language, sections } = this.props;
const { contexts, defaultContext, selectedContext } = this.state;
const options = contexts.map((ctxt) => ({
@@ -127,6 +137,7 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
<h2 className="sw-body-sm-highlight sw-mb-4">
{translate('coding_rules.description_context.title')}
</h2>
+
{defaultContext && (
<FlagMessage variant="info" className="sw-mb-4">
{translateWithParameters(
@@ -135,6 +146,7 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
)}
</FlagMessage>
)}
+
<div className="sw-mb-4">
<ToggleButton
label={translate('coding_rules.description_context.title')}
@@ -142,6 +154,7 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
options={options}
value={selectedContext.displayName}
/>
+
{selectedContext.key !== OTHERS_KEY && (
<h2>
{translateWithParameters(
@@ -151,12 +164,13 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
</h2>
)}
</div>
+
{selectedContext.key === OTHERS_KEY ? (
<OtherContextOption />
) : (
- <div
- /* eslint-disable-next-line react/no-danger */
- dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }}
+ <CodeSyntaxHighlighter
+ htmlAsString={sanitizeString(selectedContext.content)}
+ language={language}
/>
)}
</StyledHtmlFormatter>
@@ -169,11 +183,12 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
ref={(node: HTMLDivElement) => {
applyCodeDifferences(node);
}}
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: sanitizeString(sections[0].content),
- }}
- />
+ >
+ <CodeSyntaxHighlighter
+ htmlAsString={sanitizeString(sections[0].content)}
+ language={language}
+ />
+ </StyledHtmlFormatter>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx
index 33528e4f61c..fa7ea48c6b9 100644
--- a/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx
+++ b/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx
@@ -17,6 +17,7 @@
* 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 { cloneDeep, debounce, groupBy } from 'lodash';
import * as React from 'react';
@@ -82,7 +83,9 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
constructor(props: RuleTabViewerProps) {
super(props);
+
this.educationPrinciplesRef = React.createRef();
+
this.checkIfEducationPrinciplesAreVisible = debounce(
this.checkIfEducationPrinciplesAreVisible,
DEBOUNCE_FOR_SCROLL
@@ -96,6 +99,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
const tabs = this.computeTabs(Boolean(this.state.displayEducationalPrinciplesNotification));
const query = new URLSearchParams(this.props.location.search);
+
if (query.has('why')) {
this.setState({
selectedTab: tabs.find((tab) => tab.key === TabKeys.WhyIsThisAnIssue) ?? tabs[0],
@@ -112,6 +116,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
selectedFlowIndex,
selectedLocationIndex,
} = this.props;
+
const { selectedTab } = this.state;
if (
@@ -161,6 +166,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
ruleDetails.educationPrinciples.length > 0 &&
isLoggedIn &&
!dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
+
const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
return {
@@ -173,7 +179,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
const {
codeTabContent,
- ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
+ ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType },
ruleDescriptionContextKey,
extendedDescription,
activityTabContent,
@@ -193,8 +199,8 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
} else {
descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
{
- key: RuleDescriptionSections.RESOURCES,
content: extendedDescription,
+ key: RuleDescriptionSections.RESOURCES,
},
];
}
@@ -202,74 +208,78 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
const tabs: Tab[] = [
{
- key: TabKeys.WhyIsThisAnIssue,
- label:
- ruleType === 'SECURITY_HOTSPOT'
- ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
- : translate('coding_rules.description_section.title.root_cause'),
content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
<RuleDescription
className="padded"
+ defaultContextKey={ruleDescriptionContextKey}
+ language={ruleLanguage}
sections={
descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
}
- defaultContextKey={ruleDescriptionContextKey}
/>
),
+ key: TabKeys.WhyIsThisAnIssue,
+ label:
+ ruleType === 'SECURITY_HOTSPOT'
+ ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
+ : translate('coding_rules.description_section.title.root_cause'),
},
{
- key: TabKeys.AssessTheIssue,
- label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
<RuleDescription
className="padded"
+ language={ruleLanguage}
sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
/>
),
+ key: TabKeys.AssessTheIssue,
+ label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
},
{
- key: TabKeys.HowToFixIt,
- label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
<RuleDescription
className="padded"
- sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
defaultContextKey={ruleDescriptionContextKey}
+ language={ruleLanguage}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
/>
),
+ key: TabKeys.HowToFixIt,
+ label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
},
{
+ content: activityTabContent,
key: TabKeys.Activity,
label: translate('coding_rules.description_section.title', TabKeys.Activity),
- content: activityTabContent,
},
{
- key: TabKeys.MoreInfo,
- label: (
- <>
- {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
- {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
- </>
- ),
content: ((educationPrinciples && educationPrinciples.length > 0) ||
descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
<MoreInfoRuleDescription
- educationPrinciples={educationPrinciples}
- sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
+ educationPrinciples={educationPrinciples}
educationPrinciplesRef={this.educationPrinciplesRef}
+ language={ruleLanguage}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
/>
),
+ key: TabKeys.MoreInfo,
+ label: (
+ <>
+ {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
+ {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
+ </>
+ ),
},
];
if (codeTabContent !== undefined) {
tabs.unshift({
+ content: codeTabContent,
key: TabKeys.Code,
label: translate('issue.tabs', TabKeys.Code),
- content: codeTabContent,
});
}
@@ -342,14 +352,14 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
<ScreenPositionHelper>
{({ top }) => (
<div
+ aria-labelledby={getTabId(selectedTab.key)}
+ className="bordered display-flex-column"
+ id={getTabPanelId(selectedTab.key)}
+ role="tabpanel"
style={{
// We substract the footer height with padding (80) and the main layout padding (20)
maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial',
}}
- className="bordered display-flex-column"
- role="tabpanel"
- aria-labelledby={getTabId(selectedTab.key)}
- id={getTabPanelId(selectedTab.key)}
>
{
// Preserve tabs state by always rendering all of them. Only hide them when not selected
diff --git a/server/sonar-web/src/main/js/helpers/mocks/tasks.ts b/server/sonar-web/src/main/js/helpers/mocks/tasks.ts
index 96974fb3ca3..b15212b8d65 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/tasks.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/tasks.ts
@@ -17,6 +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 { uniqueId } from 'lodash';
import { ComponentQualifier } from '../../types/component';
import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks';
@@ -36,7 +38,7 @@ export function mockTask(overrides: Partial<Task> = {}): Task {
export function mockTaskWarning(overrides: Partial<TaskWarning> = {}): TaskWarning {
return {
- key: 'foo',
+ key: uniqueId('foo'),
message: 'Lorem ipsum',
dismissable: false,
...overrides,
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 0caa169d4e6..5c9ee44f84d 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -6173,6 +6173,9 @@ __metadata:
eslint-plugin-local-rules: 1.3.2
eslint-plugin-typescript-sort-keys: 2.3.0
highlight.js: 11.7.0
+ highlightjs-apex: 1.2.0
+ highlightjs-cobol: 0.3.3
+ highlightjs-sap-abap: 0.2.0
history: 5.3.0
jest: 29.5.0
postcss: 8.4.21
@@ -7877,6 +7880,30 @@ __metadata:
languageName: node
linkType: hard
+"highlightjs-apex@npm:1.2.0":
+ version: 1.2.0
+ resolution: "highlightjs-apex@npm:1.2.0"
+ checksum: d0e1543dbdfa156c0bb6da74afcb7aaf315fe211024246917c8f967075d6c239bcd408d6a667c324ea75b928a2d8495cb1b8d160b403894aea8c9a16c293f33a
+ languageName: node
+ linkType: hard
+
+"highlightjs-cobol@npm:0.3.3":
+ version: 0.3.3
+ resolution: "highlightjs-cobol@npm:0.3.3"
+ dependencies:
+ minimist: ">=1.2.6"
+ mkdirp: ^1.0.4
+ checksum: f59a694703f883ead2fbdf262f36eab583c8bbf64649e59d5de1e13a43e43979ad9030eab69e1cb08f7558ce60ca5cfd1fe42c7a38e34fd8b0baebf06df9118d
+ languageName: node
+ linkType: hard
+
+"highlightjs-sap-abap@npm:0.2.0":
+ version: 0.2.0
+ resolution: "highlightjs-sap-abap@npm:0.2.0"
+ checksum: 685293fc2de3b333d0166f7323d38b53b70a53a462851d075484826c7ee16fa09f4c24cda44c1c957525352e112ba0a83cefe37641ee65b62e1483be93c60019
+ languageName: node
+ linkType: hard
+
"history@npm:5.3.0":
version: 5.3.0
resolution: "history@npm:5.3.0"
@@ -9816,6 +9843,13 @@ __metadata:
languageName: node
linkType: hard
+"minimist@npm:>=1.2.6":
+ version: 1.2.8
+ resolution: "minimist@npm:1.2.8"
+ checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0
+ languageName: node
+ linkType: hard
+
"minimist@npm:^1.2.0, minimist@npm:^1.2.5":
version: 1.2.5
resolution: "minimist@npm:1.2.5"