]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22495 Render the Jupyter Notebook cells where the issue is & Add preview tab
author7PH <b.raymond@protonmail.com>
Wed, 17 Jul 2024 16:40:18 +0000 (18:40 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 13 Aug 2024 20:02:46 +0000 (20:02 +0000)
13 files changed:
server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts
server/sonar-web/src/main/js/api/sources.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
server/sonar-web/src/main/js/apps/issues/jupyter-notebook/JupyterNotebookIssueViewer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/jupyter-notebook/types.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/jupyter-notebook/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/test-utils.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx
server/sonar-web/src/main/js/queries/sources.ts
server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx
server/sonar-web/src/main/js/sonar-aligned/helpers/component.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 660eca474fef1eec519d33e4d99ae7a46230f64e..7a053b225947b5024b038cf3b86211ee1c4b4083 100644 (file)
@@ -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;
   };
 }
index fade6f713a1e5c33b34b8ce0757f2b0cfb3e6cb4..6c84d1c57ba535c611451a49dcbc8e7c2698b3fe 100644 (file)
@@ -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);
 }
index 4617899d3b13e8aa51afd37eba1d6e019f2f4d71..6481815bebf94154965e500af471da442cb97203 100644 (file)
  */
 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();
+    });
+  });
 });
index 14f01cde8602f6df579c819895e150b78e201da6..f26177e1e79c286fee534d92bb9c35922156d7b2 100644 (file)
  * 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 (file)
index 0000000..2bf80cd
--- /dev/null
@@ -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 (file)
index 0000000..67e4e0c
--- /dev/null
@@ -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 (file)
index 0000000..076cedd
--- /dev/null
@@ -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,
+  };
+}
index 160bf628d7941ef95d14ffd61f3e5502a24f72a5..3963c7fb4c253b2f4c934210693110caec7fb4a1 100644 (file)
@@ -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 = {
index b4238ca4cca37343332a89c089c7858d02aea9d6..1a1fa036e18a01634f51a69ad6ad504d28e065b1 100644 (file)
  * 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}`} />
+      ))}
     </>
   );
 }
index 1b2c217f14677b2e341ece95ad099891d0486274..2e74ff6510f3855ca157f83badc1c8f767a620ca 100644 (file)
@@ -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,
index 9407cf73e4390f941a372b191e9b87f25c047009..52e0a86f04d6fa6a603430ac5b5fbcaffda30b69 100644 (file)
  */
 
 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;
+}
index 0be481e46172cca9adf8bec95f4c920634de132c..dd16696c20074b002c3e96dddbb3c88e25aa84d1 100644 (file)
@@ -28,3 +28,7 @@ export function isPortfolioLike(
     componentQualifier === ComponentQualifier.SubPortfolio
   );
 }
+
+export function isJupyterNotebookFile(componentKey: string) {
+  return componentKey.endsWith('.ipynb');
+}
index 23de63d4da5aa9a0fc47b3dbe2ba7442ea225b49..ee1e16a539ca84b42abea27a95144bf4e4f06bbb 100644 (file)
@@ -175,6 +175,7 @@ password=Password
 path=Path
 permalink=Permanent Link
 plugin=Plugin
+preview=Preview
 previous=Previous
 previous_=previous
 previous_month_x=previous month {month}
@@ -1099,6 +1100,8 @@ issue.resolution.REMOVED=Removed
 issue.resolution.REMOVED.description=Either the rule or the resource was changed (removed, relocated, parameters changed, etc.) so that analysis no longer finds these issues.
 issue.unresolved.description=Unresolved issues have not been addressed in any way.
 
+issue.preview.jupyter_notebook.error=Error while loading the Jupyter notebook. Use the Code tab to see issue details.
+
 issue.action.permalink=Get permalink
 issue.line_affected=Line affected:
 issue.introduced=Introduced: