Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
jlpm exec lint-staged
yarn exec lint-staged
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@deepnote:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ node_modules
!/package.json
jupyterlab_deepnote
.venv
coverage
**/*.ts
**/*.tsx
**/*.js
**/*.jsx
139 changes: 139 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": { "ignoreUnknown": false },
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 120,
"attributePosition": "auto",
"bracketSameLine": false,
"bracketSpacing": true,
"expand": "auto",
"useEditorconfig": true,
"includes": ["**", "!./coverage", "!./dist", "!**/package.json"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"recommended": true
},
"complexity": {
"recommended": true,
"noBannedTypes": "error",
"noUselessTypeConstraint": "error",
"noCommaOperator": "error",
"noStaticOnlyClass": "error",
"noUselessConstructor": "error",
"noUselessEmptyExport": "error",
"noUselessThisAlias": "error",
"useOptionalChain": "error"
},
"correctness": {
"recommended": true,
"noUnusedImports": "error",
"noUnusedFunctionParameters": "error",
"noUnusedPrivateClassMembers": "error",
"useHookAtTopLevel": "error",
"noConstAssign": "error",
"noGlobalObjectCalls": "error",
"noInnerDeclarations": "error",
"noInvalidUseBeforeDeclaration": "error",
"noInvalidBuiltinInstantiation": "error",
"noUnreachableSuper": "error",
"noUnsafeOptionalChaining": "error",
"noUnusedLabels": "error"
},
"performance": {
"recommended": true
},
"security": {
"recommended": true
},
"style": {
"recommended": true,
"noNonNullAssertion": "error",
"useArrayLiterals": "error",
"useConst": "error",
"useForOf": "error",
"useImportType": "error",
"useNodejsImportProtocol": "error",
"useNumberNamespace": "error",
"noNamespace": "error",
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useConsistentMemberAccessibility": {
"level": "error",
"options": {
"accessibility": "noPublic"
}
},
"useShorthandFunctionType": "error",
"useExportType": "error",
"useLiteralEnumMembers": "error",
"useUnifiedTypeSignatures": "error"
},
"suspicious": {
"recommended": true,
"noEmptyInterface": "error",
"noExplicitAny": "error",
"noConsole": "warn",
"noDebugger": "error",
"noConfusingVoidType": "error",
"noDuplicateClassMembers": "error",
"noDuplicateObjectKeys": "error",
"noDuplicateParameters": "error",
"noExtraNonNullAssertion": "error",
"noFunctionAssign": "error",
"noLabelVar": "error",
"noMisleadingInstantiator": "error",
"noRedeclare": "error",
"noUnsafeDeclarationMerging": "error",
"noUnsafeNegation": "error",
"useAdjacentOverloadSignatures": "error",
"useNamespaceKeyword": "error"
},
"nursery": {
"noFloatingPromises": "error",
"noMisusedPromises": "error",
"noNonNullAssertedOptionalChain": "error",
"useConsistentTypeDefinitions": "error"
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "single",
"quoteProperties": "preserve",
"trailingCommas": "es5",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded",
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto",
"bracketSpacing": true
}
},
"overrides": [
{
"includes": ["*.tsx"],
"linter": {
"rules": {
"style": {
"useImportType": "off"
}
Comment on lines +124 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

TSX override glob misses nested files.

"*.tsx" only matches root-level files. Use a recursive glob to apply the rule repo‑wide; otherwise TSX under src/** won’t get the intended useImportType override.

Apply this diff:

-      "includes": ["*.tsx"],
+      "includes": ["**/*.tsx"],
🤖 Prompt for AI Agents
In biome.json around lines 124 to 129, the TSX override glob currently uses
"*.tsx" which only matches root-level files; update the glob to a recursive
pattern like "**/*.tsx" so the linter override (useImportType: off) applies to
TSX files in nested directories throughout the repo.

}
}
}
],
"html": { "formatter": { "selfCloseVoidElements": "always" } },
"assist": {
"enabled": true,
"actions": { "source": { "organizeImports": "on" } }
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"zod": "^4.1.11"
},
"devDependencies": {
"@biomejs/biome": "2.2.5",
"@jupyterlab/builder": "^4.0.0",
"@jupyterlab/testutils": "^4.0.0",
"@types/jest": "^29.2.0",
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/jupyterlab_deepnote.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

describe('jupyterlab-deepnote', () => {
it('should be tested', () => {
expect(1 + 1).toEqual(2);
});
});
expect(1 + 1).toEqual(2)
})
})
94 changes: 43 additions & 51 deletions src/components/NotebookPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,103 +1,95 @@
import React from 'react';
import { ReactWidget } from '@jupyterlab/apputils';
import { NotebookPanel } from '@jupyterlab/notebook';
import { HTMLSelect } from '@jupyterlab/ui-components';
import { deepnoteMetadataSchema } from '../types';
import { Widget } from '@lumino/widgets';
import { Message, MessageLoop } from '@lumino/messaging';
import { ReactWidget } from '@jupyterlab/apputils'
import type { NotebookPanel } from '@jupyterlab/notebook'
import { HTMLSelect } from '@jupyterlab/ui-components'
import { type Message, MessageLoop } from '@lumino/messaging'
import { Widget } from '@lumino/widgets'
import React from 'react'
import { deepnoteMetadataSchema } from '../types'

