Skip to content

Commit 0ffb792

Browse files
hurstsadbywang56ashishrp-aws
authored
feat(sagemakerunifiedstudio): Add Connection Magics Selector feature to Jupyter Notebooks (aws#2169)
## Problem [feat(sagemakerunifiedstudio): Add Connection Magics Selector feature to Jupyter Notebooks](aws/aws-toolkit-vscode-staging@f0a4690): - SageMaker Unified Studio CodeEditor users should have the ability to easily select their SMUS connection/compute cell magics in the Jupyter Notebook experience. - The experience should be available when a SageMaker Unified Studio space is connected either remotely or in the portal. [feat(sagemakerunifiedstudio): Add resource metadata utils for SMUS spaces](aws/aws-toolkit-vscode-staging@55a43ad): - SageMaker Unified Studio depends on resource metadata present in the SageMaker space for various functionality. This includes cases where the space is either remotely connected or in the SMUS web portal. - Depending features include the Connection Magics Selector and other remote SageMaker Unified Studio features. - SageMaker Unified Studio spaces store this resource metadata under the `/opt/ml/metadata/resource-metadata.json` path. This resource metadata file contains SageMaker and DataZone metadata associated with the space. ## Solution [feat(sagemakerunifiedstudio): Add Connection Magics Selector feature to Jupyter Notebooks](aws/aws-toolkit-vscode-staging@f0a4690): - Adding Connection Magics Selector feature for SageMaker Unified Studio spaces. - The Connection Magics Selector allows users to easily select their SMUS connection/compute cell magics in the Jupyter Notebook experience. - This is achieved by adding two new status bar items to each Jupyter Notebook cell that when clicked show quick pick options for their SMUS connections/computes. Once a user chooses a connection/compute, the cell magics are updated accordingly. - When applying the new cell magics, automatic language syntax highlighting is also applied to the modified cell based on the connection's associated language. [feat(sagemakerunifiedstudio): Add resource metadata utils for SMUS spaces](aws/aws-toolkit-vscode-staging@55a43ad): - Adding a resource metadata helper module for SMUS spaces. - Adding `getResourceMetadata()` and other methods that allow for easy resource metadata retrieval for use by SMUS features. ## Screenshots Jupyter Notebook view (w/ Connection and Compute selectors in bottom right of notebook cells): <img width="3840" height="1942" alt="Screenshot 2025-07-30 at 11-30-55 getting_started ipynb — src — SageMaker Code Editor" src="https://github.com/user-attachments/assets/19d4f441-127a-4258-8b2e-0c24b7f40f7d" /> Connection Selector: <img width="847" height="241" alt="Screenshot 2025-07-30 at 11 31 59 AM" src="https://github.com/user-attachments/assets/93aedb12-12c1-4aaf-8629-99d74ac4cadd" /> Compute Selector: <img width="986" height="251" alt="Screenshot 2025-07-30 at 11 32 38 AM" src="https://github.com/user-attachments/assets/f91e1e46-285b-4db8-ad20-5a23b29a33c4" /> ## Testing Done [feat(sagemakerunifiedstudio): Add Connection Magics Selector feature to Jupyter Notebooks](aws/aws-toolkit-vscode-staging@f0a4690): - WIP - will update [feat(sagemakerunifiedstudio): Add resource metadata utils for SMUS spaces](aws/aws-toolkit-vscode-staging@55a43ad): - Added unit tests, new unit tests pass: ``` resourceMetadataUtils getResourceMetadata() ✔ should return metadata when file exists and is valid JSON ✔ should return undefined when file does not exist ✔ should return undefined and log error when file contains invalid JSON ✔ should return undefined and log error when readFileText throws ✔ should cache metadata and not re-read file on subsequent calls ✔ should handle metadata with missing optional fields ✔ should handle metadata with empty AdditionalMetadata ✔ should handle empty JSON file ✔ should set initialized flag to true even when initialization fails ✔ should handle very large JSON files ✔ should handle JSON with unexpected additional fields ✔ should handle JSON with undefined values ✔ should handle concurrent calls to getResourceMetadata resetResourceMetadata() ✔ should reset cached metadata and allow re-initialization resourceMetadataFileExists() ✔ should return true when file exists ✔ should return false when file does not exist ✔ should return false and log error when fs.existsFile throws ``` --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Boyu <[email protected]> Co-authored-by: invictus <[email protected]>
1 parent 573b674 commit 0ffb792

File tree

12 files changed

+1855
-2
lines changed

12 files changed

+1855
-2
lines changed

packages/core/src/sagemakerunifiedstudio/activation.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import * as vscode from 'vscode'
77
import { activate as activateConnectionMagicsSelector } from './connectionMagicsSelector/activation'
88
import { activate as activateNotebookScheduling } from './notebookScheduling/activation'
99
import { activate as activateExplorer } from './explorer/activation'
10+
import { isSageMaker } from '../shared/extensionUtilities'
11+
import { initializeResourceMetadata } from './shared/utils/resourceMetadataUtils'
1012

1113
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
12-
await activateConnectionMagicsSelector(extensionContext)
14+
// Only run when environment is a SageMaker Unified Studio space
15+
if (isSageMaker('SMUS')) {
16+
await initializeResourceMetadata()
17+
await activateConnectionMagicsSelector(extensionContext)
18+
}
1319

1420
await activateNotebookScheduling(extensionContext)
1521

packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/activation.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,77 @@
44
*/
55

66
import * as vscode from 'vscode'
7+
import { Constants } from './models/constants'
8+
import {
9+
getStatusBarProviders,
10+
showConnectionQuickPick,
11+
showProjectQuickPick,
12+
parseNotebookCells,
13+
} from './commands/commands'
714

15+
/**
16+
* Activates the SageMaker Unified Studio Connection Magics Selector feature.
17+
*
18+
* @param extensionContext The extension context
19+
*/
820
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
9-
// NOOP
21+
extensionContext.subscriptions.push(
22+
vscode.commands.registerCommand(Constants.CONNECTION_COMMAND, () => showConnectionQuickPick()),
23+
vscode.commands.registerCommand(Constants.PROJECT_COMMAND, () => showProjectQuickPick())
24+
)
25+
26+
if ('NotebookEdit' in vscode) {
27+
const { connectionProvider, projectProvider, separatorProvider } = getStatusBarProviders()
28+
29+
extensionContext.subscriptions.push(
30+
vscode.notebooks.registerNotebookCellStatusBarItemProvider('jupyter-notebook', connectionProvider),
31+
vscode.notebooks.registerNotebookCellStatusBarItemProvider('jupyter-notebook', projectProvider),
32+
vscode.notebooks.registerNotebookCellStatusBarItemProvider('jupyter-notebook', separatorProvider)
33+
)
34+
35+
extensionContext.subscriptions.push(
36+
vscode.window.onDidChangeActiveNotebookEditor(async () => {
37+
await parseNotebookCells()
38+
})
39+
)
40+
41+
extensionContext.subscriptions.push(vscode.workspace.onDidChangeTextDocument(handleTextDocumentChange))
42+
43+
void parseNotebookCells()
44+
}
45+
}
46+
47+
/**
48+
* Handles text document changes to update status bar when cells are manually edited
49+
*/
50+
function handleTextDocumentChange(event: vscode.TextDocumentChangeEvent): void {
51+
if (event.document.uri.scheme !== 'vscode-notebook-cell') {
52+
return
53+
}
54+
55+
const editor = vscode.window.activeNotebookEditor
56+
if (!editor) {
57+
return
58+
}
59+
60+
let changedCell: vscode.NotebookCell | undefined
61+
for (let i = 0; i < editor.notebook.cellCount; i++) {
62+
const cell = editor.notebook.cellAt(i)
63+
if (cell.document.uri.toString() === event.document.uri.toString()) {
64+
changedCell = cell
65+
break
66+
}
67+
}
68+
69+
if (changedCell && changedCell.kind === vscode.NotebookCellKind.Code) {
70+
const { notebookStateManager } = require('./services/notebookStateManager')
71+
72+
notebookStateManager.parseCellMagic(changedCell)
73+
74+
setTimeout(() => {
75+
const { connectionProvider, projectProvider } = getStatusBarProviders()
76+
connectionProvider.refreshCellStatusBar()
77+
projectProvider.refreshCellStatusBar()
78+
}, 100)
79+
}
1080
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { DataZone, ListConnectionsCommandOutput } from '@aws-sdk/client-datazone'
7+
import { getLogger } from '../../../shared/logger/logger'
8+
9+
/**
10+
* Represents a DataZone connection
11+
*/
12+
export interface DataZoneConnection {
13+
connectionId: string
14+
name: string
15+
type: string
16+
props?: Record<string, any>
17+
}
18+
19+
/**
20+
* DataZone client for use in a SageMaker Unified Studio connected space
21+
* Uses the user's current AWS credentials (project role credentials)
22+
*/
23+
export class ConnectedSpaceDataZoneClient {
24+
private datazoneClient: DataZone | undefined
25+
private readonly logger = getLogger()
26+
27+
constructor(
28+
private readonly region: string,
29+
private readonly customEndpoint?: string
30+
) {}
31+
32+
/**
33+
* Gets the DataZone client, initializing it if necessary
34+
* Uses default AWS credentials from the environment (project role)
35+
* Supports custom endpoints for non-production environments
36+
*/
37+
private getDataZoneClient(): DataZone {
38+
if (!this.datazoneClient) {
39+
try {
40+
const clientConfig: any = {
41+
region: this.region,
42+
}
43+
44+
// Use custom endpoint if provided (for non-prod environments)
45+
if (this.customEndpoint) {
46+
clientConfig.endpoint = this.customEndpoint
47+
this.logger.debug(
48+
`ConnectedSpaceDataZoneClient: Using custom DataZone endpoint: ${this.customEndpoint}`
49+
)
50+
} else {
51+
this.logger.debug(
52+
`ConnectedSpaceDataZoneClient: Using default AWS DataZone endpoint for region: ${this.region}`
53+
)
54+
}
55+
56+
this.logger.debug('ConnectedSpaceDataZoneClient: Creating DataZone client with default credentials')
57+
this.datazoneClient = new DataZone(clientConfig)
58+
this.logger.debug('ConnectedSpaceDataZoneClient: Successfully created DataZone client')
59+
} catch (err) {
60+
this.logger.error('ConnectedSpaceDataZoneClient: Failed to create DataZone client: %s', err as Error)
61+
throw err
62+
}
63+
}
64+
return this.datazoneClient
65+
}
66+
67+
/**
68+
* Lists the connections in a DataZone domain and project
69+
* @param domainId The DataZone domain identifier
70+
* @param projectId The DataZone project identifier
71+
* @returns List of connections
72+
*/
73+
public async listConnections(domainId: string, projectId: string): Promise<DataZoneConnection[]> {
74+
try {
75+
this.logger.info(
76+
`ConnectedSpaceDataZoneClient: Listing connections for domain ${domainId}, project ${projectId}`
77+
)
78+
79+
const datazoneClient = this.getDataZoneClient()
80+
81+
const response: ListConnectionsCommandOutput = await datazoneClient.listConnections({
82+
domainIdentifier: domainId,
83+
projectIdentifier: projectId,
84+
})
85+
86+
if (!response.items || response.items.length === 0) {
87+
this.logger.info(
88+
`ConnectedSpaceDataZoneClient: No connections found for domain ${domainId}, project ${projectId}`
89+
)
90+
return []
91+
}
92+
93+
const connections: DataZoneConnection[] = response.items.map((connection) => ({
94+
connectionId: connection.connectionId || '',
95+
name: connection.name || '',
96+
type: connection.type || '',
97+
props: connection.props || {},
98+
}))
99+
100+
this.logger.info(
101+
`ConnectedSpaceDataZoneClient: Found ${connections.length} connections for domain ${domainId}, project ${projectId}`
102+
)
103+
return connections
104+
} catch (err) {
105+
this.logger.error('ConnectedSpaceDataZoneClient: Failed to list connections: %s', err as Error)
106+
throw err
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)