diff --git a/.changeset/fast-planes-confess.md b/.changeset/fast-planes-confess.md new file mode 100644 index 0000000..ba50d22 --- /dev/null +++ b/.changeset/fast-planes-confess.md @@ -0,0 +1,5 @@ +--- +"amplify-backend-vscode": minor +--- + +feat: add resource filters on resource explorer diff --git a/README.md b/README.md index 511468f..6bc2345 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,42 @@ AWS Amplify Backend VSCode let you following features. ## Features +### Resource Explorer + The AWS Backend Explorer gives you a view of the AWS resources in Amplify Sandbox environment that you can work with when using the AWS Backend Explorer.You can open the AWS Resource page of your choice in the AWS Console of your browser. ![Amplify Backend Explorer](images/explorer.gif) +#### Filter resources + +You can filter resources in the AWS Resource Explorer. +You can then switch which filter to use with the filter switching action. +In addition, in `settings.json` you can define custom filters with a pair of names and an array of AWS resources in the tree, as shown below. + +```json +{ + "amplifyBackend.explorerFilters": [ + { + "name": "simple", + "resources": [ + "AWS::AppSync::GraphQLApi", + "Custom::AmplifyDynamoDBTable", + "AWS::Lambda::Function", + "AWS::S3::Bucket" + ] + } + ] +} +``` + +#### Switch AWS Profile + You can switch AWS Profile to explor the AWS resources. ![Switch AWS Profile](images/switch_profile.gif) +### Secret in sandbox environment + You can view/add/edit/remove secrets in your sandbox environment. ![Secrets Explorer](images/secrets_explorer.gif) diff --git a/package.json b/package.json index f261a7b..1801e66 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,11 @@ "dark": "resources/dark/refresh.svg" } }, + { + "command": "amplify-backend-explorer.switchFilter", + "title": "Switch Filter", + "icon": "$(filter)" + }, { "command": "amplify-backend-explorer.openConsole", "title": "Open AWS Console" @@ -67,6 +72,11 @@ "when": "view == amplify-backend-explorer", "group": "navigation" }, + { + "command": "amplify-backend-explorer.switchFilter", + "when": "view == amplify-backend-explorer", + "group": "navigation" + }, { "command": "amplify-backend-secrets-explorer.refresh", "when": "view == amplify-backend-secrets-explorer", @@ -108,6 +118,33 @@ "name": "Amplify Backend Secrets Explorer" } ] + }, + "configuration": { + "title": "Amplify Backend", + "properties": { + "amplifyBackend.explorerFilters": { + "type": "array", + "default": [], + "description": "List of filters to apply to the Amplify Backend Explorer", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of filter to apply to the Amplify Backend Explorer" + }, + "resources": { + "type": "array", + "description": "AWS resource types shown in the Amplify Backend Explorer", + "items": { + "type": "string" + } + } + } + } + } + } } }, "repository": { diff --git a/src/console-url-builder/index.ts b/src/console-url-builder/index.ts index fb35de4..f396b96 100644 --- a/src/console-url-builder/index.ts +++ b/src/console-url-builder/index.ts @@ -1,5 +1,9 @@ import type { StackResource } from "@aws-sdk/client-cloudformation"; +export function isSupportedResourceType(resourceType: string) { + return urlBuilders[resourceType] !== undefined; +} + export function buildUrl( stackResource: Pick< StackResource, diff --git a/src/explorer/amplify-backend-tree-data-provider.ts b/src/explorer/amplify-backend-tree-data-provider.ts index 984f01b..ae45ef1 100644 --- a/src/explorer/amplify-backend-tree-data-provider.ts +++ b/src/explorer/amplify-backend-tree-data-provider.ts @@ -12,6 +12,10 @@ import { AmplifyBackendBaseNode } from "./amplify-backend-base-node"; import { isStackNode } from "./utils"; import { detectAmplifyProjects } from "./amplify-project-detector"; import { AmplifyProject, getAmplifyProject } from "../project"; +import { + DefaultResourceFilterProvider, + ResourceFilterProvider, +} from "./resource-filter"; export class AmplifyBackendTreeDataProvider implements vscode.TreeDataProvider @@ -23,7 +27,10 @@ export class AmplifyBackendTreeDataProvider AmplifyBackendBaseNode | undefined | void > = this._onDidChangeTreeData.event; - constructor(private workspaceRoot: string) {} + constructor( + private workspaceRoot: string, + private resourceFilterProvider: ResourceFilterProvider + ) {} refresh() { this._onDidChangeTreeData.fire(); @@ -56,7 +63,8 @@ export class AmplifyBackendTreeDataProvider if (!response.StackResources) { return []; } - return response.StackResources.map((resource) => { + const predicate = this.resourceFilterProvider.getResourceFilterPredicate(); + return response.StackResources.filter(predicate).map((resource) => { return new AmplifyBackendResourceTreeNode( resource.LogicalResourceId!, resource.ResourceType!, @@ -75,7 +83,12 @@ export class AmplifyBackendTreeDataProvider const children: AmplifyBackendBaseNode[] = []; const profile = Auth.instance.getProfile(); - children.push(new AuthNode(`Connected with profile: ${profile}`, profile)); + const filterName = this.resourceFilterProvider.getResourceFilterName(); + const label = + filterName === DefaultResourceFilterProvider.NONE_FILTER_NAME + ? `Connected with profile: ${profile}` + : `Connected with profile ${profile} and resources filtered with ${filterName}`; + children.push(new AuthNode(label, profile)); if (nodes.length) { children.push(...nodes); } else { diff --git a/src/explorer/resource-filter.ts b/src/explorer/resource-filter.ts new file mode 100644 index 0000000..9ea29ee --- /dev/null +++ b/src/explorer/resource-filter.ts @@ -0,0 +1,87 @@ +import * as vscode from "vscode"; +import { StackResource } from "@aws-sdk/client-cloudformation"; +import { isSupportedResourceType } from "../console-url-builder"; + +export type ResourceFilter = { + name: string; + resources: string[]; +}; + +export const getResourceFilters = () => { + const config = vscode.workspace.getConfiguration(); + return config.get("amplifyBackend.explorerFilters") ?? []; +}; + +export type ResourceFilterPredicate = (resource: StackResource) => boolean; + +const stackPredicate = (resource: StackResource) => + "AWS::CloudFormation::Stack" === resource.ResourceType; + +const defaultPredicate = (resource: StackResource) => + isSupportedResourceType(resource.ResourceType!); + +const or = + (predicates: ResourceFilterPredicate[]) => (resource: StackResource) => + predicates.some((predicate) => predicate(resource)); + +const all = () => true; + +export interface ResourceFilterProvider { + getResourceFilterName(): string; + setResourceFilterName(filterName: string): Thenable; + getResourceFilterPredicate(): ResourceFilterPredicate; +} + +export class DefaultResourceFilterProvider implements ResourceFilterProvider { + static readonly NONE_FILTER_NAME = "none"; + private static readonly PRESET_FILTER_NAME = "default"; + private static readonly KEY_FILTER_NAME = "filterName"; + + private readonly state: vscode.Memento; + + constructor(state: vscode.Memento) { + this.state = state; + } + + getResourceFilterName(): string { + return this.state.get( + DefaultResourceFilterProvider.KEY_FILTER_NAME, + DefaultResourceFilterProvider.NONE_FILTER_NAME + ); + } + + setResourceFilterName(filterName: string): Thenable { + return this.state.update( + DefaultResourceFilterProvider.KEY_FILTER_NAME, + filterName + ); + } + + getResourceFilterPredicate(): ResourceFilterPredicate { + const filters = getResourceFilters(); + const filterName = this.getResourceFilterName(); + if (filterName === DefaultResourceFilterProvider.NONE_FILTER_NAME) { + return all; + } + if (filterName === DefaultResourceFilterProvider.PRESET_FILTER_NAME) { + return or([stackPredicate, defaultPredicate]); + } + const filter = filters.find((filter) => filter.name === filterName); + if (!filter) { + return all; + } + return or([ + stackPredicate, + (resource: StackResource) => + filter.resources.includes(resource.ResourceType!), + ]); + } + + getResourceFilterNames(): string[] { + return [ + DefaultResourceFilterProvider.NONE_FILTER_NAME, + DefaultResourceFilterProvider.PRESET_FILTER_NAME, + ...getResourceFilters().map((filter) => filter.name), + ]; + } +} diff --git a/src/extension.ts b/src/extension.ts index c81bd4e..676b9f3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { SecretsTreeDataProvider } from "./secrets/secrets-tree-data-provider"; import { editSecretCommand } from "./secrets/edit-secret-command"; import { removeSecretCommand } from "./secrets/remove-secret-command"; import { addSecretCommand } from "./secrets/add-secret-command"; +import { DefaultResourceFilterProvider } from "./explorer/resource-filter"; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( @@ -34,8 +35,13 @@ export function activate(context: vscode.ExtensionContext) { ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined; + const resourceFilterProvider = new DefaultResourceFilterProvider( + context.workspaceState + ); + const amplifyBackendTreeDataProvider = new AmplifyBackendTreeDataProvider( - rootPath || "" + rootPath || "", + resourceFilterProvider ); vscode.window.createTreeView("amplify-backend-explorer", { treeDataProvider: amplifyBackendTreeDataProvider, @@ -103,6 +109,26 @@ export function activate(context: vscode.ExtensionContext) { } ) ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "amplify-backend-explorer.switchFilter", + async () => { + const names = resourceFilterProvider.getResourceFilterNames(); + const quickPick = vscode.window.createQuickPick(); + quickPick.items = names.map((label) => ({ label })); + quickPick.onDidChangeSelection((selection) => { + if (selection[0]) { + resourceFilterProvider.setResourceFilterName(selection[0].label); + quickPick.hide(); + amplifyBackendTreeDataProvider.refresh(); + } + }); + quickPick.onDidHide(() => quickPick.dispose()); + quickPick.show(); + } + ) + ); } export function deactivate() {}