aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorLucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com>2024-07-22 12:25:55 +0300
committersonartech <sonartech@sonarsource.com>2024-08-13 20:02:46 +0000
commitfe9c1ab62eec9e15f806c884f274c6d32a6c8f92 (patch)
tree43ecda108f514bb3b1d54c8619a64ffa41e5075d /server/sonar-web/src
parente6adb0980a1db8a356a7283c240a95b01a90a472 (diff)
downloadsonarqube-fe9c1ab62eec9e15f806c884f274c6d32a6c8f92.tar.gz
sonarqube-fe9c1ab62eec9e15f806c884f274c6d32a6c8f92.zip
SONAR-22499 CodeViewer supports ipynb files (#11371)
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts43
-rw-r--r--server/sonar-web/src/main/js/api/mocks/data/sources.ts87
-rw-r--r--server/sonar-web/src/main/js/api/sources.ts24
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts69
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx43
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx73
-rw-r--r--server/sonar-web/src/main/js/queries/sources.ts41
-rw-r--r--server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx90
8 files changed, 467 insertions, 3 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
new file mode 100644
index 00000000000..660eca474fe
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/mocks/SourcesServiceMock.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { cloneDeep } from 'lodash';
+import { getRawSource } from '../sources';
+import { mockIpynbFile } from './data/sources';
+
+jest.mock('../sources');
+
+export default class SourcesServiceMock {
+ constructor() {
+ jest.mocked(getRawSource).mockImplementation(this.handleGetRawSource);
+ }
+
+ handleGetRawSource = () => {
+ return this.reply(mockIpynbFile);
+ };
+
+ reply<T>(response: T): Promise<T> {
+ return Promise.resolve(cloneDeep(response));
+ }
+
+ reset = () => {
+ return this;
+ };
+}
diff --git a/server/sonar-web/src/main/js/api/mocks/data/sources.ts b/server/sonar-web/src/main/js/api/mocks/data/sources.ts
new file mode 100644
index 00000000000..d77794ecf00
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/mocks/data/sources.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 const mockIpynbFile = JSON.stringify({
+ cells: [
+ {
+ cell_type: 'markdown',
+ metadata: {},
+ source: ['# Learning a cosine with keras'],
+ },
+ {
+ cell_type: 'code',
+ execution_count: 2,
+ metadata: {
+ collapsed: false,
+ jupyter: {
+ outputs_hidden: false,
+ },
+ },
+ outputs: [
+ {
+ name: 'stdout',
+ output_type: 'stream',
+ text: ['(7500,)\n', '(2500,)\n'],
+ },
+ ],
+ source: [
+ 'import numpy as np\n',
+ 'import sklearn.cross_validation as skcv\n',
+ '#x = np.linspace(0, 5*np.pi, num=10000, dtype=np.float32)\n',
+ 'x = np.linspace(0, 4*np.pi, num=10000, dtype=np.float32)\n',
+ 'y = np.cos(x)\n',
+ '\n',
+ 'train, test = skcv.train_test_split(np.arange(x.shape[0]))\n',
+ 'print train.shape\n',
+ 'print test.shape',
+ ],
+ },
+ {
+ cell_type: 'code',
+ execution_count: 3,
+ metadata: {
+ collapsed: false,
+ jupyter: {
+ outputs_hidden: false,
+ },
+ },
+ outputs: [
+ {
+ data: {
+ 'text/plain': ['[<matplotlib.lines.Line2D at 0x7fb588176b90>]'],
+ },
+ execution_count: 3,
+ metadata: {},
+ output_type: 'execute_result',
+ },
+ {
+ data: {
+ 'image/png':
+ 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAG0lEQVR4nGIJn1mo28/GzPDiV+yTNYAAAAD//yPBBfrGshAGAAAAAElFTkSuQmCC',
+ 'text/plain': ['<matplotlib.figure.Figure at 0x7fb58e57c850>'],
+ },
+ metadata: {},
+ output_type: 'display_data',
+ },
+ ],
+ source: ['import pylab as pl\n', '%matplotlib inline\n', 'pl.plot(x, y)'],
+ },
+ ],
+});
diff --git a/server/sonar-web/src/main/js/api/sources.ts b/server/sonar-web/src/main/js/api/sources.ts
new file mode 100644
index 00000000000..fade6f713a1
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/sources.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { get, parseText, RequestData } from '../helpers/request';
+
+export function getRawSource(data: RequestData): Promise<string> {
+ return get('/api/sources/raw', data).then(parseText);
+}
diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
index e19565fd7c4..156bbafe15b 100644
--- a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
+++ b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
@@ -27,9 +27,11 @@ import { MetricKey } from '~sonar-aligned/types/metrics';
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 { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants';
import { isDiffMetric } from '../../../helpers/measures';
import { mockComponent } from '../../../helpers/mocks/component';
+import { mockSourceLine, mockSourceViewerFile } from '../../../helpers/mocks/sources';
import { mockMeasure } from '../../../helpers/testMocks';
import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
import { Component } from '../../../types/types';
@@ -54,6 +56,7 @@ const originalScrollTo = window.scrollTo;
const branchesHandler = new BranchesServiceMock();
const componentsHandler = new ComponentsServiceMock();
+const sourcesHandler = new SourcesServiceMock();
const issuesHandler = new IssuesServiceMock();
beforeAll(() => {
@@ -75,6 +78,7 @@ afterAll(() => {
beforeEach(() => {
branchesHandler.reset();
componentsHandler.reset();
+ sourcesHandler.reset();
issuesHandler.reset();
});
@@ -138,7 +142,7 @@ it('should behave correctly when using search', async () => {
expect(await ui.searchResult(/folderA/).find()).toBeInTheDocument();
});
-it('should correcly handle long lists of components', async () => {
+it('should correctly handle long lists of components', async () => {
const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
componentsHandler.registerComponentTree({
component,
@@ -434,6 +438,56 @@ it('should correctly show new VS overall measures for Portfolios', async () => {
});
});
+it('should render correctly for ipynb files', async () => {
+ const component = mockComponent({
+ ...componentsHandler.findComponentTree('foo')?.component,
+ qualifier: ComponentQualifier.Project,
+ canBrowseAllChildProjects: true,
+ });
+ componentsHandler.sourceFiles = [
+ {
+ component: mockSourceViewerFile('file0.ipynb', 'foo'),
+ lines: times(1, (n) =>
+ mockSourceLine({
+ line: n,
+ code: 'function Test() {}',
+ }),
+ ),
+ },
+ ];
+ componentsHandler.registerComponentTree({
+ component,
+ ancestors: [],
+ children: times(1, (n) => ({
+ component: mockComponent({
+ key: `foo:file${n}.ipynb`,
+ name: `file${n}.ipynb`,
+ qualifier: ComponentQualifier.File,
+ }),
+ ancestors: [component],
+ children: [],
+ })),
+ });
+ const ui = getPageObject(userEvent.setup());
+ renderCode({ component });
+
+ await ui.appLoaded();
+
+ await ui.clickOnChildComponent(/ipynb$/);
+
+ expect(ui.previewToggle.get()).toBeInTheDocument();
+ expect(ui.previewToggleOption().get()).toBeChecked();
+ expect(ui.previewMarkdown.get()).toBeInTheDocument();
+ expect(ui.previewCode.get()).toBeInTheDocument();
+ expect(ui.previewOutputImage.get()).toBeInTheDocument();
+ expect(ui.previewOutputText.get()).toBeInTheDocument();
+ expect(ui.previewOutputStream.get()).toBeInTheDocument();
+
+ await ui.clickToggleCode();
+
+ expect(ui.sourceCode.get()).toBeInTheDocument();
+});
+
function getPageObject(user: UserEvent) {
const ui = {
componentName: (name: string) => byText(name),
@@ -442,8 +496,18 @@ function getPageObject(user: UserEvent) {
componentIsEmptyTxt: (qualifier: ComponentQualifier) =>
byText(`code_viewer.no_source_code_displayed_due_to_empty_analysis.${qualifier}`),
searchInput: byRole('searchbox'),
+ previewToggle: byRole('radiogroup'),
+ previewToggleOption: (name: string = 'Preview') =>
+ byRole('radio', {
+ name,
+ }),
noResultsTxt: byText('no_results'),
sourceCode: byText('function Test() {}'),
+ previewCode: byText('numpy', { exact: false }),
+ previewMarkdown: byText('Learning a cosine with keras'),
+ previewOutputImage: byRole('img', { name: 'source_viewer.jupyter.output.image' }),
+ previewOutputText: byText('[<matplotlib.lines.Line2D at 0x7fb588176b90>]'),
+ previewOutputStream: byText('(7500,) (2500,)'),
notAccessToAllChildrenTxt: byText('code_viewer.not_all_measures_are_shown'),
showingOutOfTxt: (x: number, y: number) => byText(`x_of_y_shown.${x}.${y}`),
newCodeBtn: byRole('radio', { name: 'projects.view.new_code' }),
@@ -480,6 +544,9 @@ function getPageObject(user: UserEvent) {
async clickOnChildComponent(name: string | RegExp) {
await user.click(screen.getByRole('link', { name }));
},
+ async clickToggleCode() {
+ await user.click(ui.previewToggleOption('Code').get());
+ },
async appLoaded(name = 'Foo') {
await waitFor(() => {
expect(ui.componentName(name).get()).toBeInTheDocument();
diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
index 2e23d1a35e3..219423b9c49 100644
--- a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
@@ -17,10 +17,12 @@
* 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 { Location } from '~sonar-aligned/types/router';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
+import SourceViewerPreview from '../../../components/SourceViewer/SourceViewerPreview';
import { BranchLike } from '../../../types/branch-like';
import { Measure } from '../../../types/types';
@@ -31,8 +33,18 @@ export interface SourceViewerWrapperProps {
location: Location;
}
+const PREVIEW_MODE_SUPPORTED_EXTENSIONS = ['ipynb'];
+
function SourceViewerWrapper(props: SourceViewerWrapperProps) {
const { branchLike, component, componentMeasures, location } = props;
+
+ const isPreviewSupported = React.useMemo(
+ () => PREVIEW_MODE_SUPPORTED_EXTENSIONS.includes(component.split('.').pop() ?? ''),
+ [component],
+ );
+
+ const [tab, setTab] = React.useState('preview');
+
const { line } = location.query;
const finalLine = line ? Number(line) : undefined;
@@ -45,7 +57,34 @@ function SourceViewerWrapper(props: SourceViewerWrapperProps) {
}
}, [line]);
- return (
+ return isPreviewSupported ? (
+ <>
+ <div className="sw-mb-4">
+ <ToggleButton
+ options={[
+ { label: 'Preview', value: 'preview' },
+ { label: 'Code', value: 'code' },
+ ]}
+ value={tab}
+ onChange={(value) => setTab(value)}
+ />
+ </div>
+
+ {tab === 'preview' ? (
+ <SourceViewerPreview branchLike={branchLike} component={component} />
+ ) : (
+ <SourceViewer
+ aroundLine={finalLine}
+ branchLike={branchLike}
+ component={component}
+ componentMeasures={componentMeasures}
+ highlightedLine={finalLine}
+ onLoaded={handleLoaded}
+ showMeasures
+ />
+ )}
+ </>
+ ) : (
<SourceViewer
aroundLine={finalLine}
branchLike={branchLike}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx
new file mode 100644
index 00000000000..b4238ca4cca
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerPreview.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 { ICell, isCode, isMarkdown } 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 { 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';
+
+export interface Props {
+ branchLike: BranchLike | undefined;
+ component: string;
+}
+
+export default function SourceViewerPreview(props: Readonly<Props>) {
+ const { component, branchLike } = props;
+
+ const { data, isLoading } = useRawSourceQuery(
+ omitNil({ key: component, ...getBranchLikeQuery(branchLike) }),
+ );
+
+ if (isLoading) {
+ return <Spinner isLoading={isLoading} />;
+ }
+
+ if (typeof data !== 'string') {
+ return (
+ <FlagMessage className="sw-mt-2" variant="warning">
+ {translate('component_viewer.no_component')}
+ </FlagMessage>
+ );
+ }
+
+ const jupyterFile: { cells: ICell[] } = JSON.parse(data);
+
+ 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;
+ })}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/queries/sources.ts b/server/sonar-web/src/main/js/queries/sources.ts
new file mode 100644
index 00000000000..1b2c217f146
--- /dev/null
+++ b/server/sonar-web/src/main/js/queries/sources.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { useQuery } from '@tanstack/react-query';
+import { getRawSource } from '../api/sources';
+import { RequestData } from '../helpers/request';
+
+function getIssuesQueryKey(data: RequestData) {
+ return ['issues', JSON.stringify(data ?? '')];
+}
+
+function fetchRawSources({ queryKey: [, query] }: { queryKey: string[] }) {
+ if (typeof query !== 'string') {
+ return null;
+ }
+
+ return getRawSource(JSON.parse(query));
+}
+
+export function useRawSourceQuery(data: RequestData) {
+ 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
new file mode 100644
index 00000000000..9407cf73e43
--- /dev/null
+++ b/server/sonar-web/src/main/js/sonar-aligned/components/SourceViewer/JupyterNotebookViewer.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 {
+ ICodeCell,
+ IMarkdownCell,
+ IOutput,
+ isDisplayData,
+ isExecuteResult,
+ isStream,
+} from '@jupyterlab/nbformat';
+import { CodeSnippet } from 'design-system/lib';
+import { isArray } from 'lodash';
+import React from 'react';
+import Markdown from 'react-markdown';
+import { Image } from '../../../components/common/Image';
+import { translate } from '../../../helpers/l10n';
+
+export function JupyterMarkdownCell({ cell }: Readonly<{ cell: IMarkdownCell }>) {
+ const markdown = isArray(cell.source) ? cell.source.join('') : cell.source;
+ return (
+ <div className="sw-m-4 sw-ml-0">
+ <Markdown>{markdown}</Markdown>
+ </div>
+ );
+}
+
+function CellOutput({ output }: Readonly<{ output: IOutput }>) {
+ if (isExecuteResult(output) || isDisplayData(output)) {
+ const components = Object.entries(output.data).map(([mimeType, dataValue], index) => {
+ if (mimeType === 'image/png') {
+ return (
+ <Image
+ src={`data:image/png;base64,${dataValue}`}
+ alt={translate('source_viewer.jupyter.output.image')}
+ key={`${mimeType}_${index}`}
+ />
+ );
+ } else if (mimeType === 'text/plain') {
+ const bundle = isArray(dataValue) ? dataValue.join('') : dataValue;
+
+ return (
+ <pre key={`${mimeType}_${index}`}>
+ {typeof bundle === 'string' ? bundle : JSON.stringify(bundle)}
+ </pre>
+ );
+ }
+ return null;
+ });
+ return components;
+ } else if (isStream(output)) {
+ const text = isArray(output.text) ? output.text.join('') : output.text;
+ return <pre>{text}</pre>;
+ }
+ return null;
+}
+
+export function JupyterCodeCell({ cell }: Readonly<{ cell: ICodeCell }>) {
+ const snippet = isArray(cell.source) ? cell.source.join('') : cell.source;
+
+ return (
+ <div className="sw-m-4 sw-ml-0">
+ <div>
+ <CodeSnippet language="python" noCopy snippet={snippet} wrap className="sw-p-4" />
+ </div>
+ <div>
+ {cell.outputs?.map((output: IOutput, outputIndex: number) => (
+ <CellOutput key={`${cell.cell_type}-output-${outputIndex}`} output={output} />
+ ))}
+ </div>
+ </div>
+ );
+}