Skip to content

Commit c612781

Browse files
committed
fix(cloudformation): Hide deployment button when change set is not CREATE_COMPLETE and add delete button for CREATE_FAILED change sets
1 parent 0e609ac commit c612781

File tree

4 files changed

+233
-15
lines changed

4 files changed

+233
-15
lines changed

packages/core/src/awsService/cloudformation/commands/cfnCommands.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ export function viewChangeSetCommand(client: LanguageClient, diffProvider: DiffW
148148
params.changeSetName,
149149
true,
150150
[],
151-
describeChangeSetResult.deploymentMode
151+
describeChangeSetResult.deploymentMode,
152+
describeChangeSetResult.status
152153
)
153154
void commands.executeCommand(commandKey('diff.focus'))
154155
} catch (error) {
@@ -435,7 +436,7 @@ async function changeSetSteps(
435436
try {
436437
await environmentManager.refreshSelectedEnvironment()
437438
} catch (error) {
438-
getLogger().warn(`Failed to refresh selected environment: ${extractErrorMessage(error)}`)
439+
getLogger().warn(`Failed to refresh seelcted environment: ${extractErrorMessage(error)}`)
439440
}
440441

441442
templateUri ??= await getTemplatePath(documentManager)

packages/core/src/awsService/cloudformation/stacks/actions/validationWorkflow.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { v4 as uuidv4 } from 'uuid'
7-
import { Parameter, Capability } from '@aws-sdk/client-cloudformation'
7+
import { Parameter, Capability, ChangeSetStatus } from '@aws-sdk/client-cloudformation'
88
import {
99
StackActionPhase,
1010
StackChange,
@@ -163,7 +163,8 @@ export class Validation {
163163
this.changeSetName,
164164
this.shouldEnableDeployment,
165165
validationDetail,
166-
deploymentMode
166+
deploymentMode,
167+
ChangeSetStatus.CREATE_COMPLETE
167168
)
168169
void commands.executeCommand(commandKey('diff.focus'))
169170
}

packages/core/src/awsService/cloudformation/ui/diffWebviewProvider.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class DiffWebviewProvider implements WebviewViewProvider, Disposable {
2424
private readonly disposables: Disposable[] = []
2525
private validationDetail: ValidationDetail[] = []
2626
private deploymentMode?: DeploymentMode
27+
private changeSetStatus?: string
2728

2829
constructor(private readonly coordinator: StackViewCoordinator) {
2930
this.disposables.push(
@@ -46,7 +47,8 @@ export class DiffWebviewProvider implements WebviewViewProvider, Disposable {
4647
changeSetName?: string,
4748
enableDeployments = false,
4849
validationDetail?: ValidationDetail[],
49-
deploymentMode?: DeploymentMode
50+
deploymentMode?: DeploymentMode,
51+
changeSetStatus?: string
5052
) {
5153
this.stackName = stackName
5254
this.changes = changes
@@ -58,6 +60,7 @@ export class DiffWebviewProvider implements WebviewViewProvider, Disposable {
5860
this.validationDetail = validationDetail
5961
}
6062
this.deploymentMode = deploymentMode
63+
this.changeSetStatus = changeSetStatus
6164

6265
await this.coordinator.setChangeSetMode(stackName, true)
6366
if (this._view) {
@@ -121,6 +124,18 @@ export class DiffWebviewProvider implements WebviewViewProvider, Disposable {
121124
const hasNext = this.currentPage < this.totalPages - 1
122125
const hasPrev = this.currentPage > 0
123126

127+
const deletionButton = `
128+
<button id="deleteChangeSet" onclick="deleteChangeSet()" style="
129+
background-color: var(--vscode-button-secondaryBackground);
130+
color: var(--vscode-button-secondaryForeground);
131+
border: none;
132+
padding: 8px 16px;
133+
margin: 0 5px;
134+
cursor: pointer;
135+
border-radius: 2px;
136+
">Delete Changeset</button>
137+
`
138+
124139
if (!changes || changes.length === 0) {
125140
return `
126141
<!DOCTYPE html>
@@ -137,6 +152,21 @@ export class DiffWebviewProvider implements WebviewViewProvider, Disposable {
137152
</head>
138153
<body>
139154
<p>No changes detected for stack: ${this.stackName}</p>
155+
${
156+
this.changeSetName
157+
? `
158+
<div class="deletion-button" style="margin: 10px 0; text-align: left; display: inline-block;">
159+
${deletionButton}
160+
</div>
161+
<script>
162+
const vscode = acquireVsCodeApi();
163+
function deleteChangeSet() {
164+
vscode.postMessage({ command: 'deleteChangeSet' });
165+
}
166+
</script>
167+
`
168+
: ''
169+
}
140170
</body>
141171
</html>
142172
`
@@ -354,6 +384,9 @@ export class DiffWebviewProvider implements WebviewViewProvider, Disposable {
354384
this.changeSetName && this.enableDeployments
355385
? `
356386
<div class="deployment-actions" style="margin: 10px 0; text-align: left; display: inline-block;">
387+
${
388+
this.changeSetStatus === 'CREATE_COMPLETE'
389+
? `
357390
<button id="confirmDeploy" onclick="confirmDeploy()" style="
358391
background-color: var(--vscode-button-background);
359392
color: var(--vscode-button-foreground);
@@ -362,16 +395,10 @@ export class DiffWebviewProvider implements WebviewViewProvider, Disposable {
362395
margin: 0 5px;
363396
cursor: pointer;
364397
border-radius: 2px;
365-
">Deploy Changes</button>
366-
<button id="deleteChangeSet" onclick="deleteChangeSet()" style="
367-
background-color: var(--vscode-button-secondaryBackground);
368-
color: var(--vscode-button-secondaryForeground);
369-
border: none;
370-
padding: 8px 16px;
371-
margin: 0 5px;
372-
cursor: pointer;
373-
border-radius: 2px;
374-
">Delete Changeset</button>
398+
">Deploy Changes</button>`
399+
: ''
400+
}
401+
${deletionButton}
375402
</div>
376403
`
377404
: ''

packages/core/src/test/awsService/cloudformation/ui/diffWebviewProvider.test.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,193 @@ describe('DiffWebviewProvider', function () {
310310
assert.ok(html.includes('Drift Status'))
311311
})
312312
})
313+
314+
describe('deployment button conditional rendering', function () {
315+
it('should show deploy button when changeset is CREATE_COMPLETE and deployments enabled', function () {
316+
const changes: StackChange[] = [
317+
{
318+
resourceChange: {
319+
action: 'Add',
320+
logicalResourceId: 'TestResource',
321+
},
322+
},
323+
]
324+
325+
void provider.updateData(
326+
'test-stack',
327+
changes,
328+
'test-changeset',
329+
true,
330+
undefined,
331+
undefined,
332+
'CREATE_COMPLETE'
333+
)
334+
const mockWebview = createMockWebview()
335+
provider.resolveWebviewView(mockWebview as any)
336+
337+
assert.ok(mockWebview.webview.html.includes('Deploy Changes'))
338+
assert.ok(mockWebview.webview.html.includes('Delete Changeset'))
339+
})
340+
341+
it('should not show deploy button when changeset is not CREATE_COMPLETE', function () {
342+
const changes: StackChange[] = [
343+
{
344+
resourceChange: {
345+
action: 'Add',
346+
logicalResourceId: 'TestResource',
347+
},
348+
},
349+
]
350+
351+
void provider.updateData(
352+
'test-stack',
353+
changes,
354+
'test-changeset',
355+
true,
356+
undefined,
357+
undefined,
358+
'CREATE_IN_PROGRESS'
359+
)
360+
const mockWebview = createMockWebview()
361+
provider.resolveWebviewView(mockWebview as any)
362+
363+
assert.ok(!mockWebview.webview.html.includes('Deploy Changes'))
364+
assert.ok(mockWebview.webview.html.includes('Delete Changeset'))
365+
})
366+
367+
it('should not show deployment buttons when deployments not enabled', function () {
368+
const changes: StackChange[] = [
369+
{
370+
resourceChange: {
371+
action: 'Add',
372+
logicalResourceId: 'TestResource',
373+
},
374+
},
375+
]
376+
377+
void provider.updateData(
378+
'test-stack',
379+
changes,
380+
'test-changeset',
381+
false,
382+
undefined,
383+
undefined,
384+
'CREATE_COMPLETE'
385+
)
386+
const mockWebview = createMockWebview()
387+
provider.resolveWebviewView(mockWebview as any)
388+
389+
assert.ok(!mockWebview.webview.html.includes('Deploy Changes'))
390+
assert.ok(!mockWebview.webview.html.includes('deployment-actions'))
391+
})
392+
393+
it('should not show deployment buttons when no changeset name', function () {
394+
const changes: StackChange[] = [
395+
{
396+
resourceChange: {
397+
action: 'Add',
398+
logicalResourceId: 'TestResource',
399+
},
400+
},
401+
]
402+
403+
void provider.updateData('test-stack', changes, undefined, true, undefined, undefined, 'CREATE_COMPLETE')
404+
const mockWebview = createMockWebview()
405+
provider.resolveWebviewView(mockWebview as any)
406+
407+
assert.ok(!mockWebview.webview.html.includes('Deploy Changes'))
408+
assert.ok(!mockWebview.webview.html.includes('deployment-actions'))
409+
})
410+
})
411+
412+
describe('pagination', function () {
413+
it('should show pagination controls when changes exceed page size', function () {
414+
// Create 60 changes (exceeds default pageSize of 50)
415+
const changes: StackChange[] = Array.from({ length: 60 }, (_, i) => ({
416+
resourceChange: {
417+
action: 'Add',
418+
logicalResourceId: `Resource${i}`,
419+
resourceType: 'AWS::S3::Bucket',
420+
},
421+
}))
422+
423+
const html = setupProviderWithChanges('test-stack', changes)
424+
425+
assert.ok(html.includes('Page 1 of 2'))
426+
assert.ok(html.includes('nextPage()'))
427+
assert.ok(html.includes('prevPage()'))
428+
assert.ok(html.includes('pagination-controls'))
429+
})
430+
431+
it('should not show pagination for small change sets', function () {
432+
const changes: StackChange[] = [
433+
{
434+
resourceChange: {
435+
action: 'Add',
436+
logicalResourceId: 'SingleResource',
437+
resourceType: 'AWS::S3::Bucket',
438+
},
439+
},
440+
]
441+
442+
const html = setupProviderWithChanges('test-stack', changes)
443+
444+
assert.ok(!html.includes('pagination-controls'))
445+
assert.ok(!html.includes('Page 1 of'))
446+
})
447+
448+
it('should display correct page numbers and navigation state', function () {
449+
const changes: StackChange[] = Array.from({ length: 150 }, (_, i) => ({
450+
resourceChange: {
451+
action: 'Add',
452+
logicalResourceId: `Resource${i}`,
453+
},
454+
}))
455+
456+
const html = setupProviderWithChanges('test-stack', changes)
457+
458+
assert.ok(html.includes('Page 1 of 3'))
459+
// Previous button should be disabled on first page
460+
assert.ok(html.includes('opacity: 0.5'))
461+
assert.ok(html.includes('cursor: not-allowed'))
462+
})
463+
})
464+
465+
describe('empty changes handling', function () {
466+
it('should show no changes message when changes is undefined', function () {
467+
void provider.updateData('test-stack', undefined as any, 'test-changeset')
468+
const mockWebview = createMockWebview()
469+
provider.resolveWebviewView(mockWebview as any)
470+
471+
assert.ok(mockWebview.webview.html.includes('No changes detected'))
472+
assert.ok(mockWebview.webview.html.includes('test-stack'))
473+
assert.ok(mockWebview.webview.html.includes('Delete Changeset'))
474+
})
475+
476+
it('should show no changes message when changes array is empty', function () {
477+
void provider.updateData('empty-stack', [], 'test-changeset')
478+
const mockWebview = createMockWebview()
479+
provider.resolveWebviewView(mockWebview as any)
480+
const html = mockWebview.webview.html
481+
482+
assert.ok(html.includes('No changes detected'))
483+
assert.ok(html.includes('empty-stack'))
484+
assert.ok(html.includes('Delete Changeset'))
485+
})
486+
487+
it('should not show delete button when no changeset name', function () {
488+
const html = setupProviderWithChanges('empty-stack', [])
489+
490+
assert.ok(html.includes('No changes detected'))
491+
assert.ok(!html.includes('Delete Changeset'))
492+
})
493+
494+
it('should not show table when no changes', function () {
495+
const html = setupProviderWithChanges('empty-stack', [])
496+
497+
assert.ok(!html.includes('<table'))
498+
assert.ok(!html.includes('Action'))
499+
assert.ok(!html.includes('LogicalResourceId'))
500+
})
501+
})
313502
})

0 commit comments

Comments
 (0)