diff options
author | 7PH <b.raymond@protonmail.com> | 2024-07-17 18:40:18 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-08-13 20:02:46 +0000 |
commit | 9dcda2987391f2cf2d5370e96202e9f6b07879b6 (patch) | |
tree | e0d2f6335fb496961a9dc855ed59998487ad277d /server | |
parent | fe9c1ab62eec9e15f806c884f274c6d32a6c8f92 (diff) | |
download | sonarqube-9dcda2987391f2cf2d5370e96202e9f6b07879b6.tar.gz sonarqube-9dcda2987391f2cf2d5370e96202e9f6b07879b6.zip |
SONAR-22495 Render the Jupyter Notebook cells where the issue is & Add preview tab
Diffstat (limited to 'server')
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'); +} |