Skip to content

Commit 8ce40c2

Browse files
bharathGuntamaduguguntamb
andauthored
feat(smus): improve project selection error handling and remove auto-selection (aws#2205)
## Problem: Project selection had poor error handling and automatically invoked after sign-in causing some unintended UX issues. ## Solution: - Add access denied error handling with user-friendly messages - Remove automatic project selection after sign-in - Refactor selection logic with helper functions for better error management - Updated the test cases. --- - 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: guntamb <[email protected]>
1 parent df42b76 commit 8ce40c2

File tree

7 files changed

+265
-86
lines changed

7 files changed

+265
-86
lines changed

packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,6 @@ export class SageMakerUnifiedStudioProjectNode implements TreeNode {
6969
}
7070
item.iconPath = getIcon('vscode-folder-opened')
7171

72-
// Auto-invoke project selection after sign-in
73-
void vscode.commands.executeCommand('aws.smus.projectView', this)
74-
7572
return item
7673
}
7774

packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.ts

Lines changed: 64 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -340,74 +340,84 @@ export const smusSignOutCommand = Commands.declare('aws.smus.signOut', () => asy
340340
}
341341
})
342342

343+
function isAccessDenied(error: Error): boolean {
344+
return error.name.includes('AccessDenied')
345+
}
346+
347+
function createProjectQuickPickItems(projects: any[]) {
348+
return projects
349+
.sort(
350+
(a, b) =>
351+
(b.updatedAt ? new Date(b.updatedAt).getTime() : 0) -
352+
(a.updatedAt ? new Date(a.updatedAt).getTime() : 0)
353+
)
354+
.filter((project) => project.name !== 'GenerativeAIModelGovernanceProject')
355+
.map((project) => ({
356+
label: project.name,
357+
detail: 'ID: ' + project.id,
358+
description: project.description,
359+
data: project,
360+
}))
361+
}
362+
363+
async function showQuickPick(items: any[]) {
364+
const quickPick = createQuickPick(items, {
365+
title: projectPickerTitle,
366+
placeholder: projectPickerPlaceholder,
367+
})
368+
return await quickPick.prompt()
369+
}
370+
343371
export async function selectSMUSProject(projectNode?: SageMakerUnifiedStudioProjectNode) {
344372
const logger = getLogger()
373+
345374
try {
346375
const authProvider = SmusAuthenticationProvider.fromContext()
347-
const activeConnection = authProvider.activeConnection
348-
if (!activeConnection) {
349-
logger.error('There is no active connection to display project view')
376+
if (!authProvider.activeConnection) {
377+
logger.error('No active connection to display project view')
350378
return
351379
}
352-
const authenticatedDataZoneClient = await DataZoneClient.getInstance(authProvider)
353-
logger.debug('SMUS: DataZone client instance obtained successfully')
354380

355-
// Fetching all projects in the specified domain using the client's fetchAllProjects method
356-
const allProjects = await authenticatedDataZoneClient.fetchAllProjects()
381+
const client = await DataZoneClient.getInstance(authProvider)
382+
logger.debug('DataZone client instance obtained successfully')
357383

358-
const smusProjects = allProjects
384+
const allProjects = await client.fetchAllProjects()
385+
const items = createProjectQuickPickItems(allProjects)
359386

360-
// Process projects: sort by updatedAt, and map to quick pick items
361-
const items = [...smusProjects]
362-
.sort(
363-
(a, b) =>
364-
(b.updatedAt ? new Date(b.updatedAt).getTime() : 0) -
365-
(a.updatedAt ? new Date(a.updatedAt).getTime() : 0)
366-
)
367-
.filter(
368-
(project) =>
369-
// Filter out the Generative AI Model Governance project that is part of QiuckStart resources
370-
project.name !== 'GenerativeAIModelGovernanceProject'
371-
)
372-
.map((project) => ({
373-
label: project.name,
374-
detail: 'ID: ' + project.id,
375-
description: project.description,
376-
data: project,
377-
}))
378387
if (items.length === 0) {
379388
logger.info('No projects found in the domain')
380389
void vscode.window.showInformationMessage('No projects found in the domain')
381-
// If no projects are found, show "No projects found" in the quick pick
382-
const quickPickItem = [
390+
await showQuickPick([{ label: 'No projects found', detail: '', description: '', data: {} }])
391+
return
392+
}
393+
394+
const selectedProject = await showQuickPick(items)
395+
if (
396+
selectedProject &&
397+
typeof selectedProject === 'object' &&
398+
selectedProject !== null &&
399+
!('type' in selectedProject) &&
400+
projectNode
401+
) {
402+
await projectNode.setProject(selectedProject)
403+
await vscode.commands.executeCommand('aws.smus.rootView.refresh')
404+
}
405+
406+
return selectedProject
407+
} catch (err) {
408+
const error = err as Error
409+
410+
if (isAccessDenied(error)) {
411+
await showQuickPick([
383412
{
384-
label: 'No projects found',
385-
detail: '',
386-
description: '',
387-
data: {},
413+
label: '$(error)',
414+
description: "You don't have permissions to view projects. Please contact your administrator",
388415
},
389-
]
390-
const quickPick = createQuickPick(quickPickItem, {
391-
title: projectPickerTitle,
392-
placeholder: projectPickerPlaceholder,
393-
})
394-
await quickPick.prompt()
395-
} else {
396-
const quickPick = createQuickPick(items, {
397-
title: projectPickerTitle,
398-
placeholder: projectPickerPlaceholder,
399-
})
400-
401-
const selectedProject = await quickPick.prompt()
402-
if (selectedProject && !('type' in selectedProject) && projectNode) {
403-
await projectNode.setProject(selectedProject)
404-
// Refresh the entire tree view
405-
await vscode.commands.executeCommand('aws.smus.rootView.refresh')
406-
}
407-
return selectedProject
416+
])
417+
return
408418
}
409-
} catch (err) {
410-
logger.error('Failed to select project: %s', (err as Error).message)
411-
void vscode.window.showErrorMessage(`Failed to select project: ${(err as Error).message}`)
419+
420+
logger.error('Failed to select project: %s', error.message)
421+
void vscode.window.showErrorMessage(`Failed to select project: ${error.message}`)
412422
}
413423
}

packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SagemakerUnifiedStudioSpaceNode } from './sageMakerUnifiedStudioSpaceNo
1818
import { PollingSet } from '../../../shared/utilities/pollingSet'
1919
import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider'
2020
import { SmusUtils } from '../../shared/smusUtils'
21+
import { getIcon } from '../../../shared/icons'
2122