export class NotebookPicker extends ReactWidget {
private selected: string | null = null;
private selected: string | null = null

constructor(private panel: NotebookPanel) {
super();
super()

void panel.context.ready.then(() => {
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote');
const metadataNames = deepnoteMetadata?.notebook_names;
const names =
Array.isArray(metadataNames) &&
metadataNames.every(n => typeof n === 'string')
? metadataNames
: [];
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote')
const metadataNames = deepnoteMetadata?.notebook_names
const names = Array.isArray(metadataNames) && metadataNames.every(n => typeof n === 'string') ? metadataNames : []

this.selected = names.length === 0 ? null : (names[0] ?? null);
this.update();
});
this.selected = names.length === 0 ? null : (names[0] ?? null)
this.update()
})
}

private handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const model = this.panel.model;
const model = this.panel.model
if (!model) {
return;
return
}

const selected = event.target.value;
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote');
const deepnoteMetadataValidated =
deepnoteMetadataSchema.safeParse(deepnoteMetadata);
const selected = event.target.value
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote')
const deepnoteMetadataValidated = deepnoteMetadataSchema.safeParse(deepnoteMetadata)

if (!deepnoteMetadataValidated.success) {
console.error(
'Invalid deepnote metadata:',
deepnoteMetadataValidated.error
);
return;
return
}

const notebooks = deepnoteMetadataValidated.data.notebooks;
const notebooks = deepnoteMetadataValidated.data.notebooks

if (selected in notebooks) {
model.fromJSON({
cells: notebooks[selected]?.cells ?? [],
metadata: {
deepnote: {
notebooks
}
notebooks,
},
},
nbformat: 4,
nbformat_minor: 0
});
model.dirty = false;
nbformat_minor: 0,
})
model.dirty = false
}

this.selected = selected;
this.update();
};
this.selected = selected
this.update()
}

protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
super.onAfterAttach(msg)
requestAnimationFrame(() => {
MessageLoop.sendMessage(this.parent!, Widget.ResizeMessage.UnknownSize);
});
if (this.parent) {
MessageLoop.sendMessage(this.parent, Widget.ResizeMessage.UnknownSize)
}
})
}

render(): JSX.Element {
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote');
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote')

const deepnoteMetadataValidated =
deepnoteMetadataSchema.safeParse(deepnoteMetadata);
const deepnoteMetadataValidated = deepnoteMetadataSchema.safeParse(deepnoteMetadata)

const names = deepnoteMetadataValidated.success
? Object.values(deepnoteMetadataValidated.data.notebooks).map(n => n.name)
: [];
: []

return (
<HTMLSelect
value={this.selected ?? '-'}
onChange={this.handleChange}
onKeyDown={() => {}}
aria-label="Select active notebook"
title="Select active notebook"
aria-label='Select active notebook'
title='Select active notebook'
style={{
width: '120px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden'
overflow: 'hidden',
}}
>
{names.length === 0 ? (
<option value="-">-</option>
<option value='-'>-</option>
) : (
names.map(n => (
<option key={n} value={n}>
Expand All @@ -106,6 +98,6 @@ export class NotebookPicker extends ReactWidget {
))
)}
</HTMLSelect>
);
)
}
}
40 changes: 18 additions & 22 deletions src/convert-deepnote-block-to-jupyter-cell.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,40 @@
import {
createMarkdown,
createPythonCode,
DeepnoteBlock
} from '@deepnote/blocks';
import _cloneDeep from 'lodash/cloneDeep';
import { ICodeCell, IMarkdownCell } from '@jupyterlab/nbformat';
import { convertDeepnoteBlockTypeToJupyter } from './convert-deepnote-block-type-to-jupyter';
import { createMarkdown, createPythonCode, type DeepnoteBlock } from '@deepnote/blocks'
import type { ICodeCell, IMarkdownCell } from '@jupyterlab/nbformat'
import _cloneDeep from 'lodash/cloneDeep'
import { convertDeepnoteBlockTypeToJupyter } from './convert-deepnote-block-type-to-jupyter'

