diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 99461cd1069..92b7600fa50 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -437,5 +437,5 @@ "AWS.toolkit.lambda.walkthrough.step1.description": "Locally test and debug your code.", "AWS.toolkit.lambda.walkthrough.step2.title": "Deploy to the cloud", "AWS.toolkit.lambda.walkthrough.step2.description": "Test your application in the cloud from within VS Code. \n\nNote: The AWS CLI and the SAM CLI require AWS Credentials to interact with the cloud. For information on setting up your credentials, see [Authentication and access credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). \n\n[Configure credentials](command:aws.toolkit.lambda.walkthrough.credential)", - "AWS.toolkit.lambda.serverlessLand.quickpickTitle": "Create Lambda Application from template" + "AWS.toolkit.lambda.serverlessLand.quickpickTitle": "Create application with Serverless template" } diff --git a/packages/core/src/awsService/appBuilder/activation.ts b/packages/core/src/awsService/appBuilder/activation.ts index b605ac3528c..0c1d70bdfe2 100644 --- a/packages/core/src/awsService/appBuilder/activation.ts +++ b/packages/core/src/awsService/appBuilder/activation.ts @@ -32,14 +32,14 @@ export const templateToOpenAppComposer = 'aws.toolkit.appComposer.templateToOpen * IMPORTANT: Views that should work in all vscode environments (node or web) * should be setup in {@link activateViewsShared}. */ -export async function activate(context: ExtContext): Promise { +export async function activate(context: ExtContext, ctx: vscode.ExtensionContext): Promise { // recover context variables from global state when activate const walkthroughSelected = globals.globalState.get(walkthroughContextString) if (walkthroughSelected !== undefined) { await setContext(walkthroughContextString, walkthroughSelected) } - await registerAppBuilderCommands(context) + await registerAppBuilderCommands(context, ctx) const appBuilderNode: ToolView[] = [ { @@ -123,7 +123,7 @@ async function setWalkthrough(walkthroughSelected: string = 'S3'): Promise * * @param context VScode Context */ -async function registerAppBuilderCommands(context: ExtContext): Promise { +async function registerAppBuilderCommands(context: ExtContext, ctx: vscode.ExtensionContext): Promise { const source = 'AppBuilderWalkthrough' context.extensionContext.subscriptions.push( Commands.register('aws.toolkit.installSAMCLI', async () => { @@ -202,7 +202,7 @@ async function registerAppBuilderCommands(context: ExtContext): Promise { } }), Commands.register({ id: 'aws.toolkit.lambda.createServerlessLandProject', autoconnect: false }, async () => { - await createNewServerlessLandProject(context) + await createNewServerlessLandProject(context, ctx) }) ) } diff --git a/packages/core/src/awsService/appBuilder/serverlessLand/main.ts b/packages/core/src/awsService/appBuilder/serverlessLand/main.ts index 3b4efbacf04..d18b4de2688 100644 --- a/packages/core/src/awsService/appBuilder/serverlessLand/main.ts +++ b/packages/core/src/awsService/appBuilder/serverlessLand/main.ts @@ -18,6 +18,7 @@ import { ToolkitError } from '../../../shared/errors' import { fs } from '../../../shared/fs/fs' import { getPattern } from '../../../shared/utilities/downloadPatterns' import { MetadataManager } from './metadataManager' +import type { ExtensionContext } from 'vscode' export const readmeFile: string = 'README.md' const serverlessLandOwner = 'aws-samples' @@ -36,7 +37,7 @@ const serverlessLandRepo = 'serverless-patterns' * 5. Opens the README.md file if available * 6. Handles errors and emits telemetry */ -export async function createNewServerlessLandProject(extContext: ExtContext): Promise { +export async function createNewServerlessLandProject(extContext: ExtContext, ctx: ExtensionContext): Promise { let createResult: Result = 'Succeeded' let reason: string | undefined let metadataManager: MetadataManager @@ -44,7 +45,7 @@ export async function createNewServerlessLandProject(extContext: ExtContext): Pr try { metadataManager = MetadataManager.getInstance() // Launch the project creation wizard - const config = await launchProjectCreationWizard(extContext) + const config = await launchProjectCreationWizard(extContext, ctx) if (!config) { createResult = 'Cancelled' reason = 'userCancelled' @@ -83,13 +84,15 @@ export async function createNewServerlessLandProject(extContext: ExtContext): Pr } async function launchProjectCreationWizard( - extContext: ExtContext + extContext: ExtContext, + ctx: ExtensionContext ): Promise { const awsContext = extContext.awsContext const credentials = await awsContext.getCredentials() const defaultRegion = awsContext.getCredentialDefaultRegion() return new CreateServerlessLandWizard({ + ctx, credentials, defaultRegion, }).run() @@ -115,9 +118,10 @@ async function openReadmeFile(config: CreateServerlessLandWizardForm): Promise setTimeout(resolve, 1000)) await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') - await vscode.window.showTextDocument(readmeUri) + await vscode.commands.executeCommand('markdown.showPreview', readmeUri) } catch (err) { getLogger().error(`Error in openReadmeFile: ${err}`) throw new ToolkitError('Error processing README file') diff --git a/packages/core/src/awsService/appBuilder/serverlessLand/metadataManager.ts b/packages/core/src/awsService/appBuilder/serverlessLand/metadataManager.ts index 1cca874de4d..26ae15a5f10 100644 --- a/packages/core/src/awsService/appBuilder/serverlessLand/metadataManager.ts +++ b/packages/core/src/awsService/appBuilder/serverlessLand/metadataManager.ts @@ -5,6 +5,7 @@ import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports import { ToolkitError } from '../../../shared/errors' import path from 'path' +import { ExtensionContext } from 'vscode' interface Implementation { iac: string @@ -17,6 +18,10 @@ interface PatternData { implementation: Implementation[] } +interface PatternUrls { + githubUrl: string + previewUrl: string +} export interface ProjectMetadata { patterns: Record } @@ -28,14 +33,6 @@ export interface ProjectMetadata { export class MetadataManager { private static instance: MetadataManager private metadata: ProjectMetadata | undefined - private static readonly metadataPath = path.join( - path.resolve(__dirname, '../../../../../'), - 'src', - 'awsService', - 'appBuilder', - 'serverlessLand', - 'metadata.json' - ) private constructor() {} @@ -46,14 +43,19 @@ export class MetadataManager { return MetadataManager.instance } - public static initialize(): MetadataManager { + public static initialize(ctx: ExtensionContext): MetadataManager { const instance = MetadataManager.getInstance() - instance.loadMetadata(MetadataManager.metadataPath).catch((err) => { + const metadataPath = instance.getMetadataPath(ctx) + instance.loadMetadata(metadataPath).catch((err) => { throw new ToolkitError(`Failed to load metadata: ${err}`) }) return instance } + public getMetadataPath(ctx: ExtensionContext): string { + return ctx.asAbsolutePath(path.join('dist', 'src', 'serverlessLand', 'metadata.json')) + } + /** * Loads metadata from a JSON file * @param metadataPath Path to the metadata JSON file @@ -117,6 +119,24 @@ export class MetadataManager { })) } + public getUrl(pattern: string): PatternUrls { + const patternData = this.metadata?.patterns?.[pattern] + if (!patternData || !patternData.implementation) { + return { + githubUrl: '', + previewUrl: '', + } + } + const asset = patternData.implementation[0].assetName + + return { + // GitHub URL for the pattern + githubUrl: `https://github.com/aws-samples/serverless-patterns/tree/main/${asset}`, + // Serverless Land preview URL + previewUrl: `https://serverlessland.com/patterns/${asset}`, + } + } + /** * Gets available Infrastructure as Code options for a specific pattern * @param pattern The pattern name to get IaC options for diff --git a/packages/core/src/awsService/appBuilder/serverlessLand/webViewManager.ts b/packages/core/src/awsService/appBuilder/serverlessLand/webViewManager.ts new file mode 100644 index 00000000000..250ee883241 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/serverlessLand/webViewManager.ts @@ -0,0 +1,37 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +export class WebviewService { + constructor() {} + + public static getWebviewContent(url: string) { + return ` + + + + + + + + + + + + + ` + } +} diff --git a/packages/core/src/awsService/appBuilder/serverlessLand/wizard.ts b/packages/core/src/awsService/appBuilder/serverlessLand/wizard.ts index 7bcc17b780c..52ae66b48ff 100644 --- a/packages/core/src/awsService/appBuilder/serverlessLand/wizard.ts +++ b/packages/core/src/awsService/appBuilder/serverlessLand/wizard.ts @@ -13,6 +13,7 @@ import { createQuickPick } from '../../../shared/ui/pickerPrompter' import { createFolderPrompt } from '../../../shared/ui/common/location' import { createExitPrompter } from '../../../shared/ui/common/exitPrompter' import { MetadataManager } from './metadataManager' +import type { ExtensionContext } from 'vscode' import { ToolkitError } from '../../../shared/errors' const localize = nls.loadMessageBundle() @@ -22,6 +23,7 @@ export interface CreateServerlessLandWizardForm { pattern: string runtime: string iac: string + assetName: string } function promptPattern(metadataManager: MetadataManager) { @@ -134,11 +136,11 @@ function promptName() { export class CreateServerlessLandWizard extends Wizard { private metadataManager: MetadataManager - public constructor(context: { defaultRegion?: string; credentials?: AWS.Credentials }) { + public constructor(context: { ctx: ExtensionContext; defaultRegion?: string; credentials?: AWS.Credentials }) { super({ exitPrompterProvider: createExitPrompter, }) - this.metadataManager = MetadataManager.initialize() + this.metadataManager = MetadataManager.initialize(context.ctx) this.form.pattern.bindPrompter(() => promptPattern(this.metadataManager)) this.form.runtime.bindPrompter((state) => promptRuntime(this.metadataManager, state.pattern)) this.form.iac.bindPrompter((state) => promptIac(this.metadataManager, state.pattern)) diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts index a3e5e03b4ab..6f98f6c69e9 100644 --- a/packages/core/src/extensionNode.ts +++ b/packages/core/src/extensionNode.ts @@ -200,7 +200,7 @@ export async function activate(context: vscode.ExtensionContext) { await activateRedshift(extContext) - await activateAppBuilder(extContext) + await activateAppBuilder(extContext, context) await activateDocumentDb(extContext) diff --git a/packages/core/src/shared/ui/pickerPrompter.ts b/packages/core/src/shared/ui/pickerPrompter.ts index daa65246fed..8b8b5ad542d 100644 --- a/packages/core/src/shared/ui/pickerPrompter.ts +++ b/packages/core/src/shared/ui/pickerPrompter.ts @@ -12,6 +12,8 @@ import { Prompter, PromptResult, Transform } from './prompter' import { assign, isAsyncIterable } from '../utilities/collectionUtils' import { recentlyUsed } from '../localizedText' import { getLogger } from '../logger/logger' +import { MetadataManager } from '../../awsService/appBuilder/serverlessLand/metadataManager' +import { WebviewService } from '../../awsService/appBuilder/serverlessLand/webViewManager' const localize = nls.loadMessageBundle() @@ -142,6 +144,43 @@ export function createQuickPick( const mergedOptions = { ...defaultQuickpickOptions, ...options } assign(mergedOptions, picker) picker.buttons = mergedOptions.buttons ?? [] + let serverlessPanel: vscode.WebviewPanel | undefined + + picker.onDidTriggerItemButton(async (event) => { + const metadataManager = MetadataManager.getInstance() + if (event.button.tooltip === 'Open in GitHub' || event.button.tooltip === 'Open in Serverless Land') { + const selectedPattern = event.item + if (selectedPattern) { + const patternUrl = metadataManager.getUrl(selectedPattern.label) + if (patternUrl) { + if (event.button.tooltip === 'Open in GitHub') { + await vscode.env.openExternal(vscode.Uri.parse(patternUrl.githubUrl)) + } else if (event.button.tooltip === 'Open in Serverless Land') { + if (!serverlessPanel) { + serverlessPanel = vscode.window.createWebviewPanel( + 'serverlessLandPreview', + `${selectedPattern.label}`, + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + enableCommandUris: false, + enableFindWidget: true, + } + ) + serverlessPanel.onDidDispose(() => { + serverlessPanel = undefined + }) + } else { + serverlessPanel.title = `${selectedPattern.label}` + } + serverlessPanel.webview.html = WebviewService.getWebviewContent(patternUrl.previewUrl) + serverlessPanel.reveal() + } + } + } + } + }) const prompter = mergedOptions.filterBoxInputSettings !== undefined diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index c662436830d..749b05f16ec 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -1327,24 +1327,29 @@ "group": "1_account@3" }, { - "command": "aws.lambda.createNewSamApp", + "command": "aws.toolkit.lambda.createServerlessLandProject", "when": "view == aws.explorer", "group": "3_lambda@1" }, { - "command": "aws.launchConfigForm", + "command": "aws.lambda.createNewSamApp", "when": "view == aws.explorer", "group": "3_lambda@2" }, + { + "command": "aws.launchConfigForm", + "when": "view == aws.explorer", + "group": "3_lambda@3" + }, { "command": "aws.deploySamApplication", "when": "config.aws.samcli.legacyDeploy && view == aws.explorer", - "group": "3_lambda@3" + "group": "3_lambda@4" }, { "command": "aws.samcli.sync", "when": "!config.aws.samcli.legacyDeploy && view == aws.explorer", - "group": "3_lambda@3" + "group": "3_lambda@4" }, { "submenu": "aws.toolkit.submenu.feedback", diff --git a/packages/toolkit/scripts/build/copyFiles.ts b/packages/toolkit/scripts/build/copyFiles.ts index 6cea899b4ca..7d065040416 100644 --- a/packages/toolkit/scripts/build/copyFiles.ts +++ b/packages/toolkit/scripts/build/copyFiles.ts @@ -69,6 +69,19 @@ const tasks: CopyTask[] = [ destination: path.join('src', 'stepFunctions', 'asl', 'aslServer.js'), }, + // Serverless Land + { + target: path.join( + '../../node_modules/aws-core-vscode', + 'src', + 'awsService', + 'appBuilder', + 'serverlessLand', + 'metadata.json' + ), + destination: path.join('src', 'serverlessLand', 'metadata.json'), + }, + // Vue { target: path.join('../core', 'resources', 'js', 'vscode.js'),