Skip to content

Commit 7b0fe38

Browse files
committed
feat: open in Deepnote command
1 parent e683e42 commit 7b0fe38

File tree

8 files changed

+558
-0
lines changed

8 files changed

+558
-0
lines changed

package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@
9999
"category": "Deepnote",
100100
"icon": "$(plug)"
101101
},
102+
{
103+
"command": "deepnote.openInDeepnote",
104+
"title": "Open in Deepnote",
105+
"category": "Deepnote",
106+
"icon": "$(globe)"
107+
},
102108
{
103109
"command": "deepnote.newProject",
104110
"title": "New project",
@@ -761,6 +767,11 @@
761767
"when": "editorFocus && editorLangId == python && jupyter.hascodecells && !notebookEditorFocused && isWorkspaceTrusted",
762768
"command": "jupyter.exportfileasnotebook",
763769
"group": "Jupyter3@2"
770+
},
771+
{
772+
"when": "resourceExtname == .deepnote",
773+
"command": "deepnote.openInDeepnote",
774+
"group": "navigation"
764775
}
765776
],
766777
"editor.interactiveWindow.context": [
@@ -1386,6 +1397,18 @@
13861397
"type": "object",
13871398
"title": "Deepnote",
13881399
"properties": {
1400+
"deepnote.apiEndpoint": {
1401+
"type": "string",
1402+
"default": "https://deepnote.com",
1403+
"description": "Deepnote API endpoint",
1404+
"scope": "application"
1405+
},
1406+
"deepnote.disableSSLVerification": {
1407+
"type": "boolean",
1408+
"default": false,
1409+
"description": "Disable SSL certificate verification (for development only)",
1410+
"scope": "application"
1411+
},
13891412
"jupyter.experiments.enabled": {
13901413
"type": "boolean",
13911414
"default": true,

src/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,5 @@ export interface ICommandNameArgumentTypeMapping {
197197
[DSCommands.AddInputDateRangeBlock]: [];
198198
[DSCommands.AddInputFileBlock]: [];
199199
[DSCommands.AddButtonBlock]: [];
200+
[DSCommands.OpenInDeepnote]: [];
200201
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { workspace } from 'vscode';
5+
import { logger } from '../../platform/logging';
6+
import * as https from 'https';
7+
import fetch from 'node-fetch';
8+
9+
/**
10+
* Response from the import initialization endpoint
11+
*/
12+
export interface InitImportResponse {
13+
importId: string;
14+
uploadUrl: string;
15+
expiresAt: string;
16+
}
17+
18+
/**
19+
* Error response from the API
20+
*/
21+
export interface ApiError {
22+
message: string;
23+
statusCode: number;
24+
}
25+
26+
/**
27+
* Maximum file size for uploads (100MB)
28+
*/
29+
export const MAX_FILE_SIZE = 100 * 1024 * 1024;
30+
31+
/**
32+
* Gets the API endpoint from configuration
33+
*/
34+
function getApiEndpoint(): string {
35+
const config = workspace.getConfiguration('deepnote');
36+
return config.get<string>('apiEndpoint', 'https://api.deepnote.com');
37+
}
38+
39+
/**
40+
* Checks if SSL verification should be disabled
41+
*/
42+
function shouldDisableSSLVerification(): boolean {
43+
const config = workspace.getConfiguration('deepnote');
44+
return config.get<boolean>('disableSSLVerification', false);
45+
}
46+
47+
/**
48+
* Creates an HTTPS agent with optional SSL verification disabled
49+
*/
50+
function createHttpsAgent(): https.Agent | undefined {
51+
if (shouldDisableSSLVerification()) {
52+
logger.warn('SSL certificate verification is disabled. This should only be used in development.');
53+
return new https.Agent({
54+
rejectUnauthorized: false
55+
});
56+
}
57+
return undefined;
58+
}
59+
60+
/**
61+
* Initializes an import by requesting a presigned upload URL
62+
*
63+
* @param fileName - Name of the file to import
64+
* @param fileSize - Size of the file in bytes
65+
* @returns Promise with import ID, upload URL, and expiration time
66+
* @throws ApiError if the request fails
67+
*/
68+
export async function initImport(fileName: string, fileSize: number): Promise<InitImportResponse> {
69+
const apiEndpoint = getApiEndpoint();
70+
const url = `${apiEndpoint}/v1/import/init`;
71+
72+
const agent = createHttpsAgent();
73+
const response = await fetch(url, {
74+
method: 'POST',
75+
headers: {
76+
'Content-Type': 'application/json'
77+
},
78+
body: JSON.stringify({
79+
fileName,
80+
fileSize
81+
}),
82+
agent
83+
});
84+
85+
if (!response.ok) {
86+
const responseBody = await response.text();
87+
logger.error(`Init import failed - Status: ${response.status}, URL: ${url}, Body: ${responseBody}`);
88+
89+
const error: ApiError = {
90+
message: responseBody,
91+
statusCode: response.status
92+
};
93+
throw error;
94+
}
95+
96+
return await response.json();
97+
}
98+
99+
/**
100+
* Uploads a file to the presigned S3 URL using XMLHttpRequest for progress tracking
101+
*
102+
* @param uploadUrl - Presigned S3 URL for uploading
103+
* @param fileBuffer - File contents as a Buffer
104+
* @param onProgress - Optional callback for upload progress (0-100)
105+
* @returns Promise that resolves when upload is complete
106+
* @throws ApiError if the upload fails
107+
*/
108+
export async function uploadFile(
109+
uploadUrl: string,
110+
fileBuffer: Buffer,
111+
onProgress?: (progress: number) => void
112+
): Promise<void> {
113+
return new Promise((resolve, reject) => {
114+
const xhr = new XMLHttpRequest();
115+
116+
// Track upload progress
117+
if (onProgress) {
118+
xhr.upload.addEventListener('progress', (event) => {
119+
if (event.lengthComputable) {
120+
const percentComplete = Math.round((event.loaded / event.total) * 100);
121+
onProgress(percentComplete);
122+
}
123+
});
124+
}
125+
126+
// Handle completion
127+
xhr.addEventListener('load', () => {
128+
if (xhr.status >= 200 && xhr.status < 300) {
129+
resolve();
130+
} else {
131+
logger.error(`Upload failed - Status: ${xhr.status}, Response: ${xhr.responseText}, URL: ${uploadUrl}`);
132+
const error: ApiError = {
133+
message: xhr.responseText || 'Upload failed',
134+
statusCode: xhr.status
135+
};
136+
reject(error);
137+
}
138+
});
139+
140+
// Handle errors
141+
xhr.addEventListener('error', () => {
142+
logger.error(`Network error during upload to: ${uploadUrl}`);
143+
const error: ApiError = {
144+
message: 'Network error during upload',
145+
statusCode: 0
146+
};
147+
reject(error);
148+
});
149+
150+
xhr.addEventListener('abort', () => {
151+
const error: ApiError = {
152+
message: 'Upload aborted',
153+
statusCode: 0
154+
};
155+
reject(error);
156+
});
157+
158+
// Start upload
159+
xhr.open('PUT', uploadUrl);
160+
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
161+
162+
// Convert Buffer to Uint8Array then Blob for XMLHttpRequest
163+
const uint8Array = new Uint8Array(
164+
fileBuffer.buffer as ArrayBuffer,
165+
fileBuffer.byteOffset,
166+
fileBuffer.byteLength
167+
);
168+
const blob = new Blob([uint8Array], { type: 'application/octet-stream' });
169+
xhr.send(blob);
170+
});
171+
}
172+
173+
/**
174+
* Gets a user-friendly error message for an API error
175+
* Logs the full error details for debugging
176+
*
177+
* @param error - The error object
178+
* @returns A user-friendly error message
179+
*/
180+
export function getErrorMessage(error: unknown): string {
181+
// Log the full error details for debugging
182+
logger.error('Import error details:', error);
183+
184+
if (typeof error === 'object' && error !== null && 'statusCode' in error) {
185+
const apiError = error as ApiError;
186+
187+
// Log API error specifics
188+
logger.error(`API Error - Status: ${apiError.statusCode}, Message: ${apiError.message}`);
189+
190+
// Handle rate limiting specifically
191+
if (apiError.statusCode === 429) {
192+
return 'Too many requests. Please try again in a few minutes.';
193+
}
194+
195+
// All other API errors return the message from the server
196+
if (apiError.statusCode >= 400) {
197+
return apiError.message || 'An error occurred. Please try again.';
198+
}
199+
}
200+
201+
if (error instanceof Error) {
202+
logger.error(`Error message: ${error.message}`, error.stack);
203+
if (error.message.includes('fetch') || error.message.includes('Network')) {
204+
return 'Failed to connect. Check your connection and try again.';
205+
}
206+
return error.message;
207+
}
208+
209+
logger.error('Unknown error type:', typeof error, error);
210+
return 'An unknown error occurred';
211+
}

0 commit comments

Comments
 (0)