export function convertDeepnoteBlockToJupyterCell(block: DeepnoteBlock) {
const blockCopy = _cloneDeep(block);
const jupyterCellMetadata = { ...blockCopy.metadata, cell_id: blockCopy.id };
const jupyterCellType = convertDeepnoteBlockTypeToJupyter(blockCopy.type);
const blockCopy = _cloneDeep(block)
const jupyterCellMetadata = { ...blockCopy.metadata, cell_id: blockCopy.id }
const jupyterCellType = convertDeepnoteBlockTypeToJupyter(blockCopy.type)

if (jupyterCellType === 'code') {
const blockOutputs = blockCopy.outputs ?? [];
const blockOutputs = blockCopy.outputs ?? []

if (Array.isArray(blockOutputs)) {
blockOutputs.forEach(output => {
delete output.truncated;
});
delete output.truncated
})
}
Comment on lines 14 to 18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Comply with Biome style.useForOf.

forEach will violate the configured rule. Use for‑of.

-    if (Array.isArray(blockOutputs)) {
-      blockOutputs.forEach(output => {
-        delete output.truncated
-      })
-    }
+    if (Array.isArray(blockOutputs)) {
+      for (const output of blockOutputs) {
+        // Remove Deepnote-only flag
+        // @ts-expect-error property may not exist on Jupyter outputs
+        delete (output as any).truncated
+      }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Array.isArray(blockOutputs)) {
blockOutputs.forEach(output => {
delete output.truncated;
});
delete output.truncated
})
}
if (Array.isArray(blockOutputs)) {
for (const output of blockOutputs) {
// Remove Deepnote-only flag
// @ts-expect-error property may not exist on Jupyter outputs
delete (output as any).truncated
}
}
🤖 Prompt for AI Agents
In src/convert-deepnote-block-to-jupyter-cell.ts around lines 14 to 18, the code
uses blockOutputs.forEach which violates the Biome style.useForOf rule; replace
the forEach with a for-of loop (e.g., for (const output of blockOutputs) {
delete output.truncated }) and keep the existing Array.isArray check and
null/undefined safety so types remain correct and behavior unchanged.


const source = createPythonCode(blockCopy);
const source = createPythonCode(blockCopy)

const jupyterCell: ICodeCell = {
cell_type: 'code',
metadata: jupyterCellMetadata,
execution_count: blockCopy.executionCount ?? null,
outputs: blockOutputs,
source
};
return jupyterCell;
source,
}
return jupyterCell
} else {
// Markdown cell
const source = createMarkdown(blockCopy);
const source = createMarkdown(blockCopy)
const jupyterCell: IMarkdownCell = {
cell_type: 'markdown',
metadata: {},
source
};
return jupyterCell;
source,
}
Comment on lines 34 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve metadata (incl. cell_id) for markdown cells.

You build jupyterCellMetadata but drop it for markdown. Keep consistency and IDs.

-    const jupyterCell: IMarkdownCell = {
+    const jupyterCell: IMarkdownCell = {
       cell_type: 'markdown',
-      metadata: {},
+      metadata: jupyterCellMetadata,
       source,
     }
🤖 Prompt for AI Agents
In src/convert-deepnote-block-to-jupyter-cell.ts around lines 34 to 37, the
function constructs jupyterCellMetadata but currently drops it when creating
markdown cells (returns metadata: {}); update the markdown branch to set
metadata: jupyterCellMetadata instead of an empty object so markdown cells keep
their full metadata (including cell_id) to match code cell behavior and preserve
IDs.

return jupyterCell
}
}
Loading
Loading