Skip to content

Commit 82a2368

Browse files
authored
feat(ec2): status icons in QuickPick, Explorer (#3661)
Problem While we have added functionality to start/stop/restart instances from explorer, we don't have anyway of knowing the existing state. Solution First, I want to point out that the icons used here are temporary and I don't believe they are the best fit. They are put there currently to demonstrate the functionality. The three different icons to the left of the label indicate the status of the instance with the 'X' meaning stopped, the check mark meaning 'running', and the wheel indicating that its 'pending' (starting up, booting down). We also added icons to run/stop/restart the instances, such that the icons are only visible for valid actions (i.e. we cannot stop an already stopped instance). Additionally, we only display the `openTerminal` command next to `running` instances. Notes on Explorer Updates: - Currently the command is not accessible from the command palette, because we have no way to link the selection back to the node to update. One approach could be to refresh the entire explorer or find a way to expose the `ec2ParentNode` such that we can refresh just that node. - When executing a command, the node will visually update, but only once. Thus, starting a node will leave it stuck in pending status until another refresh from elsewhere allows it to update. This issue will be addressed in a subsequent PR. - Explorer updates are limited to the EC2ParentNode, so any actions here will not impact updates for other nodes.
1 parent ac13f26 commit 82a2368

File tree

10 files changed

+190
-45
lines changed

10 files changed

+190
-45
lines changed

package.json

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,10 @@
11201120
"command": "aws.ec2.openTerminal",
11211121
"when": "aws.isDevMode"
11221122
},
1123+
{
1124+
"command": "aws.ec2.openTerminal",
1125+
"when": "aws.isDevMode"
1126+
},
11231127
{
11241128
"command": "aws.ec2.startInstance",
11251129
"when": "aws.isDevMode"
@@ -1349,7 +1353,42 @@
13491353
{
13501354
"command": "aws.ec2.rebootInstance",
13511355
"group": "0@1",
1352-
"when": "viewItem == awsEc2Node"
1356+
"when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/"
1357+
},
1358+
{
1359+
"command": "aws.ec2.openTerminal",
1360+
"group": "inline@1",
1361+
"when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/"
1362+
},
1363+
{
1364+
"command": "aws.ec2.startInstance",
1365+
"group": "0@1",
1366+
"when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/"
1367+
},
1368+
{
1369+
"command": "aws.ec2.startInstance",
1370+
"group": "inline@1",
1371+
"when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/"
1372+
},
1373+
{
1374+
"command": "aws.ec2.stopInstance",
1375+
"group": "0@1",
1376+
"when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/"
1377+
},
1378+
{
1379+
"command": "aws.ec2.stopInstance",
1380+
"group": "inline@1",
1381+
"when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/"
1382+
},
1383+
{
1384+
"command": "aws.ec2.rebootInstance",
1385+
"group": "0@1",
1386+
"when": "viewItem =~ /^(awsEc2RunningNode)$/"
1387+
},
1388+
{
1389+
"command": "aws.ec2.rebootInstance",
1390+
"group": "inline@1",
1391+
"when": "viewItem =~ /^(awsEc2RunningNode)$/"
13531392
},
13541393
{
13551394
"command": "aws.ecr.createRepository",
@@ -2122,6 +2161,7 @@
21222161
{
21232162
"command": "aws.ec2.startInstance",
21242163
"title": "%AWS.command.ec2.startInstance%",
2164+
"icon": "$(debug-start)",
21252165
"category": "%AWS.title%",
21262166
"cloud9": {
21272167
"cn": {
@@ -2132,6 +2172,7 @@
21322172
{
21332173
"command": "aws.ec2.stopInstance",
21342174
"title": "%AWS.command.ec2.stopInstance%",
2175+
"icon": "$(debug-stop)",
21352176
"category": "%AWS.title%",
21362177
"cloud9": {
21372178
"cn": {
@@ -2142,6 +2183,7 @@
21422183
{
21432184
"command": "aws.ec2.rebootInstance",
21442185
"title": "%AWS.command.ec2.rebootInstance%",
2186+
"icon": "$(debug-restart)",
21452187
"category": "%AWS.title%",
21462188
"cloud9": {
21472189
"cn": {

src/ec2/activation.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import { telemetry } from '../shared/telemetry/telemetry'
88
import { Ec2InstanceNode } from './explorer/ec2InstanceNode'
99
import { copyTextCommand } from '../awsexplorer/commands/copyText'
1010
import { Ec2Node } from './explorer/ec2ParentNode'
11-
import { openRemoteConnection, openTerminal, rebootInstance, startInstance, stopInstance } from './commands'
11+
import {
12+
openRemoteConnection,
13+
openTerminal,
14+
rebootInstance,
15+
startInstance,
16+
stopInstance,
17+
refreshExplorer,
18+
} from './commands'
1219

1320
export async function activate(ctx: ExtContext): Promise<void> {
1421
ctx.extensionContext.subscriptions.push(
@@ -28,14 +35,17 @@ export async function activate(ctx: ExtContext): Promise<void> {
2835

2936
Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => {
3037
await startInstance(node)
38+
refreshExplorer(node)
3139
}),
3240

3341
Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => {
3442
await stopInstance(node)
43+
refreshExplorer(node)
3544
}),
3645

3746
Commands.register('aws.ec2.rebootInstance', async (node?: Ec2Node) => {
3847
await rebootInstance(node)
48+
refreshExplorer(node)
3949
})
4050
)
4151
}

src/ec2/commands.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66
import { Ec2InstanceNode } from './explorer/ec2InstanceNode'
77
import { Ec2Node } from './explorer/ec2ParentNode'
88
import { Ec2ConnectionManager } from './model'
9-
import { Ec2Prompter, instanceFilter } from './prompter'
10-
import { Ec2Selection } from './prompter'
11-
import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client'
9+
import { Ec2Prompter, instanceFilter, Ec2Selection } from './prompter'
10+
import { Ec2Instance, Ec2Client } from '../shared/clients/ec2Client'
1211
import { copyToClipboard } from '../shared/utilities/messages'
1312

13+
export function refreshExplorer(node?: Ec2Node) {
14+
if (node) {
15+
node instanceof Ec2InstanceNode ? node.parent.refreshNode() : node.refreshNode()
16+
}
17+
}
18+
1419
export async function openTerminal(node?: Ec2Node) {
1520
const selection = await getSelection(node)
1621

src/ec2/explorer/ec2InstanceNode.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,58 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
6-
import { getNameOfInstance } from '../../shared/clients/ec2Client'
5+
import * as vscode from 'vscode'
6+
import { Ec2Client, getNameOfInstance } from '../../shared/clients/ec2Client'
77
import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode'
88
import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
99
import { Ec2Instance } from '../../shared/clients/ec2Client'
1010
import globals from '../../shared/extensionGlobals'
11+
import { getIconCode } from '../utils'
1112
import { Ec2Selection } from '../prompter'
13+
import { Ec2ParentNode } from './ec2ParentNode'
14+
15+
export const Ec2InstanceRunningContext = 'awsEc2RunningNode'
16+
export const Ec2InstanceStoppedContext = 'awsEc2StoppedNode'
17+
export const Ec2InstancePendingContext = 'awsEc2PendingNode'
18+
19+
type Ec2InstanceNodeContext = 'awsEc2RunningNode' | 'awsEc2StoppedNode' | 'awsEc2PendingNode'
1220

1321
export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode {
1422
public constructor(
23+
public readonly parent: Ec2ParentNode,
24+
public readonly client: Ec2Client,
1525
public override readonly regionCode: string,
1626
private readonly partitionId: string,
17-
private instance: Ec2Instance,
18-
public override readonly contextValue: string
27+
protected instance: Ec2Instance
1928
) {
2029
super('')
21-
this.update(instance)
30+
this.updateInstance(instance)
2231
this.id = this.InstanceId
2332
}
2433

25-
public update(newInstance: Ec2Instance) {
34+
public updateInstance(newInstance: Ec2Instance) {
2635
this.setInstance(newInstance)
2736
this.label = `${this.name} (${this.InstanceId})`
28-
this.tooltip = `${this.name}\n${this.InstanceId}\n${this.arn}`
37+
this.contextValue = this.getContext()
38+
this.iconPath = new vscode.ThemeIcon(getIconCode(this.instance))
39+
this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.status}\n${this.arn}`
40+
}
41+
42+
public async updateStatus() {
43+
const newStatus = await this.client.getInstanceStatus(this.InstanceId)
44+
this.updateInstance({ ...this.instance, status: newStatus })
45+
}
46+
47+
private getContext(): Ec2InstanceNodeContext {
48+
if (this.instance.status == 'running') {
49+
return Ec2InstanceRunningContext
50+
}
51+
52+
if (this.instance.status == 'stopped') {
53+
return Ec2InstanceStoppedContext
54+
}
55+
56+
return Ec2InstancePendingContext
2957
}
3058

3159
public setInstance(newInstance: Ec2Instance) {

src/ec2/explorer/ec2ParentNode.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@ import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode'
99
import { Ec2InstanceNode } from './ec2InstanceNode'
1010
import { Ec2Client } from '../../shared/clients/ec2Client'
1111
import { updateInPlace } from '../../shared/utilities/collectionUtils'
12+
import { Commands } from '../../shared/vscode/commands'
1213

13-
export const contextValueEc2 = 'awsEc2Node'
14+
export const parentContextValue = 'awsEc2ParentNode'
1415
export type Ec2Node = Ec2InstanceNode | Ec2ParentNode
1516

1617
export class Ec2ParentNode extends AWSTreeNodeBase {
1718
protected readonly placeHolderMessage = '[No EC2 Instances Found]'
18-
protected readonly ec2InstanceNodes: Map<string, Ec2InstanceNode>
19-
public override readonly contextValue: string = contextValueEc2
19+
protected ec2InstanceNodes: Map<string, Ec2InstanceNode>
20+
public override readonly contextValue: string = parentContextValue
2021

2122
public constructor(
2223
public override readonly regionCode: string,
23-
private readonly partitionId: string,
24+
public readonly partitionId: string,
2425
protected readonly ec2Client: Ec2Client
2526
) {
2627
super('EC2', vscode.TreeItemCollapsibleState.Collapsed)
@@ -44,8 +45,17 @@ export class Ec2ParentNode extends AWSTreeNodeBase {
4445
updateInPlace(
4546
this.ec2InstanceNodes,
4647
ec2Instances.keys(),
47-
key => this.ec2InstanceNodes.get(key)!.update(ec2Instances.get(key)!),
48-
key => new Ec2InstanceNode(this.regionCode, this.partitionId, ec2Instances.get(key)!, contextValueEc2)
48+
key => this.ec2InstanceNodes.get(key)!.updateInstance(ec2Instances.get(key)!),
49+
key => new Ec2InstanceNode(this, this.ec2Client, this.regionCode, this.partitionId, ec2Instances.get(key)!)
4950
)
5051
}
52+
53+
public async clearChildren() {
54+
this.ec2InstanceNodes = new Map<string, Ec2InstanceNode>()
55+
}
56+
57+
public async refreshNode(): Promise<void> {
58+
this.clearChildren()
59+
Commands.vscode().execute('aws.refreshAwsExplorerNode', this)
60+
}
5161
}

src/ec2/prompter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client'
99
import { isValidResponse } from '../shared/wizards/wizard'
1010
import { CancellationError } from '../shared/utilities/timeoutUtils'
1111
import { AsyncCollection } from '../shared/utilities/asyncCollection'
12+
import { getIconCode } from './utils'
1213

1314
export type instanceFilter = (instance: Ec2Instance) => boolean
1415
export interface Ec2Selection {
@@ -20,8 +21,9 @@ export class Ec2Prompter {
2021
public constructor(protected filter?: instanceFilter) {}
2122

2223
protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem<string> {
24+
const icon = `$(${getIconCode(instance)})`
2325
return {
24-
label: '$(terminal) \t' + (instance.name ?? '(no name)'),
26+
label: `${icon} \t ${instance.name ?? '(no name)'}`,
2527
detail: instance.InstanceId,
2628
data: instance.InstanceId,
2729
}

src/ec2/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Ec2Instance } from '../shared/clients/ec2Client'
7+
8+
export function getIconCode(instance: Ec2Instance) {
9+
if (instance.status === 'running') {
10+
return 'check'
11+
}
12+
13+
if (instance.status === 'stopped') {
14+
return 'stop'
15+
}
16+
17+
return 'loading~spin'
18+
}

src/test/ec2/explorer/ec2InstanceNode.test.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44
*/
55

66
import * as assert from 'assert'
7-
import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode'
8-
import { Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client'
9-
import { contextValueEc2 } from '../../../ec2/explorer/ec2ParentNode'
7+
import {
8+
Ec2InstanceNode,
9+
Ec2InstancePendingContext,
10+
Ec2InstanceRunningContext,
11+
Ec2InstanceStoppedContext,
12+
} from '../../../ec2/explorer/ec2InstanceNode'
13+
import { Ec2Client, Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client'
14+
import { Ec2ParentNode } from '../../../ec2/explorer/ec2ParentNode'
1015

1116
describe('ec2InstanceNode', function () {
1217
let testNode: Ec2InstanceNode
1318
let testInstance: Ec2Instance
19+
const testRegion = 'testRegion'
20+
const testPartition = 'testPartition'
1421

1522
before(function () {
1623
testInstance = {
@@ -21,9 +28,15 @@ describe('ec2InstanceNode', function () {
2128
Value: 'testName',
2229
},
2330
],
31+
status: 'running',
2432
}
33+
const testClient = new Ec2Client('')
34+
const testParentNode = new Ec2ParentNode(testRegion, testPartition, testClient)
35+
testNode = new Ec2InstanceNode(testParentNode, testClient, 'testRegion', 'testPartition', testInstance)
36+
})
2537

26-
testNode = new Ec2InstanceNode('testRegion', 'testPartition', testInstance, contextValueEc2)
38+
this.beforeEach(function () {
39+
testNode.updateInstance(testInstance)
2740
})
2841

2942
it('instantiates without issue', async function () {
@@ -42,13 +55,37 @@ describe('ec2InstanceNode', function () {
4255
assert.strictEqual(testNode.name, getNameOfInstance(testInstance))
4356
})
4457

45-
it('initializes the tooltip', async function () {
46-
assert.strictEqual(testNode.tooltip, `${testNode.name}\n${testNode.InstanceId}\n${testNode.arn}`)
47-
})
48-
4958
it('has no children', async function () {
5059
const childNodes = await testNode.getChildren()
5160
assert.ok(childNodes)
5261
assert.strictEqual(childNodes.length, 0, 'Expected node to have no children')
5362
})
63+
64+
it('has an EC2ParentNode as parent', async function () {
65+
assert.ok(testNode.parent instanceof Ec2ParentNode)
66+
})
67+
68+
it('intializes the client', async function () {
69+
assert.ok(testNode.client instanceof Ec2Client)
70+
})
71+
72+
it('sets context value based on status', async function () {
73+
const stoppedInstance = { ...testInstance, status: 'stopped' }
74+
testNode.updateInstance(stoppedInstance)
75+
assert.strictEqual(testNode.contextValue, Ec2InstanceStoppedContext)
76+
77+
const runningInstance = { ...testInstance, status: 'running' }
78+
testNode.updateInstance(runningInstance)
79+
assert.strictEqual(testNode.contextValue, Ec2InstanceRunningContext)
80+
81+
const pendingInstance = { ...testInstance, status: 'pending' }
82+
testNode.updateInstance(pendingInstance)
83+
assert.strictEqual(testNode.contextValue, Ec2InstancePendingContext)
84+
})
85+
86+
it('updates label with new instance', async function () {
87+
const newIdInstance = { ...testInstance, InstanceId: 'testId2' }
88+
testNode.updateInstance(newIdInstance)
89+
assert.strictEqual(testNode.label, `${getNameOfInstance(newIdInstance)} (${newIdInstance.InstanceId})`)
90+
})
5491
})

src/test/ec2/explorer/ec2ParentNode.test.ts

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

66
import * as assert from 'assert'
7-
import { Ec2ParentNode, contextValueEc2 } from '../../../ec2/explorer/ec2ParentNode'
7+
import { Ec2ParentNode } from '../../../ec2/explorer/ec2ParentNode'
88
import { stub } from '../../utilities/stubber'
99
import { Ec2Client, Ec2Instance } from '../../../shared/clients/ec2Client'
1010
import { intoCollection } from '../../../shared/utilities/collectionUtils'
@@ -36,8 +36,8 @@ describe('ec2ParentNode', function () {
3636

3737
beforeEach(function () {
3838
instances = [
39-
{ name: 'firstOne', InstanceId: '0' },
40-
{ name: 'secondOne', InstanceId: '1' },
39+
{ name: 'firstOne', InstanceId: '0', status: 'running' },
40+
{ name: 'secondOne', InstanceId: '1', status: 'stopped' },
4141
]
4242

4343
testNode = new Ec2ParentNode(testRegion, testPartition, createClient())
@@ -61,14 +61,6 @@ describe('ec2ParentNode', function () {
6161
)
6262
})
6363

64-
it('has child nodes with ec2 contextValuue', async function () {
65-
const childNodes = await testNode.getChildren()
66-
67-
childNodes.forEach(node =>
68-
assert.strictEqual(node.contextValue, contextValueEc2, 'expected the node to have a ec2 contextValue')
69-
)
70-
})
71-
7264
it('sorts child nodes', async function () {
7365
const sortedText = ['aa', 'ab', 'bb', 'bc', 'cc', 'cd']
7466
instances = [

0 commit comments

Comments
 (0)