aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author7PH <benjamin.raymond@sonarsource.com>2024-07-29 12:45:24 +0200
committersonartech <sonartech@sonarsource.com>2024-08-13 20:02:47 +0000
commit6428ede698941f0627f5e6c1c65e4ec6f0a320d1 (patch)
tree5491c23217019700cda0a44388c1c959dc5f2840
parent176045866b4d946f1ff764719d29024de10c261c (diff)
downloadsonarqube-6428ede698941f0627f5e6c1c65e4ec6f0a320d1.tar.gz
sonarqube-6428ede698941f0627f5e6c1c65e4ec6f0a320d1.zip
SONAR-22495 Show issue red message box below its exact location in the code snippet
-rw-r--r--server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx134
-rw-r--r--server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx5
-rw-r--r--server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx12
-rw-r--r--server/sonar-web/tailwind.base.config.js1
4 files changed, 90 insertions, 62 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx b/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx
index 66e5f7bd168..fa94574436b 100644
--- a/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx
@@ -17,7 +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 { ICell, INotebookContent } from '@jupyterlab/nbformat';
+import { ICodeCell, INotebookContent, isCode } from '@jupyterlab/nbformat';
import { Spinner } from '@sonarsource/echoes-react';
import {
FlagMessage,
@@ -26,7 +26,7 @@ import {
LineFinding,
} from 'design-system';
import React from 'react';
-import { JupyterCell } from '~sonar-aligned/components/SourceViewer/JupyterNotebookViewer';
+import { JupyterCodeCell } from '~sonar-aligned/components/SourceViewer/JupyterNotebookViewer';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { JsonIssueMapper } from '~sonar-aligned/helpers/json-issue-mapper';
import { translate } from '../../../helpers/l10n';
@@ -46,7 +46,10 @@ export function JupyterNotebookIssueViewer(props: Readonly<JupyterNotebookIssueV
key: issue.component,
...getBranchLikeQuery(branchLike),
});
- const [renderedCells, setRenderedCells] = React.useState<ICell[] | null>(null);
+ const [renderedCells, setRenderedCells] = React.useState<{
+ after: ICodeCell;
+ before: ICodeCell[];
+ } | null>(null);
React.useEffect(() => {
if (!issue.textRange || typeof data !== 'string') {
@@ -62,59 +65,67 @@ export function JupyterNotebookIssueViewer(props: Readonly<JupyterNotebookIssueV
}
const mapper = new JsonIssueMapper(data);
- const start = mapper.lineOffsetToCursorPosition(
- issue.textRange.startLine,
- issue.textRange.startOffset,
+ const startOffset = pathToCursorInCell(
+ mapper.get(
+ mapper.lineOffsetToCursorPosition(issue.textRange.startLine, issue.textRange.startOffset),
+ ),
);
- const end = mapper.lineOffsetToCursorPosition(
- issue.textRange.endLine,
- issue.textRange.endOffset,
+ const endOffset = pathToCursorInCell(
+ mapper.get(
+ mapper.lineOffsetToCursorPosition(issue.textRange.endLine, issue.textRange.endOffset),
+ ),
);
- const startOffset = pathToCursorInCell(mapper.get(start));
- const endOffset = pathToCursorInCell(mapper.get(end));
if (!startOffset || !endOffset) {
setRenderedCells(null);
return;
}
- if (startOffset.cell === endOffset.cell) {
- const startCell = jupyterNotebook.cells[startOffset.cell];
- startCell.source = Array.isArray(startCell.source) ? startCell.source : [startCell.source];
- startCell.source = hljsUnderlinePlugin.tokenize(startCell.source, [
- {
- start: startOffset,
- end: endOffset,
- },
- ]);
- } else {
- // Each cell is a separate code block, so we have to underline them separately
- // We underilne the first cell from the start offset to the end of the cell, and the last cell from the start of the cell to the end offset
- const startCell = jupyterNotebook.cells[startOffset.cell];
- startCell.source = Array.isArray(startCell.source) ? startCell.source : [startCell.source];
- startCell.source = hljsUnderlinePlugin.tokenize(startCell.source, [
- {
- start: startOffset,
- end: {
- line: startCell.source.length - 1,
- cursorOffset: startCell.source[startCell.source.length - 1].length,
- },
- },
- ]);
- const endCell = jupyterNotebook.cells[endOffset.cell];
- endCell.source = Array.isArray(endCell.source) ? endCell.source : [endCell.source];
- endCell.source = hljsUnderlinePlugin.tokenize(endCell.source, [
+ // When the primary location spans over multiple cells, we show all cells that are part of the range
+ const cells: ICodeCell[] = jupyterNotebook.cells
+ .slice(startOffset.cell, endOffset.cell + 1)
+ .filter((cell) => isCode(cell));
+
+ // Split the last cell because we want to show the issue message at the end of the primary location
+ const sourceBefore = cells[cells.length - 1].source.slice(0, endOffset.line + 1);
+ const sourceAfter = cells[cells.length - 1].source.slice(endOffset.line + 1);
+ const lastCell = {
+ ...cells[cells.length - 1],
+ source: sourceAfter,
+ };
+ cells[cells.length - 1] = {
+ cell_type: 'code',
+ source: sourceBefore,
+ execution_count: 0,
+ outputs: [],
+ metadata: {},
+ };
+
+ for (let i = 0; i < cells.length; i++) {
+ const cell = cells[i];
+ cell.source = Array.isArray(cell.source) ? cell.source : [cell.source];
+
+ // Any cell between the first and last cell should be fully underlined
+ const start = i === 0 ? startOffset : { line: 0, cursorOffset: 0 };
+ const end =
+ i === cells.length - 1
+ ? endOffset
+ : {
+ line: cell.source.length - 1,
+ cursorOffset: cell.source[cell.source.length - 1].length,
+ };
+
+ cell.source = hljsUnderlinePlugin.tokenize(cell.source, [
{
- start: { line: 0, cursorOffset: 0 },
- end: endOffset,
+ start,
+ end,
},
]);
}
- const cells = Array.from(new Set([startOffset.cell, endOffset.cell])).map(
- (cellIndex) => jupyterNotebook.cells[cellIndex],
- );
-
- setRenderedCells(cells);
+ setRenderedCells({
+ before: cells,
+ after: lastCell,
+ });
}, [issue, data]);
if (isLoading) {
@@ -131,19 +142,30 @@ export function JupyterNotebookIssueViewer(props: Readonly<JupyterNotebookIssueV
return (
<>
- <LineFinding
- issueKey={issue.key}
- message={
- <IssueMessageHighlighting
- message={issue.message}
- messageFormattings={issue.messageFormattings}
- />
- }
- selected
- />
- {renderedCells.map((cell, index) => (
- <JupyterCell key={'cell-' + index} cell={cell} />
+ {renderedCells.before.map((cell, index) => (
+ <JupyterCodeCell
+ key={'cell-' + index}
+ source={cell.source as string[]}
+ className={index === renderedCells.before.length - 1 ? '-sw-mb-6 sw-relative' : undefined}
+ />
))}
+ <div className="-sw-mt-4 -sw-mb-4 -sw-ml-1 sw-mr-5 sw-relative sw-z-normal">
+ <LineFinding
+ issueKey={issue.key}
+ message={
+ <IssueMessageHighlighting
+ message={issue.message}
+ messageFormattings={issue.messageFormattings}
+ />
+ }
+ selected
+ />
+ </div>
+ <JupyterCodeCell
+ className="-sw-mt-6 sw-relative"
+ source={renderedCells.after.source as string[]}
+ outputs={renderedCells.after.outputs}
+ />
</>
);
}
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 b43525899e8..c1ccaa506e1 100644
--- a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx
+++ b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx
@@ -356,7 +356,10 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
// See IssueGuide for an explanation on why we want top > 0.
run={top > 0}
/>
- <StyledHeader headerHeight={this.headerNode?.clientHeight ?? 0} className="sw-z-normal">
+ <StyledHeader
+ headerHeight={this.headerNode?.clientHeight ?? 0}
+ className="sw-z-issue-header"
+ >
<div className="sw-p-6 sw-pb-4" ref={(node) => (this.headerNode = node)}>
<IssueHeader
issue={issue}
diff --git a/server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx b/server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx
index a33fd67a171..180566a7ad9 100644
--- a/server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx
+++ b/server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx
@@ -28,6 +28,7 @@ import {
isMarkdown,
isStream,
} from '@jupyterlab/nbformat';
+import classNames from 'classnames';
import { CodeSnippet } from 'design-system/lib';
import { isArray } from 'lodash';
import React from 'react';
@@ -74,12 +75,13 @@ function CellOutput({ output }: Readonly<{ output: IOutput }>) {
return null;
}
-export function JupyterCodeCell({
- source,
- outputs,
-}: Readonly<{ outputs: IOutput[]; source: string[] }>) {
+export function JupyterCodeCell(
+ props: Readonly<{ className?: string; outputs?: IOutput[]; source: string[] }>,
+) {
+ const { source, outputs, className } = props;
+
return (
- <div className="sw-m-4 sw-ml-0">
+ <div className={classNames('sw-m-4 sw-ml-0', className)}>
<div>
<CodeSnippet language="python" noCopy snippet={source.join('')} wrap className="sw-p-4" />
</div>
diff --git a/server/sonar-web/tailwind.base.config.js b/server/sonar-web/tailwind.base.config.js
index 8f73e0c950b..3580352def3 100644
--- a/server/sonar-web/tailwind.base.config.js
+++ b/server/sonar-web/tailwind.base.config.js
@@ -104,6 +104,7 @@ module.exports = {
},
zIndex: {
normal: '2',
+ 'issue-header': '10',
'project-list-header': '30',
filterbar: '50',
'content-popup': '52',