aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
author7PH <b.raymond@protonmail.com>2024-07-17 18:40:18 +0200
committersonartech <sonartech@sonarsource.com>2024-08-13 20:02:46 +0000
commit9dcda2987391f2cf2d5370e96202e9f6b07879b6 (patch)
treee0d2f6335fb496961a9dc855ed59998487ad277d /server
parentfe9c1ab62eec9e15f806c884f274c6d32a6c8f92 (diff)
downloadsonarqube-9dcda2987391f2cf2d5370e96202e9f6b07879b6.tar.gz
sonarqube-9dcda2987391f2cf2d5370e96202e9f6b07879b6.zip
SONAR-22495 Render the Jupyter Notebook cells where the issue is & Add preview tab
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts12
-rw-r--r--server/sonar-web/src/main/js/api/sources.ts5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx164
-rw-r--r--server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx124
-rw-r--r--server/sonar-web/src/main/js/apps/issues/jupyter-notebook/types.ts36
-rw-r--r--server/sonar-web/src/main/js/apps/issues/jupyter-notebook/utils.ts40
-rw-r--r--server/sonar-web/src/main/js/apps/issues/test-utils.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx26
-rw-r--r--server/sonar-web/src/main/js/queries/sources.ts5
-rw-r--r--server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx15
-rw-r--r--server/sonar-web/src/main/js/sonar-aligned/helpers/component.ts4
12 files changed, 417 insertions, 95 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts
index 660eca474fe..7a053b22594 100644
--- a/server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts
@@ -25,12 +25,20 @@ import { mockIpynbFile } from './data/sources';
jest.mock('../sources');
export default class SourcesServiceMock {
+ private source: string;
+
constructor() {
+ this.source = mockIpynbFile;
+
jest.mocked(getRawSource).mockImplementation(this.handleGetRawSource);
}
+ setSource(source: string) {
+ this.source = source;
+ }
+
handleGetRawSource = () => {
- return this.reply(mockIpynbFile);
+ return this.reply(this.source);
};
reply<T>(response: T): Promise<T> {
@@ -38,6 +46,6 @@ export default class SourcesServiceMock {
}
reset = () => {
- return this;
+ this.source = mockIpynbFile;
};
}
diff --git a/server/sonar-web/src/main/js/api/sources.ts b/server/sonar-web/src/main/js/api/sources.ts
index fade6f713a1..6c84d1c57ba 100644
--- a/server/sonar-web/src/main/js/api/sources.ts
+++ b/server/sonar-web/src/main/js/api/sources.ts
@@ -17,8 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { get, parseText, RequestData } from '../helpers/request';
+import { get, parseText } from '../helpers/request';
+import { BranchParameters } from '../sonar-aligned/types/branch-like';
-export function getRawSource(data: RequestData): Promise<string> {
+export function getRawSource(data: BranchParameters & { key: string }): Promise<string> {
return get('/api/sources/raw', data).then(parseText);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx
index 4617899d3b1..6481815bebf 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx
@@ -19,11 +19,17 @@
*/
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import { keyBy, times } from 'lodash';
import { byLabelText, byRole } from '~sonar-aligned/helpers/testSelector';
+import { PARENT_COMPONENT_KEY, RULE_1 } from '../../../api/mocks/data/ids';
+import { mockSnippetsByComponent } from '../../../helpers/mocks/sources';
+import { mockRawIssue } from '../../../helpers/testMocks';
+import { IssueStatus } from '../../../types/issues';
import {
componentsHandler,
issuesHandler,
renderProjectIssuesApp,
+ sourcesHandler,
usersHandler,
waitOnDataLoaded,
} from '../test-utils';
@@ -31,6 +37,7 @@ import {
beforeEach(() => {
issuesHandler.reset();
componentsHandler.reset();
+ sourcesHandler.reset();
usersHandler.reset();
window.scrollTo = jest.fn();
window.HTMLElement.prototype.scrollTo = jest.fn();
@@ -41,6 +48,9 @@ const ui = {
expandLinesAbove: byRole('button', { name: 'source_viewer.expand_above' }),
expandLinesBelow: byRole('button', { name: 'source_viewer.expand_below' }),
+ preview: byRole('radio', { name: 'preview' }),
+ code: byRole('radio', { name: 'code' }),
+
line1: byLabelText('source_viewer.line_X.1'),
line44: byLabelText('source_viewer.line_X.44'),
line45: byLabelText('source_viewer.line_X.45'),
@@ -52,6 +62,40 @@ const ui = {
),
};
+const JUPYTER_ISSUE = {
+ issue: mockRawIssue(false, {
+ key: 'some-issue',
+ component: `${PARENT_COMPONENT_KEY}:jpt.ipynb`,
+ message: 'Issue on Jupyter Notebook',
+ rule: RULE_1,
+ textRange: {
+ startLine: 1,
+ endLine: 1,
+ startOffset: 1142,
+ endOffset: 1144,
+ },
+ ruleDescriptionContextKey: 'spring',
+ ruleStatus: 'DEPRECATED',
+ quickFixAvailable: true,
+ tags: ['unused'],
+ project: 'org.sonarsource.javascript:javascript',
+ assignee: 'email1@sonarsource.com',
+ author: 'email3@sonarsource.com',
+ issueStatus: IssueStatus.Confirmed,
+ prioritizedRule: true,
+ }),
+ snippets: keyBy(
+ [
+ mockSnippetsByComponent(
+ 'jpt.ipynb',
+ PARENT_COMPONENT_KEY,
+ times(40, (i) => i + 20),
+ ),
+ ],
+ 'component.key',
+ ),
+};
+
describe('issues source viewer', () => {
it('should show source across components', async () => {
const user = userEvent.setup();
@@ -122,4 +166,39 @@ describe('issues source viewer', () => {
// eslint-disable-next-line jest-dom/prefer-in-document
expect(screen.getAllByRole('table')).toHaveLength(1);
});
+
+ describe('should render jupyter notebook issues correctly', () => {
+ it('should render error when jupyter issue can not be parsed', async () => {
+ issuesHandler.setIssueList([JUPYTER_ISSUE]);
+ sourcesHandler.setSource('{not a JSON file}');
+ renderProjectIssuesApp('project/issues?issues=some-issue&open=some-issue&id=myproject');
+ await waitOnDataLoaded();
+
+ // Preview tab should be shown
+ expect(ui.preview.get()).toBeChecked();
+ expect(ui.code.get()).toBeInTheDocument();
+
+ expect(
+ await screen.findByRole('button', { name: 'Issue on Jupyter Notebook' }),
+ ).toBeInTheDocument();
+
+ expect(screen.getByText('issue.preview.jupyter_notebook.error')).toBeInTheDocument();
+ });
+
+ it('should show preview tab when jupyter notebook issue', async () => {
+ issuesHandler.setIssueList([JUPYTER_ISSUE]);
+ renderProjectIssuesApp('project/issues?issues=some-issue&open=some-issue&id=myproject');
+ await waitOnDataLoaded();
+
+ // Preview tab should be shown
+ expect(ui.preview.get()).toBeChecked();
+ expect(ui.code.get()).toBeInTheDocument();
+
+ expect(
+ await screen.findByRole('button', { name: 'Issue on Jupyter Notebook' }),
+ ).toBeInTheDocument();
+
+ expect(screen.queryByText('issue.preview.jupyter_notebook.error')).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
index 14f01cde860..f26177e1e79 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
@@ -17,10 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ToggleButton } from 'design-system/lib';
import * as React from 'react';
+import { isJupyterNotebookFile } from '~sonar-aligned/helpers/component';
+import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { Issue } from '../../../types/types';
import CrossComponentSourceViewer from '../crossComponentSourceViewer/CrossComponentSourceViewer';
+import { JupyterNotebookIssueViewer } from '../jupyter-notebook/JupyterNotebookIssueViewer';
import { getLocations, getSelectedLocation } from '../utils';
import { IssueSourceViewerScrollContext } from './IssueSourceViewerScrollContext';
@@ -35,98 +39,114 @@ export interface IssuesSourceViewerProps {
selectedLocationIndex: number | undefined;
}
-export default class IssuesSourceViewer extends React.PureComponent<IssuesSourceViewerProps> {
- primaryLocationRef?: HTMLElement;
- selectedSecondaryLocationRef?: HTMLElement;
+export default function IssuesSourceViewer(props: Readonly<IssuesSourceViewerProps>) {
+ const {
+ openIssue,
+ selectedFlowIndex,
+ selectedLocationIndex,
+ locationsNavigator,
+ branchLike,
+ issues,
+ onIssueSelect,
+ onLocationSelect,
+ } = props;
- componentDidUpdate() {
- if (this.props.selectedLocationIndex === -1) {
- this.refreshScroll();
- }
- }
-
- registerPrimaryLocationRef = (ref: HTMLElement) => {
- this.primaryLocationRef = ref;
-
- if (ref) {
- this.refreshScroll();
- }
- };
+ const [primaryLocationRef, setPrimaryLocationRef] = React.useState<HTMLElement | null>(null);
+ const [selectedSecondaryLocationRef, setSelectedSecondaryLocationRef] =
+ React.useState<HTMLElement | null>(null);
- registerSelectedSecondaryLocationRef = (ref: HTMLElement) => {
- this.selectedSecondaryLocationRef = ref;
-
- if (ref) {
- this.refreshScroll();
- }
- };
-
- refreshScroll() {
- const { selectedLocationIndex } = this.props;
+ const isJupyterNotebook = isJupyterNotebookFile(openIssue.component);
+ const [tab, setTab] = React.useState(isJupyterNotebook ? 'preview' : 'code');
+ const refreshScroll = React.useCallback(() => {
if (
selectedLocationIndex !== undefined &&
selectedLocationIndex !== -1 &&
- this.selectedSecondaryLocationRef
+ selectedSecondaryLocationRef
) {
- this.selectedSecondaryLocationRef.scrollIntoView({
+ selectedSecondaryLocationRef.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
- } else if (this.primaryLocationRef) {
- this.primaryLocationRef.scrollIntoView({
+ } else if (primaryLocationRef) {
+ primaryLocationRef.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
}
+ }, [selectedSecondaryLocationRef, primaryLocationRef, selectedLocationIndex]);
+
+ function registerPrimaryLocationRef(ref: HTMLElement) {
+ setPrimaryLocationRef(ref);
+
+ if (ref) {
+ refreshScroll();
+ }
+ }
+
+ function registerSelectedSecondaryLocationRef(ref: HTMLElement) {
+ setSelectedSecondaryLocationRef(ref);
+
+ if (ref) {
+ refreshScroll();
+ }
}
- render() {
- const {
- openIssue,
- selectedFlowIndex,
- selectedLocationIndex,
- locationsNavigator,
- branchLike,
- issues,
- } = this.props;
+ React.useEffect(() => {
+ if (selectedLocationIndex === -1) {
+ refreshScroll();
+ }
+ }, [selectedLocationIndex, refreshScroll]);
- const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => {
- loc.index = index;
- return loc;
- });
+ const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => {
+ loc.index = index;
+ return loc;
+ });
- const selectedLocation = getSelectedLocation(
- openIssue,
- selectedFlowIndex,
- selectedLocationIndex,
- );
+ const selectedLocation = getSelectedLocation(openIssue, selectedFlowIndex, selectedLocationIndex);
- const highlightedLocationMessage =
- locationsNavigator && selectedLocationIndex !== undefined
- ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
- : undefined;
+ const highlightedLocationMessage =
+ locationsNavigator && selectedLocationIndex !== undefined
+ ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
+ : undefined;
- return (
- <IssueSourceViewerScrollContext.Provider
- value={{
- registerPrimaryLocationRef: this.registerPrimaryLocationRef,
- registerSelectedSecondaryLocationRef: this.registerSelectedSecondaryLocationRef,
- }}
- >
- <CrossComponentSourceViewer
- branchLike={branchLike}
- highlightedLocationMessage={highlightedLocationMessage}
- issue={openIssue}
- issues={issues}
- locations={locations}
- onIssueSelect={this.props.onIssueSelect}
- onLocationSelect={this.props.onLocationSelect}
- selectedFlowIndex={selectedFlowIndex}
- />
- </IssueSourceViewerScrollContext.Provider>
- );
- }
+ return (
+ <>
+ {isJupyterNotebook && (
+ <div className="sw-mb-2">
+ <ToggleButton
+ options={[
+ { label: translate('preview'), value: 'preview' },
+ { label: translate('code'), value: 'code' },
+ ]}
+ value={tab}
+ onChange={(value) => setTab(value)}
+ />
+ </div>
+ )}
+ {tab === 'code' ? (
+ <IssueSourceViewerScrollContext.Provider
+ value={{
+ registerPrimaryLocationRef,
+ registerSelectedSecondaryLocationRef,
+ }}
+ >
+ <CrossComponentSourceViewer
+ branchLike={branchLike}
+ highlightedLocationMessage={highlightedLocationMessage}
+ issue={openIssue}
+ issues={issues}
+ locations={locations}
+ onIssueSelect={onIssueSelect}
+ onLocationSelect={onLocationSelect}
+ selectedFlowIndex={selectedFlowIndex}
+ />
+ </IssueSourceViewerScrollContext.Provider>
+ ) : (
+ <JupyterNotebookIssueViewer branchLike={branchLike} issue={openIssue} />
+ )}
+ </>
+ );
}
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
new file mode 100644
index 00000000000..2bf80cdd209
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { INotebookContent } from '@jupyterlab/nbformat';
+import { Spinner } from '@sonarsource/echoes-react';
+import { FlagMessage, IssueMessageHighlighting, LineFinding } from 'design-system';
+import React, { useMemo } from 'react';
+import { JupyterCell } 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';
+import { useRawSourceQuery } from '../../../queries/sources';
+import { BranchLike } from '../../../types/branch-like';
+import { Issue } from '../../../types/types';
+import { JupyterNotebookCursorPath } from './types';
+import { pathToCursorInCell } from './utils';
+
+export interface JupyterNotebookIssueViewerProps {
+ branchLike?: BranchLike;
+ issue: Issue;
+}
+
+export function JupyterNotebookIssueViewer(props: Readonly<JupyterNotebookIssueViewerProps>) {
+ const { issue, branchLike } = props;
+ const { data, isLoading } = useRawSourceQuery({
+ key: issue.component,
+ ...getBranchLikeQuery(branchLike),
+ });
+ const [startOffset, setStartOffset] = React.useState<JupyterNotebookCursorPath | null>(null);
+ const [endPath, setEndPath] = React.useState<JupyterNotebookCursorPath | null>(null);
+
+ const jupyterNotebook = useMemo(() => {
+ if (typeof data !== 'string') {
+ return null;
+ }
+ try {
+ return JSON.parse(data) as INotebookContent;
+ } catch (error) {
+ return null;
+ }
+ }, [data]);
+
+ React.useEffect(() => {
+ if (typeof data !== 'string') {
+ return;
+ }
+
+ if (!issue.textRange) {
+ return;
+ }
+
+ const mapper = new JsonIssueMapper(data);
+ const start = mapper.lineOffsetToCursorPosition(
+ issue.textRange.startLine,
+ issue.textRange.startOffset,
+ );
+ const end = mapper.lineOffsetToCursorPosition(
+ issue.textRange.endLine,
+ issue.textRange.endOffset,
+ );
+ const startOffset = pathToCursorInCell(mapper.get(start));
+ const endOffset = pathToCursorInCell(mapper.get(end));
+ if (
+ startOffset &&
+ endOffset &&
+ startOffset.cell === endOffset.cell &&
+ startOffset.line === endOffset.line
+ ) {
+ setStartOffset(startOffset);
+ setEndPath(endOffset);
+ }
+ }, [issue, data]);
+
+ if (isLoading) {
+ return <Spinner />;
+ }
+
+ if (!jupyterNotebook || !startOffset || !endPath) {
+ return (
+ <FlagMessage className="sw-mt-2" variant="warning">
+ {translate('issue.preview.jupyter_notebook.error')}
+ </FlagMessage>
+ );
+ }
+
+ // Cells to display
+ const cells = Array.from(new Set([startOffset.cell, endPath.cell])).map(
+ (cellIndex) => jupyterNotebook.cells[cellIndex],
+ );
+
+ return (
+ <>
+ <LineFinding
+ issueKey={issue.key}
+ message={
+ <IssueMessageHighlighting
+ message={issue.message}
+ messageFormattings={issue.messageFormattings}
+ />
+ }
+ selected
+ />
+ {cells.map((cell, index) => (
+ <JupyterCell key={'cell-' + index} cell={cell} />
+ ))}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/types.ts b/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/types.ts
new file mode 100644
index 00000000000..67e4e0cfd64
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/types.ts
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.
+ */
+export interface JupyterNotebookOutput {
+ data: {
+ [key: string]: string | string[];
+ 'image/png': string;
+ 'text/html': string[];
+ 'text/plain': string[];
+ };
+ metadata: { [key: string]: string };
+ output_type: string;
+ text?: string[];
+}
+
+export interface JupyterNotebookCursorPath {
+ cell: number;
+ cursorOffset: number;
+ line: number;
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/utils.ts b/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/utils.ts
new file mode 100644
index 00000000000..076cedd2726
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/jupyter-notebook/utils.ts
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { PathToCursor } from '~sonar-aligned/helpers/json-issue-mapper';
+
+export function pathToCursorInCell(path: PathToCursor): {
+ cell: number;
+ cursorOffset: number;
+ line: number;
+} | null {
+ const [, cellEntry, , lineEntry, stringEntry] = path;
+ if (
+ cellEntry?.type !== 'array' ||
+ lineEntry?.type !== 'array' ||
+ stringEntry?.type !== 'string'
+ ) {
+ return null;
+ }
+ return {
+ cell: cellEntry.index,
+ line: lineEntry.index,
+ cursorOffset: stringEntry.index,
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
index 160bf628d79..3963c7fb4c2 100644
--- a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
@@ -24,6 +24,7 @@ import { byPlaceholderText, byRole, byTestId, byText } from '~sonar-aligned/help
import BranchesServiceMock from '../../api/mocks/BranchesServiceMock';
import ComponentsServiceMock from '../../api/mocks/ComponentsServiceMock';
import IssuesServiceMock from '../../api/mocks/IssuesServiceMock';
+import SourcesServiceMock from '../../api/mocks/SourcesServiceMock';
import UsersServiceMock from '../../api/mocks/UsersServiceMock';
import { mockComponent } from '../../helpers/mocks/component';
import { mockCurrentUser } from '../../helpers/testMocks';
@@ -42,6 +43,7 @@ import { projectIssuesRoutes } from './routes';
export const usersHandler = new UsersServiceMock();
export const issuesHandler = new IssuesServiceMock(usersHandler);
export const componentsHandler = new ComponentsServiceMock();
+export const sourcesHandler = new SourcesServiceMock();
export const branchHandler = new BranchesServiceMock();
export const ui = {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx
index b4238ca4cca..1a1fa036e18 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx
@@ -18,17 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ICell, isCode, isMarkdown } from '@jupyterlab/nbformat';
+import { ICell } from '@jupyterlab/nbformat';
import { Spinner } from '@sonarsource/echoes-react';
import { FlagMessage } from 'design-system/lib';
import React from 'react';
-import {
- JupyterCodeCell,
- JupyterMarkdownCell,
-} from '~sonar-aligned/components/SourceViewer/JupyterNotebookViewer';
+import { JupyterCell } from '~sonar-aligned/components/SourceViewer/JupyterNotebookViewer';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { translate } from '../../helpers/l10n';
-import { omitNil } from '../../helpers/request';
import { useRawSourceQuery } from '../../queries/sources';
import { BranchLike } from '../../types/branch-like';
@@ -40,9 +36,10 @@ export interface Props {
export default function SourceViewerPreview(props: Readonly<Props>) {
const { component, branchLike } = props;
- const { data, isLoading } = useRawSourceQuery(
- omitNil({ key: component, ...getBranchLikeQuery(branchLike) }),
- );
+ const { data, isLoading } = useRawSourceQuery({
+ key: component,
+ ...getBranchLikeQuery(branchLike),
+ });
if (isLoading) {
return <Spinner isLoading={isLoading} />;
@@ -60,14 +57,9 @@ export default function SourceViewerPreview(props: Readonly<Props>) {
return (
<>
- {jupyterFile.cells.map((cell: ICell, index: number) => {
- if (isCode(cell)) {
- return <JupyterCodeCell cell={cell} key={`${cell.cell_type}-${index}`} />;
- } else if (isMarkdown(cell)) {
- return <JupyterMarkdownCell cell={cell} key={`${cell.cell_type}-${index}`} />;
- }
- return null;
- })}
+ {jupyterFile.cells.map((cell: ICell, index: number) => (
+ <JupyterCell cell={cell} key={`${cell.cell_type}-${index}`} />
+ ))}
</>
);
}
diff --git a/server/sonar-web/src/main/js/queries/sources.ts b/server/sonar-web/src/main/js/queries/sources.ts
index 1b2c217f146..2e74ff6510f 100644
--- a/server/sonar-web/src/main/js/queries/sources.ts
+++ b/server/sonar-web/src/main/js/queries/sources.ts
@@ -20,6 +20,7 @@
import { useQuery } from '@tanstack/react-query';
import { getRawSource } from '../api/sources';
import { RequestData } from '../helpers/request';
+import { BranchParameters } from '../sonar-aligned/types/branch-like';
function getIssuesQueryKey(data: RequestData) {
return ['issues', JSON.stringify(data ?? '')];
@@ -30,10 +31,10 @@ function fetchRawSources({ queryKey: [, query] }: { queryKey: string[] }) {
return null;
}
- return getRawSource(JSON.parse(query));
+ return getRawSource(JSON.parse(query) as BranchParameters & { key: string });
}
-export function useRawSourceQuery(data: RequestData) {
+export function useRawSourceQuery(data: BranchParameters & { key: string }) {
return useQuery({
queryKey: getIssuesQueryKey(data),
queryFn: fetchRawSources,
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 9407cf73e43..52e0a86f04d 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
@@ -19,11 +19,14 @@
*/
import {
+ ICell,
ICodeCell,
IMarkdownCell,
IOutput,
+ isCode,
isDisplayData,
isExecuteResult,
+ isMarkdown,
isStream,
} from '@jupyterlab/nbformat';
import { CodeSnippet } from 'design-system/lib';
@@ -88,3 +91,15 @@ export function JupyterCodeCell({ cell }: Readonly<{ cell: ICodeCell }>) {
</div>
);
}
+
+export function JupyterCell({ cell }: Readonly<{ cell: ICell }>) {
+ if (isCode(cell)) {
+ return <JupyterCodeCell cell={cell} />;
+ }
+
+ if (isMarkdown(cell)) {
+ return <JupyterMarkdownCell cell={cell} />;
+ }
+
+ return null;
+}
diff --git a/server/sonar-web/src/main/js/sonar-aligned/helpers/component.ts b/server/sonar-web/src/main/js/sonar-aligned/helpers/component.ts
index 0be481e4617..dd16696c200 100644
--- a/server/sonar-web/src/main/js/sonar-aligned/helpers/component.ts
+++ b/server/sonar-web/src/main/js/sonar-aligned/helpers/component.ts
@@ -28,3 +28,7 @@ export function isPortfolioLike(
componentQualifier === ComponentQualifier.SubPortfolio
);
}
+
+export function isJupyterNotebookFile(componentKey: string) {
+ return componentKey.endsWith('.ipynb');
+}