Skip to content

Commit 4b94492

Browse files
authored
Merge #3211 from aws/samsync
feat(sam): "Sync SAM" shows more info if no template.yaml found
2 parents eaf4b14 + d68bd06 commit 4b94492

File tree

9 files changed

+117
-22
lines changed

9 files changed

+117
-22
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "\"Sync SAM Application\" command provides Create and Help actions at each step (SAM template, S3 bucket, ECR repo, CloudFormation stack)"
4+
}

src/shared/awsConsole.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*!
2+
* Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
8+
export function getAwsConsoleUrl(service: 'ecr' | 'cloudformation', region: string): vscode.Uri {
9+
switch (service) {
10+
case 'ecr':
11+
return vscode.Uri.parse(`https://${region}.console.aws.amazon.com/ecr/repositories?region=${region}`)
12+
case 'cloudformation':
13+
return vscode.Uri.parse(`https://${region}.console.aws.amazon.com/cloudformation/home?region=${region}`)
14+
default:
15+
throw Error()
16+
}
17+
}

src/shared/constants.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import * as vscode from 'vscode'
7+
68
import { isCloud9 } from './extensionUtilities'
79

810
export const extensionSettingsPrefix = 'aws'
@@ -38,9 +40,11 @@ export const supportedLambdaRuntimesUrl: string =
3840
'https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html'
3941
export const createUrlForLambdaFunctionUrl = 'https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html'
4042
// URLs for samInitWizard
41-
export const samInitDocUrl: string = isCloud9()
42-
? 'https://docs.aws.amazon.com/cloud9/latest/user-guide/serverless-apps-toolkit.html#sam-create'
43-
: 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html#serverless-apps-create'
43+
export const samInitDocUrl = vscode.Uri.parse(
44+
isCloud9()
45+
? 'https://docs.aws.amazon.com/cloud9/latest/user-guide/serverless-apps-toolkit.html#sam-create'
46+
: 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html#serverless-apps-create'
47+
)
4448
export const launchConfigDocUrl: string = isCloud9()
4549
? 'https://docs.aws.amazon.com/cloud9/latest/user-guide/sam-debug-config-ref.html'
4650
: 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps-run-debug-config-ref.html'
@@ -50,8 +54,9 @@ export const samDeployDocUrl: string = isCloud9()
5054
: 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html#serverless-apps-deploy'
5155
export const lambdaFunctionUrlConfigUrl: string = 'https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html'
5256
// URLs for "sam sync" wizard.
53-
export const samSyncUrl: string =
57+
export const samSyncUrl = vscode.Uri.parse(
5458
'https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/accelerate-getting-started.html'
59+
)
5560

5661
// URLs for CDK
5762
export const cdkProvideFeedbackUrl: string = `${githubUrl}/issues/new/choose`

src/shared/logger/winstonToolkitLogger.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,18 @@ export class WinstonToolkitLogger implements Logger, vscode.Disposable {
119119
}
120120

121121
private mapError(level: LogLevel, err: Error): Error | string {
122+
// Use ToolkitError.trace even if we have source mapping (see below), because:
123+
// 1. it is what users will see, we want visibility into that when debugging
124+
// 2. it is often more useful than the stacktrace anyway
125+
if (err instanceof ToolkitError) {
126+
return err.trace
127+
}
128+
122129
if (isSourceMappingAvailable() && level === 'error') {
123130
return err
124131
}
125132

126-
return err instanceof ToolkitError ? err.trace : formatError(UnknownError.cast(err))
133+
return formatError(UnknownError.cast(err))
127134
}
128135

129136
private writeToLogs(level: LogLevel, message: string | Error, ...meta: any[]): number {

src/shared/sam/sync.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import globals from '../extensionGlobals'
77

88
import * as vscode from 'vscode'
99
import * as path from 'path'
10+
import * as nls from 'vscode-nls'
1011
import * as localizedText from '../localizedText'
1112
import { DefaultS3Client } from '../clients/s3Client'
1213
import { Wizard } from '../wizards/wizard'
@@ -40,7 +41,10 @@ import { parse } from 'semver'
4041
import { isAutomation } from '../vscode/env'
4142
import { getOverriddenParameters } from '../../lambda/config/parameterUtils'
4243
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
43-
import { samSyncUrl } from '../constants'
44+
import { samSyncUrl, samInitDocUrl } from '../constants'
45+
import { getAwsConsoleUrl } from '../awsConsole'
46+
47+
const localize = nls.loadMessageBundle()
4448

4549
export interface SyncParams {
4650
readonly region: string
@@ -68,13 +72,22 @@ function createBucketPrompter(client: DefaultS3Client) {
6872

6973
return createQuickPick(items, {
7074
title: 'Select an S3 Bucket',
71-
placeholder: 'Filter or enter a new bucket name',
75+
placeholder: 'Select a bucket (or enter a name to create one)',
7276
buttons: createCommonButtons(samSyncUrl),
7377
filterBoxInputSettings: {
7478
label: 'Create a New Bucket',
7579
// This is basically a hack. I need to refactor `createQuickPick` a bit.
7680
transform: v => prefixNewBucketName(v),
7781
},
82+
noItemsFoundItem: {
83+
label: localize(
84+
'aws.cfn.noStacks',
85+
'No S3 buckets for region "{0}". Enter a name to create a new one.',
86+
client.regionCode
87+
),
88+
data: undefined,
89+
onClick: undefined,
90+
},
7891
})
7992
}
8093

@@ -84,6 +97,7 @@ const canShowStack = (s: StackSummary) =>
8497

8598
function createStackPrompter(client: DefaultCloudFormationClient) {
8699
const recentStack = getRecentResponse(client.regionCode, 'stackName')
100+
const consoleUrl = getAwsConsoleUrl('cloudformation', client.regionCode)
87101
const items = client.listAllStacks().map(stacks =>
88102
stacks.filter(canShowStack).map(s => ({
89103
label: s.StackName,
@@ -96,17 +110,27 @@ function createStackPrompter(client: DefaultCloudFormationClient) {
96110

97111
return createQuickPick(items, {
98112
title: 'Select a CloudFormation Stack',
99-
placeholder: 'Filter or enter a new stack name',
113+
placeholder: 'Select a stack (or enter a name to create one)',
100114
filterBoxInputSettings: {
101115
label: 'Create a New Stack',
102116
transform: v => v,
103117
},
104-
buttons: createCommonButtons(samSyncUrl),
118+
buttons: createCommonButtons(samSyncUrl, consoleUrl),
119+
noItemsFoundItem: {
120+
label: localize(
121+
'aws.cfn.noStacks',
122+
'No stacks in region "{0}". Enter a name to create a new one.',
123+
client.regionCode
124+
),
125+
data: undefined,
126+
onClick: undefined,
127+
},
105128
})
106129
}
107130

108131
function createEcrPrompter(client: DefaultEcrClient) {
109132
const recentEcrRepo = getRecentResponse(client.regionCode, 'ecrRepoUri')
133+
const consoleUrl = getAwsConsoleUrl('ecr', client.regionCode)
110134
const items = client.listAllRepositories().map(list =>
111135
list.map(repo => ({
112136
label: repo.repositoryName,
@@ -118,12 +142,21 @@ function createEcrPrompter(client: DefaultEcrClient) {
118142

119143
return createQuickPick(items, {
120144
title: 'Select an ECR Repository',
121-
placeholder: 'Filter or enter an existing repository URI',
122-
buttons: createCommonButtons(samSyncUrl),
145+
placeholder: 'Select a repository (or enter repository URI)',
146+
buttons: createCommonButtons(samSyncUrl, consoleUrl),
123147
filterBoxInputSettings: {
124148
label: 'Existing repository URI',
125149
transform: v => v,
126150
},
151+
noItemsFoundItem: {
152+
label: localize(
153+
'aws.ecr.noRepos',
154+
'No ECR repositories in region "{0}". Enter a name to create a new one.',
155+
client.regionCode
156+
),
157+
data: undefined,
158+
onClick: undefined,
159+
},
127160
})
128161
}
129162

@@ -138,6 +171,7 @@ export function createEnvironmentPrompter(config: SamConfig, environments = conf
138171

139172
return createQuickPick(items, {
140173
title: 'Select an Environment to Use',
174+
placeholder: 'Select an environment',
141175
buttons: createCommonButtons(samSyncUrl),
142176
})
143177
}
@@ -166,8 +200,14 @@ function createTemplatePrompter() {
166200

167201
const trimmedItems = folders.size === 1 ? items.map(item => ({ ...item, description: undefined })) : items
168202
return createQuickPick(trimmedItems, {
169-
title: 'Select a CloudFormation Template',
203+
title: 'Select a SAM CloudFormation Template',
204+
placeholder: 'Select a SAM template.yaml file',
170205
buttons: createCommonButtons(samSyncUrl),
206+
noItemsFoundItem: {
207+
label: localize('aws.sam.noWorkspace', 'No SAM template.yaml file(s) found. Select for help'),
208+
data: undefined,
209+
onClick: () => vscode.env.openExternal(samInitDocUrl),
210+
},
171211
})
172212
}
173213

src/shared/ui/buttons.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import * as nls from 'vscode-nls'
88
import { documentationUrl } from '../constants'
99
import { getIcon } from '../icons'
1010
import { WizardControl, WIZARD_EXIT, WIZARD_RETRY } from '../wizards/wizard'
11+
import { getIdeProperties } from '../extensionUtilities'
1112

1213
const localize = nls.loadMessageBundle()
1314
const helpTooltip = localize('AWS.command.help', 'View Toolkit Documentation')
15+
const awsConsoleTooltip = () => localize('AWS.button.awsConsole', 'Open {0} Console', getIdeProperties().company)
1416

1517
type WizardButton<T> = QuickInputButton<T | WizardControl> | QuickInputButton<void>
1618
export type PrompterButtons<T> = readonly WizardButton<T>[]
@@ -109,6 +111,14 @@ export function createBackButton(): QuickInputButton<WizardControl> {
109111
return vscode.QuickInputButtons.Back as QuickInputButton<WizardControl>
110112
}
111113

114+
export function createAwsConsoleButton(
115+
uri: string | vscode.Uri,
116+
tooltip: string = awsConsoleTooltip()
117+
): QuickInputLinkButton {
118+
const iconPath = getIcon('vscode-link-external')
119+
return new QuickInputLinkButton(uri, iconPath, tooltip)
120+
}
121+
112122
export function createExitButton(): QuickInputButton<WizardControl> {
113123
return {
114124
iconPath: getIcon('vscode-close'),
@@ -137,8 +147,14 @@ export function createPlusButton(tooltip?: string): QuickInputButton<void> {
137147
* Currently has: 'help', 'exit', and 'back'
138148
*
139149
* @param helpUri optional URI to link to for the 'help' button (see {@link createHelpButton} for defaults)
150+
* @param awsConsoleUri optional URI to AWS web console
140151
* @returns An array of buttons
141152
*/
142-
export function createCommonButtons(helpUri?: string | vscode.Uri): PrompterButtons<WizardControl> {
143-
return [createHelpButton(helpUri), createBackButton(), createExitButton()]
153+
export function createCommonButtons(
154+
helpUri?: string | vscode.Uri,
155+
awsConsoleUri?: string | vscode.Uri
156+
): PrompterButtons<WizardControl> {
157+
const buttons2 = [createHelpButton(helpUri), createBackButton(), createExitButton()]
158+
const buttons1: typeof buttons2 = awsConsoleUri ? [createAwsConsoleButton(awsConsoleUri)] : []
159+
return buttons1.concat(buttons2)
144160
}

src/shared/ui/pickerPrompter.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function createQuickPick<T>(
137137

138138
const prompter =
139139
mergedOptions.filterBoxInputSettings !== undefined
140-
? new FilterBoxQuickPickPrompter<T>(picker, mergedOptions.filterBoxInputSettings)
140+
? new FilterBoxQuickPickPrompter<T>(picker, mergedOptions)
141141
: new QuickPickPrompter<T>(picker, mergedOptions)
142142

143143
prompter.loadItems(items)
@@ -578,12 +578,12 @@ export class FilterBoxQuickPickPrompter<T> extends QuickPickPrompter<T> {
578578
}
579579
}
580580

581-
constructor(quickPick: DataQuickPick<T>, private readonly settings: FilterBoxInputSettings<T>) {
581+
constructor(quickPick: DataQuickPick<T>, protected override options: ExtendedQuickPickOptions<T>) {
582582
super(quickPick)
583583

584584
this.transform(selection => {
585585
if ((selection as T | typeof customUserInput) === customUserInput) {
586-
return settings.transform(quickPick.value) ?? selection
586+
return options?.filterBoxInputSettings?.transform(quickPick.value) ?? selection
587587
}
588588
return selection
589589
})
@@ -600,10 +600,13 @@ export class FilterBoxQuickPickPrompter<T> extends QuickPickPrompter<T> {
600600

601601
private addFilterBoxInput(): void {
602602
const picker = this.quickPick as DataQuickPick<T | symbol>
603-
const validator = (input: string) =>
604-
this.settings.validator !== undefined ? this.settings.validator(input) : undefined
603+
const settings = this.options?.filterBoxInputSettings
604+
if (!settings) {
605+
throw Error()
606+
}
607+
const validator = (input: string) => (settings.validator !== undefined ? settings.validator(input) : undefined)
605608
const items = picker.items.filter(item => item.data !== customUserInput)
606-
const { label } = this.settings
609+
const { label } = settings
607610

608611
function update(value: string = '') {
609612
if (value !== '') {

src/test/shared/sam/sync.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('SyncWizard', function () {
2121
const createTester = (params?: Partial<SyncParams>) =>
2222
createWizardTester(new SyncWizard({ deployType: 'code', ...params }))
2323

24-
it('prompts for region -> template -> stackName -> bucketName', function () {
24+
it('shows steps in correct order', function () {
2525
const tester = createTester()
2626
tester.region.assertShowFirst()
2727
tester.template.assertShowSecond()

src/test/shared/ui/pickerPrompter.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ describe('FilterBoxQuickPickPrompter', function () {
296296
transform: (resp: string) => Number.parseInt(resp),
297297
validator: (resp: string) => (Number.isNaN(Number.parseInt(resp)) ? 'NaN' : undefined),
298298
}
299+
const options = {
300+
filterBoxInputSettings: filterBoxInputSettings,
301+
}
299302

300303
let picker: TestQuickPick<DataQuickPickItem<number>>
301304
let testPrompter: FilterBoxQuickPickPrompter<number>
@@ -306,7 +309,7 @@ describe('FilterBoxQuickPickPrompter', function () {
306309

307310
beforeEach(function () {
308311
picker = getTestWindow().createQuickPick() as typeof picker
309-
testPrompter = new FilterBoxQuickPickPrompter(picker, filterBoxInputSettings)
312+
testPrompter = new FilterBoxQuickPickPrompter(picker, options)
310313
})
311314

312315
it('adds a new item based off the filter box', async function () {

0 commit comments

Comments
 (0)