Skip to content

Commit 2b9ac24

Browse files
authored
fix(ecs): update Copy ARN command and region node (#2916)
1 parent 856dc1b commit 2b9ac24

File tree

13 files changed

+145
-80
lines changed

13 files changed

+145
-80
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
},
110110
"aws.ecs.openTerminalCommand": {
111111
"type": "string",
112-
"default": "/usr/bin/env bash",
112+
"default": "/bin/sh",
113113
"markdownDescription": "%AWS.configuration.description.ecs.openTerminalCommand%"
114114
},
115115
"aws.iot.maxItemsPerPage": {

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"AWS.ecs.enableEcsExec": "Enable Command Execution",
103103
"AWS.ecs.disableEcsExec": "Disable Command Execution",
104104
"AWS.ecs.runCommandInContainer": "Run Command in Container",
105-
"AWS.ecs.openTaskInTerminal": "Open Task in Terminal",
105+
"AWS.ecs.openTaskInTerminal": "Open Terminal...",
106106
"AWS.command.samcli.detect": "Detect SAM CLI",
107107
"AWS.command.deleteCloudFormation": "Delete CloudFormation Stack",
108108
"AWS.command.viewSchemaItem": "View Schema",

src/awsexplorer/commands/copyArn.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import { copyToClipboard } from '../../shared/utilities/messages'
1313
import { Window } from '../../shared/vscode/window'
1414
import { Commands } from '../../shared/vscode/commands'
1515
import { getIdeProperties } from '../../shared/extensionUtilities'
16+
import { TreeShim } from '../../shared/treeview/utils'
1617

1718
/**
1819
* Copies the arn of the resource represented by the given node.
1920
*/
2021
export async function copyArnCommand(
21-
node: AWSResourceNode,
22+
node: AWSResourceNode | TreeShim<AWSResourceNode>,
2223
window = Window.vscode(),
2324
env = Env.vscode(),
2425
commands = Commands.vscode()
2526
): Promise<void> {
27+
node = node instanceof TreeShim ? node.node.resource : node
28+
2629
try {
2730
copyToClipboard(node.arn, 'ARN', window, env)
2831
} catch (e) {

src/awsexplorer/commands/copyName.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,17 @@ import { Env } from '../../shared/vscode/env'
77
import { copyToClipboard } from '../../shared/utilities/messages'
88
import { Window } from '../../shared/vscode/window'
99
import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode'
10+
import { TreeShim } from '../../shared/treeview/utils'
1011

1112
/**
1213
* Copies the name of the resource represented by the given node.
1314
*/
1415
export async function copyNameCommand(
15-
node: AWSResourceNode,
16+
node: AWSResourceNode | TreeShim<AWSResourceNode>,
1617
window = Window.vscode(),
1718
env = Env.vscode()
1819
): Promise<void> {
19-
copyToClipboard(node.name, 'name', window, env)
20-
recordCopyName()
21-
}
20+
node = node instanceof TreeShim ? node.node.resource : node
2221

23-
// TODO add telemetry for copy name
24-
function recordCopyName(): void {}
22+
await copyToClipboard(node.name, 'name', window, env)
23+
}

src/awsexplorer/regionNode.ts

Lines changed: 66 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -28,77 +28,97 @@ import { DefaultSchemaClient } from '../shared/clients/schemaClient'
2828
import { getEcsRootNode } from '../ecs/model'
2929
import { TreeShim } from '../shared/treeview/utils'
3030

31+
const serviceCandidates = [
32+
{
33+
serviceId: 'apigateway',
34+
createFn: (regionCode: string, partitionId: string) => new ApiGatewayNode(partitionId, regionCode),
35+
},
36+
{
37+
serviceId: 'apprunner',
38+
createFn: (regionCode: string) => new AppRunnerNode(regionCode, new DefaultAppRunnerClient(regionCode)),
39+
},
40+
{
41+
serviceId: 'cloudformation',
42+
createFn: (regionCode: string) => new CloudFormationNode(regionCode),
43+
},
44+
{
45+
serviceId: 'logs',
46+
createFn: (regionCode: string) => new CloudWatchLogsNode(regionCode),
47+
},
48+
{
49+
serviceId: 'ecr',
50+
createFn: (regionCode: string) => new EcrNode(new DefaultEcrClient(regionCode)),
51+
},
52+
{
53+
serviceId: 'ecs',
54+
createFn: (regionCode: string) => new TreeShim(getEcsRootNode(regionCode)),
55+
},
56+
{
57+
serviceId: 'iot',
58+
createFn: (regionCode: string) => new IotNode(new DefaultIotClient(regionCode)),
59+
},
60+
{
61+
serviceId: 'lambda',
62+
createFn: (regionCode: string) => new LambdaNode(regionCode),
63+
},
64+
{
65+
serviceId: 's3',
66+
createFn: (regionCode: string) => new S3Node(new DefaultS3Client(regionCode)),
67+
},
68+
{
69+
serviceId: 'schemas',
70+
createFn: (regionCode: string) =>
71+
!isCloud9() ? new SchemasNode(new DefaultSchemaClient(regionCode)) : undefined,
72+
},
73+
{
74+
serviceId: 'states',
75+
createFn: (regionCode: string) => new StepFunctionsNode(regionCode),
76+
},
77+
{
78+
serviceId: 'ssm',
79+
createFn: (regionCode: string) => new SsmDocumentNode(regionCode),
80+
},
81+
]
82+
3183
/**
3284
* An AWS Explorer node representing a region.
3385
* Contains resource types as child nodes (for example, nodes representing
3486
* an account's Lambda Functions and CloudFormation stacks for this region)
3587
*/
3688
export class RegionNode extends AWSTreeNodeBase {
3789
private region: Region
38-
private readonly childNodes: AWSTreeNodeBase[] = []
3990
public readonly regionCode: string
4091

4192
public get regionName(): string {
4293
return this.region.name
4394
}
4495

45-
public constructor(region: Region, regionProvider: RegionProvider) {
96+
public constructor(region: Region, private readonly regionProvider: RegionProvider) {
4697
super(region.name, TreeItemCollapsibleState.Expanded)
4798
this.contextValue = 'awsRegionNode'
4899
this.region = region
49100
this.regionCode = region.id
50101
this.update(region)
102+
}
51103

104+
public async getChildren(): Promise<AWSTreeNodeBase[]> {
52105
// Services that are candidates to add to the region explorer.
53106
// `serviceId`s are checked against ~/resources/endpoints.json to see whether or not the service is available in the given region.
54107
// If the service is available, we use the `createFn` to generate the node for the region.
55108
// This interface exists so we can add additional nodes to the array (otherwise Typescript types the array to what's already in the array at creation)
56-
const partitionId = regionProvider.getPartitionId(this.regionCode) ?? DEFAULT_PARTITION
57-
const serviceCandidates = [
58-
{ serviceId: 'apigateway', createFn: () => new ApiGatewayNode(partitionId, this.regionCode) },
59-
{
60-
serviceId: 'apprunner',
61-
createFn: () => new AppRunnerNode(this.regionCode, new DefaultAppRunnerClient(this.regionCode)),
62-
},
63-
{ serviceId: 'cloudformation', createFn: () => new CloudFormationNode(this.regionCode) },
64-
{ serviceId: 'logs', createFn: () => new CloudWatchLogsNode(this.regionCode) },
65-
{
66-
serviceId: 'ecr',
67-
createFn: () => new EcrNode(new DefaultEcrClient(this.regionCode)),
68-
},
69-
{
70-
serviceId: 'ecs',
71-
createFn: () => new TreeShim(getEcsRootNode(this.regionCode)),
72-
},
73-
{
74-
serviceId: 'iot',
75-
createFn: () => new IotNode(new DefaultIotClient(this.regionCode)),
76-
},
77-
{ serviceId: 'lambda', createFn: () => new LambdaNode(this.regionCode) },
78-
{
79-
serviceId: 's3',
80-
createFn: () => new S3Node(new DefaultS3Client(this.regionCode)),
81-
},
82-
...(isCloud9()
83-
? []
84-
: [
85-
{
86-
serviceId: 'schemas',
87-
createFn: () => new SchemasNode(new DefaultSchemaClient(this.regionCode)),
88-
},
89-
]),
90-
{ serviceId: 'states', createFn: () => new StepFunctionsNode(this.regionCode) },
91-
{ serviceId: 'ssm', createFn: () => new SsmDocumentNode(this.regionCode) },
92-
]
93-
94-
for (const serviceCandidate of serviceCandidates) {
95-
this.addChildNodeIfInRegion(serviceCandidate.serviceId, regionProvider, serviceCandidate.createFn)
109+
const partitionId = this.regionProvider.getPartitionId(this.regionCode) ?? DEFAULT_PARTITION
110+
const childNodes: AWSTreeNodeBase[] = []
111+
for (const { serviceId, createFn } of serviceCandidates) {
112+
if (this.regionProvider.isServiceInRegion(serviceId, this.regionCode)) {
113+
const node = createFn(this.regionCode, partitionId)
114+
if (node !== undefined) {
115+
childNodes.push(node)
116+
}
117+
}
96118
}
97-
this.childNodes.push(new ResourcesNode(this.regionCode))
98-
}
119+
childNodes.push(new ResourcesNode(this.regionCode))
99120

100-
public async getChildren(): Promise<AWSTreeNodeBase[]> {
101-
return this.sortNodes(this.childNodes)
121+
return this.sortNodes(childNodes)
102122
}
103123

104124
private sortNodes(nodes: AWSTreeNodeBase[]) {
@@ -116,14 +136,4 @@ export class RegionNode extends AWSTreeNodeBase {
116136
this.label = this.regionName
117137
this.tooltip = `${this.regionName} [${this.regionCode}]`
118138
}
119-
120-
private addChildNodeIfInRegion(
121-
serviceId: string,
122-
regionProvider: RegionProvider,
123-
childNodeProducer: () => AWSTreeNodeBase
124-
) {
125-
if (regionProvider.isServiceInRegion(serviceId, this.regionCode)) {
126-
this.childNodes.push(childNodeProducer())
127-
}
128-
}
129139
}

src/ecs/commands.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,7 @@ export const openTaskInTerminal = Commands.register('aws.ecs.openTaskInTerminal'
173173
terminal.show()
174174
})
175175
} catch (err) {
176-
throw ToolkitError.chain(
177-
err,
178-
localize('AWS.ecs.openTaskInTerminal.error', 'Failed to open task in terminal.')
179-
)
176+
throw ToolkitError.chain(err, localize('AWS.ecs.openTaskInTerminal.error', 'Failed to open terminal.'))
180177
}
181178
})
182179
})

src/ecs/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class Container {
6868

6969
export class Service {
7070
public readonly id = this.description.serviceArn!
71+
public readonly arn = this.description.serviceArn!
7172

7273
private readonly onDidChangeEmitter = new vscode.EventEmitter<void>()
7374
public readonly onDidChangeTreeItem = this.onDidChangeEmitter.event

src/ecs/wizards/executeCommand.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function createTaskPrompter(node: Container) {
2222
const taskItems = (async () => {
2323
// Filter for only 'Running' tasks
2424
return (await node.listTasks()).map(task => {
25+
// TODO: get task definition name and include it in the item detail
2526
// The last 32 digits of the task arn is the task identifier
2627
const taskId = task.taskArn.substring(task.taskArn.length - 32)
2728
const invalidSelection = task.lastStatus !== 'RUNNING'
@@ -31,7 +32,7 @@ function createTaskPrompter(node: Container) {
3132
detail: `Status: ${task.lastStatus} Desired status: ${task.desiredStatus}`,
3233
description:
3334
invalidSelection && task.desiredStatus === 'RUNNING'
34-
? 'Task starting, try again later.'
35+
? 'Container instance starting, try again later.'
3536
: undefined,
3637
data: taskId,
3738
invalidSelection,
@@ -40,12 +41,15 @@ function createTaskPrompter(node: Container) {
4041
})()
4142

4243
return createQuickPick(taskItems, {
43-
title: localize('AWS.command.ecs.runCommandInContainer.chooseTask', 'Choose a task'),
44+
title: localize('AWS.command.ecs.runCommandInContainer.chooseInstance', 'Choose a container instance'),
4445
buttons: createCommonButtons(ecsExecToolkitGuideUrl),
4546
noItemsFoundItem: {
46-
label: localize('AWS.command.ecs.runCommandInContainer.noTasks', 'No valid tasks for this container'),
47+
label: localize(
48+
'AWS.command.ecs.runCommandInContainer.noInstances',
49+
'No valid instances for this container'
50+
),
4751
detail: localize(
48-
'AWS.command.ecs.runCommandInContainer.noTasks.description',
52+
'AWS.command.ecs.runCommandInContainer.noInstances.description',
4953
'If command execution was recently enabled, try again in a few minutes.'
5054
),
5155
data: WIZARD_BACK,

src/shared/clients/ecsClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class DefaultEcsClient {
7777
return []
7878
}
7979

80-
const resp = await (await client).describeServices({ services }).promise()
80+
const resp = await (await client).describeServices({ cluster: request.cluster, services }).promise()
8181
return resp.services!
8282
})
8383
}

src/shared/treeview/resource.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode'
77
import * as localizedText from '../localizedText'
88
import { UnknownError } from '../errors'
9-
import { AsyncCollection, isAsyncCollection } from '../utilities/asyncCollection'
9+
import { isAsyncCollection } from '../utilities/asyncCollection'
1010
import { isAsyncIterable } from '../utilities/collectionUtils'
1111
import { once } from '../utilities/functionUtils'
1212
import { Commands } from '../vscode/commands2'
@@ -22,7 +22,7 @@ interface SimpleResourceProvider<T = unknown> {
2222
interface PaginatedResourceProvider<T = unknown> {
2323
readonly paginated: true
2424
readonly onDidChange?: vscode.Event<void>
25-
listResources(): AsyncCollection<T | T[]>
25+
listResources(): AsyncIterable<T | T[]>
2626
}
2727

2828
export type ResourceProvider<T> = SimpleResourceProvider<T> | PaginatedResourceProvider<T>
@@ -94,10 +94,33 @@ const loadMore = <T>(controller: LoadMoreable<T>) => controller.loadMore()
9494
export const loadMoreCommand = Commands.instance.register('_aws.resources.loadMore', loadMore)
9595

9696
interface TreeNodeOptions<T> {
97+
/**
98+
* A message or tree node to be used when no children are available.
99+
*/
97100
readonly placeholder?: string | TreeNode
101+
102+
/**
103+
* An object that describes how the tree node should load child nodes. If absent, it is
104+
* assumed that the tree node will never have children.
105+
*
106+
* This can come in two variants: paginated and non-paginated. Paginated providers must
107+
* return a {@link AsyncIterable} while non-paginated providers can be sync or async.
108+
*/
98109
readonly childrenProvider?: ResourceProvider<TreeNode<T>>
110+
111+
/**
112+
* Factory function to produce a tree node from an error.
113+
*
114+
* A default handler is used if this is not provided.
115+
*/
99116
readonly onError?: (error: Error) => TreeNode
100-
sort?(a: TreeNode<T>, b: TreeNode<T>): number
117+
118+
/**
119+
* Callback used to sort nodes as they are loaded in.
120+
*
121+
* For paginated lists of children, the entire list is re-sorted with each new page.
122+
*/
123+
sort?(this: void, a: TreeNode<T>, b: TreeNode<T>): number
101124
}
102125

103126
type TreeResource<T> = Pick<TreeNode<T>, 'id' | 'getTreeItem' | 'onDidChangeTreeItem'>
@@ -152,7 +175,7 @@ export class ResourceTreeNode<T extends TreeResource<unknown>, U = never> implem
152175

153176
if (succeeded) {
154177
if (this.options.sort) {
155-
result.sort(this.options.sort.bind(this.options))
178+
result.sort(this.options.sort)
156179
}
157180

158181
if (this.loader && !this.loader.done) {

0 commit comments

Comments
 (0)