2223
export class SageMakerUnifiedStudioSpacesParentNode implements TreeNode {
2324
public readonly id = 'smusSpacesParentNode'
@@ -59,6 +60,10 @@ export class SageMakerUnifiedStudioSpacesParentNode implements TreeNode {
5960
try {
6061
await this.updateChildren()
6162
} catch (err) {
63+
const error = err as Error
64+
if (error.name === 'AccessDeniedException') {
65+
return this.getAccessDeniedChildren()
66+
}
6267
return this.getNoSpacesFoundChildren()
6368
}
6469
const nodes = [...this.sagemakerSpaceNodes.values()]
@@ -79,6 +84,24 @@ export class SageMakerUnifiedStudioSpacesParentNode implements TreeNode {
7984
]
8085
}
8186

87+
private getAccessDeniedChildren(): TreeNode[] {
88+
return [
89+
{
90+
id: 'smusAccessDenied',
91+
resource: {},
92+
getTreeItem: () => {
93+
const item = new vscode.TreeItem(
94+
"You don't have permission to view spaces. Please contact your administrator.",
95+
vscode.TreeItemCollapsibleState.None
96+
)
97+
item.iconPath = getIcon('vscode-error')
98+
return item
99+
},
100+
getParent: () => this,
101+
},
102+
]
103+
}
104+
82105
public getParent(): TreeNode | undefined {
83106
return this.parent
84107
}

packages/core/src/sagemakerunifiedstudio/shared/client/datazoneClient.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ export class DataZoneClient {
342342
)
343343
return { memberships: response.members, nextToken: response.nextToken }
344344
} catch (err) {
345-
this.logger.error('DataZoneClient: Failed to list project memberships: %s', err as Error)
345+
this.logger.error('DataZoneClient: Failed to list project memberships: %s', (err as Error).message)
346346
throw err
347347
}
348348
}
@@ -370,7 +370,7 @@ export class DataZoneClient {
370370
this.logger.debug(`DataZoneClient: Fetched a total of ${allMemberships.length} project memberships`)
371371
return allMemberships
372372
} catch (err) {
373-
this.logger.error('DataZoneClient: Failed to fetch all project memberships: %s', err as Error)
373+
this.logger.error('DataZoneClient: Failed to fetch all project memberships: %s', (err as Error).message)
374374
throw err
375375
}
376376
}
@@ -420,7 +420,7 @@ export class DataZoneClient {
420420
this.logger.debug(`DataZoneClient: Found ${projects.length} projects for domain ${this.domainId}`)
421421
return { projects, nextToken: response.nextToken }
422422
} catch (err) {
423-
this.logger.error('DataZoneClient: Failed to list projects: %s', err as Error)
423+
this.logger.error('DataZoneClient: Failed to list projects: %s', (err as Error).message)
424424
throw err
425425
}
426426
}

packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.test.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -117,46 +117,26 @@ describe('SageMakerUnifiedStudioProjectNode', function () {
117117
})
118118

119119
describe('setProject', function () {
120-
it('updates the project without firing change event', async function () {
120+
it('updates the project and calls cleanupProjectResources', async function () {
121+
const cleanupSpy = sinon.spy(projectNode as any, 'cleanupProjectResources')
121122
await projectNode.setProject(mockProject)
122123
assert.strictEqual(projectNode['project'], mockProject)
123-
})
124-
125-
it('invalidates credentials and disposes existing sagemaker client', async function () {
126-
// Set up existing sagemaker client with mock
127-
const mockClient = { dispose: sinon.stub() } as any
128-
projectNode['sagemakerClient'] = mockClient
129-
130-
await projectNode.setProject(mockProject)
131-
132-
assert((projectNode['authProvider'].invalidateAllProjectCredentialsInCache as sinon.SinonStub).calledOnce)
133-
assert(mockClient.dispose.calledOnce)
134-
assert.strictEqual(projectNode['sagemakerClient'], undefined)
124+
assert(cleanupSpy.calledOnce)
135125
})
136126
})
137127

138128
describe('clearProject', function () {
139-
it('clears the project and fires change event', async function () {
129+
it('clears the project, calls cleanupProjectResources and fires change event', async function () {
140130
await projectNode.setProject(mockProject)
131+
const cleanupSpy = sinon.spy(projectNode as any, 'cleanupProjectResources')
141132
const emitterSpy = sinon.spy(projectNode['onDidChangeEmitter'], 'fire')
142133

143134
await projectNode.clearProject()
144135

145136
assert.strictEqual(projectNode['project'], undefined)
137+
assert(cleanupSpy.calledOnce)
146138
assert(emitterSpy.calledOnce)
147139
})
148-
149-
it('invalidates credentials and disposes existing sagemaker client', async function () {
150-
// Set up existing sagemaker client with mock
151-
const mockClient = { dispose: sinon.stub() } as any
152-
projectNode['sagemakerClient'] = mockClient
153-
154-
await projectNode.clearProject()
155-
156-
assert((projectNode['authProvider'].invalidateAllProjectCredentialsInCache as sinon.SinonStub).calledOnce)
157-
assert(mockClient.dispose.calledOnce)
158-
assert.strictEqual(projectNode['sagemakerClient'], undefined)
159-
})
160140
})
161141

162142
describe('getProject', function () {
@@ -320,4 +300,26 @@ describe('SageMakerUnifiedStudioProjectNode', function () {
320300
assert.strictEqual(hasAccess, false)
321301
})
322302
})
303+
304+
describe('cleanupProjectResources', function () {
305+
it('invalidates credentials and disposes existing sagemaker client', async function () {
306+
// Set up existing sagemaker client with mock
307+
const mockClient = { dispose: sinon.stub() } as any
308+
projectNode['sagemakerClient'] = mockClient
309+
310+
await projectNode['cleanupProjectResources']()
311+
312+
assert((projectNode['authProvider'].invalidateAllProjectCredentialsInCache as sinon.SinonStub).calledOnce)
313+
assert(mockClient.dispose.calledOnce)
314+
assert.strictEqual(projectNode['sagemakerClient'], undefined)
315+
})
316+
317+
it('handles case when no sagemaker client exists', async function () {
318+
projectNode['sagemakerClient'] = undefined
319+
320+
await projectNode['cleanupProjectResources']()
321+
322+
assert((projectNode['authProvider'].invalidateAllProjectCredentialsInCache as sinon.SinonStub).calledOnce)
323+
})
324+
})
323325
})

0 commit comments

Comments
 (0)