From 80e57f67d78b1f525b254f3020a05a8d1da20a2f Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 30 Aug 2022 13:37:59 -0400 Subject: [PATCH 01/49] It's full of stars --- utils/src/tree/AzExtParentTreeItem.ts | 2 +- utils/src/tree/AzExtTreeDataProvider.ts | 4 +- utils/src/tree/InternalInterfaces.ts | 7 +- .../ContextValueQuickPickStep.ts | 76 ++++++++++++ .../quickPickWizard/GenericQuickPickStep.ts | 110 ++++++++++++++++++ .../QuickPickWithCreateStep.ts | 42 +++++++ .../quickPickWizard/QuickPickWizardContext.ts | 18 +++ .../quickPickWizard/RecursiveQuickPickStep.ts | 33 ++++++ .../experiences/appResourceExperience.ts | 58 +++++++++ .../experiences/contextValueExperience.ts | 39 +++++++ .../AzureResourceQuickPickWizardContext.ts | 13 +++ .../QuickPickAppResourceStep.ts | 45 +++++++ .../QuickPickAzureResourceGroupStep.ts | 26 +++++ .../QuickPickAzureSubscriptionStep.ts | 40 +++++++ .../QuickPickGroupStep.ts | 36 ++++++ .../quickPickAzureResource/tempTypes.ts | 40 +++++++ 16 files changed, 583 insertions(+), 6 deletions(-) create mode 100644 utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts create mode 100644 utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts create mode 100644 utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts create mode 100644 utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts create mode 100644 utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts create mode 100644 utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts create mode 100644 utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts create mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts create mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts create mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts create mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts create mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts create mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts diff --git a/utils/src/tree/AzExtParentTreeItem.ts b/utils/src/tree/AzExtParentTreeItem.ts index dd351ea0d2..f4b02d8899 100644 --- a/utils/src/tree/AzExtParentTreeItem.ts +++ b/utils/src/tree/AzExtParentTreeItem.ts @@ -246,7 +246,7 @@ export abstract class AzExtParentTreeItem extends AzExtTreeItem implements types // Just in case implementers of `loadMoreChildrenImpl` re-use the same child node, we want to clear those caches as well for (const child of this._cachedChildren) { if (isAzExtParentTreeItem(child)) { - (child).clearCache(); + child.clearCache(); } } this._cachedChildren = []; diff --git a/utils/src/tree/AzExtTreeDataProvider.ts b/utils/src/tree/AzExtTreeDataProvider.ts index 691a7bdb3a..7832040116 100644 --- a/utils/src/tree/AzExtTreeDataProvider.ts +++ b/utils/src/tree/AzExtTreeDataProvider.ts @@ -147,7 +147,7 @@ export class AzExtTreeDataProvider implements IAzExtTreeDataProviderInternal, ty } if (isAzExtParentTreeItem(treeItem)) { - (treeItem).clearCache(); + treeItem.clearCache(); } this.refreshUIOnly(treeItem); @@ -179,7 +179,7 @@ export class AzExtTreeDataProvider implements IAzExtTreeDataProviderInternal, ty while (!treeItem.matchesContextValue(expectedContextValues)) { if (isAzExtParentTreeItem(treeItem)) { - const pickedItems: AzExtTreeItem | AzExtTreeItem[] = await (treeItem).pickChildTreeItem(expectedContextValues, context); + const pickedItems: AzExtTreeItem | AzExtTreeItem[] = await treeItem.pickChildTreeItem(expectedContextValues, context); if (Array.isArray(pickedItems)) { // canPickMany is only supported at the last stage of the picker, so automatically return if this is an array return pickedItems; diff --git a/utils/src/tree/InternalInterfaces.ts b/utils/src/tree/InternalInterfaces.ts index e0318566f3..0afdb5c4f2 100644 --- a/utils/src/tree/InternalInterfaces.ts +++ b/utils/src/tree/InternalInterfaces.ts @@ -5,13 +5,14 @@ import { EventEmitter } from 'vscode'; import * as types from '../../index'; +import { AzExtParentTreeItem } from './AzExtParentTreeItem'; import { AzExtTreeItem } from './AzExtTreeItem'; import { CollapsibleStateTracker } from './CollapsibleStateTracker'; // Interfaces for methods on the tree that aren't exposed outside of this package // We can't reference the classes directly because it would result in circular dependencies -export interface IAzExtParentTreeItemInternal extends types.AzExtParentTreeItem, AzExtTreeItem { +export interface IAzExtParentTreeItemInternal extends AzExtParentTreeItem { _isAzExtParentTreeItem: boolean; parent: IAzExtParentTreeItemInternal | undefined; treeDataProvider: IAzExtTreeDataProviderInternal; @@ -28,6 +29,6 @@ export interface IAzExtTreeDataProviderInternal extends types.AzExtTreeDataProvi /** * Using instanceof AzExtParentTreeItem causes issues whenever packages are linked for dev testing. Instead, check _isAzExtParentTreeItem */ -export function isAzExtParentTreeItem(item: {}): boolean { - return !!(item)._isAzExtParentTreeItem; +export function isAzExtParentTreeItem(maybeParent: unknown): maybeParent is IAzExtParentTreeItemInternal { + return !!(maybeParent as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem; } diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts new file mode 100644 index 0000000000..2a6bf09291 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../index'; +import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep'; +import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces'; +import { QuickPickWizardContext } from './QuickPickWizardContext'; + +export interface ContextValueFilter { + include: string | RegExp | (string | RegExp)[]; + exclude?: string | RegExp | (string | RegExp)[]; +} + +interface ContextValueFilterableTreeNodeV2 { + readonly quickPickOptions: { + readonly contextValues: Array; + readonly isLeaf: boolean; + } +} + +export type ContextValueFilterableTreeNode = ContextValueFilterableTreeNodeV2 | types.AzExtTreeItem; + +export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions { + contextValueFilter: ContextValueFilter; +} + +export class ContextValueQuickPickStep, TOptions extends ContextValueFilterQuickPickOptions> extends GenericQuickPickStep { + protected override isDirectPick(node: TNode): boolean { + const includeOption = this.pickOptions.contextValueFilter.include; + const excludeOption = this.pickOptions.contextValueFilter.exclude; + + const includeArray: (string | RegExp)[] = Array.isArray(includeOption) ? includeOption : [includeOption]; + const excludeArray: (string | RegExp)[] = excludeOption ? + (Array.isArray(excludeOption) ? excludeOption : [excludeOption]) : + []; + + const nodeContextValues: string[] = isV2TreeNode(node) ? + node.quickPickOptions.contextValues : + [node.contextValue]; + + return includeArray.some(i => this.matchesSingleFilter(i, nodeContextValues)) && + !excludeArray.some(e => this.matchesSingleFilter(e, nodeContextValues)); + } + + protected override isIndirectPick(node: TNode): boolean { + if (isV2TreeNode(node)) { + return node.quickPickOptions.isLeaf === false; + } else if (isAzExtParentTreeItem(node)) { + return true; + } + + return false; + } + + private matchesSingleFilter(matcher: string | RegExp, nodeContextValues: string[]): boolean { + return nodeContextValues.some(c => { + if (matcher instanceof RegExp) { + return matcher.test(c); + } + + // Context value matcher is a string, do full equality (same as old behavior) + return c === matcher; + }) + } +} + +function isV2TreeNode(maybeNode: unknown): maybeNode is ContextValueFilterableTreeNodeV2 { + if (typeof maybeNode === 'object') { + return Array.isArray((maybeNode as ContextValueFilterableTreeNodeV2).quickPickOptions?.contextValues) && + (maybeNode as ContextValueFilterableTreeNodeV2).quickPickOptions?.isLeaf !== undefined; + } + + return false; +} diff --git a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts new file mode 100644 index 0000000000..cbc7d51b97 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../index'; +import * as vscode from 'vscode'; +import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; +import { AzureWizardPromptStep } from '../../wizard/AzureWizardPromptStep'; +import { NoResourceFoundError } from '../../errors'; +import { parseError } from '../../parseError'; + +export interface GenericQuickPickOptions { + skipIfOne?: boolean; +} + +export interface SkipIfOneQuickPickOptions extends GenericQuickPickOptions { + skipIfOne?: true; +} + +export abstract class GenericQuickPickStep, TOptions extends GenericQuickPickOptions> extends AzureWizardPromptStep { + public readonly supportsDuplicateSteps = true; + + public constructor( + protected readonly treeDataProvider: vscode.TreeDataProvider, + protected readonly pickOptions: TOptions + ) { + super(); + } + + public async prompt(wizardContext: TContext): Promise { + try { + const pick = await this.promptInternal(wizardContext); + wizardContext.pickedNodes.push(pick); + } catch (err) { + const error = parseError(err); + if (error.errorType === 'GoBackError') { + // Instead of wiping out a property value, which is the default wizard behavior for `GoBackError`, pop the most recent + // value off from the provenance of the picks + wizardContext.pickedNodes.pop(); + } + + // And rethrow + throw err; + } + } + + public shouldPrompt(_wizardContext: TContext): boolean { + return true; + } + + protected async promptInternal(wizardContext: TContext): Promise { + const picks = await this.getPicks(wizardContext); + + if (picks.length === 1 && this.pickOptions.skipIfOne) { + return picks[0].data; + } else { + const selected = await wizardContext.ui.showQuickPick(picks, { /* TODO: options */ }); + return selected.data; + } + } + + protected async getPicks(wizardContext: TContext): Promise[]> { + const lastPickedItem: TNode | undefined = getLastNode(wizardContext); + const children = (await this.treeDataProvider.getChildren(lastPickedItem)) || []; + + const directChoices = children.filter(c => this.isDirectPick(c)); + const indirectChoices = children.filter(c => this.isIndirectPick(c)); + + let promptChoices: TNode[]; + if (directChoices.length === 0) { + if (indirectChoices.length === 0) { + throw new NoResourceFoundError(); + } else { + promptChoices = indirectChoices; + } + } else { + promptChoices = directChoices; + } + + const picks: types.IAzureQuickPickItem[] = []; + for (const choice of promptChoices) { + picks.push(await this.getQuickPickItem(choice)); + } + + return picks; + } + + /** + * Filters for nodes that match the final target. + * @param node The node to apply the filter to + */ + protected abstract isDirectPick(node: TNode): boolean; + + /** + * Filters for nodes that could have a descendant matching the final target. + * @param node The node to apply the filter to + */ + protected abstract isIndirectPick(node: TNode): boolean; + + private async getQuickPickItem(resource: TNode): Promise> { + const treeItem = await Promise.resolve(this.treeDataProvider.getTreeItem(resource)); + + return { + label: ((treeItem.label as vscode.TreeItemLabel)?.label || treeItem.label) as string, + description: treeItem.description as string, + data: resource, + }; + } +} diff --git a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts new file mode 100644 index 0000000000..a7e7fa36a1 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../index'; +import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; +import { ContextValueFilterableTreeNode, ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; +import { localize } from '../../localize'; + +type CreateCallback = () => TNode | Promise; +interface CreateQuickPickOptions extends ContextValueFilterQuickPickOptions { + skipIfOne?: never; // Not allowed in CreateQuickPickStep + createLabel?: string; + createCallback: CreateCallback; +} + +export class CreateQuickPickStep> extends ContextValueQuickPickStep { + public override async prompt(wizardContext: TContext): Promise { + await super.prompt(wizardContext); + + const lastNode = getLastNode(wizardContext) as (TNode | CreateCallback); + if (typeof lastNode === 'function') { + // If the last node is a function, pop it off the list and execute it + const callback = wizardContext.pickedNodes.pop() as unknown as CreateCallback; + wizardContext.pickedNodes.push(await callback()); + } + } + + protected override async getPicks(wizardContext: TContext): Promise[]> { + const picks: types.IAzureQuickPickItem[] = await super.getPicks(wizardContext); + picks.push(this.getCreatePick()); + return picks as types.IAzureQuickPickItem[]; + } + + private getCreatePick(): types.IAzureQuickPickItem { + return { + label: this.pickOptions.createLabel || localize('createQuickPickLabel', '$(add) Create...'), + data: this.pickOptions.createCallback, + }; + } +} diff --git a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts new file mode 100644 index 0000000000..961df90959 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../index'; + +export interface QuickPickWizardContext extends types.IActionContext { + pickedNodes: TNode[]; +} + +export function getLastNode(context: QuickPickWizardContext): TNode | undefined { + if (context.pickedNodes.length) { + return context.pickedNodes[context.pickedNodes.length - 1]; + } + + return undefined; +} diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts new file mode 100644 index 0000000000..0456b7e632 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../index'; +import { ContextValueFilterableTreeNode, ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; +import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; + +export class RecursiveQuickPickStep> extends ContextValueQuickPickStep { + public async getSubWizard(wizardContext: TContext): Promise | undefined> { + const lastPickedItem = getLastNode(wizardContext); + + if (!lastPickedItem) { + // Something went wrong, no node was chosen + throw new Error('No node was set after prompt step.'); + } + + if (super.isDirectPick(lastPickedItem)) { + // The last picked node matches the expected filter + // No need to continue prompting + return undefined; + } else { + // Need to keep going because the last picked node is not a match + return { + hideStepCount: true, + promptSteps: [ + new RecursiveQuickPickStep(this.treeDataProvider, this.pickOptions) + ], + }; + } + } +} diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts new file mode 100644 index 0000000000..daeb6c515d --- /dev/null +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../index'; +import * as vscode from 'vscode'; +import { ContextValueFilter } from '../ContextValueQuickPickStep'; +import { QuickPickAzureSubscriptionStep } from '../quickPickAzureResource/QuickPickAzureSubscriptionStep'; +import { QuickPickGroupStep } from '../quickPickAzureResource/QuickPickGroupStep'; +import { QuickPickAppResourceStep } from '../quickPickAzureResource/QuickPickAppResourceStep'; +import { AzureResourceQuickPickWizardContext } from '../quickPickAzureResource/AzureResourceQuickPickWizardContext'; +import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; +import { Box, ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; +import { getLastNode } from '../QuickPickWizardContext'; +import { NoResourceFoundError } from '../../../errors'; + +export async function appResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, resourceType: types.AzExtResourceType, childItemFilter?: ContextValueFilter): Promise { + const promptSteps: types.AzureWizardPromptStep[] = [ + new QuickPickAzureSubscriptionStep(tdp), + new QuickPickGroupStep(tdp, { + groupType: resourceType, + }), + new QuickPickAppResourceStep(tdp, { + resourceType: resourceType, + skipIfOne: false, + }), + ]; + + if (childItemFilter) { + promptSteps.push(new RecursiveQuickPickStep(tdp, { + contextValueFilter: childItemFilter, + skipIfOne: false, + })); + } + + // Fill in the `pickedNodes` property + const wizardContext = context as AzureResourceQuickPickWizardContext; + wizardContext.pickedNodes = []; + + const wizard = new types.AzureWizard(context, { + hideStepCount: true, + promptSteps: promptSteps, + }); + + await wizard.prompt(); + + const lastPickedItem = getLastNode(wizardContext); + + if (!lastPickedItem) { + throw new NoResourceFoundError(wizardContext); + } else { + // Treat lastPickedItem as a box containing the desired end object + // TODO + const pickedAsBox = lastPickedItem as unknown as Box; + return pickedAsBox.unwrap(); + } +} diff --git a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts new file mode 100644 index 0000000000..570fa14cc8 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../index'; +import * as vscode from 'vscode'; +import { ContextValueFilter, ContextValueFilterableTreeNode } from '../ContextValueQuickPickStep'; +import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; +import { getLastNode, QuickPickWizardContext } from '../QuickPickWizardContext'; +import { NoResourceFoundError } from '../../../errors'; + +export async function contextValueExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: ContextValueFilter): Promise { + const promptSteps: types.AzureWizardPromptStep>[] = [ + new RecursiveQuickPickStep(tdp, { + contextValueFilter: contextValueFilter, + skipIfOne: false, + }), + ]; + + // Fill in the `pickedNodes` property + const wizardContext = context as QuickPickWizardContext; + wizardContext.pickedNodes = []; + + const wizard = new types.AzureWizard(context, { + hideStepCount: true, + promptSteps: promptSteps, + }); + + await wizard.prompt(); + + const lastPickedItem = getLastNode(wizardContext); + + if (!lastPickedItem) { + throw new NoResourceFoundError(wizardContext); + } else { + return lastPickedItem; + } +} diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts new file mode 100644 index 0000000000..be86bd9dd6 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { QuickPickWizardContext } from "../QuickPickWizardContext"; +import { ApplicationResource, ApplicationSubscription, ResourceGroupsItem } from "./tempTypes"; + +export interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext { + subscription: ApplicationSubscription | undefined; + resource: ApplicationResource | undefined; + resourceGroup: string | undefined; +} diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts new file mode 100644 index 0000000000..0e076c6aff --- /dev/null +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../index'; +import { ContextValueFilter } from '../ContextValueQuickPickStep'; +import { GenericQuickPickOptions, GenericQuickPickStep } from '../GenericQuickPickStep'; +import { AzureResourceQuickPickWizardContext } from './AzureResourceQuickPickWizardContext'; +import { AppResourceItem, ResourceGroupsItem } from './tempTypes'; + +interface AppResourceQuickPickOptions extends GenericQuickPickOptions { + resourceType: types.AzExtResourceType; + childItemFilter?: ContextValueFilter; +} + +export class QuickPickAppResourceStep extends GenericQuickPickStep { + protected override async promptInternal(wizardContext: AzureResourceQuickPickWizardContext): Promise { + const pickedAppResource = await super.promptInternal(wizardContext) as AppResourceItem; + + // TODO + wizardContext.resource = pickedAppResource; + wizardContext.resourceGroup = pickedAppResource.resourceGroup; + + return pickedAppResource; + } + + protected isDirectPick(node: AppResourceItem): boolean { + // If childItemFilter is defined, this cannot be a direct pick + if (this.pickOptions.childItemFilter) { + return false; + } + + return node.azExtResourceType === this.pickOptions.resourceType; + } + + protected isIndirectPick(node: AppResourceItem): boolean { + // If childItemFilter is undefined, this cannot be an indirect pick + if (!this.pickOptions.childItemFilter) { + return false; + } + + return node.azExtResourceType === this.pickOptions.resourceType; + } +} diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts new file mode 100644 index 0000000000..31c8052ada --- /dev/null +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from "../../../../index"; +import { ContextValueFilterableTreeNode } from "../ContextValueQuickPickStep"; +import { GenericQuickPickOptions, GenericQuickPickStep } from "../GenericQuickPickStep"; +import { AzureResourceQuickPickWizardContext } from "./AzureResourceQuickPickWizardContext"; +import { ResourceGroupsItem } from "./tempTypes"; + +// TODO: implement this for picking resource group +// The resource group may NOT be the grouping method used in the tree +export class QuickPickAzureResourceGroupStep extends GenericQuickPickStep { + protected override getPicks(_wizardContext: AzureResourceQuickPickWizardContext): Promise[]> { + throw new Error("Method not implemented."); + } + + protected isDirectPick(_node: ContextValueFilterableTreeNode): boolean { + throw new Error("Method not implemented."); + } + + protected isIndirectPick(_node: ContextValueFilterableTreeNode): boolean { + throw new Error("Method not implemented."); + } +} diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts new file mode 100644 index 0000000000..7cbbfdad0e --- /dev/null +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { GenericQuickPickOptions, GenericQuickPickStep, SkipIfOneQuickPickOptions } from '../GenericQuickPickStep'; +import { AzureResourceQuickPickWizardContext } from './AzureResourceQuickPickWizardContext'; +import { ResourceGroupsItem, SubscriptionItem } from './tempTypes'; + +export class QuickPickAzureSubscriptionStep extends GenericQuickPickStep { + public constructor(tdp: vscode.TreeDataProvider, options?: GenericQuickPickOptions) { + super( + tdp, + { + ...options, + skipIfOne: true, // Subscription is always skip-if-one + } + ) + } + + protected override async promptInternal(wizardContext: AzureResourceQuickPickWizardContext): Promise { + const pickedSubscription = await super.promptInternal(wizardContext) as SubscriptionItem; + + // TODO + wizardContext.subscription = pickedSubscription.subscription; + + return pickedSubscription; + } + + protected isDirectPick(_node: SubscriptionItem): boolean { + // Subscription is never a direct pick + return false; + } + + protected isIndirectPick(_node: SubscriptionItem): boolean { + // All nodes at this level are always subscription nodes + return true; + } +} diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts new file mode 100644 index 0000000000..f2633c1a7d --- /dev/null +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../index'; +import * as vscode from 'vscode'; +import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from '../GenericQuickPickStep'; +import { AzureResourceQuickPickWizardContext } from './AzureResourceQuickPickWizardContext'; +import { GroupingItem, ResourceGroupsItem } from './tempTypes'; + +interface GroupQuickPickOptions extends SkipIfOneQuickPickOptions { + groupType: types.AzExtResourceType; + skipIfOne?: true; +} + +export class QuickPickGroupStep extends GenericQuickPickStep { + public constructor(tdp: vscode.TreeDataProvider, options: GroupQuickPickOptions) { + super( + tdp, + { + ...options, + skipIfOne: true, // Group is always skip-if-one + } + ); + } + + protected isDirectPick(_node: GroupingItem): boolean { + // Group is never a direct pick + return false; + } + + protected isIndirectPick(node: GroupingItem): boolean { + return !node.resourceType || node.resourceType === this.pickOptions.groupType; + } +} diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts new file mode 100644 index 0000000000..4a4a5d258f --- /dev/null +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../index'; +import { ContextValueFilterableTreeNode } from '../ContextValueQuickPickStep'; + +// TODO: THIS FILE IS TEMPORARY // +// It needs to be replaced by real Resources extension interfaces // + +export type ResourceGroupsItem = ContextValueFilterableTreeNode; + +export type SubscriptionItem = ResourceGroupsItem & { + subscription: ApplicationSubscription; +}; + +export type GroupingItem = ResourceGroupsItem & { + resourceType?: types.AzExtResourceType +} + +export type AppResourceItem = ResourceGroupsItem & ApplicationResource; + +type ResourceBase = {}; + +/** + * Represents an individual resource in Azure. + * @remarks The `id` property is expected to be the Azure resource ID. + */ +export interface ApplicationResource extends ResourceBase { + readonly subscription: ApplicationSubscription; + readonly azExtResourceType?: types.AzExtResourceType; + readonly resourceGroup?: string; +} + +export type ApplicationSubscription = unknown; + +export interface Box { + unwrap(): T; +} From df01530e23837a548c422f58da37a5ea59062fea Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 30 Aug 2022 13:42:42 -0400 Subject: [PATCH 02/49] Add a TODO comment --- utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts index cbc7d51b97..450d4218a4 100644 --- a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts @@ -62,6 +62,8 @@ export abstract class GenericQuickPickStep[]> { const lastPickedItem: TNode | undefined = getLastNode(wizardContext); + + // TODO: if `lastPickedItem` is an `AzExtParentTreeItem`, should we clear its cache? const children = (await this.treeDataProvider.getChildren(lastPickedItem)) || []; const directChoices = children.filter(c => this.isDirectPick(c)); From 493704b89791c5a22e31d48d5e3e3aa091589018 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:42:05 -0400 Subject: [PATCH 03/49] Some explanatory comments --- .../quickPickWizard/ContextValueQuickPickStep.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index 2a6bf09291..fc6cbbf7ff 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -8,8 +8,21 @@ import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPic import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces'; import { QuickPickWizardContext } from './QuickPickWizardContext'; +/** + * Describes filtering based on context value. Items that pass the filter will + * match at least one of the `include` filters, but none of the `exclude` filters. + */ export interface ContextValueFilter { + /** + * This filter will include items that match *any* of the values in the array. + * When a string is used, exact value comparison is done. + */ include: string | RegExp | (string | RegExp)[]; + + /** + * This filter will exclude items that match *any* of the values in the array. + * When a string is used, exact value comparison is done. + */ exclude?: string | RegExp | (string | RegExp)[]; } From 39c3dd3df50840181a50d4906c252c17949e55d4 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Fri, 2 Sep 2022 14:20:39 -0700 Subject: [PATCH 04/49] Check if isLeaf is null instead of undefined --- utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index fc6cbbf7ff..54810949d2 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -82,7 +82,7 @@ export class ContextValueQuickPickStep Date: Fri, 2 Sep 2022 15:51:40 -0700 Subject: [PATCH 05/49] Boxing / unboxing functionality (#1231) Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com> --- utils/hostapi.v2.d.ts | 39 ++++++++++++++ utils/index.d.ts | 4 +- utils/src/index.ts | 1 + .../registerCommandWithTreeNodeUnboxing.ts | 52 +++++++++++++++++++ utils/test/isBox.test.ts | 30 +++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 utils/hostapi.v2.d.ts create mode 100644 utils/src/registerCommandWithTreeNodeUnboxing.ts create mode 100644 utils/test/isBox.test.ts diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts new file mode 100644 index 0000000000..b6289593cd --- /dev/null +++ b/utils/hostapi.v2.d.ts @@ -0,0 +1,39 @@ +import { IActionContext } from "./index"; + +/** + * Interface describing an object that wraps another object. + * + * The host extension will wrap all tree nodes provided by the client + * extensions. When commands are executed, the wrapper objects are + * sent directly to the client extension, which will need to unwrap + * them. The `registerCommandWithTreeNodeUnboxing` method below, used + * in place of `registerCommand`, will intelligently do this + * unboxing automatically (i.e., will not unbox if the arguments + * aren't boxes) + */ +export interface Box { + unwrap(): Promise; +} + +/** + * Tests to see if something is a box, by ensuring it is an object + * and has an "unwrap" function + * @param maybeBox An object to test if it is a box + * @returns True if a box, false otherwise + */ +export declare function isBox(maybeBox: unknown): maybeBox is Box; + +/** + * Describes command callbacks for tree node context menu commands + */ +export type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; + +/** + * Used to register VSCode tree node context menu commands that are in the host extension's tree. It wraps your callback with consistent error and telemetry handling + * Use debounce property if you need a delay between clicks for this particular command + * A telemetry event is automatically sent whenever a command is executed. The telemetry event ID will default to the same as the + * commandId passed in, but can be overridden per command with telemetryId + * The telemetry event for this command will be named telemetryId if specified, otherwise it defaults to the commandId + * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. + */ +export declare function registerCommandWithTreeNodeUnboxing(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; diff --git a/utils/index.d.ts b/utils/index.d.ts index 192f0db440..0f01f5b888 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -635,7 +635,9 @@ export declare class UserCancelledError extends Error { constructor(stepName?: string); } -export declare class NoResourceFoundError extends Error { } +export declare class NoResourceFoundError extends Error { + constructor(context?: ITreeItemPickerContext); +} export type CommandCallback = (context: IActionContext, ...args: any[]) => any; diff --git a/utils/src/index.ts b/utils/src/index.ts index 652cb16c21..25af2ff87a 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -16,6 +16,7 @@ export { addExtensionValueToMask, callWithMaskHandling, maskValue } from './mask export * from './openReadOnlyContent'; export * from './parseError'; export * from './registerCommand'; +export * from './registerCommandWithTreeNodeUnboxing'; export * from './registerEvent'; export { registerReportIssueCommand } from './registerReportIssueCommand'; export * from './tree/AzExtParentTreeItem'; diff --git a/utils/src/registerCommandWithTreeNodeUnboxing.ts b/utils/src/registerCommandWithTreeNodeUnboxing.ts new file mode 100644 index 0000000000..33cf4a5096 --- /dev/null +++ b/utils/src/registerCommandWithTreeNodeUnboxing.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { CommandCallback, IActionContext } from '../index'; +import type { Box, TreeNodeCommandCallback } from '../hostapi.v2'; +import { registerCommand } from './registerCommand'; + +export function registerCommandWithTreeNodeUnboxing(commandId: string, treeNodeCallback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void { + const unwrappingCallback: CommandCallback = async (context: IActionContext, ...args: unknown[]) => { + const maybeNodeBox = args?.[0]; + const maybeNodeBoxArray = args?.[1]; + const remainingArgs = args.slice(2); + + let node: T | undefined; + if (maybeNodeBox && isBox(maybeNodeBox)) { + // If the first arg is a box, unwrap it + node = await maybeNodeBox.unwrap(); + } else if (maybeNodeBox) { + // Otherwise, assume it is just a T + node = maybeNodeBox as T; + } + + let nodes: T[] | undefined; + if (maybeNodeBoxArray && Array.isArray(maybeNodeBoxArray) && maybeNodeBoxArray.every(n => isBox(n))) { + // If the first arg is an array of boxes, unwrap them + const boxedNodes = maybeNodeBoxArray as Box[]; + nodes = []; + for (const n of boxedNodes) { + nodes.push(await n.unwrap()) + } + } else if (maybeNodeBoxArray && Array.isArray(maybeNodeBoxArray)) { + // Otherwise, assume it is just an array of T's + nodes = maybeNodeBoxArray as T[]; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return treeNodeCallback(context, node, nodes, ...remainingArgs); + }; + + registerCommand(commandId, unwrappingCallback, debounce, telemetryId); +} + +export function isBox(maybeBox: unknown): maybeBox is Box { + if (maybeBox && typeof maybeBox === 'object' && + (maybeBox as Box).unwrap && typeof (maybeBox as Box).unwrap === 'function') { + return true; + } + + return false; +} diff --git a/utils/test/isBox.test.ts b/utils/test/isBox.test.ts new file mode 100644 index 0000000000..a8f367d22e --- /dev/null +++ b/utils/test/isBox.test.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import type { Box } from '../hostapi.v2'; +import { isBox } from '../src/registerCommandWithTreeNodeUnboxing'; + +suite('isBox', () => { + + test('Not a box', () => { + assert.strictEqual(isBox(undefined), false); + assert.strictEqual(isBox(null), false); + assert.strictEqual(isBox(1), false); + assert.strictEqual(isBox(false), false); + assert.strictEqual(isBox('foo'), false); + assert.strictEqual(isBox({}), false); + assert.strictEqual(isBox({ unwrap: false }), false); + }); + + test('Box', () => { + const actualBox: Box = { + unwrap: () => { return Promise.resolve(undefined as unknown as T) }, + }; + + assert.strictEqual(isBox(actualBox), true); + }); + +}); From 789b44a747a9f87dc49f08e22d6826a397b4ce16 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 6 Sep 2022 10:56:50 -0400 Subject: [PATCH 06/49] Check `undefined` and `null` --- utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index 54810949d2..756d4db8ed 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -82,6 +82,7 @@ export class ContextValueQuickPickStep Date: Tue, 6 Sep 2022 12:21:32 -0400 Subject: [PATCH 07/49] Add find-by-ID experience and step --- .../ContextValueQuickPickStep.ts | 8 +- .../quickPickWizard/FindByIdQuickPickStep.ts | 122 ++++++++++++++++++ .../experiences/findByIdExperience.ts | 37 ++++++ 3 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts create mode 100644 utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index 756d4db8ed..b44002d9d7 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -26,7 +26,7 @@ export interface ContextValueFilter { exclude?: string | RegExp | (string | RegExp)[]; } -interface ContextValueFilterableTreeNodeV2 { +export interface ContextValueFilterableTreeNodeV2 { readonly quickPickOptions: { readonly contextValues: Array; readonly isLeaf: boolean; @@ -49,7 +49,7 @@ export class ContextValueQuickPickStep> extends GenericQuickPickStep { + public constructor(tdp: vscode.TreeDataProvider, options: FindByIdQuickPickOptions) { + super( + tdp, + { + ...options, + skipIfOne: true, // Find-by-id is always skip-if-one + } + ); + } + + public async getSubWizard(wizardContext: TContext): Promise | undefined> { + // TODO: this code is nearly identical to `RecursiveQuickPickStep`, but this class can't inherit from it because it's + // not at all based on context value for filtering + const lastPickedItem = getLastNode(wizardContext); + + if (!lastPickedItem) { + // Something went wrong, no node was chosen + throw new Error('No node was set after prompt step.'); + } + + if (this.isDirectPick(lastPickedItem)) { + // The last picked node matches the expected ID + // No need to continue prompting + return undefined; + } else { + // Need to keep going because the last picked node is not a match + return { + hideStepCount: true, + promptSteps: [ + new FindByIdQuickPickStep(this.treeDataProvider, this.pickOptions) + ], + }; + } + } + + protected override isIndirectPick(node: TNode): boolean { + if (isFindableByIdTreeNodeV2(node)) { + if (node.quickPickOptions.isLeaf) { + return false; + } + + if (typeof this.pickOptions.id === 'string') { + // Append '/' to 'treeItem.fullId' when checking 'startsWith' to ensure its actually an ancestor, rather than a treeItem at the same level that _happens_ to start with the same id + // For example, two databases named 'test' and 'test1' as described in this issue: https://github.com/Microsoft/vscode-cosmosdb/issues/488 + return this.pickOptions.id.startsWith(`${node.id.toString()}/`); + } else { + return this.pickOptions.id.scheme === node.id.scheme && + this.pickOptions.id.authority === node.id.authority && + this.pickOptions.id.path.startsWith(`${node.id.path}/`); + } + } else { + if (!isAzExtParentTreeItem(node)) { + return false; + } + + if (typeof this.pickOptions.id === 'string') { + return this.pickOptions.id.startsWith(`${node.fullId}/`); + } else { + return this.pickOptions.id.toString().startsWith(`${node.fullId}/`); + } + } + } + + protected override isDirectPick(node: TNode): boolean { + if (isFindableByIdTreeNodeV2(node)) { + if (typeof this.pickOptions.id === 'string') { + return this.pickOptions.id === node.id.toString(); + } else { + return this.pickOptions.id.scheme === node.id.scheme && + this.pickOptions.id.authority === node.id.authority && + this.pickOptions.id.path === node.id.path; + } + } else { + if (typeof this.pickOptions.id === 'string') { + return this.pickOptions.id === node.fullId; + } else { + return this.pickOptions.id.toString() === node.fullId; + } + } + } +} + +function isFindableByIdTreeNodeV2(maybeNode: unknown): maybeNode is FindableByIdTreeNodeV2 { + if (!isContextValueFilterableTreeNodeV2(maybeNode)) { + return false; + } + + if (typeof maybeNode === 'object') { + const idAsUri = (maybeNode as FindableByIdTreeNodeV2).id as vscode.Uri; + + return typeof idAsUri === 'object' && + typeof idAsUri.scheme === 'string' && + typeof idAsUri.authority === 'string' && + typeof idAsUri.path === 'string'; + } + + return false; +} diff --git a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts new file mode 100644 index 0000000000..d3f85e68b5 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../index'; +import * as vscode from 'vscode'; +import { getLastNode, QuickPickWizardContext } from '../QuickPickWizardContext'; +import { NoResourceFoundError } from '../../../errors'; +import { FindableByIdTreeNode, FindByIdQuickPickStep } from '../FindByIdQuickPickStep'; + +export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string | vscode.Uri): Promise { + const promptSteps: types.AzureWizardPromptStep>[] = [ + new FindByIdQuickPickStep(tdp, { + id: id, + }), + ]; + + // Fill in the `pickedNodes` property + const wizardContext = context as QuickPickWizardContext; + wizardContext.pickedNodes = []; + + const wizard = new types.AzureWizard(context, { + hideStepCount: true, + promptSteps: promptSteps, + }); + + await wizard.prompt(); + + const lastPickedItem = getLastNode(wizardContext); + + if (!lastPickedItem) { + throw new NoResourceFoundError(wizardContext); + } else { + return lastPickedItem; + } +} From 28c5e4437a941717241e26e622273c61864370a1 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Wed, 7 Sep 2022 13:01:50 -0700 Subject: [PATCH 08/49] Changes to tree item picker (#1232) Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com> --- utils/hostapi.v2.d.ts | 96 +++++++++++++++---- utils/index.d.ts | 21 ++++ utils/src/index.ts | 1 + .../registerCommandWithTreeNodeUnboxing.ts | 2 +- .../ContextValueQuickPickStep.ts | 29 +----- .../quickPickWizard/FindByIdQuickPickStep.ts | 3 +- .../QuickPickWithCreateStep.ts | 3 +- .../quickPickWizard/RecursiveQuickPickStep.ts | 3 +- .../experiences/appResourceExperience.ts | 22 ++--- .../experiences/contextValueExperience.ts | 12 ++- .../AzureResourceQuickPickWizardContext.ts | 8 +- .../QuickPickAppResourceStep.ts | 12 +-- .../QuickPickAzureResourceGroupStep.ts | 3 +- .../QuickPickAzureSubscriptionStep.ts | 3 +- .../QuickPickGroupStep.ts | 3 +- .../quickPickAzureResource/tempTypes.ts | 28 ++---- utils/test/isBox.test.ts | 2 +- 17 files changed, 149 insertions(+), 102 deletions(-) diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index b6289593cd..0405211ab8 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -1,4 +1,54 @@ -import { IActionContext } from "./index"; +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { AzExtTreeItem, IActionContext } from "./index"; +import * as vscode from 'vscode'; +import type { Environment } from '@azure/ms-rest-azure-env'; +import { AzExtResourceType } from "./src/AzExtResourceType"; + +export interface ApplicationAuthentication { + getSession(scopes?: string[]): vscode.ProviderResult; +} + +/** + * Information specific to the Subscription + */ +export interface ApplicationSubscription { + readonly authentication: ApplicationAuthentication; + readonly displayName: string; + readonly subscriptionId: string; + readonly environment: Environment; + readonly isCustomCloud: boolean; +} + +export interface ResourceBase { + readonly id: string; + readonly name: string; +} + +export interface ApplicationResourceType { + readonly type: string; + readonly kinds?: string[]; +} + +/** + * Represents an individual resource in Azure. + * @remarks The `id` property is expected to be the Azure resource ID. + */ +export interface ApplicationResource extends ResourceBase { + readonly subscription: ApplicationSubscription; + readonly type: ApplicationResourceType; + readonly azExtResourceType?: AzExtResourceType; + readonly location?: string; + readonly resourceGroup?: string; + /** Resource tags */ + readonly tags?: { + [propertyName: string]: string; + }; + /* add more properties from GenericResource if needed */ +} /** * Interface describing an object that wraps another object. @@ -12,28 +62,40 @@ import { IActionContext } from "./index"; * aren't boxes) */ export interface Box { - unwrap(): Promise; + unwrap(): T; } -/** - * Tests to see if something is a box, by ensuring it is an object - * and has an "unwrap" function - * @param maybeBox An object to test if it is a box - * @returns True if a box, false otherwise - */ -export declare function isBox(maybeBox: unknown): maybeBox is Box; - /** * Describes command callbacks for tree node context menu commands */ export type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; /** - * Used to register VSCode tree node context menu commands that are in the host extension's tree. It wraps your callback with consistent error and telemetry handling - * Use debounce property if you need a delay between clicks for this particular command - * A telemetry event is automatically sent whenever a command is executed. The telemetry event ID will default to the same as the - * commandId passed in, but can be overridden per command with telemetryId - * The telemetry event for this command will be named telemetryId if specified, otherwise it defaults to the commandId - * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. + * Describes filtering based on context value. Items that pass the filter will + * match at least one of the `include` filters, but none of the `exclude` filters. */ -export declare function registerCommandWithTreeNodeUnboxing(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; +export interface ContextValueFilter { + /** + * This filter will include items that match *any* of the values in the array. + * When a string is used, exact value comparison is done. + */ + include: string | RegExp | (string | RegExp)[]; + + /** + * This filter will exclude items that match *any* of the values in the array. + * When a string is used, exact value comparison is done. + */ + exclude?: string | RegExp | (string | RegExp)[]; +} + +export interface ContextValueFilterableTreeNodeV2 { + readonly quickPickOptions: { + readonly contextValues: Array; + readonly isLeaf: boolean; + } +} + +export type ContextValueFilterableTreeNode = ContextValueFilterableTreeNodeV2 | AzExtTreeItem; + +// temporary type until we have the real type from RGs +export type ResourceGroupsItem = ContextValueFilterableTreeNode; diff --git a/utils/index.d.ts b/utils/index.d.ts index 0f01f5b888..37a47b6d09 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -10,6 +10,7 @@ import { CancellationToken, CancellationTokenSource, Disposable, Event, Extensio import { TargetPopulation } from 'vscode-tas-client'; import { AzureExtensionApi, AzureExtensionApiProvider } from './api'; import type { Activity, ActivityTreeItemOptions, AppResource, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result +import type { Box, ContextValueFilter, ResourceGroupsItem, TreeNodeCommandCallback } from './hostapi.v2'; export declare interface RunWithTemporaryDescriptionOptions { description: string; @@ -1678,3 +1679,23 @@ export declare enum AzExtResourceType { VirtualNetworks = 'VirtualNetworks', WebHostingEnvironments = 'WebHostingEnvironments', } + +/** + * Tests to see if something is a box, by ensuring it is an object + * and has an "unwrap" function + * @param maybeBox An object to test if it is a box + * @returns True if a box, false otherwise + */ +export declare function isBox(maybeBox: unknown): maybeBox is Box; + +export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: ContextValueFilter): Promise; + +/** + * Used to register VSCode tree node context menu commands that are in the host extension's tree. It wraps your callback with consistent error and telemetry handling + * Use debounce property if you need a delay between clicks for this particular command + * A telemetry event is automatically sent whenever a command is executed. The telemetry event ID will default to the same as the + * commandId passed in, but can be overridden per command with telemetryId + * The telemetry event for this command will be named telemetryId if specified, otherwise it defaults to the commandId + * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. + */ +export declare function registerCommandWithTreeNodeUnboxing(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; diff --git a/utils/src/index.ts b/utils/src/index.ts index 25af2ff87a..288318eddc 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -37,4 +37,5 @@ export * from './utils/contextUtils'; export * from './activityLog/activities/ExecuteActivity'; export * from './getAzExtResourceType'; export * from './AzExtResourceType'; +export * from './treev2/quickPickWizard/experiences/appResourceExperience'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/registerCommandWithTreeNodeUnboxing.ts b/utils/src/registerCommandWithTreeNodeUnboxing.ts index 33cf4a5096..4adfd0ed2a 100644 --- a/utils/src/registerCommandWithTreeNodeUnboxing.ts +++ b/utils/src/registerCommandWithTreeNodeUnboxing.ts @@ -28,7 +28,7 @@ export function registerCommandWithTreeNodeUnboxing(commandId: string, treeNo const boxedNodes = maybeNodeBoxArray as Box[]; nodes = []; for (const n of boxedNodes) { - nodes.push(await n.unwrap()) + nodes.push(n.unwrap()) } } else if (maybeNodeBoxArray && Array.isArray(maybeNodeBoxArray)) { // Otherwise, assume it is just an array of T's diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index b44002d9d7..5a787fb937 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -3,37 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as types from '../../../index'; import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep'; import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces'; import { QuickPickWizardContext } from './QuickPickWizardContext'; - -/** - * Describes filtering based on context value. Items that pass the filter will - * match at least one of the `include` filters, but none of the `exclude` filters. - */ -export interface ContextValueFilter { - /** - * This filter will include items that match *any* of the values in the array. - * When a string is used, exact value comparison is done. - */ - include: string | RegExp | (string | RegExp)[]; - - /** - * This filter will exclude items that match *any* of the values in the array. - * When a string is used, exact value comparison is done. - */ - exclude?: string | RegExp | (string | RegExp)[]; -} - -export interface ContextValueFilterableTreeNodeV2 { - readonly quickPickOptions: { - readonly contextValues: Array; - readonly isLeaf: boolean; - } -} - -export type ContextValueFilterableTreeNode = ContextValueFilterableTreeNodeV2 | types.AzExtTreeItem; +import { ContextValueFilter, ContextValueFilterableTreeNode, ContextValueFilterableTreeNodeV2 } from '../../../hostapi.v2'; export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions { contextValueFilter: ContextValueFilter; diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts index cf56e2c5fe..edbcbc4497 100644 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts @@ -7,8 +7,9 @@ import * as types from '../../../index'; import * as vscode from 'vscode'; import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces'; -import { ContextValueFilterableTreeNodeV2, isContextValueFilterableTreeNodeV2 } from './ContextValueQuickPickStep'; +import { isContextValueFilterableTreeNodeV2 } from './ContextValueQuickPickStep'; import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from './GenericQuickPickStep'; +import { ContextValueFilterableTreeNodeV2 } from '../../../hostapi.v2'; interface FindableByIdTreeNodeV2 extends ContextValueFilterableTreeNodeV2 { id: vscode.Uri; diff --git a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts index a7e7fa36a1..e076d7fede 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts +++ b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts @@ -5,8 +5,9 @@ import * as types from '../../../index'; import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; -import { ContextValueFilterableTreeNode, ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; +import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; import { localize } from '../../localize'; +import { ContextValueFilterableTreeNode } from '../../../hostapi.v2'; type CreateCallback = () => TNode | Promise; interface CreateQuickPickOptions extends ContextValueFilterQuickPickOptions { diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index 0456b7e632..56050f7738 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ContextValueFilterableTreeNode } from '../../../hostapi.v2'; import * as types from '../../../index'; -import { ContextValueFilterableTreeNode, ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; +import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; export class RecursiveQuickPickStep> extends ContextValueQuickPickStep { diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts index daeb6c515d..f12e596e1e 100644 --- a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -3,20 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as types from '../../../../index'; import * as vscode from 'vscode'; -import { ContextValueFilter } from '../ContextValueQuickPickStep'; import { QuickPickAzureSubscriptionStep } from '../quickPickAzureResource/QuickPickAzureSubscriptionStep'; import { QuickPickGroupStep } from '../quickPickAzureResource/QuickPickGroupStep'; import { QuickPickAppResourceStep } from '../quickPickAzureResource/QuickPickAppResourceStep'; import { AzureResourceQuickPickWizardContext } from '../quickPickAzureResource/AzureResourceQuickPickWizardContext'; import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; -import { Box, ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; import { getLastNode } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; - -export async function appResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, resourceType: types.AzExtResourceType, childItemFilter?: ContextValueFilter): Promise { - const promptSteps: types.AzureWizardPromptStep[] = [ +import { IActionContext } from '../../../../index'; +import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; +import { AzExtResourceType } from '../../../AzExtResourceType'; +import { AzureWizard } from '../../../wizard/AzureWizard'; +import { ContextValueFilter, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { isBox } from '../../../registerCommandWithTreeNodeUnboxing'; + +export async function appResourceExperience(context: IActionContext, tdp: vscode.TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: ContextValueFilter): Promise { + const promptSteps: AzureWizardPromptStep[] = [ new QuickPickAzureSubscriptionStep(tdp), new QuickPickGroupStep(tdp, { groupType: resourceType, @@ -38,7 +41,7 @@ export async function appResourceExperience(context: types.IActionContext const wizardContext = context as AzureResourceQuickPickWizardContext; wizardContext.pickedNodes = []; - const wizard = new types.AzureWizard(context, { + const wizard = new AzureWizard(context, { hideStepCount: true, promptSteps: promptSteps, }); @@ -50,9 +53,6 @@ export async function appResourceExperience(context: types.IActionContext if (!lastPickedItem) { throw new NoResourceFoundError(wizardContext); } else { - // Treat lastPickedItem as a box containing the desired end object - // TODO - const pickedAsBox = lastPickedItem as unknown as Box; - return pickedAsBox.unwrap(); + return isBox(lastPickedItem) ? lastPickedItem.unwrap() : lastPickedItem as unknown as TPick; } } diff --git a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts index 570fa14cc8..24f672d46f 100644 --- a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts @@ -3,15 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as types from '../../../../index'; import * as vscode from 'vscode'; -import { ContextValueFilter, ContextValueFilterableTreeNode } from '../ContextValueQuickPickStep'; import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; import { getLastNode, QuickPickWizardContext } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; +import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; +import { IActionContext } from '../../../../index'; +import { AzureWizard } from '../../../wizard/AzureWizard'; +import { ContextValueFilter, ContextValueFilterableTreeNode } from '../../../../hostapi.v2'; -export async function contextValueExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: ContextValueFilter): Promise { - const promptSteps: types.AzureWizardPromptStep>[] = [ +export async function contextValueExperience(context: IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: ContextValueFilter): Promise { + const promptSteps: AzureWizardPromptStep>[] = [ new RecursiveQuickPickStep(tdp, { contextValueFilter: contextValueFilter, skipIfOne: false, @@ -22,7 +24,7 @@ export async function contextValueExperience; wizardContext.pickedNodes = []; - const wizard = new types.AzureWizard(context, { + const wizard = new AzureWizard(context, { hideStepCount: true, promptSteps: promptSteps, }); diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts index be86bd9dd6..64fc5ae076 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ApplicationResource, ApplicationSubscription, ResourceGroupsItem } from "../../../../hostapi.v2"; import { QuickPickWizardContext } from "../QuickPickWizardContext"; -import { ApplicationResource, ApplicationSubscription, ResourceGroupsItem } from "./tempTypes"; export interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext { - subscription: ApplicationSubscription | undefined; - resource: ApplicationResource | undefined; - resourceGroup: string | undefined; + subscription?: ApplicationSubscription; + resource?: ApplicationResource; + resourceGroup?: string; } diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts index 0e076c6aff..61061328e6 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ContextValueFilter, ResourceGroupsItem } from '../../../../hostapi.v2'; import * as types from '../../../../index'; -import { ContextValueFilter } from '../ContextValueQuickPickStep'; import { GenericQuickPickOptions, GenericQuickPickStep } from '../GenericQuickPickStep'; import { AzureResourceQuickPickWizardContext } from './AzureResourceQuickPickWizardContext'; -import { AppResourceItem, ResourceGroupsItem } from './tempTypes'; +import { AppResourceItem } from './tempTypes'; interface AppResourceQuickPickOptions extends GenericQuickPickOptions { resourceType: types.AzExtResourceType; @@ -19,8 +19,8 @@ export class QuickPickAppResourceStep extends GenericQuickPickStep { public constructor(tdp: vscode.TreeDataProvider, options?: GenericQuickPickOptions) { diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts index f2633c1a7d..4cd292917d 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts @@ -7,7 +7,8 @@ import * as types from '../../../../index'; import * as vscode from 'vscode'; import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from '../GenericQuickPickStep'; import { AzureResourceQuickPickWizardContext } from './AzureResourceQuickPickWizardContext'; -import { GroupingItem, ResourceGroupsItem } from './tempTypes'; +import { GroupingItem } from './tempTypes'; +import { ResourceGroupsItem } from '../../../../hostapi.v2'; interface GroupQuickPickOptions extends SkipIfOneQuickPickOptions { groupType: types.AzExtResourceType; diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts index 4a4a5d258f..16191ea3ee 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts @@ -3,38 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ApplicationResource, ApplicationSubscription, ResourceGroupsItem } from '../../../../hostapi.v2'; import * as types from '../../../../index'; -import { ContextValueFilterableTreeNode } from '../ContextValueQuickPickStep'; // TODO: THIS FILE IS TEMPORARY // // It needs to be replaced by real Resources extension interfaces // -export type ResourceGroupsItem = ContextValueFilterableTreeNode; +// These are assumptions made about the nodes in the tree export type SubscriptionItem = ResourceGroupsItem & { subscription: ApplicationSubscription; -}; +} export type GroupingItem = ResourceGroupsItem & { resourceType?: types.AzExtResourceType } -export type AppResourceItem = ResourceGroupsItem & ApplicationResource; - -type ResourceBase = {}; - -/** - * Represents an individual resource in Azure. - * @remarks The `id` property is expected to be the Azure resource ID. - */ -export interface ApplicationResource extends ResourceBase { - readonly subscription: ApplicationSubscription; - readonly azExtResourceType?: types.AzExtResourceType; - readonly resourceGroup?: string; -} - -export type ApplicationSubscription = unknown; - -export interface Box { - unwrap(): T; -} +export type AppResourceItem = ResourceGroupsItem & { + resource: ApplicationResource; +}; diff --git a/utils/test/isBox.test.ts b/utils/test/isBox.test.ts index a8f367d22e..383e65e7eb 100644 --- a/utils/test/isBox.test.ts +++ b/utils/test/isBox.test.ts @@ -21,7 +21,7 @@ suite('isBox', () => { test('Box', () => { const actualBox: Box = { - unwrap: () => { return Promise.resolve(undefined as unknown as T) }, + unwrap: () => { return undefined as unknown as T }, }; assert.strictEqual(isBox(actualBox), true); From 4a544a4f6cc9acccfefd7b56a897e4037b9daa14 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Fri, 9 Sep 2022 12:13:29 -0400 Subject: [PATCH 09/49] Commit rough progress --- utils/hostapi.d.ts | 6 +-- utils/hostapi.v2.d.ts | 49 +++++------------ utils/index.d.ts | 43 ++++++++++++++- utils/src/index.ts | 2 + .../ContextValueQuickPickStep.ts | 15 +++--- .../quickPickWizard/FindByIdQuickPickStep.ts | 52 ++++--------------- .../quickPickWizard/GenericQuickPickStep.ts | 4 +- .../QuickPickWithCreateStep.ts | 7 ++- .../quickPickWizard/QuickPickWizardContext.ts | 6 +-- .../quickPickWizard/RecursiveQuickPickStep.ts | 5 +- .../experiences/appResourceExperience.ts | 7 ++- .../experiences/contextValueExperience.ts | 11 ++-- .../experiences/findByIdExperience.ts | 10 ++-- .../AzureResourceQuickPickWizardContext.ts | 13 ----- .../QuickPickAppResourceStep.ts | 5 +- .../QuickPickAzureResourceGroupStep.ts | 7 ++- .../QuickPickAzureSubscriptionStep.ts | 3 +- .../QuickPickGroupStep.ts | 3 +- 18 files changed, 104 insertions(+), 144 deletions(-) delete mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts diff --git a/utils/hostapi.d.ts b/utils/hostapi.d.ts index 22a76daf6c..fa6a73519a 100644 --- a/utils/hostapi.d.ts +++ b/utils/hostapi.d.ts @@ -18,7 +18,7 @@ export interface AzureHostExtensionApi { /** * The VSCode TreeView for the shared app resource view */ - readonly appResourceTreeView: vscode.TreeView; + readonly appResourceTreeView: vscode.TreeView; /** * The `AzExtTreeDataProvider` for the shared workspace resource view @@ -28,7 +28,7 @@ export interface AzureHostExtensionApi { /** * The VSCode TreeView for the shared workspace resource view */ - readonly workspaceResourceTreeView: vscode.TreeView; + readonly workspaceResourceTreeView: vscode.TreeView; /** * Version of the API @@ -76,7 +76,7 @@ export interface AzureHostExtensionApi { /** * @deprecated Use `appResourceTreeView` instead */ - readonly treeView: vscode.TreeView; + readonly treeView: vscode.TreeView; /** * @deprecated Use `registerWorkspaceResourceProvider` instead diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index 0405211ab8..6d38eed663 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -3,19 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzExtTreeItem, IActionContext } from "./index"; +import type { ContextValueFilterableTreeNode, IActionContext, QuickPickWizardContext } from "./index"; import * as vscode from 'vscode'; import type { Environment } from '@azure/ms-rest-azure-env'; import { AzExtResourceType } from "./src/AzExtResourceType"; -export interface ApplicationAuthentication { +export declare interface ApplicationAuthentication { getSession(scopes?: string[]): vscode.ProviderResult; } /** * Information specific to the Subscription */ -export interface ApplicationSubscription { +export declare interface ApplicationSubscription { readonly authentication: ApplicationAuthentication; readonly displayName: string; readonly subscriptionId: string; @@ -23,12 +23,12 @@ export interface ApplicationSubscription { readonly isCustomCloud: boolean; } -export interface ResourceBase { +export declare interface ResourceBase { readonly id: string; readonly name: string; } -export interface ApplicationResourceType { +export declare interface ApplicationResourceType { readonly type: string; readonly kinds?: string[]; } @@ -37,7 +37,7 @@ export interface ApplicationResourceType { * Represents an individual resource in Azure. * @remarks The `id` property is expected to be the Azure resource ID. */ -export interface ApplicationResource extends ResourceBase { +export declare interface ApplicationResource extends ResourceBase { readonly subscription: ApplicationSubscription; readonly type: ApplicationResourceType; readonly azExtResourceType?: AzExtResourceType; @@ -61,41 +61,20 @@ export interface ApplicationResource extends ResourceBase { * unboxing automatically (i.e., will not unbox if the arguments * aren't boxes) */ -export interface Box { +export declare interface Box { unwrap(): T; } /** * Describes command callbacks for tree node context menu commands */ -export type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; +export declare type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; -/** - * Describes filtering based on context value. Items that pass the filter will - * match at least one of the `include` filters, but none of the `exclude` filters. - */ -export interface ContextValueFilter { - /** - * This filter will include items that match *any* of the values in the array. - * When a string is used, exact value comparison is done. - */ - include: string | RegExp | (string | RegExp)[]; - - /** - * This filter will exclude items that match *any* of the values in the array. - * When a string is used, exact value comparison is done. - */ - exclude?: string | RegExp | (string | RegExp)[]; -} +// temporary type until we have the real type from RGs +export declare type ResourceGroupsItem = ContextValueFilterableTreeNode; -export interface ContextValueFilterableTreeNodeV2 { - readonly quickPickOptions: { - readonly contextValues: Array; - readonly isLeaf: boolean; - } +export declare interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext { + subscription?: ApplicationSubscription; + resource?: ApplicationResource; + resourceGroup?: string; } - -export type ContextValueFilterableTreeNode = ContextValueFilterableTreeNodeV2 | AzExtTreeItem; - -// temporary type until we have the real type from RGs -export type ResourceGroupsItem = ContextValueFilterableTreeNode; diff --git a/utils/index.d.ts b/utils/index.d.ts index 37a47b6d09..60f27c755c 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -10,7 +10,7 @@ import { CancellationToken, CancellationTokenSource, Disposable, Event, Extensio import { TargetPopulation } from 'vscode-tas-client'; import { AzureExtensionApi, AzureExtensionApiProvider } from './api'; import type { Activity, ActivityTreeItemOptions, AppResource, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result -import type { Box, ContextValueFilter, ResourceGroupsItem, TreeNodeCommandCallback } from './hostapi.v2'; +import type { Box, ResourceGroupsItem, TreeNodeCommandCallback } from './hostapi.v2'; export declare interface RunWithTemporaryDescriptionOptions { description: string; @@ -1688,7 +1688,46 @@ export declare enum AzExtResourceType { */ export declare function isBox(maybeBox: unknown): maybeBox is Box; -export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: ContextValueFilter): Promise; +export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: ContextValueFilter): Promise; +export declare function contextValueExperience(context: IActionContext, tdp: TreeDataProvider, contextValueFilter: ContextValueFilter): Promise; +export declare function findByIdExperience(context: IActionContext, tdp: TreeDataProvider, id: string | Uri): Promise; + +export declare interface QuickPickWizardContext extends IActionContext { + pickedNodes: TNode[]; +} + +/** + * Describes filtering based on context value. Items that pass the filter will + * match at least one of the `include` filters, but none of the `exclude` filters. + */ +export declare interface ContextValueFilter { + /** + * This filter will include items that match *any* of the values in the array. + * When a string is used, exact value comparison is done. + */ + include: string | RegExp | (string | RegExp)[]; + + /** + * This filter will exclude items that match *any* of the values in the array. + * When a string is used, exact value comparison is done. + */ + exclude?: string | RegExp | (string | RegExp)[]; +} + +export declare interface ContextValueFilterableTreeNodeV2 { + readonly quickPickOptions: { + readonly contextValues: Array; + readonly isLeaf: boolean; + } +} + +export declare type ContextValueFilterableTreeNode = ContextValueFilterableTreeNodeV2 | AzExtTreeItem; + +export declare interface FindableByIdTreeNodeV2 extends ContextValueFilterableTreeNodeV2 { + id: string; +} + +export declare type FindableByIdTreeNode = FindableByIdTreeNodeV2 | AzExtTreeItem; /** * Used to register VSCode tree node context menu commands that are in the host extension's tree. It wraps your callback with consistent error and telemetry handling diff --git a/utils/src/index.ts b/utils/src/index.ts index 288318eddc..844cfaec9b 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -38,4 +38,6 @@ export * from './activityLog/activities/ExecuteActivity'; export * from './getAzExtResourceType'; export * from './AzExtResourceType'; export * from './treev2/quickPickWizard/experiences/appResourceExperience'; +export * from './treev2/quickPickWizard/experiences/contextValueExperience'; +export * from './treev2/quickPickWizard/experiences/findByIdExperience'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index 5a787fb937..5e2b94b41b 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as types from '../../../index'; import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep'; import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces'; -import { QuickPickWizardContext } from './QuickPickWizardContext'; -import { ContextValueFilter, ContextValueFilterableTreeNode, ContextValueFilterableTreeNodeV2 } from '../../../hostapi.v2'; export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions { - contextValueFilter: ContextValueFilter; + contextValueFilter: types.ContextValueFilter; } -export class ContextValueQuickPickStep, TOptions extends ContextValueFilterQuickPickOptions> extends GenericQuickPickStep { +export class ContextValueQuickPickStep, TOptions extends ContextValueFilterQuickPickOptions> extends GenericQuickPickStep { protected override isDirectPick(node: TNode): boolean { const includeOption = this.pickOptions.contextValueFilter.include; const excludeOption = this.pickOptions.contextValueFilter.exclude; @@ -52,11 +51,11 @@ export class ContextValueQuickPickStep> extends GenericQuickPickStep { +export class FindByIdQuickPickStep> extends GenericQuickPickStep { public constructor(tdp: vscode.TreeDataProvider, options: FindByIdQuickPickOptions) { super( tdp, @@ -64,59 +57,32 @@ export class FindByIdQuickPickStep, TOptions extends GenericQuickPickOptions> extends AzureWizardPromptStep { +export abstract class GenericQuickPickStep, TOptions extends GenericQuickPickOptions> extends AzureWizardPromptStep { public readonly supportsDuplicateSteps = true; public constructor( diff --git a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts index e076d7fede..3837921859 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts +++ b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as types from '../../../index'; -import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; +import { getLastNode } from './QuickPickWizardContext'; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; import { localize } from '../../localize'; -import { ContextValueFilterableTreeNode } from '../../../hostapi.v2'; -type CreateCallback = () => TNode | Promise; +type CreateCallback = () => TNode | Promise; interface CreateQuickPickOptions extends ContextValueFilterQuickPickOptions { skipIfOne?: never; // Not allowed in CreateQuickPickStep createLabel?: string; createCallback: CreateCallback; } -export class CreateQuickPickStep> extends ContextValueQuickPickStep { +export class CreateQuickPickStep> extends ContextValueQuickPickStep { public override async prompt(wizardContext: TContext): Promise { await super.prompt(wizardContext); diff --git a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts index 961df90959..30a24b0725 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts +++ b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts @@ -5,11 +5,7 @@ import * as types from '../../../index'; -export interface QuickPickWizardContext extends types.IActionContext { - pickedNodes: TNode[]; -} - -export function getLastNode(context: QuickPickWizardContext): TNode | undefined { +export function getLastNode(context: types.QuickPickWizardContext): TNode | undefined { if (context.pickedNodes.length) { return context.pickedNodes[context.pickedNodes.length - 1]; } diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index 56050f7738..91ac8f76af 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContextValueFilterableTreeNode } from '../../../hostapi.v2'; import * as types from '../../../index'; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; -import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext'; +import { getLastNode } from './QuickPickWizardContext'; -export class RecursiveQuickPickStep> extends ContextValueQuickPickStep { +export class RecursiveQuickPickStep> extends ContextValueQuickPickStep { public async getSubWizard(wizardContext: TContext): Promise | undefined> { const lastPickedItem = getLastNode(wizardContext); diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts index f12e596e1e..699895ef0e 100644 --- a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -4,21 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as types from '../../../../index'; import { QuickPickAzureSubscriptionStep } from '../quickPickAzureResource/QuickPickAzureSubscriptionStep'; import { QuickPickGroupStep } from '../quickPickAzureResource/QuickPickGroupStep'; import { QuickPickAppResourceStep } from '../quickPickAzureResource/QuickPickAppResourceStep'; -import { AzureResourceQuickPickWizardContext } from '../quickPickAzureResource/AzureResourceQuickPickWizardContext'; import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; import { getLastNode } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; -import { IActionContext } from '../../../../index'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzExtResourceType } from '../../../AzExtResourceType'; import { AzureWizard } from '../../../wizard/AzureWizard'; -import { ContextValueFilter, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; import { isBox } from '../../../registerCommandWithTreeNodeUnboxing'; -export async function appResourceExperience(context: IActionContext, tdp: vscode.TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: ContextValueFilter): Promise { +export async function appResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: types.ContextValueFilter): Promise { const promptSteps: AzureWizardPromptStep[] = [ new QuickPickAzureSubscriptionStep(tdp), new QuickPickGroupStep(tdp, { diff --git a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts index 24f672d46f..d386de4c16 100644 --- a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as types from '../../../../index'; import * as vscode from 'vscode'; import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; -import { getLastNode, QuickPickWizardContext } from '../QuickPickWizardContext'; +import { getLastNode } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; -import { IActionContext } from '../../../../index'; import { AzureWizard } from '../../../wizard/AzureWizard'; -import { ContextValueFilter, ContextValueFilterableTreeNode } from '../../../../hostapi.v2'; -export async function contextValueExperience(context: IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: ContextValueFilter): Promise { - const promptSteps: AzureWizardPromptStep>[] = [ +export async function contextValueExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: types.ContextValueFilter): Promise { + const promptSteps: AzureWizardPromptStep>[] = [ new RecursiveQuickPickStep(tdp, { contextValueFilter: contextValueFilter, skipIfOne: false, @@ -21,7 +20,7 @@ export async function contextValueExperience; + const wizardContext = context as types.QuickPickWizardContext; wizardContext.pickedNodes = []; const wizard = new AzureWizard(context, { diff --git a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts index d3f85e68b5..7cf6ab80e8 100644 --- a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts @@ -5,19 +5,19 @@ import * as types from '../../../../index'; import * as vscode from 'vscode'; -import { getLastNode, QuickPickWizardContext } from '../QuickPickWizardContext'; +import { getLastNode } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; -import { FindableByIdTreeNode, FindByIdQuickPickStep } from '../FindByIdQuickPickStep'; +import { FindByIdQuickPickStep } from '../FindByIdQuickPickStep'; -export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string | vscode.Uri): Promise { - const promptSteps: types.AzureWizardPromptStep>[] = [ +export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string): Promise { + const promptSteps: types.AzureWizardPromptStep>[] = [ new FindByIdQuickPickStep(tdp, { id: id, }), ]; // Fill in the `pickedNodes` property - const wizardContext = context as QuickPickWizardContext; + const wizardContext = context as types.QuickPickWizardContext; wizardContext.pickedNodes = []; const wizard = new types.AzureWizard(context, { diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts deleted file mode 100644 index 64fc5ae076..0000000000 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/AzureResourceQuickPickWizardContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ApplicationResource, ApplicationSubscription, ResourceGroupsItem } from "../../../../hostapi.v2"; -import { QuickPickWizardContext } from "../QuickPickWizardContext"; - -export interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext { - subscription?: ApplicationSubscription; - resource?: ApplicationResource; - resourceGroup?: string; -} diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts index 61061328e6..3f96b63dbe 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContextValueFilter, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; import * as types from '../../../../index'; import { GenericQuickPickOptions, GenericQuickPickStep } from '../GenericQuickPickStep'; -import { AzureResourceQuickPickWizardContext } from './AzureResourceQuickPickWizardContext'; import { AppResourceItem } from './tempTypes'; interface AppResourceQuickPickOptions extends GenericQuickPickOptions { resourceType: types.AzExtResourceType; - childItemFilter?: ContextValueFilter; + childItemFilter?: types.ContextValueFilter; } export class QuickPickAppResourceStep extends GenericQuickPickStep { diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts index 9bd31b4ddb..7a73bde70e 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceGroupsItem, ContextValueFilterableTreeNode } from "../../../../hostapi.v2"; +import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from "../../../../hostapi.v2"; import * as types from "../../../../index"; import { GenericQuickPickOptions, GenericQuickPickStep } from "../GenericQuickPickStep"; -import { AzureResourceQuickPickWizardContext } from "./AzureResourceQuickPickWizardContext"; // TODO: implement this for picking resource group // The resource group may NOT be the grouping method used in the tree @@ -15,11 +14,11 @@ export class QuickPickAzureResourceGroupStep extends GenericQuickPickStep { diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts index 4cd292917d..f177ea84c2 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickGroupStep.ts @@ -6,9 +6,8 @@ import * as types from '../../../../index'; import * as vscode from 'vscode'; import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from '../GenericQuickPickStep'; -import { AzureResourceQuickPickWizardContext } from './AzureResourceQuickPickWizardContext'; import { GroupingItem } from './tempTypes'; -import { ResourceGroupsItem } from '../../../../hostapi.v2'; +import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; interface GroupQuickPickOptions extends SkipIfOneQuickPickOptions { groupType: types.AzExtResourceType; From 77cd7d90304201c7876091121af4e837f4e3d07a Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Fri, 9 Sep 2022 12:21:17 -0400 Subject: [PATCH 10/49] Rename Box to Wrapper --- utils/hostapi.v2.d.ts | 15 ------ utils/index.d.ts | 25 +++++++-- utils/src/index.ts | 2 +- .../registerCommandWithTreeNodeUnboxing.ts | 52 ------------------- .../registerCommandWithTreeNodeUnwrapping.ts | 52 +++++++++++++++++++ .../experiences/appResourceExperience.ts | 4 +- .../experiences/contextValueExperience.ts | 3 +- .../experiences/findByIdExperience.ts | 3 +- utils/test/isBox.test.ts | 30 ----------- utils/test/isWrapper.test.ts | 30 +++++++++++ 10 files changed, 109 insertions(+), 107 deletions(-) delete mode 100644 utils/src/registerCommandWithTreeNodeUnboxing.ts create mode 100644 utils/src/registerCommandWithTreeNodeUnwrapping.ts delete mode 100644 utils/test/isBox.test.ts create mode 100644 utils/test/isWrapper.test.ts diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index 6d38eed663..3cae61792c 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -50,21 +50,6 @@ export declare interface ApplicationResource extends ResourceBase { /* add more properties from GenericResource if needed */ } -/** - * Interface describing an object that wraps another object. - * - * The host extension will wrap all tree nodes provided by the client - * extensions. When commands are executed, the wrapper objects are - * sent directly to the client extension, which will need to unwrap - * them. The `registerCommandWithTreeNodeUnboxing` method below, used - * in place of `registerCommand`, will intelligently do this - * unboxing automatically (i.e., will not unbox if the arguments - * aren't boxes) - */ -export declare interface Box { - unwrap(): T; -} - /** * Describes command callbacks for tree node context menu commands */ diff --git a/utils/index.d.ts b/utils/index.d.ts index 60f27c755c..df9e6d844a 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -10,7 +10,7 @@ import { CancellationToken, CancellationTokenSource, Disposable, Event, Extensio import { TargetPopulation } from 'vscode-tas-client'; import { AzureExtensionApi, AzureExtensionApiProvider } from './api'; import type { Activity, ActivityTreeItemOptions, AppResource, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result -import type { Box, ResourceGroupsItem, TreeNodeCommandCallback } from './hostapi.v2'; +import type { ResourceGroupsItem, TreeNodeCommandCallback } from './hostapi.v2'; export declare interface RunWithTemporaryDescriptionOptions { description: string; @@ -1681,12 +1681,27 @@ export declare enum AzExtResourceType { } /** - * Tests to see if something is a box, by ensuring it is an object + * Interface describing an object that wraps another object. + * + * The host extension will wrap all tree nodes provided by the client + * extensions. When commands are executed, the wrapper objects are + * sent directly to the client extension, which will need to unwrap + * them. The `registerCommandWithTreeNodeUnwrapping` method below, used + * in place of `registerCommand`, will intelligently do this + * unwrapping automatically (i.e., will not unwrap if the arguments + * aren't wrappers) + */ +export declare interface Wrapper { + unwrap(): T; +} + +/** + * Tests to see if something is a wrapper, by ensuring it is an object * and has an "unwrap" function - * @param maybeBox An object to test if it is a box - * @returns True if a box, false otherwise + * @param maybeWrapper An object to test if it is a wrapper + * @returns True if a wrapper, false otherwise */ -export declare function isBox(maybeBox: unknown): maybeBox is Box; +export declare function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper; export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: ContextValueFilter): Promise; export declare function contextValueExperience(context: IActionContext, tdp: TreeDataProvider, contextValueFilter: ContextValueFilter): Promise; diff --git a/utils/src/index.ts b/utils/src/index.ts index 844cfaec9b..51a1aed79f 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -16,7 +16,7 @@ export { addExtensionValueToMask, callWithMaskHandling, maskValue } from './mask export * from './openReadOnlyContent'; export * from './parseError'; export * from './registerCommand'; -export * from './registerCommandWithTreeNodeUnboxing'; +export * from './registerCommandWithTreeNodeUnwrapping'; export * from './registerEvent'; export { registerReportIssueCommand } from './registerReportIssueCommand'; export * from './tree/AzExtParentTreeItem'; diff --git a/utils/src/registerCommandWithTreeNodeUnboxing.ts b/utils/src/registerCommandWithTreeNodeUnboxing.ts deleted file mode 100644 index 4adfd0ed2a..0000000000 --- a/utils/src/registerCommandWithTreeNodeUnboxing.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ - -import type { CommandCallback, IActionContext } from '../index'; -import type { Box, TreeNodeCommandCallback } from '../hostapi.v2'; -import { registerCommand } from './registerCommand'; - -export function registerCommandWithTreeNodeUnboxing(commandId: string, treeNodeCallback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void { - const unwrappingCallback: CommandCallback = async (context: IActionContext, ...args: unknown[]) => { - const maybeNodeBox = args?.[0]; - const maybeNodeBoxArray = args?.[1]; - const remainingArgs = args.slice(2); - - let node: T | undefined; - if (maybeNodeBox && isBox(maybeNodeBox)) { - // If the first arg is a box, unwrap it - node = await maybeNodeBox.unwrap(); - } else if (maybeNodeBox) { - // Otherwise, assume it is just a T - node = maybeNodeBox as T; - } - - let nodes: T[] | undefined; - if (maybeNodeBoxArray && Array.isArray(maybeNodeBoxArray) && maybeNodeBoxArray.every(n => isBox(n))) { - // If the first arg is an array of boxes, unwrap them - const boxedNodes = maybeNodeBoxArray as Box[]; - nodes = []; - for (const n of boxedNodes) { - nodes.push(n.unwrap()) - } - } else if (maybeNodeBoxArray && Array.isArray(maybeNodeBoxArray)) { - // Otherwise, assume it is just an array of T's - nodes = maybeNodeBoxArray as T[]; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - return treeNodeCallback(context, node, nodes, ...remainingArgs); - }; - - registerCommand(commandId, unwrappingCallback, debounce, telemetryId); -} - -export function isBox(maybeBox: unknown): maybeBox is Box { - if (maybeBox && typeof maybeBox === 'object' && - (maybeBox as Box).unwrap && typeof (maybeBox as Box).unwrap === 'function') { - return true; - } - - return false; -} diff --git a/utils/src/registerCommandWithTreeNodeUnwrapping.ts b/utils/src/registerCommandWithTreeNodeUnwrapping.ts new file mode 100644 index 0000000000..75d6985e9b --- /dev/null +++ b/utils/src/registerCommandWithTreeNodeUnwrapping.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { CommandCallback, IActionContext, Wrapper } from '../index'; +import type { TreeNodeCommandCallback } from '../hostapi.v2'; +import { registerCommand } from './registerCommand'; + +export function registerCommandWithTreeNodeUnwrapping(commandId: string, treeNodeCallback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void { + const unwrappingCallback: CommandCallback = async (context: IActionContext, ...args: unknown[]) => { + const maybeNodeWrapper = args?.[0]; + const maybeNodeWrapperArray = args?.[1]; + const remainingArgs = args.slice(2); + + let node: T | undefined; + if (maybeNodeWrapper && isWrapper(maybeNodeWrapper)) { + // If the first arg is a wrapper, unwrap it + node = await maybeNodeWrapper.unwrap(); + } else if (maybeNodeWrapper) { + // Otherwise, assume it is just a T + node = maybeNodeWrapper as T; + } + + let nodes: T[] | undefined; + if (maybeNodeWrapperArray && Array.isArray(maybeNodeWrapperArray) && maybeNodeWrapperArray.every(n => isWrapper(n))) { + // If the first arg is an array of wrappers, unwrap them + const wrappedNodes = maybeNodeWrapperArray as Wrapper[]; + nodes = []; + for (const n of wrappedNodes) { + nodes.push(n.unwrap()) + } + } else if (maybeNodeWrapperArray && Array.isArray(maybeNodeWrapperArray)) { + // Otherwise, assume it is just an array of T's + nodes = maybeNodeWrapperArray as T[]; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return treeNodeCallback(context, node, nodes, ...remainingArgs); + }; + + registerCommand(commandId, unwrappingCallback, debounce, telemetryId); +} + +export function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper { + if (maybeWrapper && typeof maybeWrapper === 'object' && + (maybeWrapper as Wrapper).unwrap && typeof (maybeWrapper as Wrapper).unwrap === 'function') { + return true; + } + + return false; +} diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts index 699895ef0e..1cd1bb7e70 100644 --- a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -15,7 +15,7 @@ import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzExtResourceType } from '../../../AzExtResourceType'; import { AzureWizard } from '../../../wizard/AzureWizard'; import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; -import { isBox } from '../../../registerCommandWithTreeNodeUnboxing'; +import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; export async function appResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: types.ContextValueFilter): Promise { const promptSteps: AzureWizardPromptStep[] = [ @@ -52,6 +52,6 @@ export async function appResourceExperience(context: types.IActionContext if (!lastPickedItem) { throw new NoResourceFoundError(wizardContext); } else { - return isBox(lastPickedItem) ? lastPickedItem.unwrap() : lastPickedItem as unknown as TPick; + return isWrapper(lastPickedItem) ? lastPickedItem.unwrap() : lastPickedItem as unknown as TPick; } } diff --git a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts index d386de4c16..1fce6567a2 100644 --- a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts @@ -10,6 +10,7 @@ import { getLastNode } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzureWizard } from '../../../wizard/AzureWizard'; +import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; export async function contextValueExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: types.ContextValueFilter): Promise { const promptSteps: AzureWizardPromptStep>[] = [ @@ -35,6 +36,6 @@ export async function contextValueExperience() : lastPickedItem as unknown as TPick; } } diff --git a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts index 7cf6ab80e8..3a0e367a32 100644 --- a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { getLastNode } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; import { FindByIdQuickPickStep } from '../FindByIdQuickPickStep'; +import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string): Promise { const promptSteps: types.AzureWizardPromptStep>[] = [ @@ -32,6 +33,6 @@ export async function findByIdExperience() : lastPickedItem as unknown as TPick; } } diff --git a/utils/test/isBox.test.ts b/utils/test/isBox.test.ts deleted file mode 100644 index 383e65e7eb..0000000000 --- a/utils/test/isBox.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import type { Box } from '../hostapi.v2'; -import { isBox } from '../src/registerCommandWithTreeNodeUnboxing'; - -suite('isBox', () => { - - test('Not a box', () => { - assert.strictEqual(isBox(undefined), false); - assert.strictEqual(isBox(null), false); - assert.strictEqual(isBox(1), false); - assert.strictEqual(isBox(false), false); - assert.strictEqual(isBox('foo'), false); - assert.strictEqual(isBox({}), false); - assert.strictEqual(isBox({ unwrap: false }), false); - }); - - test('Box', () => { - const actualBox: Box = { - unwrap: () => { return undefined as unknown as T }, - }; - - assert.strictEqual(isBox(actualBox), true); - }); - -}); diff --git a/utils/test/isWrapper.test.ts b/utils/test/isWrapper.test.ts new file mode 100644 index 0000000000..c074ca0978 --- /dev/null +++ b/utils/test/isWrapper.test.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Wrapper } from '..'; +import { isWrapper } from '../src/registerCommandWithTreeNodeUnwrapping'; + +suite('isWrapper', () => { + + test('Not a Wrapper', () => { + assert.strictEqual(isWrapper(undefined), false); + assert.strictEqual(isWrapper(null), false); + assert.strictEqual(isWrapper(1), false); + assert.strictEqual(isWrapper(false), false); + assert.strictEqual(isWrapper('foo'), false); + assert.strictEqual(isWrapper({}), false); + assert.strictEqual(isWrapper({ unwrap: false }), false); + }); + + test('Wrapper', () => { + const actualWrapper: Wrapper = { + unwrap: () => { return undefined as unknown as T }, + }; + + assert.strictEqual(isWrapper(actualWrapper), true); + }); + +}); From 09ec22a83cf30d4d279eb1946e39dc9f0fad72c4 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Thu, 15 Sep 2022 09:25:24 -0700 Subject: [PATCH 11/49] Add compatibility steps (#1235) Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com> --- utils/hostapi.v2.d.ts | 3 +- utils/index.d.ts | 48 ++++++-- utils/src/index.ts | 1 + utils/src/tree/AzExtTreeItem.ts | 8 +- .../ContextValueQuickPickStep.ts | 23 +--- .../quickPickWizard/FindByIdQuickPickStep.ts | 11 +- .../quickPickWizard/GenericQuickPickStep.ts | 6 +- .../QuickPickWithCreateStep.ts | 13 ++- .../CompatibilityContextValueQuickPickStep.ts | 54 +++++++++ .../CompatibilityRecursiveQuickPickStep.ts | 105 ++++++++++++++++++ .../experiences/appResourceExperience.ts | 11 +- .../compatibilityPickResourceExperience.ts | 62 +++++++++++ .../experiences/findByIdExperience.ts | 3 +- .../QuickPickAppResourceStep.ts | 14 ++- .../QuickPickGroupStep.ts | 4 +- 15 files changed, 318 insertions(+), 48 deletions(-) create mode 100644 utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts create mode 100644 utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts create mode 100644 utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index 3cae61792c..5e00982cdb 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ContextValueFilterableTreeNode, IActionContext, QuickPickWizardContext } from "./index"; +import type { IActionContext, AzExtResourceType, ContextValueFilterableTreeNode, QuickPickWizardContext } from "./index"; import * as vscode from 'vscode'; import type { Environment } from '@azure/ms-rest-azure-env'; -import { AzExtResourceType } from "./src/AzExtResourceType"; export declare interface ApplicationAuthentication { getSession(scopes?: string[]): vscode.ProviderResult; diff --git a/utils/index.d.ts b/utils/index.d.ts index df9e6d844a..c930c1c27c 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -6,7 +6,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Environment } from '@azure/ms-rest-azure-env'; -import { CancellationToken, CancellationTokenSource, Disposable, Event, ExtensionContext, FileChangeEvent, FileChangeType, FileStat, FileSystemProvider, FileType, InputBoxOptions, MarkdownString, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, Progress, QuickPickItem, QuickPickOptions, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri } from 'vscode'; +import { CancellationToken, CancellationTokenSource, Disposable, Event, ExtensionContext, FileChangeEvent, FileChangeType, FileStat, FileSystemProvider, FileType, InputBoxOptions, MarkdownString, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, Progress, QuickPickItem, QuickPickOptions as VSCodeQuickPickOptions, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri } from 'vscode'; import { TargetPopulation } from 'vscode-tas-client'; import { AzureExtensionApi, AzureExtensionApiProvider } from './api'; import type { Activity, ActivityTreeItemOptions, AppResource, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result @@ -465,6 +465,8 @@ export declare abstract class AzExtTreeItem implements IAzExtTreeItem { public resolveTooltip?(): Promise; } +export declare function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is AzExtTreeItem; + export interface IGenericTreeItemOptions { id?: string; label: string; @@ -993,7 +995,7 @@ export interface IAzureQuickPickItem extends QuickPickItem { /** * Provides additional options for QuickPicks used in Azure Extensions */ -export interface IAzureQuickPickOptions extends QuickPickOptions, AzExtUserInputOptions { +export interface IAzureQuickPickOptions extends VSCodeQuickPickOptions, AzExtUserInputOptions { /** * An optional id to identify this QuickPick across sessions, used in persisting previous selections * If not specified, a hash of the placeHolder will be used @@ -1703,10 +1705,17 @@ export declare interface Wrapper { */ export declare function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper; -export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: ContextValueFilter): Promise; -export declare function contextValueExperience(context: IActionContext, tdp: TreeDataProvider, contextValueFilter: ContextValueFilter): Promise; +export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceTypes?: AzExtResourceType | AzExtResourceType[], childItemFilter?: ContextValueFilter): Promise; +export declare function contextValueExperience(context: IActionContext, tdp: TreeDataProvider, contextValueFilter: ContextValueFilter): Promise; export declare function findByIdExperience(context: IActionContext, tdp: TreeDataProvider, id: string | Uri): Promise; +interface CompatibilityPickResourceExperienceOptions { + resourceTypes?: AzExtResourceType | AzExtResourceType[]; + childItemFilter?: ContextValueFilter +} + +export declare function compatibilityPickAppResourceExperience(context: IActionContext, tdp: TreeDataProvider, options: CompatibilityPickResourceExperienceOptions): Promise; + export declare interface QuickPickWizardContext extends IActionContext { pickedNodes: TNode[]; } @@ -1729,16 +1738,31 @@ export declare interface ContextValueFilter { exclude?: string | RegExp | (string | RegExp)[]; } -export declare interface ContextValueFilterableTreeNodeV2 { - readonly quickPickOptions: { - readonly contextValues: Array; - readonly isLeaf: boolean; - } +interface QuickPickOptions { + readonly contextValues: Array; + readonly isLeaf: boolean; +} + +type CreateCallback = (context: IActionContext) => TNode | Promise; + +type CreateOptions = { + label?: string; + callback: CreateCallback; +} + +interface CompatibleQuickPickOptions extends QuickPickOptions { + readonly createChild?: CreateOptions; } -export declare type ContextValueFilterableTreeNode = ContextValueFilterableTreeNodeV2 | AzExtTreeItem; +export declare interface ContextValueFilterableTreeNode { + readonly quickPickOptions: QuickPickOptions; +} + +export declare interface CompatibleContextValueFilterableTreeNode { + readonly quickPickOptions: CompatibleQuickPickOptions; +} -export declare interface FindableByIdTreeNodeV2 extends ContextValueFilterableTreeNodeV2 { +export declare interface FindableByIdTreeNodeV2 extends ContextValueFilterableTreeNode { id: string; } @@ -1752,4 +1776,4 @@ export declare type FindableByIdTreeNode = FindableByIdTreeNodeV2 | AzExtTreeIte * The telemetry event for this command will be named telemetryId if specified, otherwise it defaults to the commandId * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. */ -export declare function registerCommandWithTreeNodeUnboxing(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; +export declare function registerCommandWithTreeNodeUnwrapping(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; diff --git a/utils/src/index.ts b/utils/src/index.ts index 51a1aed79f..2faa1aa911 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -38,6 +38,7 @@ export * from './activityLog/activities/ExecuteActivity'; export * from './getAzExtResourceType'; export * from './AzExtResourceType'; export * from './treev2/quickPickWizard/experiences/appResourceExperience'; +export * from './treev2/quickPickWizard/experiences/compatibilityPickResourceExperience'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; export * from './treev2/quickPickWizard/experiences/findByIdExperience'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/tree/AzExtTreeItem.ts b/utils/src/tree/AzExtTreeItem.ts index 2c5703768e..c58b6930c3 100644 --- a/utils/src/tree/AzExtTreeItem.ts +++ b/utils/src/tree/AzExtTreeItem.ts @@ -13,6 +13,8 @@ import { settingUtils } from '../utils/settingUtils'; import { showContextValueSetting } from '../constants'; export abstract class AzExtTreeItem implements types.AzExtTreeItem { + public readonly _isAzExtTreeItem = true; + //#region Properties implemented by base class public abstract label: string; public abstract contextValue: string; @@ -120,7 +122,7 @@ export abstract class AzExtTreeItem implements types.AzExtTreeItem { } public get tooltip(): string | undefined { - if(process.env.DEBUGTELEMETRY === 'v' && !!settingUtils.getWorkspaceSetting(showContextValueSetting)) { + if (process.env.DEBUGTELEMETRY === 'v' && !!settingUtils.getWorkspaceSetting(showContextValueSetting)) { return `Context: "${this.contextValue}"`; } else { return this._tooltip; @@ -213,3 +215,7 @@ export abstract class AzExtTreeItem implements types.AzExtTreeItem { } } } + +export function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is types.AzExtTreeItem { + return typeof maybeTreeItem === 'object' && (maybeTreeItem as AzExtTreeItem)._isAzExtTreeItem === true; +} diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index 5e2b94b41b..894b53f04c 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -5,7 +5,6 @@ import * as types from '../../../index'; import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep'; -import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces'; export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions { contextValueFilter: types.ContextValueFilter; @@ -21,22 +20,14 @@ export class ContextValueQuickPickStep this.matchesSingleFilter(i, nodeContextValues)) && !excludeArray.some(e => this.matchesSingleFilter(e, nodeContextValues)); } protected override isIndirectPick(node: TNode): boolean { - if (isContextValueFilterableTreeNodeV2(node)) { - return node.quickPickOptions.isLeaf === false; - } else if (isAzExtParentTreeItem(node)) { - return true; - } - - return false; + return node.quickPickOptions.isLeaf === false; } private matchesSingleFilter(matcher: string | RegExp, nodeContextValues: string[]): boolean { @@ -50,13 +41,3 @@ export class ContextValueQuickPickStep() => TNode | Promise; interface CreateQuickPickOptions extends ContextValueFilterQuickPickOptions { @@ -28,7 +29,17 @@ export class CreateQuickPickStep[]> { - const picks: types.IAzureQuickPickItem[] = await super.getPicks(wizardContext); + const picks: types.IAzureQuickPickItem[] = []; + try { + picks.push(...await super.getPicks(wizardContext)); + } catch (error) { + if (error instanceof NoResourceFoundError) { + // swallow NoResourceFoundError if create is defined, since we'll add a create pick + } else { + throw error; + } + } + picks.push(this.getCreatePick()); return picks as types.IAzureQuickPickItem[]; } diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts new file mode 100644 index 0000000000..049c8238e0 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from "../../../../index"; +import { isAzExtParentTreeItem } from "../../../tree/InternalInterfaces"; +import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from "../ContextValueQuickPickStep"; +import { getLastNode } from "../QuickPickWizardContext"; +import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; +import { AzExtParentTreeItem } from "../../../tree/AzExtParentTreeItem"; +import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; + +/** + * Provides compatability with {@link AzExtParentTreeItem.pickTreeItemImpl} + */ +export class CompatibilityContextValueQuickPickStep, TOptions extends ContextValueFilterQuickPickOptions> extends ContextValueQuickPickStep { + + public override async prompt(wizardContext: TContext): Promise { + await this.provideCompatabilityWithPickTreeItemImpl(wizardContext) || await super.prompt(wizardContext); + } + + /** + * Mimics how the legacy {@link AzExtParentTreeItem.pickChildTreeItem} + * uses {@link AzExtParentTreeItem.pickTreeItemImpl} to customize the tree item picker. + * + * An example customization is skipping having to pick a UI-only node (ex: App Settings parent node) + */ + private async provideCompatabilityWithPickTreeItemImpl(wizardContext: TContext): Promise { + const lastPickedItem = getLastNode(wizardContext); + const lastPickedItemUnwrapped = isWrapper(lastPickedItem) ? lastPickedItem.unwrap() : lastPickedItem; + if (isAzExtParentTreeItem(lastPickedItemUnwrapped)) { + const children = await this.treeDataProvider.getChildren(lastPickedItem); + if (children && children.length) { + const customChild = await this.getCustomChildren(wizardContext, lastPickedItemUnwrapped); + + const customPick = children.find((child) => { + const ti: AzExtTreeItem = isWrapper(child) ? child.unwrap() : child as unknown as AzExtTreeItem; + return ti.fullId === customChild?.fullId; + }); + + if (customPick) { + wizardContext.pickedNodes.push(customPick); + return true; + } + } + } + return false; + } + + private async getCustomChildren(context: TContext, node: AzExtParentTreeItem): Promise { + return await node.pickTreeItemImpl?.(Array.isArray(this.pickOptions.contextValueFilter.include) ? this.pickOptions.contextValueFilter.include : [this.pickOptions.contextValueFilter.include], context); + } +} diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts new file mode 100644 index 0000000000..6d97bddcd3 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from "../../../../index"; +import { getLastNode } from "../QuickPickWizardContext"; +import { CompatibilityContextValueQuickPickStep } from './CompatibilityContextValueQuickPickStep'; +import { localize } from "../../../localize"; +import { NoResourceFoundError, UserCancelledError } from "../../../errors"; +import type { ContextValueFilterQuickPickOptions } from "../ContextValueQuickPickStep"; + +export interface CompatibilityRecursiveQuickPickOptions extends ContextValueFilterQuickPickOptions { + create?: types.CreateOptions; +} + +/** + * Recursive step which is compatible which adds create picks based if the node has {@link types.CompatibleQuickPickOptions.createChild quickPickOptions.createChild} defined. + */ +export class CompatibilityRecursiveQuickPickStep> extends CompatibilityContextValueQuickPickStep { + + protected override async promptInternal(wizardContext: TContext): Promise { + const picks = await this.getPicks(wizardContext) as types.IAzureQuickPickItem[]; + + if (picks.length === 1 && this.pickOptions.skipIfOne) { + return picks[0].data; + } else { + const selected = await wizardContext.ui.showQuickPick(picks, { + /* TODO: options */ + /* TODO: set id here so recently picked items appear at the top */ + }); + + // check if the last picked item is a create callback + if (typeof selected.data === 'function') { + // If the last node is a function, pop it off the list and execute it + const callback = selected.data as unknown as types.CreateCallback; + + // context passed to callback must have the same `ui` as the wizardContext + // to prevent the wizard from being cancelled unexpectedly + const createdPick = await callback?.(wizardContext); + + if (createdPick) { + return createdPick; + } + + throw new UserCancelledError(); + } + + return selected.data; + } + } + + public async getSubWizard(wizardContext: TContext): Promise | undefined> { + const lastPickedItem = getLastNode(wizardContext); + + if (!lastPickedItem) { + // Something went wrong, no node was chosen + throw new Error('No node was set after prompt step.'); + } + + if (super.isDirectPick(lastPickedItem)) { + // The last picked node matches the expected filter + // No need to continue prompting + return undefined; + } else { + // Need to keep going because the last picked node is not a match + return { + hideStepCount: true, + promptSteps: [ + new CompatibilityRecursiveQuickPickStep(this.treeDataProvider, { + ...this.pickOptions, + skipIfOne: !lastPickedItem.quickPickOptions.createChild, + create: lastPickedItem.quickPickOptions.createChild, + }) + ], + }; + } + } + + protected override async getPicks(wizardContext: TContext): Promise[]> { + const picks: types.IAzureQuickPickItem[] = []; + try { + picks.push(...await super.getPicks(wizardContext)); + } catch (error) { + if (error instanceof NoResourceFoundError && !!this.pickOptions.create) { + // swallow NoResourceFoundError if create is defined, since we'll add a create pick + } else { + throw error; + } + } + + if (this.pickOptions.create) { + picks.push(this.getCreatePick(this.pickOptions.create)); + } + + return picks as types.IAzureQuickPickItem[]; + } + + private getCreatePick(options: types.CreateOptions): types.IAzureQuickPickItem { + return { + label: options.label || localize('createQuickPickLabel', '$(add) Create...'), + data: options.callback, + }; + } +} diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts index 1cd1bb7e70..979d2440b6 100644 --- a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -17,14 +17,18 @@ import { AzureWizard } from '../../../wizard/AzureWizard'; import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; -export async function appResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, resourceType: AzExtResourceType, childItemFilter?: types.ContextValueFilter): Promise { +export async function appResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, resourceTypes?: AzExtResourceType | AzExtResourceType[], childItemFilter?: types.ContextValueFilter): Promise { const promptSteps: AzureWizardPromptStep[] = [ new QuickPickAzureSubscriptionStep(tdp), new QuickPickGroupStep(tdp, { - groupType: resourceType, + groupType: resourceTypes ? + (Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes]) : + undefined, }), new QuickPickAppResourceStep(tdp, { - resourceType: resourceType, + resourceTypes: resourceTypes ? + (Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes]) : + undefined, skipIfOne: false, }), ]; @@ -43,6 +47,7 @@ export async function appResourceExperience(context: types.IActionContext const wizard = new AzureWizard(context, { hideStepCount: true, promptSteps: promptSteps, + showLoadingPrompt: true, }); await wizard.prompt(); diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts new file mode 100644 index 0000000000..ee72be1b8c --- /dev/null +++ b/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { QuickPickAzureSubscriptionStep } from '../quickPickAzureResource/QuickPickAzureSubscriptionStep'; +import { QuickPickGroupStep } from '../quickPickAzureResource/QuickPickGroupStep'; +import { QuickPickAppResourceStep } from '../quickPickAzureResource/QuickPickAppResourceStep'; +import { getLastNode } from '../QuickPickWizardContext'; +import { NoResourceFoundError } from '../../../errors'; +import * as types from '../../../../index'; +import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; +import { AzureWizard } from '../../../wizard/AzureWizard'; +import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { CompatibilityRecursiveQuickPickStep } from '../compatibility/CompatibilityRecursiveQuickPickStep'; +import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; + +/** + * Provides compatibility for the legacy `pickAppResource` Resource Groups API + */ +export async function compatibilityPickAppResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, options: types.CompatibilityPickResourceExperienceOptions): Promise { + const { resourceTypes, childItemFilter } = options; + + const promptSteps: AzureWizardPromptStep[] = [ + new QuickPickAzureSubscriptionStep(tdp), + new QuickPickGroupStep(tdp, { + groupType: resourceTypes ? Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes] : undefined, + }), + new QuickPickAppResourceStep(tdp, { + resourceTypes: resourceTypes ? Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes] : undefined, + skipIfOne: false, + }), + ]; + + if (childItemFilter) { + promptSteps.push(new CompatibilityRecursiveQuickPickStep(tdp, { + contextValueFilter: childItemFilter, + skipIfOne: false, + })); + } + + // Fill in the `pickedNodes` property + const wizardContext = context as AzureResourceQuickPickWizardContext; + wizardContext.pickedNodes = []; + + const wizard = new AzureWizard(context, { + hideStepCount: true, + promptSteps: promptSteps, + showLoadingPrompt: true, + }); + + await wizard.prompt(); + + const lastPickedItem = getLastNode(wizardContext); + + if (!lastPickedItem) { + throw new NoResourceFoundError(wizardContext); + } else { + return isWrapper(lastPickedItem) ? lastPickedItem.unwrap() : lastPickedItem as unknown as TPick; + } +} diff --git a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts index 3a0e367a32..8aeca71483 100644 --- a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts @@ -9,6 +9,7 @@ import { getLastNode } from '../QuickPickWizardContext'; import { NoResourceFoundError } from '../../../errors'; import { FindByIdQuickPickStep } from '../FindByIdQuickPickStep'; import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; +import { AzureWizard } from '../../../wizard/AzureWizard'; export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string): Promise { const promptSteps: types.AzureWizardPromptStep>[] = [ @@ -21,7 +22,7 @@ export async function findByIdExperience; wizardContext.pickedNodes = []; - const wizard = new types.AzureWizard(context, { + const wizard = new AzureWizard(context, { hideStepCount: true, promptSteps: promptSteps, }); diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts index 3f96b63dbe..1954232f2d 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts @@ -9,7 +9,7 @@ import { GenericQuickPickOptions, GenericQuickPickStep } from '../GenericQuickPi import { AppResourceItem } from './tempTypes'; interface AppResourceQuickPickOptions extends GenericQuickPickOptions { - resourceType: types.AzExtResourceType; + resourceTypes?: types.AzExtResourceType[]; childItemFilter?: types.ContextValueFilter; } @@ -30,7 +30,11 @@ export class QuickPickAppResourceStep extends GenericQuickPickStep Date: Fri, 16 Sep 2022 09:04:27 -0700 Subject: [PATCH 12/49] Refactor and expose `isAzExtParentTreeItem` (#1236) --- utils/index.d.ts | 1 + utils/src/tree/AzExtParentTreeItem.ts | 9 ++++++++- utils/src/tree/AzExtTreeDataProvider.ts | 4 ++-- utils/src/tree/AzExtTreeItem.ts | 3 ++- utils/src/tree/InternalInterfaces.ts | 7 ------- .../src/treev2/quickPickWizard/FindByIdQuickPickStep.ts | 2 +- .../CompatibilityContextValueQuickPickStep.ts | 3 +-- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/utils/index.d.ts b/utils/index.d.ts index c930c1c27c..83d4ca91b5 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -466,6 +466,7 @@ export declare abstract class AzExtTreeItem implements IAzExtTreeItem { } export declare function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is AzExtTreeItem; +export declare function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem; export interface IGenericTreeItemOptions { id?: string; diff --git a/utils/src/tree/AzExtParentTreeItem.ts b/utils/src/tree/AzExtParentTreeItem.ts index f4b02d8899..9b1315409e 100644 --- a/utils/src/tree/AzExtParentTreeItem.ts +++ b/utils/src/tree/AzExtParentTreeItem.ts @@ -11,7 +11,7 @@ import { localize } from '../localize'; import { randomUtils } from '../utils/randomUtils'; import { AzExtTreeItem } from './AzExtTreeItem'; import { GenericTreeItem } from './GenericTreeItem'; -import { IAzExtParentTreeItemInternal, isAzExtParentTreeItem } from './InternalInterfaces'; +import { IAzExtParentTreeItemInternal } from './InternalInterfaces'; import { runWithLoadingNotification } from './runWithLoadingNotification'; import { loadMoreLabel } from './treeConstants'; @@ -397,3 +397,10 @@ class CanPickManyError extends Error { this.picks = picks; } } + +/** + * Using instanceof AzExtParentTreeItem causes issues since each extension has their own version of the utils. Instead, check _isAzExtParentTreeItem + */ +export function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem { + return typeof maybeParentTreeItem === 'object' && (maybeParentTreeItem as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem === true; +} diff --git a/utils/src/tree/AzExtTreeDataProvider.ts b/utils/src/tree/AzExtTreeDataProvider.ts index 7832040116..20a5775e38 100644 --- a/utils/src/tree/AzExtTreeDataProvider.ts +++ b/utils/src/tree/AzExtTreeDataProvider.ts @@ -10,11 +10,11 @@ import { NoResourceFoundError, UserCancelledError } from '../errors'; import { localize } from '../localize'; import { parseError } from '../parseError'; import { addTreeItemValuesToMask } from './addTreeItemValuesToMask'; -import { AzExtParentTreeItem, InvalidTreeItem } from './AzExtParentTreeItem'; +import { AzExtParentTreeItem, InvalidTreeItem, isAzExtParentTreeItem } from './AzExtParentTreeItem'; import { AzExtTreeItem } from './AzExtTreeItem'; import { CollapsibleStateTracker } from './CollapsibleStateTracker'; import { GenericTreeItem } from './GenericTreeItem'; -import { IAzExtTreeDataProviderInternal, isAzExtParentTreeItem } from './InternalInterfaces'; +import { IAzExtTreeDataProviderInternal } from './InternalInterfaces'; import { runWithLoadingNotification } from './runWithLoadingNotification'; import { loadMoreLabel } from './treeConstants'; diff --git a/utils/src/tree/AzExtTreeItem.ts b/utils/src/tree/AzExtTreeItem.ts index c58b6930c3..613e999905 100644 --- a/utils/src/tree/AzExtTreeItem.ts +++ b/utils/src/tree/AzExtTreeItem.ts @@ -8,9 +8,10 @@ import * as types from '../../index'; import { NotImplementedError } from '../errors'; import { localize } from '../localize'; import { nonNullProp } from '../utils/nonNull'; -import { IAzExtParentTreeItemInternal, IAzExtTreeDataProviderInternal, isAzExtParentTreeItem } from "./InternalInterfaces"; +import { IAzExtParentTreeItemInternal, IAzExtTreeDataProviderInternal } from "./InternalInterfaces"; import { settingUtils } from '../utils/settingUtils'; import { showContextValueSetting } from '../constants'; +import { isAzExtParentTreeItem } from './AzExtParentTreeItem'; export abstract class AzExtTreeItem implements types.AzExtTreeItem { public readonly _isAzExtTreeItem = true; diff --git a/utils/src/tree/InternalInterfaces.ts b/utils/src/tree/InternalInterfaces.ts index 0afdb5c4f2..d4e8240787 100644 --- a/utils/src/tree/InternalInterfaces.ts +++ b/utils/src/tree/InternalInterfaces.ts @@ -25,10 +25,3 @@ export interface IAzExtTreeDataProviderInternal extends types.AzExtTreeDataProvi refreshUIOnly(treeItem: AzExtTreeItem | undefined): void; readonly collapsibleStateTracker: CollapsibleStateTracker | undefined; } - -/** - * Using instanceof AzExtParentTreeItem causes issues whenever packages are linked for dev testing. Instead, check _isAzExtParentTreeItem - */ -export function isAzExtParentTreeItem(maybeParent: unknown): maybeParent is IAzExtParentTreeItemInternal { - return !!(maybeParent as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem; -} diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts index afcfe78aee..e8f21a0d2b 100644 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts @@ -6,8 +6,8 @@ import * as types from '../../../index'; import * as vscode from 'vscode'; import { getLastNode } from './QuickPickWizardContext'; -import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces'; import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from './GenericQuickPickStep'; +import { isAzExtParentTreeItem } from '../../tree/AzExtParentTreeItem'; interface FindByIdQuickPickOptions extends SkipIfOneQuickPickOptions { id: string; diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts index 049c8238e0..ba112c2024 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as types from "../../../../index"; -import { isAzExtParentTreeItem } from "../../../tree/InternalInterfaces"; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from "../ContextValueQuickPickStep"; import { getLastNode } from "../QuickPickWizardContext"; import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; -import { AzExtParentTreeItem } from "../../../tree/AzExtParentTreeItem"; +import { AzExtParentTreeItem, isAzExtParentTreeItem } from "../../../tree/AzExtParentTreeItem"; import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; /** From d29d16d98a89f11b942b814d5ffa112240a793b1 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Fri, 16 Sep 2022 12:55:28 -0700 Subject: [PATCH 13/49] Fix circular dependency (#1238) --- utils/src/index.ts | 1 + utils/src/tree/AzExtParentTreeItem.ts | 8 +------- utils/src/tree/AzExtTreeDataProvider.ts | 3 ++- utils/src/tree/AzExtTreeItem.ts | 2 +- utils/src/tree/InternalInterfaces.ts | 2 +- utils/src/tree/isAzExtParentTreeItem.ts | 14 ++++++++++++++ .../quickPickWizard/FindByIdQuickPickStep.ts | 2 +- .../CompatibilityContextValueQuickPickStep.ts | 3 ++- 8 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 utils/src/tree/isAzExtParentTreeItem.ts diff --git a/utils/src/index.ts b/utils/src/index.ts index 2faa1aa911..e20ead7c9b 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -41,4 +41,5 @@ export * from './treev2/quickPickWizard/experiences/appResourceExperience'; export * from './treev2/quickPickWizard/experiences/compatibilityPickResourceExperience'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; export * from './treev2/quickPickWizard/experiences/findByIdExperience'; +export * from './tree/isAzExtParentTreeItem'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/tree/AzExtParentTreeItem.ts b/utils/src/tree/AzExtParentTreeItem.ts index 9b1315409e..7cfb7573ac 100644 --- a/utils/src/tree/AzExtParentTreeItem.ts +++ b/utils/src/tree/AzExtParentTreeItem.ts @@ -12,6 +12,7 @@ import { randomUtils } from '../utils/randomUtils'; import { AzExtTreeItem } from './AzExtTreeItem'; import { GenericTreeItem } from './GenericTreeItem'; import { IAzExtParentTreeItemInternal } from './InternalInterfaces'; +import { isAzExtParentTreeItem } from './isAzExtParentTreeItem'; import { runWithLoadingNotification } from './runWithLoadingNotification'; import { loadMoreLabel } from './treeConstants'; @@ -397,10 +398,3 @@ class CanPickManyError extends Error { this.picks = picks; } } - -/** - * Using instanceof AzExtParentTreeItem causes issues since each extension has their own version of the utils. Instead, check _isAzExtParentTreeItem - */ -export function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem { - return typeof maybeParentTreeItem === 'object' && (maybeParentTreeItem as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem === true; -} diff --git a/utils/src/tree/AzExtTreeDataProvider.ts b/utils/src/tree/AzExtTreeDataProvider.ts index 20a5775e38..6559d41fcc 100644 --- a/utils/src/tree/AzExtTreeDataProvider.ts +++ b/utils/src/tree/AzExtTreeDataProvider.ts @@ -10,11 +10,12 @@ import { NoResourceFoundError, UserCancelledError } from '../errors'; import { localize } from '../localize'; import { parseError } from '../parseError'; import { addTreeItemValuesToMask } from './addTreeItemValuesToMask'; -import { AzExtParentTreeItem, InvalidTreeItem, isAzExtParentTreeItem } from './AzExtParentTreeItem'; +import { AzExtParentTreeItem, InvalidTreeItem } from './AzExtParentTreeItem'; import { AzExtTreeItem } from './AzExtTreeItem'; import { CollapsibleStateTracker } from './CollapsibleStateTracker'; import { GenericTreeItem } from './GenericTreeItem'; import { IAzExtTreeDataProviderInternal } from './InternalInterfaces'; +import { isAzExtParentTreeItem } from './isAzExtParentTreeItem'; import { runWithLoadingNotification } from './runWithLoadingNotification'; import { loadMoreLabel } from './treeConstants'; diff --git a/utils/src/tree/AzExtTreeItem.ts b/utils/src/tree/AzExtTreeItem.ts index 613e999905..070214dbbf 100644 --- a/utils/src/tree/AzExtTreeItem.ts +++ b/utils/src/tree/AzExtTreeItem.ts @@ -11,7 +11,7 @@ import { nonNullProp } from '../utils/nonNull'; import { IAzExtParentTreeItemInternal, IAzExtTreeDataProviderInternal } from "./InternalInterfaces"; import { settingUtils } from '../utils/settingUtils'; import { showContextValueSetting } from '../constants'; -import { isAzExtParentTreeItem } from './AzExtParentTreeItem'; +import { isAzExtParentTreeItem } from './isAzExtParentTreeItem'; export abstract class AzExtTreeItem implements types.AzExtTreeItem { public readonly _isAzExtTreeItem = true; diff --git a/utils/src/tree/InternalInterfaces.ts b/utils/src/tree/InternalInterfaces.ts index d4e8240787..35fbaa74a9 100644 --- a/utils/src/tree/InternalInterfaces.ts +++ b/utils/src/tree/InternalInterfaces.ts @@ -6,7 +6,7 @@ import { EventEmitter } from 'vscode'; import * as types from '../../index'; import { AzExtParentTreeItem } from './AzExtParentTreeItem'; -import { AzExtTreeItem } from './AzExtTreeItem'; +import type { AzExtTreeItem } from './AzExtTreeItem'; import { CollapsibleStateTracker } from './CollapsibleStateTracker'; // Interfaces for methods on the tree that aren't exposed outside of this package diff --git a/utils/src/tree/isAzExtParentTreeItem.ts b/utils/src/tree/isAzExtParentTreeItem.ts new file mode 100644 index 0000000000..9bcc9ea80a --- /dev/null +++ b/utils/src/tree/isAzExtParentTreeItem.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { AzExtParentTreeItem } from "./AzExtParentTreeItem"; +import type { IAzExtParentTreeItemInternal } from "./InternalInterfaces"; + +/** + * Using instanceof AzExtParentTreeItem causes issues since each extension has their own version of the utils. Instead, check _isAzExtParentTreeItem + */ +export function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem { + return typeof maybeParentTreeItem === 'object' && (maybeParentTreeItem as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem === true; +} diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts index e8f21a0d2b..66bf49be77 100644 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts @@ -7,7 +7,7 @@ import * as types from '../../../index'; import * as vscode from 'vscode'; import { getLastNode } from './QuickPickWizardContext'; import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from './GenericQuickPickStep'; -import { isAzExtParentTreeItem } from '../../tree/AzExtParentTreeItem'; +import { isAzExtParentTreeItem } from '../../tree/isAzExtParentTreeItem'; interface FindByIdQuickPickOptions extends SkipIfOneQuickPickOptions { id: string; diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts index ba112c2024..f406964b8b 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts @@ -7,8 +7,9 @@ import * as types from "../../../../index"; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from "../ContextValueQuickPickStep"; import { getLastNode } from "../QuickPickWizardContext"; import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; -import { AzExtParentTreeItem, isAzExtParentTreeItem } from "../../../tree/AzExtParentTreeItem"; +import { AzExtParentTreeItem } from "../../../tree/AzExtParentTreeItem"; import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; +import { isAzExtParentTreeItem } from "../../../tree/isAzExtParentTreeItem"; /** * Provides compatability with {@link AzExtParentTreeItem.pickTreeItemImpl} From 25018eaf86935cbf7ad6b5a4cf8a526f198977d8 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Fri, 23 Sep 2022 11:26:54 -0700 Subject: [PATCH 14/49] Remove reliance on `quickPickOptions`, use `TreeItem`s directly (#1239) Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com> --- utils/hostapi.v2.d.ts | 7 +-- utils/index.d.ts | 34 ++++-------- .../ContextValueQuickPickStep.ts | 13 +++-- .../quickPickWizard/FindByIdQuickPickStep.ts | 52 ++++--------------- .../quickPickWizard/GenericQuickPickStep.ts | 38 +++++++------- .../QuickPickWithCreateStep.ts | 12 ++--- .../quickPickWizard/QuickPickWizardContext.ts | 4 +- .../quickPickWizard/RecursiveQuickPickStep.ts | 4 +- .../CompatibilityContextValueQuickPickStep.ts | 2 +- .../CompatibilityRecursiveQuickPickStep.ts | 30 +++++++---- .../experiences/appResourceExperience.ts | 5 +- .../compatibilityPickResourceExperience.ts | 6 ++- .../experiences/contextValueExperience.ts | 7 +-- .../experiences/findByIdExperience.ts | 7 +-- .../QuickPickAppResourceStep.ts | 24 +++++---- .../QuickPickAzureResourceGroupStep.ts | 11 ++-- .../QuickPickAzureSubscriptionStep.ts | 10 ++-- .../QuickPickGroupStep.ts | 18 ++++--- .../quickPickAzureResource/tempTypes.ts | 4 +- utils/src/utils/contextUtils.ts | 4 ++ 20 files changed, 140 insertions(+), 152 deletions(-) diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index 5e00982cdb..8b1a49c5b0 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IActionContext, AzExtResourceType, ContextValueFilterableTreeNode, QuickPickWizardContext } from "./index"; +import type { IActionContext, AzExtResourceType, QuickPickWizardContext } from "./index"; import * as vscode from 'vscode'; import type { Environment } from '@azure/ms-rest-azure-env'; @@ -54,10 +54,7 @@ export declare interface ApplicationResource extends ResourceBase { */ export declare type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; -// temporary type until we have the real type from RGs -export declare type ResourceGroupsItem = ContextValueFilterableTreeNode; - -export declare interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext { +export declare interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext { subscription?: ApplicationSubscription; resource?: ApplicationResource; resourceGroup?: string; diff --git a/utils/index.d.ts b/utils/index.d.ts index 83d4ca91b5..f8ad075a89 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -10,7 +10,7 @@ import { CancellationToken, CancellationTokenSource, Disposable, Event, Extensio import { TargetPopulation } from 'vscode-tas-client'; import { AzureExtensionApi, AzureExtensionApiProvider } from './api'; import type { Activity, ActivityTreeItemOptions, AppResource, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result -import type { ResourceGroupsItem, TreeNodeCommandCallback } from './hostapi.v2'; +import type { TreeNodeCommandCallback } from './hostapi.v2'; export declare interface RunWithTemporaryDescriptionOptions { description: string; @@ -1698,6 +1698,9 @@ export declare interface Wrapper { unwrap(): T; } +// temporary +type ResourceGroupsItem = unknown; + /** * Tests to see if something is a wrapper, by ensuring it is an object * and has an "unwrap" function @@ -1706,9 +1709,9 @@ export declare interface Wrapper { */ export declare function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper; -export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceTypes?: AzExtResourceType | AzExtResourceType[], childItemFilter?: ContextValueFilter): Promise; -export declare function contextValueExperience(context: IActionContext, tdp: TreeDataProvider, contextValueFilter: ContextValueFilter): Promise; -export declare function findByIdExperience(context: IActionContext, tdp: TreeDataProvider, id: string | Uri): Promise; +export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceTypes?: AzExtResourceType | AzExtResourceType[], childItemFilter?: ContextValueFilter): Promise; +export declare function contextValueExperience(context: IActionContext, tdp: TreeDataProvider, contextValueFilter: ContextValueFilter): Promise; +export declare function findByIdExperience(context: IActionContext, tdp: TreeDataProvider, id: string | Uri): Promise; interface CompatibilityPickResourceExperienceOptions { resourceTypes?: AzExtResourceType | AzExtResourceType[]; @@ -1717,8 +1720,8 @@ interface CompatibilityPickResourceExperienceOptions { export declare function compatibilityPickAppResourceExperience(context: IActionContext, tdp: TreeDataProvider, options: CompatibilityPickResourceExperienceOptions): Promise; -export declare interface QuickPickWizardContext extends IActionContext { - pickedNodes: TNode[]; +export declare interface QuickPickWizardContext extends IActionContext { + pickedNodes: unknown[]; } /** @@ -1739,11 +1742,6 @@ export declare interface ContextValueFilter { exclude?: string | RegExp | (string | RegExp)[]; } -interface QuickPickOptions { - readonly contextValues: Array; - readonly isLeaf: boolean; -} - type CreateCallback = (context: IActionContext) => TNode | Promise; type CreateOptions = { @@ -1751,19 +1749,7 @@ type CreateOptions = { callback: CreateCallback; } -interface CompatibleQuickPickOptions extends QuickPickOptions { - readonly createChild?: CreateOptions; -} - -export declare interface ContextValueFilterableTreeNode { - readonly quickPickOptions: QuickPickOptions; -} - -export declare interface CompatibleContextValueFilterableTreeNode { - readonly quickPickOptions: CompatibleQuickPickOptions; -} - -export declare interface FindableByIdTreeNodeV2 extends ContextValueFilterableTreeNode { +export declare interface FindableByIdTreeNodeV2 { id: string; } diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index 894b53f04c..0cc463ce48 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -3,15 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TreeItem } from 'vscode'; import * as types from '../../../index'; +import { parseContextValue } from '../../utils/contextUtils'; import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep'; export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions { contextValueFilter: types.ContextValueFilter; } -export class ContextValueQuickPickStep, TOptions extends ContextValueFilterQuickPickOptions> extends GenericQuickPickStep { - protected override isDirectPick(node: TNode): boolean { +export class ContextValueQuickPickStep extends GenericQuickPickStep { + protected override isDirectPick(node: TreeItem): boolean { const includeOption = this.pickOptions.contextValueFilter.include; const excludeOption = this.pickOptions.contextValueFilter.exclude; @@ -20,14 +22,15 @@ export class ContextValueQuickPickStep this.matchesSingleFilter(i, nodeContextValues)) && !excludeArray.some(e => this.matchesSingleFilter(e, nodeContextValues)); } - protected override isIndirectPick(node: TNode): boolean { - return node.quickPickOptions.isLeaf === false; + protected override isIndirectPick(node: TreeItem): boolean { + // `TreeItemCollapsibleState.None` and `undefined` are both falsy, and indicate that a `TreeItem` cannot have children--and therefore, cannot be an indirect pick + return !node.collapsibleState; } private matchesSingleFilter(matcher: string | RegExp, nodeContextValues: string[]): boolean { diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts index 66bf49be77..ea7c8f675d 100644 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts @@ -7,15 +7,14 @@ import * as types from '../../../index'; import * as vscode from 'vscode'; import { getLastNode } from './QuickPickWizardContext'; import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from './GenericQuickPickStep'; -import { isAzExtParentTreeItem } from '../../tree/isAzExtParentTreeItem'; interface FindByIdQuickPickOptions extends SkipIfOneQuickPickOptions { id: string; skipIfOne?: true; } -export class FindByIdQuickPickStep> extends GenericQuickPickStep { - public constructor(tdp: vscode.TreeDataProvider, options: FindByIdQuickPickOptions) { +export class FindByIdQuickPickStep extends GenericQuickPickStep { + public constructor(tdp: vscode.TreeDataProvider, options: FindByIdQuickPickOptions) { super( tdp, { @@ -35,7 +34,7 @@ export class FindByIdQuickPickStep, TOptions extends GenericQuickPickOptions> extends AzureWizardPromptStep { +export abstract class GenericQuickPickStep extends AzureWizardPromptStep { public readonly supportsDuplicateSteps = true; public constructor( - protected readonly treeDataProvider: vscode.TreeDataProvider, + protected readonly treeDataProvider: vscode.TreeDataProvider, protected readonly pickOptions: TOptions ) { super(); @@ -49,7 +49,7 @@ export abstract class GenericQuickPickStep { + protected async promptInternal(wizardContext: TContext): Promise { const picks = await this.getPicks(wizardContext); if (picks.length === 1 && this.pickOptions.skipIfOne) { @@ -64,16 +64,18 @@ export abstract class GenericQuickPickStep[]> { - const lastPickedItem: TNode | undefined = getLastNode(wizardContext); + protected async getPicks(wizardContext: TContext): Promise[]> { + const lastPickedItem: unknown | undefined = getLastNode(wizardContext); // TODO: if `lastPickedItem` is an `AzExtParentTreeItem`, should we clear its cache? - const children = (await this.treeDataProvider.getChildren(lastPickedItem)) || []; + const childNodes = (await this.treeDataProvider.getChildren(lastPickedItem)) || []; + const childItems = await Promise.all(childNodes.map(async (childElement: unknown) => await this.treeDataProvider.getTreeItem(childElement))); + const childPairs: [unknown, vscode.TreeItem][] = childNodes.map((childElement: unknown, i: number) => [childElement, childItems[i]]); - const directChoices = children.filter(c => this.isDirectPick(c)); - const indirectChoices = children.filter(c => this.isIndirectPick(c)); + const directChoices = childPairs.filter(([, ti]) => this.isDirectPick(ti)); + const indirectChoices = childPairs.filter(([, ti]) => this.isIndirectPick(ti)); - let promptChoices: TNode[]; + let promptChoices: [unknown, vscode.TreeItem][]; if (directChoices.length === 0) { if (indirectChoices.length === 0) { throw new NoResourceFoundError(); @@ -84,9 +86,9 @@ export abstract class GenericQuickPickStep[] = []; + const picks: types.IAzureQuickPickItem[] = []; for (const choice of promptChoices) { - picks.push(await this.getQuickPickItem(choice)); + picks.push(await this.getQuickPickItem(...choice)); } return picks; @@ -96,21 +98,19 @@ export abstract class GenericQuickPickStep> { - const treeItem = await Promise.resolve(this.treeDataProvider.getTreeItem(resource)); + protected abstract isIndirectPick(node: vscode.TreeItem): boolean; + private async getQuickPickItem(node: unknown, item: vscode.TreeItem): Promise> { return { - label: ((treeItem.label as vscode.TreeItemLabel)?.label || treeItem.label) as string, - description: treeItem.description as string, - data: resource, + label: ((item.label as vscode.TreeItemLabel)?.label || item.label) as string, + description: item.description as string, + data: node, }; } } diff --git a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts index ffd6eb9a8e..c11fb76cee 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts +++ b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts @@ -9,18 +9,18 @@ import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from '. import { localize } from '../../localize'; import { NoResourceFoundError } from '../../errors'; -type CreateCallback = () => TNode | Promise; +type CreateCallback = () => TNode | Promise; interface CreateQuickPickOptions extends ContextValueFilterQuickPickOptions { skipIfOne?: never; // Not allowed in CreateQuickPickStep createLabel?: string; createCallback: CreateCallback; } -export class CreateQuickPickStep> extends ContextValueQuickPickStep { +export class CreateQuickPickStep extends ContextValueQuickPickStep { public override async prompt(wizardContext: TContext): Promise { await super.prompt(wizardContext); - const lastNode = getLastNode(wizardContext) as (TNode | CreateCallback); + const lastNode = getLastNode(wizardContext); if (typeof lastNode === 'function') { // If the last node is a function, pop it off the list and execute it const callback = wizardContext.pickedNodes.pop() as unknown as CreateCallback; @@ -28,8 +28,8 @@ export class CreateQuickPickStep[]> { - const picks: types.IAzureQuickPickItem[] = []; + protected override async getPicks(wizardContext: TContext): Promise[]> { + const picks: types.IAzureQuickPickItem[] = []; try { picks.push(...await super.getPicks(wizardContext)); } catch (error) { @@ -41,7 +41,7 @@ export class CreateQuickPickStep[]; + return picks as types.IAzureQuickPickItem[]; } private getCreatePick(): types.IAzureQuickPickItem { diff --git a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts index 30a24b0725..18e25a26a0 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts +++ b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts @@ -5,9 +5,9 @@ import * as types from '../../../index'; -export function getLastNode(context: types.QuickPickWizardContext): TNode | undefined { +export function getLastNode(context: types.QuickPickWizardContext): TNode | undefined { if (context.pickedNodes.length) { - return context.pickedNodes[context.pickedNodes.length - 1]; + return context.pickedNodes[context.pickedNodes.length - 1] as TNode; } return undefined; diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index 91ac8f76af..aab980241b 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -7,7 +7,7 @@ import * as types from '../../../index'; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; import { getLastNode } from './QuickPickWizardContext'; -export class RecursiveQuickPickStep> extends ContextValueQuickPickStep { +export class RecursiveQuickPickStep extends ContextValueQuickPickStep { public async getSubWizard(wizardContext: TContext): Promise | undefined> { const lastPickedItem = getLastNode(wizardContext); @@ -16,7 +16,7 @@ export class RecursiveQuickPickStep, TOptions extends ContextValueFilterQuickPickOptions> extends ContextValueQuickPickStep { +export class CompatibilityContextValueQuickPickStep extends ContextValueQuickPickStep { public override async prompt(wizardContext: TContext): Promise { await this.provideCompatabilityWithPickTreeItemImpl(wizardContext) || await super.prompt(wizardContext); diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts index 6d97bddcd3..8d165033e9 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -9,6 +9,9 @@ import { CompatibilityContextValueQuickPickStep } from './CompatibilityContextVa import { localize } from "../../../localize"; import { NoResourceFoundError, UserCancelledError } from "../../../errors"; import type { ContextValueFilterQuickPickOptions } from "../ContextValueQuickPickStep"; +import { AzExtTreeItem, isAzExtTreeItem } from "../../../tree/AzExtTreeItem"; +import { isAzExtParentTreeItem } from "../../../tree/isAzExtParentTreeItem"; +import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; export interface CompatibilityRecursiveQuickPickOptions extends ContextValueFilterQuickPickOptions { create?: types.CreateOptions; @@ -17,10 +20,10 @@ export interface CompatibilityRecursiveQuickPickOptions extends ContextValueFilt /** * Recursive step which is compatible which adds create picks based if the node has {@link types.CompatibleQuickPickOptions.createChild quickPickOptions.createChild} defined. */ -export class CompatibilityRecursiveQuickPickStep> extends CompatibilityContextValueQuickPickStep { +export class CompatibilityRecursiveQuickPickStep extends CompatibilityContextValueQuickPickStep { - protected override async promptInternal(wizardContext: TContext): Promise { - const picks = await this.getPicks(wizardContext) as types.IAzureQuickPickItem[]; + protected override async promptInternal(wizardContext: TContext): Promise { + const picks = await this.getPicks(wizardContext) as types.IAzureQuickPickItem[]; if (picks.length === 1 && this.pickOptions.skipIfOne) { return picks[0].data; @@ -33,7 +36,7 @@ export class CompatibilityRecursiveQuickPickStep; + const callback = selected.data as unknown as types.CreateCallback; // context passed to callback must have the same `ui` as the wizardContext // to prevent the wizard from being cancelled unexpectedly @@ -58,27 +61,34 @@ export class CompatibilityRecursiveQuickPickStep() : lastPickedItem; // Need to keep going because the last picked node is not a match return { hideStepCount: true, promptSteps: [ new CompatibilityRecursiveQuickPickStep(this.treeDataProvider, { ...this.pickOptions, - skipIfOne: !lastPickedItem.quickPickOptions.createChild, - create: lastPickedItem.quickPickOptions.createChild, + skipIfOne: isAzExtParentTreeItem(lastPickedItemTi) && !!lastPickedItemTi.createChildImpl, + create: (isAzExtParentTreeItem(lastPickedItemTi) && !!lastPickedItemTi.createChildImpl) ? { + callback: lastPickedItemTi.createChild.bind(lastPickedItemTi) as typeof lastPickedItemTi.createChild, + label: lastPickedItemTi.createNewLabel ?? localize('createNewItem', '$(plus) Create new {0}', lastPickedItemTi.childTypeLabel) + } : undefined }) ], }; } } - protected override async getPicks(wizardContext: TContext): Promise[]> { - const picks: types.IAzureQuickPickItem[] = []; + protected override async getPicks(wizardContext: TContext): Promise[]> { + const picks: types.IAzureQuickPickItem[] = []; try { picks.push(...await super.getPicks(wizardContext)); } catch (error) { @@ -93,7 +103,7 @@ export class CompatibilityRecursiveQuickPickStep[]; + return picks as types.IAzureQuickPickItem[]; } private getCreatePick(options: types.CreateOptions): types.IAzureQuickPickItem { diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts index 979d2440b6..387372ea9c 100644 --- a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -14,8 +14,9 @@ import { NoResourceFoundError } from '../../../errors'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzExtResourceType } from '../../../AzExtResourceType'; import { AzureWizard } from '../../../wizard/AzureWizard'; -import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { AzureResourceQuickPickWizardContext } from '../../../../hostapi.v2'; import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; +import { ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; export async function appResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, resourceTypes?: AzExtResourceType | AzExtResourceType[], childItemFilter?: types.ContextValueFilter): Promise { const promptSteps: AzureWizardPromptStep[] = [ @@ -34,7 +35,7 @@ export async function appResourceExperience(context: types.IActionContext ]; if (childItemFilter) { - promptSteps.push(new RecursiveQuickPickStep(tdp, { + promptSteps.push(new RecursiveQuickPickStep(tdp, { contextValueFilter: childItemFilter, skipIfOne: false, })); diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts index ee72be1b8c..aa481a6968 100644 --- a/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts @@ -12,9 +12,10 @@ import { NoResourceFoundError } from '../../../errors'; import * as types from '../../../../index'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzureWizard } from '../../../wizard/AzureWizard'; -import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { AzureResourceQuickPickWizardContext } from '../../../../hostapi.v2'; import { CompatibilityRecursiveQuickPickStep } from '../compatibility/CompatibilityRecursiveQuickPickStep'; import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; +import { ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; /** * Provides compatibility for the legacy `pickAppResource` Resource Groups API @@ -30,11 +31,12 @@ export async function compatibilityPickAppResourceExperience(tdp, { + promptSteps.push(new CompatibilityRecursiveQuickPickStep(tdp, { contextValueFilter: childItemFilter, skipIfOne: false, })); diff --git a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts index 1fce6567a2..808d406e30 100644 --- a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts @@ -11,9 +11,10 @@ import { NoResourceFoundError } from '../../../errors'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzureWizard } from '../../../wizard/AzureWizard'; import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; +import { ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; -export async function contextValueExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: types.ContextValueFilter): Promise { - const promptSteps: AzureWizardPromptStep>[] = [ +export async function contextValueExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: types.ContextValueFilter): Promise { + const promptSteps: AzureWizardPromptStep[] = [ new RecursiveQuickPickStep(tdp, { contextValueFilter: contextValueFilter, skipIfOne: false, @@ -21,7 +22,7 @@ export async function contextValueExperience; + const wizardContext = context as types.QuickPickWizardContext; wizardContext.pickedNodes = []; const wizard = new AzureWizard(context, { diff --git a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts index 8aeca71483..281b727676 100644 --- a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts @@ -10,16 +10,17 @@ import { NoResourceFoundError } from '../../../errors'; import { FindByIdQuickPickStep } from '../FindByIdQuickPickStep'; import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; import { AzureWizard } from '../../../wizard/AzureWizard'; +import { ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; -export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string): Promise { - const promptSteps: types.AzureWizardPromptStep>[] = [ +export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string): Promise { + const promptSteps: types.AzureWizardPromptStep[] = [ new FindByIdQuickPickStep(tdp, { id: id, }), ]; // Fill in the `pickedNodes` property - const wizardContext = context as types.QuickPickWizardContext; + const wizardContext = context as types.QuickPickWizardContext; wizardContext.pickedNodes = []; const wizard = new AzureWizard(context, { diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts index 1954232f2d..46bff0d2bc 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAppResourceStep.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { TreeItem } from 'vscode'; +import { AzureResourceQuickPickWizardContext } from '../../../../hostapi.v2'; import * as types from '../../../../index'; +import { parseContextValue } from '../../../utils/contextUtils'; import { GenericQuickPickOptions, GenericQuickPickStep } from '../GenericQuickPickStep'; import { AppResourceItem } from './tempTypes'; @@ -13,9 +15,9 @@ interface AppResourceQuickPickOptions extends GenericQuickPickOptions { childItemFilter?: types.ContextValueFilter; } -export class QuickPickAppResourceStep extends GenericQuickPickStep { +export class QuickPickAppResourceStep extends GenericQuickPickStep { protected override async promptInternal(wizardContext: AzureResourceQuickPickWizardContext): Promise { - const pickedAppResource = await super.promptInternal(wizardContext) as AppResourceItem; + const pickedAppResource = (await super.promptInternal(wizardContext)) as unknown as AppResourceItem; // TODO wizardContext.resource = pickedAppResource.resource; @@ -24,29 +26,33 @@ export class QuickPickAppResourceStep extends GenericQuickPickStep contextValues.includes(type)); } - protected isIndirectPick(node: AppResourceItem): boolean { + protected isIndirectPick(node: TreeItem): boolean { // If childItemFilter is undefined, this cannot be an indirect pick if (!this.pickOptions.childItemFilter) { return false; } - if (!node.resource.azExtResourceType) { + const contextValues = parseContextValue(node.contextValue); + + if (!contextValues.includes('azureResource')) { return false; } - return !this.pickOptions.resourceTypes || this.pickOptions.resourceTypes.includes(node.resource.azExtResourceType); + return !this.pickOptions.resourceTypes || this.pickOptions.resourceTypes.some((type) => contextValues.includes(type)); } } diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts index 7a73bde70e..8869318584 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts @@ -3,22 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from "../../../../hostapi.v2"; +import { TreeItem } from "vscode"; +import { AzureResourceQuickPickWizardContext } from "../../../../hostapi.v2"; import * as types from "../../../../index"; import { GenericQuickPickOptions, GenericQuickPickStep } from "../GenericQuickPickStep"; // TODO: implement this for picking resource group // The resource group may NOT be the grouping method used in the tree -export class QuickPickAzureResourceGroupStep extends GenericQuickPickStep { - protected override getPicks(_wizardContext: AzureResourceQuickPickWizardContext): Promise[]> { +export class QuickPickAzureResourceGroupStep extends GenericQuickPickStep { + protected override getPicks(_wizardContext: AzureResourceQuickPickWizardContext): Promise[]> { throw new Error("Method not implemented."); } - protected isDirectPick(_node: types.ContextValueFilterableTreeNode): boolean { + protected isDirectPick(_node: TreeItem): boolean { throw new Error("Method not implemented."); } - protected isIndirectPick(_node: types.ContextValueFilterableTreeNode): boolean { + protected isIndirectPick(_node: TreeItem): boolean { throw new Error("Method not implemented."); } } diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts index 397c30347e..8d78d7dd63 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { AzureResourceQuickPickWizardContext, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { AzureResourceQuickPickWizardContext } from '../../../../hostapi.v2'; import { GenericQuickPickOptions, GenericQuickPickStep, SkipIfOneQuickPickOptions } from '../GenericQuickPickStep'; -import { SubscriptionItem } from './tempTypes'; +import { ResourceGroupsItem, SubscriptionItem } from './tempTypes'; -export class QuickPickAzureSubscriptionStep extends GenericQuickPickStep { +export class QuickPickAzureSubscriptionStep extends GenericQuickPickStep { public constructor(tdp: vscode.TreeDataProvider, options?: GenericQuickPickOptions) { super( tdp, @@ -28,12 +28,12 @@ export class QuickPickAzureSubscriptionStep extends GenericQuickPickStep { - public constructor(tdp: vscode.TreeDataProvider, options: GroupQuickPickOptions) { +export class QuickPickGroupStep extends GenericQuickPickStep { + public constructor(tdp: vscode.TreeDataProvider, options: GroupQuickPickOptions) { super( tdp, { @@ -25,12 +25,16 @@ export class QuickPickGroupStep extends GenericQuickPickStep contextValues.includes(groupType)); } } diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts index 16191ea3ee..8d0701cda8 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/tempTypes.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ApplicationResource, ApplicationSubscription, ResourceGroupsItem } from '../../../../hostapi.v2'; +import { ApplicationResource, ApplicationSubscription } from '../../../../hostapi.v2'; import * as types from '../../../../index'; // TODO: THIS FILE IS TEMPORARY // @@ -22,3 +22,5 @@ export type GroupingItem = ResourceGroupsItem & { export type AppResourceItem = ResourceGroupsItem & { resource: ApplicationResource; }; + +export type ResourceGroupsItem = unknown; diff --git a/utils/src/utils/contextUtils.ts b/utils/src/utils/contextUtils.ts index 2082841e14..109255d98c 100644 --- a/utils/src/utils/contextUtils.ts +++ b/utils/src/utils/contextUtils.ts @@ -6,3 +6,7 @@ export function createContextValue(values: string[]): string { return Array.from(new Set(values)).sort().join(';'); } + +export function parseContextValue(contextValue?: string): string[] { + return contextValue?.split(';') ?? []; +} From 2c67a2511c282e59701a8588d696ebf7192f1a25 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 3 Oct 2022 10:19:39 -0700 Subject: [PATCH 15/49] Add `PickFilter` interface and refactor steps (#1240) --- .../ContextValueQuickPickStep.ts | 11 +++++++-- .../quickPickWizard/FindByIdQuickPickStep.ts | 24 +++++++++++-------- .../quickPickWizard/GenericQuickPickStep.ts | 19 ++++----------- .../quickPickWizard/RecursiveQuickPickStep.ts | 2 +- .../quickPickWizard/common/PickFilter.ts | 20 ++++++++++++++++ .../CompatibilityRecursiveQuickPickStep.ts | 2 +- .../QuickPickAppResourceStep.ts | 24 +++++++++++-------- .../QuickPickAzureResourceGroupStep.ts | 9 ++----- .../QuickPickAzureSubscriptionStep.ts | 20 +++++++++------- .../QuickPickGroupStep.ts | 22 ++++++++++------- 10 files changed, 90 insertions(+), 63 deletions(-) create mode 100644 utils/src/treev2/quickPickWizard/common/PickFilter.ts diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index 0cc463ce48..e4d4039a66 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -6,6 +6,7 @@ import { TreeItem } from 'vscode'; import * as types from '../../../index'; import { parseContextValue } from '../../utils/contextUtils'; +import { PickFilter } from './common/PickFilter'; import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep'; export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions { @@ -13,7 +14,13 @@ export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOpti } export class ContextValueQuickPickStep extends GenericQuickPickStep { - protected override isDirectPick(node: TreeItem): boolean { + protected readonly pickFilter: PickFilter = new ContextValuePickFilter(this.pickOptions); +} + +class ContextValuePickFilter implements PickFilter { + constructor(private readonly pickOptions: ContextValueFilterQuickPickOptions) { } + + isDirectPick(node: TreeItem): boolean { const includeOption = this.pickOptions.contextValueFilter.include; const excludeOption = this.pickOptions.contextValueFilter.exclude; @@ -28,7 +35,7 @@ export class ContextValueQuickPickStep this.matchesSingleFilter(e, nodeContextValues)); } - protected override isIndirectPick(node: TreeItem): boolean { + isIndirectPick(node: TreeItem): boolean { // `TreeItemCollapsibleState.None` and `undefined` are both falsy, and indicate that a `TreeItem` cannot have children--and therefore, cannot be an indirect pick return !node.collapsibleState; } diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts index ea7c8f675d..acc317e1c6 100644 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts @@ -7,6 +7,7 @@ import * as types from '../../../index'; import * as vscode from 'vscode'; import { getLastNode } from './QuickPickWizardContext'; import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from './GenericQuickPickStep'; +import { PickFilter } from './common/PickFilter'; interface FindByIdQuickPickOptions extends SkipIfOneQuickPickOptions { id: string; @@ -15,15 +16,14 @@ interface FindByIdQuickPickOptions extends SkipIfOneQuickPickOptions { export class FindByIdQuickPickStep extends GenericQuickPickStep { public constructor(tdp: vscode.TreeDataProvider, options: FindByIdQuickPickOptions) { - super( - tdp, - { - ...options, - skipIfOne: true, // Find-by-id is always skip-if-one - } - ); + super(tdp, { + ...options, + skipIfOne: true, // Find-by-id is always skip-if-one + }); } + protected readonly pickFilter: PickFilter = new FindByIdPickFilter(this.pickOptions); + public async getSubWizard(wizardContext: TContext): Promise | undefined> { // TODO: this code is nearly identical to `RecursiveQuickPickStep`, but this class can't inherit from it because it's // not at all based on context value for filtering @@ -34,7 +34,7 @@ export class FindByIdQuickPickStep extends AzureWizardPromptStep { public readonly supportsDuplicateSteps = true; + protected readonly abstract pickFilter: PickFilter; + public constructor( protected readonly treeDataProvider: vscode.TreeDataProvider, protected readonly pickOptions: TOptions @@ -72,8 +75,8 @@ export abstract class GenericQuickPickStep await this.treeDataProvider.getTreeItem(childElement))); const childPairs: [unknown, vscode.TreeItem][] = childNodes.map((childElement: unknown, i: number) => [childElement, childItems[i]]); - const directChoices = childPairs.filter(([, ti]) => this.isDirectPick(ti)); - const indirectChoices = childPairs.filter(([, ti]) => this.isIndirectPick(ti)); + const directChoices = childPairs.filter(([, ti]) => this.pickFilter.isDirectPick(ti)); + const indirectChoices = childPairs.filter(([, ti]) => this.pickFilter.isIndirectPick(ti)); let promptChoices: [unknown, vscode.TreeItem][]; if (directChoices.length === 0) { @@ -94,18 +97,6 @@ export abstract class GenericQuickPickStep> { return { label: ((item.label as vscode.TreeItemLabel)?.label || item.label) as string, diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index aab980241b..99e2f58ea1 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -16,7 +16,7 @@ export class RecursiveQuickPickStep { + /** + * Filters for nodes that match the final target. + * @param node The node to apply the filter to + */ + isDirectPick(node: TPick): boolean; + + /** + * Filters for nodes that could have a descendant matching the final target. + * @param node The node to apply the filter to + */ + isIndirectPick(node: TPick): boolean; +} diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts index 8d165033e9..bcd4bacaa8 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -64,7 +64,7 @@ export class CompatibilityRecursiveQuickPickStep contextValues.includes(type)); + return this.matchesResourceType(parseContextValue(node.contextValue)); } - protected isIndirectPick(node: TreeItem): boolean { + isIndirectPick(node: TreeItem): boolean { // If childItemFilter is undefined, this cannot be an indirect pick if (!this.pickOptions.childItemFilter) { return false; } - const contextValues = parseContextValue(node.contextValue); + return this.matchesResourceType(parseContextValue(node.contextValue)); + } + private matchesResourceType(contextValues: string[]): boolean { if (!contextValues.includes('azureResource')) { return false; } diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts index 8869318584..9f5246ec5b 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts @@ -6,6 +6,7 @@ import { TreeItem } from "vscode"; import { AzureResourceQuickPickWizardContext } from "../../../../hostapi.v2"; import * as types from "../../../../index"; +import { PickFilter } from "../common/PickFilter"; import { GenericQuickPickOptions, GenericQuickPickStep } from "../GenericQuickPickStep"; // TODO: implement this for picking resource group @@ -15,11 +16,5 @@ export class QuickPickAzureResourceGroupStep extends GenericQuickPickStep; } diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts index 8d78d7dd63..6d139b8aa7 100644 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts +++ b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureSubscriptionStep.ts @@ -5,20 +5,20 @@ import * as vscode from 'vscode'; import { AzureResourceQuickPickWizardContext } from '../../../../hostapi.v2'; +import { PickFilter } from '../common/PickFilter'; import { GenericQuickPickOptions, GenericQuickPickStep, SkipIfOneQuickPickOptions } from '../GenericQuickPickStep'; import { ResourceGroupsItem, SubscriptionItem } from './tempTypes'; export class QuickPickAzureSubscriptionStep extends GenericQuickPickStep { public constructor(tdp: vscode.TreeDataProvider, options?: GenericQuickPickOptions) { - super( - tdp, - { - ...options, - skipIfOne: true, // Subscription is always skip-if-one - } - ) + super(tdp, { + ...options, + skipIfOne: true, // Subscription is always skip-if-one + }); } + protected readonly pickFilter = new AzureSubscriptionPickFilter(); + protected override async promptInternal(wizardContext: AzureResourceQuickPickWizardContext): Promise { const pickedSubscription = await super.promptInternal(wizardContext) as SubscriptionItem; @@ -27,13 +27,15 @@ export class QuickPickAzureSubscriptionStep extends GenericQuickPickStep { public constructor(tdp: vscode.TreeDataProvider, options: GroupQuickPickOptions) { - super( - tdp, - { - ...options, - skipIfOne: true, // Group is always skip-if-one - } - ); + super(tdp, { + ...options, + skipIfOne: true, // Group is always skip-if-one + }); } - protected isDirectPick(_node: vscode.TreeItem): boolean { + protected readonly pickFilter: PickFilter = new GroupPickFilter(this.pickOptions); +} + +class GroupPickFilter implements PickFilter { + constructor(private readonly pickOptions: GroupQuickPickOptions) { } + + isDirectPick(_node: vscode.TreeItem): boolean { // Group is never a direct pick return false; } - protected isIndirectPick(node: vscode.TreeItem): boolean { + isIndirectPick(node: vscode.TreeItem): boolean { const contextValues = parseContextValue(node.contextValue); return !this.pickOptions.groupType || From 904f0a2075d91507c8e31dde09c41097143943bb Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Thu, 6 Oct 2022 12:06:29 -0700 Subject: [PATCH 16/49] Fix go back behavior (#1241) --- .../quickPickWizard/GenericQuickPickStep.ts | 26 +++++++------------ utils/src/wizard/AzureWizard.ts | 1 + utils/src/wizard/AzureWizardPromptStep.ts | 1 + 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts index a974ac6917..89a71df1c3 100644 --- a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts @@ -7,9 +7,8 @@ import * as types from '../../../index'; import * as vscode from 'vscode'; import { getLastNode } from './QuickPickWizardContext'; import { AzureWizardPromptStep } from '../../wizard/AzureWizardPromptStep'; -import { NoResourceFoundError } from '../../errors'; -import { parseError } from '../../parseError'; import { PickFilter } from './common/PickFilter'; +import { localize } from '../../localize'; export interface GenericQuickPickOptions { skipIfOne?: boolean; @@ -32,20 +31,12 @@ export abstract class GenericQuickPickStep { - try { - const pick = await this.promptInternal(wizardContext); - wizardContext.pickedNodes.push(pick); - } catch (err) { - const error = parseError(err); - if (error.errorType === 'GoBackError') { - // Instead of wiping out a property value, which is the default wizard behavior for `GoBackError`, pop the most recent - // value off from the provenance of the picks - wizardContext.pickedNodes.pop(); - } + const pick = await this.promptInternal(wizardContext); + wizardContext.pickedNodes.push(pick); + } - // And rethrow - throw err; - } + public undo(wizardContext: TContext): void { + wizardContext.pickedNodes.pop(); } public shouldPrompt(_wizardContext: TContext): boolean { @@ -59,6 +50,7 @@ export abstract class GenericQuickPickStep this.pickFilter.isDirectPick(ti)); const indirectChoices = childPairs.filter(([, ti]) => this.pickFilter.isIndirectPick(ti)); - let promptChoices: [unknown, vscode.TreeItem][]; + let promptChoices: [unknown, vscode.TreeItem][] = []; if (directChoices.length === 0) { if (indirectChoices.length === 0) { - throw new NoResourceFoundError(); + // Don't throw and end the wizard, let user use back button instead } else { promptChoices = indirectChoices; } diff --git a/utils/src/wizard/AzureWizard.ts b/utils/src/wizard/AzureWizard.ts index f862c18069..c563880627 100644 --- a/utils/src/wizard/AzureWizard.ts +++ b/utils/src/wizard/AzureWizard.ts @@ -221,6 +221,7 @@ export class AzureWizard impl public abstract prompt(wizardContext: T): Promise; public getSubWizard?(wizardContext: T): Promise | undefined>; + public undo?(wizardContext: T): void; public abstract shouldPrompt(wizardContext: T): boolean; From a159f8cfc83881450069a7b3e773f532a3277ade Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Thu, 13 Oct 2022 10:36:57 -0700 Subject: [PATCH 17/49] Add compatibility pick subscription experience (#1245) --- utils/hostapi.v2.d.ts | 10 +++- utils/index.d.ts | 1 + utils/src/index.ts | 1 + ...compatibilityPickSubscriptionExperience.ts | 19 +++++++ .../experiences/subscriptionExperience.ts | 32 ++++++++++++ utils/src/utils/credentialUtils.ts | 49 +++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts create mode 100644 utils/src/treev2/quickPickWizard/experiences/subscriptionExperience.ts create mode 100644 utils/src/utils/credentialUtils.ts diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index 8b1a49c5b0..a62ae929b8 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -6,6 +6,7 @@ import type { IActionContext, AzExtResourceType, QuickPickWizardContext } from "./index"; import * as vscode from 'vscode'; import type { Environment } from '@azure/ms-rest-azure-env'; +import { AzureExtensionApi } from "./api"; export declare interface ApplicationAuthentication { getSession(scopes?: string[]): vscode.ProviderResult; @@ -14,10 +15,12 @@ export declare interface ApplicationAuthentication { /** * Information specific to the Subscription */ -export declare interface ApplicationSubscription { +export interface ApplicationSubscription { readonly authentication: ApplicationAuthentication; readonly displayName: string; readonly subscriptionId: string; + readonly subscriptionPath: string; + readonly tenantId: string; readonly environment: Environment; readonly isCustomCloud: boolean; } @@ -54,8 +57,11 @@ export declare interface ApplicationResource extends ResourceBase { */ export declare type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; -export declare interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext { +export declare interface PickSubscriptionWizardContext extends QuickPickWizardContext { subscription?: ApplicationSubscription; +} + +export declare interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext, PickSubscriptionWizardContext { resource?: ApplicationResource; resourceGroup?: string; } diff --git a/utils/index.d.ts b/utils/index.d.ts index f8ad075a89..d673303ac5 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -1719,6 +1719,7 @@ interface CompatibilityPickResourceExperienceOptions { } export declare function compatibilityPickAppResourceExperience(context: IActionContext, tdp: TreeDataProvider, options: CompatibilityPickResourceExperienceOptions): Promise; +export declare function compatibilitySubscriptionExperience(context: IActionContext, tdp: TreeDataProvider): Promise; export declare interface QuickPickWizardContext extends IActionContext { pickedNodes: unknown[]; diff --git a/utils/src/index.ts b/utils/src/index.ts index e20ead7c9b..d551b091f2 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -41,5 +41,6 @@ export * from './treev2/quickPickWizard/experiences/appResourceExperience'; export * from './treev2/quickPickWizard/experiences/compatibilityPickResourceExperience'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; export * from './treev2/quickPickWizard/experiences/findByIdExperience'; +export * from './treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience'; export * from './tree/isAzExtParentTreeItem'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts new file mode 100644 index 0000000000..a09893028b --- /dev/null +++ b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../../index'; +import * as vscode from 'vscode'; +import { ISubscriptionContext } from '@microsoft/vscode-azext-dev'; +import { subscriptionExperience } from '../subscriptionExperience'; +import { createSubscriptionContext } from '../../../../utils/credentialUtils'; +import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; + +/** + * Returns `ISubscriptionContext` instead of `ApplicationSubscription` for compatibility. + */ +export async function compatibilitySubscriptionExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider): Promise { + const applicationSubscription = await subscriptionExperience(context, tdp); + return createSubscriptionContext(applicationSubscription); +} diff --git a/utils/src/treev2/quickPickWizard/experiences/subscriptionExperience.ts b/utils/src/treev2/quickPickWizard/experiences/subscriptionExperience.ts new file mode 100644 index 0000000000..2d4f8f3cde --- /dev/null +++ b/utils/src/treev2/quickPickWizard/experiences/subscriptionExperience.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import * as types from '../../../../index'; +import * as vscode from 'vscode'; +import { AzureWizard } from '../../../wizard/AzureWizard'; +import { QuickPickAzureSubscriptionStep } from '../quickPickAzureResource/QuickPickAzureSubscriptionStep'; +import { ApplicationSubscription, PickSubscriptionWizardContext } from '../../../../hostapi.v2'; +import { ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; +import { NoResourceFoundError } from '../../../errors'; + +export async function subscriptionExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider): Promise { + + const wizardContext = context as PickSubscriptionWizardContext; + wizardContext.pickedNodes = []; + + const wizard = new AzureWizard(wizardContext, { + hideStepCount: true, + promptSteps: [new QuickPickAzureSubscriptionStep(tdp)], + showLoadingPrompt: true, + }); + + await wizard.prompt(); + + if (!wizardContext.subscription) { + throw new NoResourceFoundError(wizardContext); + } else { + return wizardContext.subscription; + } +} diff --git a/utils/src/utils/credentialUtils.ts b/utils/src/utils/credentialUtils.ts new file mode 100644 index 0000000000..6bdd0646f9 --- /dev/null +++ b/utils/src/utils/credentialUtils.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ApplicationSubscription } from '../../hostapi.v2'; +import { AzExtServiceClientCredentials, ISubscriptionContext } from '../../index'; +import { localize } from '../localize'; + +/** + * Converts a VS Code authentication session to an Azure Track 1 & 2 compatible compatible credential. + */ +export function createCredential(getSession: (scopes?: string[]) => vscode.ProviderResult): AzExtServiceClientCredentials { + return { + getToken: async (scopes?: string | string[]) => { + if (typeof scopes === 'string') { + scopes = [scopes]; + } + + const session = await getSession(scopes); + + if (session) { + return { + token: session.accessToken + }; + } else { + return null; + } + }, + signRequest: async () => { + throw new Error((localize('signRequestError', 'Track 1 credentials are not (currently) supported.'))); + } + }; +} + +/** + * Creates a subscription context from an application subscription. + * + * TODO: expose these utils and remove duplicate code in resource groups + client extensions + */ +export function createSubscriptionContext(subscription: ApplicationSubscription): ISubscriptionContext { + return { + subscriptionDisplayName: subscription.displayName, + userId: '', // TODO + ...subscription, + credentials: createCredential(subscription.authentication.getSession) + }; +} From 086856a4bf4a296d0b3701d4f9e73ae048837b15 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 17 Oct 2022 13:33:29 -0700 Subject: [PATCH 18/49] Add `unwrapArgs` util (#1246) --- utils/index.d.ts | 2 ++ utils/src/registerCommandWithTreeNodeUnwrapping.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/utils/index.d.ts b/utils/index.d.ts index d673303ac5..f352386ce4 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -1765,3 +1765,5 @@ export declare type FindableByIdTreeNode = FindableByIdTreeNodeV2 | AzExtTreeIte * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. */ export declare function registerCommandWithTreeNodeUnwrapping(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; + +export declare function unwrapArgs(treeNodeCallback: TreeNodeCommandCallback): TreeNodeCommandCallback; diff --git a/utils/src/registerCommandWithTreeNodeUnwrapping.ts b/utils/src/registerCommandWithTreeNodeUnwrapping.ts index 75d6985e9b..8a559ff5e3 100644 --- a/utils/src/registerCommandWithTreeNodeUnwrapping.ts +++ b/utils/src/registerCommandWithTreeNodeUnwrapping.ts @@ -3,12 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { CommandCallback, IActionContext, Wrapper } from '../index'; +import type { IActionContext, Wrapper } from '../index'; import type { TreeNodeCommandCallback } from '../hostapi.v2'; import { registerCommand } from './registerCommand'; export function registerCommandWithTreeNodeUnwrapping(commandId: string, treeNodeCallback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void { - const unwrappingCallback: CommandCallback = async (context: IActionContext, ...args: unknown[]) => { + registerCommand(commandId, unwrapArgs(treeNodeCallback), debounce, telemetryId); +} + +export function unwrapArgs(treeNodeCallback: TreeNodeCommandCallback): TreeNodeCommandCallback { + return async (context: IActionContext, ...args: unknown[]) => { const maybeNodeWrapper = args?.[0]; const maybeNodeWrapperArray = args?.[1]; const remainingArgs = args.slice(2); @@ -35,11 +39,9 @@ export function registerCommandWithTreeNodeUnwrapping(commandId: string, tree nodes = maybeNodeWrapperArray as T[]; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return treeNodeCallback(context, node, nodes, ...remainingArgs); }; - - registerCommand(commandId, unwrappingCallback, debounce, telemetryId); } export function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper { From 06b63aca4c96585a705f30dc37256ae73e0d73f3 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 17 Oct 2022 13:37:52 -0700 Subject: [PATCH 19/49] Add minimal v2 api types --- utils/hostapi.v2.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index a62ae929b8..f0a843cfec 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -65,3 +65,8 @@ export declare interface AzureResourceQuickPickWizardContext extends QuickPickWi resource?: ApplicationResource; resourceGroup?: string; } + +export interface V2AzureResourcesApi extends AzureExtensionApi { + readonly applicationResourceTreeDataProvider: vscode.TreeDataProvider; + readonly workspaceResourceTreeDataProvider: vscode.TreeDataProvider; +} From df3e93c4953ad4cf92b91fe046f23f4ed214075a Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 18 Oct 2022 09:47:12 -0700 Subject: [PATCH 20/49] Add `getResourceGroupsApi` util (#1248) --- utils/hostapi.d.ts | 2 +- utils/hostapi.v2.d.ts | 1 + utils/index.d.ts | 13 ++++++++++++- utils/src/index.ts | 1 + utils/src/utils/apiUtils.ts | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 utils/src/utils/apiUtils.ts diff --git a/utils/hostapi.d.ts b/utils/hostapi.d.ts index fa6a73519a..a7a9167040 100644 --- a/utils/hostapi.d.ts +++ b/utils/hostapi.d.ts @@ -33,7 +33,7 @@ export interface AzureHostExtensionApi { /** * Version of the API */ - readonly apiVersion: string; + readonly apiVersion: '0.0.1'; /** * Reveals an item in the shared app resource tree diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index f0a843cfec..5cfe3e64c0 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -7,6 +7,7 @@ import type { IActionContext, AzExtResourceType, QuickPickWizardContext } from " import * as vscode from 'vscode'; import type { Environment } from '@azure/ms-rest-azure-env'; import { AzureExtensionApi } from "./api"; +import { ResourceGroupsItem } from "./src/treev2/quickPickWizard/quickPickAzureResource/tempTypes"; export declare interface ApplicationAuthentication { getSession(scopes?: string[]): vscode.ProviderResult; diff --git a/utils/index.d.ts b/utils/index.d.ts index f352386ce4..4853153f90 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -9,7 +9,7 @@ import type { Environment } from '@azure/ms-rest-azure-env'; import { CancellationToken, CancellationTokenSource, Disposable, Event, ExtensionContext, FileChangeEvent, FileChangeType, FileStat, FileSystemProvider, FileType, InputBoxOptions, MarkdownString, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, Progress, QuickPickItem, QuickPickOptions as VSCodeQuickPickOptions, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri } from 'vscode'; import { TargetPopulation } from 'vscode-tas-client'; import { AzureExtensionApi, AzureExtensionApiProvider } from './api'; -import type { Activity, ActivityTreeItemOptions, AppResource, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result +import type { Activity, ActivityTreeItemOptions, AppResource, AzureHostExtensionApi, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result import type { TreeNodeCommandCallback } from './hostapi.v2'; export declare interface RunWithTemporaryDescriptionOptions { @@ -1767,3 +1767,14 @@ export declare type FindableByIdTreeNode = FindableByIdTreeNodeV2 | AzExtTreeIte export declare function registerCommandWithTreeNodeUnwrapping(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; export declare function unwrapArgs(treeNodeCallback: TreeNodeCommandCallback): TreeNodeCommandCallback; + +/** + * Gets the Azure Resource Groups API + */ +export declare function getResourceGroupsApi(apiVersionRange: AzureHostExtensionApi['apiVersion']): Promise; +export declare function getResourceGroupsApi(apiVersionRange: string): Promise; + +/** + * Get exported API from an extension + */ +export declare function getApiExport(extensionId: string): Promise; diff --git a/utils/src/index.ts b/utils/src/index.ts index d551b091f2..17a8abe410 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -43,4 +43,5 @@ export * from './treev2/quickPickWizard/experiences/contextValueExperience'; export * from './treev2/quickPickWizard/experiences/findByIdExperience'; export * from './treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience'; export * from './tree/isAzExtParentTreeItem'; +export * from './utils/apiUtils'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/utils/apiUtils.ts b/utils/src/utils/apiUtils.ts new file mode 100644 index 0000000000..6031b2caa2 --- /dev/null +++ b/utils/src/utils/apiUtils.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Extension, extensions } from "vscode"; +import { localize } from "vscode-nls"; +import type { AzureExtensionApi, AzureExtensionApiProvider } from "../../api"; +import { AzureHostExtensionApi } from "../../hostapi"; + +export async function getResourceGroupsApi(apiVersionRange: AzureExtensionApi['apiVersion']): Promise; +export async function getResourceGroupsApi(apiVersionRange: string): Promise { + const rgApiProvider = await getApiExport('ms-azuretools.vscode-azureresourcegroups'); + if (rgApiProvider) { + return rgApiProvider.getApi(apiVersionRange); + } else { + throw new Error(localize('noResourceGroupExt', 'Could not find the Azure Resource Groups extension')); + } +} + +export async function getApiExport(extensionId: string): Promise { + const extension: Extension | undefined = extensions.getExtension(extensionId); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + + return extension.exports; + } + + return undefined; +} From 81217741d4d5fcd36a12b35bcdbfcff9a234ab76 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 18 Oct 2022 16:11:57 -0700 Subject: [PATCH 21/49] Fixup --- utils/hostapi.v2.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index 5cfe3e64c0..f0a843cfec 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -7,7 +7,6 @@ import type { IActionContext, AzExtResourceType, QuickPickWizardContext } from " import * as vscode from 'vscode'; import type { Environment } from '@azure/ms-rest-azure-env'; import { AzureExtensionApi } from "./api"; -import { ResourceGroupsItem } from "./src/treev2/quickPickWizard/quickPickAzureResource/tempTypes"; export declare interface ApplicationAuthentication { getSession(scopes?: string[]): vscode.ProviderResult; From cfd9468482828044cb58c616422042d46fa14532 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Wed, 19 Oct 2022 16:22:38 -0700 Subject: [PATCH 22/49] Fix `compatibilitySubscriptionExperience` --- .../compatibilityPickSubscriptionExperience.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts index a09893028b..b7139eddf2 100644 --- a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts @@ -9,11 +9,17 @@ import { ISubscriptionContext } from '@microsoft/vscode-azext-dev'; import { subscriptionExperience } from '../subscriptionExperience'; import { createSubscriptionContext } from '../../../../utils/credentialUtils'; import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; +import { isAzExtTreeItem } from '../../../../tree/AzExtTreeItem'; /** * Returns `ISubscriptionContext` instead of `ApplicationSubscription` for compatibility. */ export async function compatibilitySubscriptionExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider): Promise { const applicationSubscription = await subscriptionExperience(context, tdp); + + if (isAzExtTreeItem(applicationSubscription)) { + return applicationSubscription.subscription; + } + return createSubscriptionContext(applicationSubscription); } From 7db798f201f665c02c76356dc7a4565453c80c12 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Thu, 3 Nov 2022 15:47:56 -0700 Subject: [PATCH 23/49] Rename `PickFilter` methods --- .../src/treev2/quickPickWizard/ContextValueQuickPickStep.ts | 4 ++-- utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts | 6 +++--- utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts | 4 ++-- utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts | 2 +- utils/src/treev2/quickPickWizard/common/PickFilter.ts | 4 ++-- .../compatibility/CompatibilityRecursiveQuickPickStep.ts | 2 +- .../quickPickAzureResource/QuickPickAppResourceStep.ts | 4 ++-- .../QuickPickAzureSubscriptionStep.ts | 4 ++-- .../quickPickAzureResource/QuickPickGroupStep.ts | 4 ++-- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts index e4d4039a66..4fef52c5f5 100644 --- a/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts @@ -20,7 +20,7 @@ export class ContextValueQuickPickStep this.matchesSingleFilter(e, nodeContextValues)); } - isIndirectPick(node: TreeItem): boolean { + isAncestorPick(node: TreeItem): boolean { // `TreeItemCollapsibleState.None` and `undefined` are both falsy, and indicate that a `TreeItem` cannot have children--and therefore, cannot be an indirect pick return !node.collapsibleState; } diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts index acc317e1c6..56566978f8 100644 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts @@ -34,7 +34,7 @@ export class FindByIdQuickPickStep await this.treeDataProvider.getTreeItem(childElement))); const childPairs: [unknown, vscode.TreeItem][] = childNodes.map((childElement: unknown, i: number) => [childElement, childItems[i]]); - const directChoices = childPairs.filter(([, ti]) => this.pickFilter.isDirectPick(ti)); - const indirectChoices = childPairs.filter(([, ti]) => this.pickFilter.isIndirectPick(ti)); + const directChoices = childPairs.filter(([, ti]) => this.pickFilter.isFinalPick(ti)); + const indirectChoices = childPairs.filter(([, ti]) => this.pickFilter.isAncestorPick(ti)); let promptChoices: [unknown, vscode.TreeItem][] = []; if (directChoices.length === 0) { diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index 99e2f58ea1..55fde4590a 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -16,7 +16,7 @@ export class RecursiveQuickPickStep { * Filters for nodes that match the final target. * @param node The node to apply the filter to */ - isDirectPick(node: TPick): boolean; + isFinalPick(node: TPick): boolean; /** * Filters for nodes that could have a descendant matching the final target. * @param node The node to apply the filter to */ - isIndirectPick(node: TPick): boolean; + isAncestorPick(node: TPick): boolean; } diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts index bcd4bacaa8..87c86c4543 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -64,7 +64,7 @@ export class CompatibilityRecursiveQuickPickStep Date: Thu, 3 Nov 2022 15:55:08 -0700 Subject: [PATCH 24/49] Default `hideStepCount` to `true` in `RecursiveQuickPickStep` --- utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index 55fde4590a..ec99f1f46e 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -8,6 +8,8 @@ import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from '. import { getLastNode } from './QuickPickWizardContext'; export class RecursiveQuickPickStep extends ContextValueQuickPickStep { + hideStepCount: boolean = true; + public async getSubWizard(wizardContext: TContext): Promise | undefined> { const lastPickedItem = getLastNode(wizardContext); @@ -23,7 +25,6 @@ export class RecursiveQuickPickStep Date: Thu, 3 Nov 2022 18:12:46 -0700 Subject: [PATCH 25/49] Remove redundant `skipIfOne` property --- utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts index 56566978f8..1ef2e568c5 100644 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts @@ -11,7 +11,6 @@ import { PickFilter } from './common/PickFilter'; interface FindByIdQuickPickOptions extends SkipIfOneQuickPickOptions { id: string; - skipIfOne?: true; } export class FindByIdQuickPickStep extends GenericQuickPickStep { From 4d3502bd220bb4b8f1ee5ff7a6d33b70bee314c5 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 7 Nov 2022 10:10:58 -0800 Subject: [PATCH 26/49] Add registerCommandWithTreeNodeUnwrapping util (#1262) --- utils/hostapi.v2.d.ts | 39 -------------- utils/index.d.ts | 40 ++++++++++++++ utils/src/index.ts | 2 +- .../registerCommandWithTreeNodeUnboxing.ts | 52 ------------------ .../registerCommandWithTreeNodeUnwrapping.ts | 53 +++++++++++++++++++ utils/test/isBox.test.ts | 30 ----------- utils/test/isWrapper.test.ts | 30 +++++++++++ 7 files changed, 124 insertions(+), 122 deletions(-) delete mode 100644 utils/hostapi.v2.d.ts delete mode 100644 utils/src/registerCommandWithTreeNodeUnboxing.ts create mode 100644 utils/src/registerCommandWithTreeNodeUnwrapping.ts delete mode 100644 utils/test/isBox.test.ts create mode 100644 utils/test/isWrapper.test.ts diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts deleted file mode 100644 index b6289593cd..0000000000 --- a/utils/hostapi.v2.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { IActionContext } from "./index"; - -/** - * Interface describing an object that wraps another object. - * - * The host extension will wrap all tree nodes provided by the client - * extensions. When commands are executed, the wrapper objects are - * sent directly to the client extension, which will need to unwrap - * them. The `registerCommandWithTreeNodeUnboxing` method below, used - * in place of `registerCommand`, will intelligently do this - * unboxing automatically (i.e., will not unbox if the arguments - * aren't boxes) - */ -export interface Box { - unwrap(): Promise; -} - -/** - * Tests to see if something is a box, by ensuring it is an object - * and has an "unwrap" function - * @param maybeBox An object to test if it is a box - * @returns True if a box, false otherwise - */ -export declare function isBox(maybeBox: unknown): maybeBox is Box; - -/** - * Describes command callbacks for tree node context menu commands - */ -export type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; - -/** - * Used to register VSCode tree node context menu commands that are in the host extension's tree. It wraps your callback with consistent error and telemetry handling - * Use debounce property if you need a delay between clicks for this particular command - * A telemetry event is automatically sent whenever a command is executed. The telemetry event ID will default to the same as the - * commandId passed in, but can be overridden per command with telemetryId - * The telemetry event for this command will be named telemetryId if specified, otherwise it defaults to the commandId - * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. - */ -export declare function registerCommandWithTreeNodeUnboxing(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; diff --git a/utils/index.d.ts b/utils/index.d.ts index 0f01f5b888..3125e36b09 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -1678,3 +1678,43 @@ export declare enum AzExtResourceType { VirtualNetworks = 'VirtualNetworks', WebHostingEnvironments = 'WebHostingEnvironments', } + +/** + * Describes command callbacks for tree node context menu commands + */ +export type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; + +/** + * Used to register VSCode tree node context menu commands that are in the host extension's tree. It wraps your callback with consistent error and telemetry handling + * Use debounce property if you need a delay between clicks for this particular command + * A telemetry event is automatically sent whenever a command is executed. The telemetry event ID will default to the same as the + * commandId passed in, but can be overridden per command with telemetryId + * The telemetry event for this command will be named telemetryId if specified, otherwise it defaults to the commandId + * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. + */ +export declare function registerCommandWithTreeNodeUnwrapping(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; + +export declare function unwrapArgs(treeNodeCallback: TreeNodeCommandCallback): TreeNodeCommandCallback; + +/** + * Interface describing an object that wraps another object. + * + * The host extension will wrap all tree nodes provided by the client + * extensions. When commands are executed, the wrapper objects are + * sent directly to the client extension, which will need to unwrap + * them. The `registerCommandWithTreeNodeUnwrapping` method below, used + * in place of `registerCommand`, will intelligently do this + * unwrapping automatically (i.e., will not unwrap if the arguments + * aren't wrappers) + */ +export declare interface Wrapper { + unwrap(): T; +} + +/** + * Tests to see if something is a wrapper, by ensuring it is an object + * and has an "unwrap" function + * @param maybeWrapper An object to test if it is a wrapper + * @returns True if a wrapper, false otherwise + */ +export declare function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper; diff --git a/utils/src/index.ts b/utils/src/index.ts index 25af2ff87a..068bc7ab52 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -16,7 +16,7 @@ export { addExtensionValueToMask, callWithMaskHandling, maskValue } from './mask export * from './openReadOnlyContent'; export * from './parseError'; export * from './registerCommand'; -export * from './registerCommandWithTreeNodeUnboxing'; +export * from './registerCommandWithTreeNodeUnwrapping'; export * from './registerEvent'; export { registerReportIssueCommand } from './registerReportIssueCommand'; export * from './tree/AzExtParentTreeItem'; diff --git a/utils/src/registerCommandWithTreeNodeUnboxing.ts b/utils/src/registerCommandWithTreeNodeUnboxing.ts deleted file mode 100644 index 33cf4a5096..0000000000 --- a/utils/src/registerCommandWithTreeNodeUnboxing.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ - -import type { CommandCallback, IActionContext } from '../index'; -import type { Box, TreeNodeCommandCallback } from '../hostapi.v2'; -import { registerCommand } from './registerCommand'; - -export function registerCommandWithTreeNodeUnboxing(commandId: string, treeNodeCallback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void { - const unwrappingCallback: CommandCallback = async (context: IActionContext, ...args: unknown[]) => { - const maybeNodeBox = args?.[0]; - const maybeNodeBoxArray = args?.[1]; - const remainingArgs = args.slice(2); - - let node: T | undefined; - if (maybeNodeBox && isBox(maybeNodeBox)) { - // If the first arg is a box, unwrap it - node = await maybeNodeBox.unwrap(); - } else if (maybeNodeBox) { - // Otherwise, assume it is just a T - node = maybeNodeBox as T; - } - - let nodes: T[] | undefined; - if (maybeNodeBoxArray && Array.isArray(maybeNodeBoxArray) && maybeNodeBoxArray.every(n => isBox(n))) { - // If the first arg is an array of boxes, unwrap them - const boxedNodes = maybeNodeBoxArray as Box[]; - nodes = []; - for (const n of boxedNodes) { - nodes.push(await n.unwrap()) - } - } else if (maybeNodeBoxArray && Array.isArray(maybeNodeBoxArray)) { - // Otherwise, assume it is just an array of T's - nodes = maybeNodeBoxArray as T[]; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - return treeNodeCallback(context, node, nodes, ...remainingArgs); - }; - - registerCommand(commandId, unwrappingCallback, debounce, telemetryId); -} - -export function isBox(maybeBox: unknown): maybeBox is Box { - if (maybeBox && typeof maybeBox === 'object' && - (maybeBox as Box).unwrap && typeof (maybeBox as Box).unwrap === 'function') { - return true; - } - - return false; -} diff --git a/utils/src/registerCommandWithTreeNodeUnwrapping.ts b/utils/src/registerCommandWithTreeNodeUnwrapping.ts new file mode 100644 index 0000000000..651a82f2b2 --- /dev/null +++ b/utils/src/registerCommandWithTreeNodeUnwrapping.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { IActionContext, Wrapper, TreeNodeCommandCallback } from '../index'; +import { registerCommand } from './registerCommand'; + +export function registerCommandWithTreeNodeUnwrapping(commandId: string, treeNodeCallback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void { + registerCommand(commandId, unwrapArgs(treeNodeCallback), debounce, telemetryId); +} + +export function unwrapArgs(treeNodeCallback: TreeNodeCommandCallback): TreeNodeCommandCallback { + return async (context: IActionContext, ...args: unknown[]) => { + const maybeNodeWrapper = args?.[0]; + const maybeNodeWrapperArray = args?.[1]; + const remainingArgs = args.slice(2); + + let node: T | undefined; + if (maybeNodeWrapper && isWrapper(maybeNodeWrapper)) { + // If the first arg is a wrapper, unwrap it + node = await maybeNodeWrapper.unwrap(); + } else if (maybeNodeWrapper) { + // Otherwise, assume it is just a T + node = maybeNodeWrapper as T; + } + + let nodes: T[] | undefined; + if (maybeNodeWrapperArray && Array.isArray(maybeNodeWrapperArray) && maybeNodeWrapperArray.every(n => isWrapper(n))) { + // If the first arg is an array of wrappers, unwrap them + const wrappedNodes = maybeNodeWrapperArray as Wrapper[]; + nodes = []; + for (const n of wrappedNodes) { + nodes.push(n.unwrap()) + } + } else if (maybeNodeWrapperArray && Array.isArray(maybeNodeWrapperArray)) { + // Otherwise, assume it is just an array of T's + nodes = maybeNodeWrapperArray as T[]; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return treeNodeCallback(context, node, nodes, ...remainingArgs); + }; +} + +export function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper { + if (maybeWrapper && typeof maybeWrapper === 'object' && + (maybeWrapper as Wrapper).unwrap && typeof (maybeWrapper as Wrapper).unwrap === 'function') { + return true; + } + + return false; +} diff --git a/utils/test/isBox.test.ts b/utils/test/isBox.test.ts deleted file mode 100644 index a8f367d22e..0000000000 --- a/utils/test/isBox.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import type { Box } from '../hostapi.v2'; -import { isBox } from '../src/registerCommandWithTreeNodeUnboxing'; - -suite('isBox', () => { - - test('Not a box', () => { - assert.strictEqual(isBox(undefined), false); - assert.strictEqual(isBox(null), false); - assert.strictEqual(isBox(1), false); - assert.strictEqual(isBox(false), false); - assert.strictEqual(isBox('foo'), false); - assert.strictEqual(isBox({}), false); - assert.strictEqual(isBox({ unwrap: false }), false); - }); - - test('Box', () => { - const actualBox: Box = { - unwrap: () => { return Promise.resolve(undefined as unknown as T) }, - }; - - assert.strictEqual(isBox(actualBox), true); - }); - -}); diff --git a/utils/test/isWrapper.test.ts b/utils/test/isWrapper.test.ts new file mode 100644 index 0000000000..c074ca0978 --- /dev/null +++ b/utils/test/isWrapper.test.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Wrapper } from '..'; +import { isWrapper } from '../src/registerCommandWithTreeNodeUnwrapping'; + +suite('isWrapper', () => { + + test('Not a Wrapper', () => { + assert.strictEqual(isWrapper(undefined), false); + assert.strictEqual(isWrapper(null), false); + assert.strictEqual(isWrapper(1), false); + assert.strictEqual(isWrapper(false), false); + assert.strictEqual(isWrapper('foo'), false); + assert.strictEqual(isWrapper({}), false); + assert.strictEqual(isWrapper({ unwrap: false }), false); + }); + + test('Wrapper', () => { + const actualWrapper: Wrapper = { + unwrap: () => { return undefined as unknown as T }, + }; + + assert.strictEqual(isWrapper(actualWrapper), true); + }); + +}); From 580ffc77a6310fec1fa7e189848162ac9de389f9 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 7 Nov 2022 16:41:01 -0800 Subject: [PATCH 27/49] Merge remote-tracking branch 'origin/api-v2' into bmw/quickPick_v8.30 --- utils/hostapi.v2.d.ts | 5 ----- utils/index.d.ts | 18 +++++++++++++++++- .../registerCommandWithTreeNodeUnwrapping.ts | 3 +-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index f0a843cfec..df9d1ff696 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -52,11 +52,6 @@ export declare interface ApplicationResource extends ResourceBase { /* add more properties from GenericResource if needed */ } -/** - * Describes command callbacks for tree node context menu commands - */ -export declare type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; - export declare interface PickSubscriptionWizardContext extends QuickPickWizardContext { subscription?: ApplicationSubscription; } diff --git a/utils/index.d.ts b/utils/index.d.ts index 4853153f90..e7c8a38c6d 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -10,7 +10,6 @@ import { CancellationToken, CancellationTokenSource, Disposable, Event, Extensio import { TargetPopulation } from 'vscode-tas-client'; import { AzureExtensionApi, AzureExtensionApiProvider } from './api'; import type { Activity, ActivityTreeItemOptions, AppResource, AzureHostExtensionApi, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result -import type { TreeNodeCommandCallback } from './hostapi.v2'; export declare interface RunWithTemporaryDescriptionOptions { description: string; @@ -1683,6 +1682,23 @@ export declare enum AzExtResourceType { WebHostingEnvironments = 'WebHostingEnvironments', } +/** + * Describes command callbacks for tree node context menu commands + */ +export type TreeNodeCommandCallback = (context: IActionContext, node?: T, nodes?: T[], ...args: any[]) => any; + +/** + * Used to register VSCode tree node context menu commands that are in the host extension's tree. It wraps your callback with consistent error and telemetry handling + * Use debounce property if you need a delay between clicks for this particular command + * A telemetry event is automatically sent whenever a command is executed. The telemetry event ID will default to the same as the + * commandId passed in, but can be overridden per command with telemetryId + * The telemetry event for this command will be named telemetryId if specified, otherwise it defaults to the commandId + * NOTE: If the environment variable `DEBUGTELEMETRY` is set to a non-empty, non-zero value, then telemetry will not be sent. If the value is 'verbose' or 'v', telemetry will be displayed in the console window. + */ +export declare function registerCommandWithTreeNodeUnwrapping(commandId: string, callback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void; + +export declare function unwrapArgs(treeNodeCallback: TreeNodeCommandCallback): TreeNodeCommandCallback; + /** * Interface describing an object that wraps another object. * diff --git a/utils/src/registerCommandWithTreeNodeUnwrapping.ts b/utils/src/registerCommandWithTreeNodeUnwrapping.ts index 8a559ff5e3..651a82f2b2 100644 --- a/utils/src/registerCommandWithTreeNodeUnwrapping.ts +++ b/utils/src/registerCommandWithTreeNodeUnwrapping.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IActionContext, Wrapper } from '../index'; -import type { TreeNodeCommandCallback } from '../hostapi.v2'; +import type { IActionContext, Wrapper, TreeNodeCommandCallback } from '../index'; import { registerCommand } from './registerCommand'; export function registerCommandWithTreeNodeUnwrapping(commandId: string, treeNodeCallback: TreeNodeCommandCallback, debounce?: number, telemetryId?: string): void { From cc4bdc7b9a6a9cb284e0ae7de0db5b5e593455ab Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Thu, 10 Nov 2022 10:58:29 -0800 Subject: [PATCH 28/49] Move `isAzExtTreeItem` --- utils/src/tree/AzExtTreeItem.ts | 4 ---- utils/src/tree/isAzExtParentTreeItem.ts | 5 +++++ .../compatibility/CompatibilityRecursiveQuickPickStep.ts | 4 ++-- .../compatibility/compatibilityPickSubscriptionExperience.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/utils/src/tree/AzExtTreeItem.ts b/utils/src/tree/AzExtTreeItem.ts index 070214dbbf..111feabe59 100644 --- a/utils/src/tree/AzExtTreeItem.ts +++ b/utils/src/tree/AzExtTreeItem.ts @@ -216,7 +216,3 @@ export abstract class AzExtTreeItem implements types.AzExtTreeItem { } } } - -export function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is types.AzExtTreeItem { - return typeof maybeTreeItem === 'object' && (maybeTreeItem as AzExtTreeItem)._isAzExtTreeItem === true; -} diff --git a/utils/src/tree/isAzExtParentTreeItem.ts b/utils/src/tree/isAzExtParentTreeItem.ts index 9bcc9ea80a..99c0aa5fc0 100644 --- a/utils/src/tree/isAzExtParentTreeItem.ts +++ b/utils/src/tree/isAzExtParentTreeItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { AzExtParentTreeItem } from "./AzExtParentTreeItem"; +import { AzExtTreeItem } from "./AzExtTreeItem"; import type { IAzExtParentTreeItemInternal } from "./InternalInterfaces"; /** @@ -12,3 +13,7 @@ import type { IAzExtParentTreeItemInternal } from "./InternalInterfaces"; export function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem { return typeof maybeParentTreeItem === 'object' && (maybeParentTreeItem as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem === true; } + +export function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is AzExtTreeItem { + return typeof maybeTreeItem === 'object' && (maybeTreeItem as AzExtTreeItem)._isAzExtTreeItem === true; +} diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts index 87c86c4543..879c570732 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -9,8 +9,8 @@ import { CompatibilityContextValueQuickPickStep } from './CompatibilityContextVa import { localize } from "../../../localize"; import { NoResourceFoundError, UserCancelledError } from "../../../errors"; import type { ContextValueFilterQuickPickOptions } from "../ContextValueQuickPickStep"; -import { AzExtTreeItem, isAzExtTreeItem } from "../../../tree/AzExtTreeItem"; -import { isAzExtParentTreeItem } from "../../../tree/isAzExtParentTreeItem"; +import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; +import { isAzExtParentTreeItem, isAzExtTreeItem } from "../../../tree/isAzExtParentTreeItem"; import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; export interface CompatibilityRecursiveQuickPickOptions extends ContextValueFilterQuickPickOptions { diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts index b7139eddf2..4dbf0a7a6b 100644 --- a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts @@ -9,7 +9,7 @@ import { ISubscriptionContext } from '@microsoft/vscode-azext-dev'; import { subscriptionExperience } from '../subscriptionExperience'; import { createSubscriptionContext } from '../../../../utils/credentialUtils'; import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; -import { isAzExtTreeItem } from '../../../../tree/AzExtTreeItem'; +import { isAzExtTreeItem } from '../../../../tree/isAzExtParentTreeItem'; /** * Returns `ISubscriptionContext` instead of `ApplicationSubscription` for compatibility. From a9501cc6b41036769c60f5376efce32563501f89 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Fri, 11 Nov 2022 14:42:01 -0800 Subject: [PATCH 29/49] Expose a subset of `vscode.TreeDataProvider`. --- utils/hostapi.v2.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts index df9d1ff696..3bd424a479 100644 --- a/utils/hostapi.v2.d.ts +++ b/utils/hostapi.v2.d.ts @@ -61,7 +61,9 @@ export declare interface AzureResourceQuickPickWizardContext extends QuickPickWi resourceGroup?: string; } +export type ResourceGroupsTreeDataProvider = Pick, 'getChildren' | 'getTreeItem'>; + export interface V2AzureResourcesApi extends AzureExtensionApi { - readonly applicationResourceTreeDataProvider: vscode.TreeDataProvider; - readonly workspaceResourceTreeDataProvider: vscode.TreeDataProvider; + readonly applicationResourceTreeDataProvider: ResourceGroupsTreeDataProvider; + readonly workspaceResourceTreeDataProvider: ResourceGroupsTreeDataProvider; } From ee794bde7ad4828600f3755a11cc2002aa5cec14 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Fri, 11 Nov 2022 14:42:53 -0800 Subject: [PATCH 30/49] Fixup `isAncestorPick` jsdoc --- utils/src/treev2/quickPickWizard/common/PickFilter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/treev2/quickPickWizard/common/PickFilter.ts b/utils/src/treev2/quickPickWizard/common/PickFilter.ts index bfc6d6646d..1e6a4b5e5a 100644 --- a/utils/src/treev2/quickPickWizard/common/PickFilter.ts +++ b/utils/src/treev2/quickPickWizard/common/PickFilter.ts @@ -13,7 +13,7 @@ export interface PickFilter { isFinalPick(node: TPick): boolean; /** - * Filters for nodes that could have a descendant matching the final target. + * Filters for nodes that could be an ancestor of a node matching the final target. * @param node The node to apply the filter to */ isAncestorPick(node: TPick): boolean; From f448f8a71e6b453ad971fe34684532277c42a9b7 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Fri, 11 Nov 2022 17:00:44 -0800 Subject: [PATCH 31/49] Use `Array.prototype.at` Updated @types/node to v16 --- utils/package-lock.json | 14 +++++++------- utils/package.json | 2 +- .../quickPickWizard/QuickPickWizardContext.ts | 6 +----- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/utils/package-lock.json b/utils/package-lock.json index 8014a23d19..693ab69c15 100644 --- a/utils/package-lock.json +++ b/utils/package-lock.json @@ -24,7 +24,7 @@ "@microsoft/vscode-azext-dev": "^0.1.4", "@types/html-to-text": "^8.1.0", "@types/mocha": "^7.0.2", - "@types/node": "^14.0.0", + "@types/node": "^16.0.0", "@types/semver": "^7.3.9", "@types/vscode": "1.64.0", "@typescript-eslint/eslint-plugin": "^4.28.3", @@ -582,9 +582,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "14.17.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.34.tgz", - "integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==", + "version": "16.18.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", + "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==", "dev": true }, "node_modules/@types/semver": { @@ -7830,9 +7830,9 @@ "dev": true }, "@types/node": { - "version": "14.17.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.34.tgz", - "integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==", + "version": "16.18.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", + "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==", "dev": true }, "@types/semver": { diff --git a/utils/package.json b/utils/package.json index 3962ea5ecf..b6fac81bec 100644 --- a/utils/package.json +++ b/utils/package.json @@ -47,7 +47,7 @@ "@microsoft/vscode-azext-dev": "^0.1.4", "@types/html-to-text": "^8.1.0", "@types/mocha": "^7.0.2", - "@types/node": "^14.0.0", + "@types/node": "^16.0.0", "@types/semver": "^7.3.9", "@types/vscode": "1.64.0", "@typescript-eslint/eslint-plugin": "^4.28.3", diff --git a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts index 18e25a26a0..52a75732b0 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts +++ b/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts @@ -6,9 +6,5 @@ import * as types from '../../../index'; export function getLastNode(context: types.QuickPickWizardContext): TNode | undefined { - if (context.pickedNodes.length) { - return context.pickedNodes[context.pickedNodes.length - 1] as TNode; - } - - return undefined; + return context.pickedNodes.at(-1) as TNode | undefined; } From 616493223decc97873b5d278140b2138f7440d8e Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Fri, 11 Nov 2022 17:04:52 -0800 Subject: [PATCH 32/49] Fix double parens --- utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index ec99f1f46e..e3d32015ad 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -18,7 +18,7 @@ export class RecursiveQuickPickStep Date: Tue, 15 Nov 2022 15:04:53 -0800 Subject: [PATCH 33/49] Make a copy of context --- .../treev2/quickPickWizard/experiences/appResourceExperience.ts | 2 +- .../experiences/compatibilityPickResourceExperience.ts | 2 +- .../quickPickWizard/experiences/contextValueExperience.ts | 2 +- .../treev2/quickPickWizard/experiences/findByIdExperience.ts | 2 +- .../quickPickWizard/experiences/subscriptionExperience.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts index 387372ea9c..b524485ef9 100644 --- a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -42,7 +42,7 @@ export async function appResourceExperience(context: types.IActionContext } // Fill in the `pickedNodes` property - const wizardContext = context as AzureResourceQuickPickWizardContext; + const wizardContext = { ...context } as AzureResourceQuickPickWizardContext; wizardContext.pickedNodes = []; const wizard = new AzureWizard(context, { diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts index aa481a6968..30a8c41e2e 100644 --- a/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts @@ -43,7 +43,7 @@ export async function compatibilityPickAppResourceExperience(context: typ ]; // Fill in the `pickedNodes` property - const wizardContext = context as types.QuickPickWizardContext; + const wizardContext = { ...context } as types.QuickPickWizardContext; wizardContext.pickedNodes = []; const wizard = new AzureWizard(context, { diff --git a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts index 281b727676..6ffec5b3da 100644 --- a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts @@ -20,7 +20,7 @@ export async function findByIdExperience): Promise { - const wizardContext = context as PickSubscriptionWizardContext; + const wizardContext = { ...context } as PickSubscriptionWizardContext; wizardContext.pickedNodes = []; const wizard = new AzureWizard(wizardContext, { From 6d0c31c910fb714f541a74391b8b4496725b5c24 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 15 Nov 2022 15:06:04 -0800 Subject: [PATCH 34/49] Move file --- utils/src/index.ts | 2 +- .../compatibilityPickResourceExperience.ts | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) rename utils/src/treev2/quickPickWizard/experiences/{ => compatibility}/compatibilityPickResourceExperience.ts (71%) diff --git a/utils/src/index.ts b/utils/src/index.ts index 17a8abe410..3c9e4ef184 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -38,7 +38,7 @@ export * from './activityLog/activities/ExecuteActivity'; export * from './getAzExtResourceType'; export * from './AzExtResourceType'; export * from './treev2/quickPickWizard/experiences/appResourceExperience'; -export * from './treev2/quickPickWizard/experiences/compatibilityPickResourceExperience'; +export * from './treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; export * from './treev2/quickPickWizard/experiences/findByIdExperience'; export * from './treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience'; diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience.ts similarity index 71% rename from utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts rename to utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience.ts index 30a8c41e2e..6c988600f6 100644 --- a/utils/src/treev2/quickPickWizard/experiences/compatibilityPickResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { QuickPickAzureSubscriptionStep } from '../quickPickAzureResource/QuickPickAzureSubscriptionStep'; -import { QuickPickGroupStep } from '../quickPickAzureResource/QuickPickGroupStep'; -import { QuickPickAppResourceStep } from '../quickPickAzureResource/QuickPickAppResourceStep'; -import { getLastNode } from '../QuickPickWizardContext'; -import { NoResourceFoundError } from '../../../errors'; -import * as types from '../../../../index'; -import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; -import { AzureWizard } from '../../../wizard/AzureWizard'; -import { AzureResourceQuickPickWizardContext } from '../../../../hostapi.v2'; -import { CompatibilityRecursiveQuickPickStep } from '../compatibility/CompatibilityRecursiveQuickPickStep'; -import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; -import { ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; +import { QuickPickAzureSubscriptionStep } from '../../quickPickAzureResource/QuickPickAzureSubscriptionStep'; +import { QuickPickGroupStep } from '../../quickPickAzureResource/QuickPickGroupStep'; +import { QuickPickAppResourceStep } from '../../quickPickAzureResource/QuickPickAppResourceStep'; +import { getLastNode } from '../../QuickPickWizardContext'; +import { NoResourceFoundError } from '../../../../errors'; +import * as types from '../../../../../index'; +import { AzureWizardPromptStep } from '../../../../wizard/AzureWizardPromptStep'; +import { AzureWizard } from '../../../../wizard/AzureWizard'; +import { AzureResourceQuickPickWizardContext } from '../../../../../hostapi.v2'; +import { CompatibilityRecursiveQuickPickStep } from '../../compatibility/CompatibilityRecursiveQuickPickStep'; +import { isWrapper } from '../../../../registerCommandWithTreeNodeUnwrapping'; +import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; /** * Provides compatibility for the legacy `pickAppResource` Resource Groups API From d9367fbcc1612bb7ec12fbbceca9202083e5df14 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 15 Nov 2022 15:15:24 -0800 Subject: [PATCH 35/49] import type --- utils/src/tree/isAzExtParentTreeItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/tree/isAzExtParentTreeItem.ts b/utils/src/tree/isAzExtParentTreeItem.ts index 99c0aa5fc0..0e6c52cc31 100644 --- a/utils/src/tree/isAzExtParentTreeItem.ts +++ b/utils/src/tree/isAzExtParentTreeItem.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { AzExtParentTreeItem } from "./AzExtParentTreeItem"; -import { AzExtTreeItem } from "./AzExtTreeItem"; +import type { AzExtTreeItem } from "./AzExtTreeItem"; import type { IAzExtParentTreeItemInternal } from "./InternalInterfaces"; /** From 23fe238dc3ca83f5e37365df04ab5d0452c0e6fe Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 15 Nov 2022 15:51:15 -0800 Subject: [PATCH 36/49] Organize compatibility experiences into namespace --- utils/index.d.ts | 12 +++- utils/src/index.ts | 3 +- .../PickTreeItemWithCompatibility.ts | 31 +++++++++ .../compatibilityPickResourceExperience.ts | 64 ------------------- ...compatibilityPickSubscriptionExperience.ts | 25 -------- 5 files changed, 42 insertions(+), 93 deletions(-) create mode 100644 utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts delete mode 100644 utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience.ts delete mode 100644 utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts diff --git a/utils/index.d.ts b/utils/index.d.ts index aac04f20c6..6ad7eebe06 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -1734,8 +1734,16 @@ interface CompatibilityPickResourceExperienceOptions { childItemFilter?: ContextValueFilter } -export declare function compatibilityPickAppResourceExperience(context: IActionContext, tdp: TreeDataProvider, options: CompatibilityPickResourceExperienceOptions): Promise; -export declare function compatibilitySubscriptionExperience(context: IActionContext, tdp: TreeDataProvider): Promise; +export declare namespace PickTreeItemWithCompatibility { + /** + * Provides compatibility for the legacy `pickAppResource` Resource Groups API + */ + export function resource(context: IActionContext, tdp: TreeDataProvider, options: CompatibilityPickResourceExperienceOptions): Promise; + /** + * Returns `ISubscriptionContext` instead of `ApplicationSubscription` for compatibility. + */ + export function subscription(context: IActionContext, tdp: TreeDataProvider): Promise; +} export declare interface QuickPickWizardContext extends IActionContext { pickedNodes: unknown[]; diff --git a/utils/src/index.ts b/utils/src/index.ts index 3c9e4ef184..bd972dfae7 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -38,10 +38,9 @@ export * from './activityLog/activities/ExecuteActivity'; export * from './getAzExtResourceType'; export * from './AzExtResourceType'; export * from './treev2/quickPickWizard/experiences/appResourceExperience'; -export * from './treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience'; +export * from './treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; export * from './treev2/quickPickWizard/experiences/findByIdExperience'; -export * from './treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience'; export * from './tree/isAzExtParentTreeItem'; export * from './utils/apiUtils'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts new file mode 100644 index 0000000000..a35b94b7b2 --- /dev/null +++ b/utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts @@ -0,0 +1,31 @@ +import * as types from '../../../../../index'; +import * as vscode from 'vscode'; +import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; +import { appResourceExperience } from '../appResourceExperience'; +import { subscriptionExperience } from '../subscriptionExperience'; +import { isAzExtTreeItem } from '../../../../tree/isAzExtParentTreeItem'; +import { createSubscriptionContext } from '../../../../utils/credentialUtils'; +import { ISubscriptionContext } from '@microsoft/vscode-azext-dev'; + +export namespace PickTreeItemWithCompatibility { + /** + * Provides compatibility for the legacy `pickAppResource` Resource Groups API + */ + export async function resource(context: types.IActionContext, tdp: vscode.TreeDataProvider, options: types.CompatibilityPickResourceExperienceOptions): Promise { + const { resourceTypes, childItemFilter } = options; + return appResourceExperience(context, tdp, resourceTypes ? Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes] : undefined, childItemFilter); + } + + /** + * Returns `ISubscriptionContext` instead of `ApplicationSubscription` for compatibility. + */ + export async function subscription(context: types.IActionContext, tdp: vscode.TreeDataProvider): Promise { + const applicationSubscription = await subscriptionExperience(context, tdp); + + if (isAzExtTreeItem(applicationSubscription)) { + return applicationSubscription.subscription; + } + + return createSubscriptionContext(applicationSubscription); + } +} diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience.ts deleted file mode 100644 index 6c988600f6..0000000000 --- a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickResourceExperience.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import { QuickPickAzureSubscriptionStep } from '../../quickPickAzureResource/QuickPickAzureSubscriptionStep'; -import { QuickPickGroupStep } from '../../quickPickAzureResource/QuickPickGroupStep'; -import { QuickPickAppResourceStep } from '../../quickPickAzureResource/QuickPickAppResourceStep'; -import { getLastNode } from '../../QuickPickWizardContext'; -import { NoResourceFoundError } from '../../../../errors'; -import * as types from '../../../../../index'; -import { AzureWizardPromptStep } from '../../../../wizard/AzureWizardPromptStep'; -import { AzureWizard } from '../../../../wizard/AzureWizard'; -import { AzureResourceQuickPickWizardContext } from '../../../../../hostapi.v2'; -import { CompatibilityRecursiveQuickPickStep } from '../../compatibility/CompatibilityRecursiveQuickPickStep'; -import { isWrapper } from '../../../../registerCommandWithTreeNodeUnwrapping'; -import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; - -/** - * Provides compatibility for the legacy `pickAppResource` Resource Groups API - */ -export async function compatibilityPickAppResourceExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, options: types.CompatibilityPickResourceExperienceOptions): Promise { - const { resourceTypes, childItemFilter } = options; - - const promptSteps: AzureWizardPromptStep[] = [ - new QuickPickAzureSubscriptionStep(tdp), - new QuickPickGroupStep(tdp, { - groupType: resourceTypes ? Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes] : undefined, - }), - new QuickPickAppResourceStep(tdp, { - resourceTypes: resourceTypes ? Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes] : undefined, - skipIfOne: false, - childItemFilter, - }), - ]; - - if (childItemFilter) { - promptSteps.push(new CompatibilityRecursiveQuickPickStep(tdp, { - contextValueFilter: childItemFilter, - skipIfOne: false, - })); - } - - // Fill in the `pickedNodes` property - const wizardContext = { ...context } as AzureResourceQuickPickWizardContext; - wizardContext.pickedNodes = []; - - const wizard = new AzureWizard(context, { - hideStepCount: true, - promptSteps: promptSteps, - showLoadingPrompt: true, - }); - - await wizard.prompt(); - - const lastPickedItem = getLastNode(wizardContext); - - if (!lastPickedItem) { - throw new NoResourceFoundError(wizardContext); - } else { - return isWrapper(lastPickedItem) ? lastPickedItem.unwrap() : lastPickedItem as unknown as TPick; - } -} diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts deleted file mode 100644 index 4dbf0a7a6b..0000000000 --- a/utils/src/treev2/quickPickWizard/experiences/compatibility/compatibilityPickSubscriptionExperience.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ - -import * as types from '../../../../../index'; -import * as vscode from 'vscode'; -import { ISubscriptionContext } from '@microsoft/vscode-azext-dev'; -import { subscriptionExperience } from '../subscriptionExperience'; -import { createSubscriptionContext } from '../../../../utils/credentialUtils'; -import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; -import { isAzExtTreeItem } from '../../../../tree/isAzExtParentTreeItem'; - -/** - * Returns `ISubscriptionContext` instead of `ApplicationSubscription` for compatibility. - */ -export async function compatibilitySubscriptionExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider): Promise { - const applicationSubscription = await subscriptionExperience(context, tdp); - - if (isAzExtTreeItem(applicationSubscription)) { - return applicationSubscription.subscription; - } - - return createSubscriptionContext(applicationSubscription); -} From d31f406e9823a7c89110e922e73ed7971cd66f5e Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 28 Nov 2022 16:12:07 -0800 Subject: [PATCH 37/49] Remove findById steps and experience --- utils/index.d.ts | 7 -- utils/src/index.ts | 1 - .../quickPickWizard/FindByIdQuickPickStep.ts | 71 ------------------- .../experiences/findByIdExperience.ts | 40 ----------- 4 files changed, 119 deletions(-) delete mode 100644 utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts delete mode 100644 utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts diff --git a/utils/index.d.ts b/utils/index.d.ts index 6ad7eebe06..edfdb465b7 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -1727,7 +1727,6 @@ export declare function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrappe export declare function appResourceExperience(context: IActionContext, tdp: TreeDataProvider, resourceTypes?: AzExtResourceType | AzExtResourceType[], childItemFilter?: ContextValueFilter): Promise; export declare function contextValueExperience(context: IActionContext, tdp: TreeDataProvider, contextValueFilter: ContextValueFilter): Promise; -export declare function findByIdExperience(context: IActionContext, tdp: TreeDataProvider, id: string | Uri): Promise; interface CompatibilityPickResourceExperienceOptions { resourceTypes?: AzExtResourceType | AzExtResourceType[]; @@ -1774,12 +1773,6 @@ type CreateOptions = { callback: CreateCallback; } -export declare interface FindableByIdTreeNodeV2 { - id: string; -} - -export declare type FindableByIdTreeNode = FindableByIdTreeNodeV2 | AzExtTreeItem; - /** * Gets the Azure Resource Groups API */ diff --git a/utils/src/index.ts b/utils/src/index.ts index bd972dfae7..f3e758ac57 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -40,7 +40,6 @@ export * from './AzExtResourceType'; export * from './treev2/quickPickWizard/experiences/appResourceExperience'; export * from './treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; -export * from './treev2/quickPickWizard/experiences/findByIdExperience'; export * from './tree/isAzExtParentTreeItem'; export * from './utils/apiUtils'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts b/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts deleted file mode 100644 index 1ef2e568c5..0000000000 --- a/utils/src/treev2/quickPickWizard/FindByIdQuickPickStep.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as types from '../../../index'; -import * as vscode from 'vscode'; -import { getLastNode } from './QuickPickWizardContext'; -import { GenericQuickPickStep, SkipIfOneQuickPickOptions } from './GenericQuickPickStep'; -import { PickFilter } from './common/PickFilter'; - -interface FindByIdQuickPickOptions extends SkipIfOneQuickPickOptions { - id: string; -} - -export class FindByIdQuickPickStep extends GenericQuickPickStep { - public constructor(tdp: vscode.TreeDataProvider, options: FindByIdQuickPickOptions) { - super(tdp, { - ...options, - skipIfOne: true, // Find-by-id is always skip-if-one - }); - } - - protected readonly pickFilter: PickFilter = new FindByIdPickFilter(this.pickOptions); - - public async getSubWizard(wizardContext: TContext): Promise | undefined> { - // TODO: this code is nearly identical to `RecursiveQuickPickStep`, but this class can't inherit from it because it's - // not at all based on context value for filtering - const lastPickedItem = getLastNode(wizardContext); - - if (!lastPickedItem) { - // Something went wrong, no node was chosen - throw new Error('No node was set after prompt step.'); - } - - if (this.pickFilter.isFinalPick(await this.treeDataProvider.getTreeItem(lastPickedItem))) { - // The last picked node matches the expected ID - // No need to continue prompting - return undefined; - } else { - // Need to keep going because the last picked node is not a match - return { - hideStepCount: true, - promptSteps: [ - new FindByIdQuickPickStep(this.treeDataProvider, this.pickOptions) - ], - }; - } - } -} - -class FindByIdPickFilter implements PickFilter { - constructor(private readonly pickOptions: FindByIdQuickPickOptions) { } - - isAncestorPick(node: vscode.TreeItem): boolean { - if (!node.collapsibleState) { - // can't be an indirect pick if it doesn't have children - return false; - } - - if (node.id) { - return this.pickOptions.id.startsWith(node.id); - } - - return false; - } - - isFinalPick(node: vscode.TreeItem): boolean { - return this.pickOptions.id === node.id; - } -} diff --git a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts b/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts deleted file mode 100644 index 6ffec5b3da..0000000000 --- a/utils/src/treev2/quickPickWizard/experiences/findByIdExperience.ts +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as types from '../../../../index'; -import * as vscode from 'vscode'; -import { getLastNode } from '../QuickPickWizardContext'; -import { NoResourceFoundError } from '../../../errors'; -import { FindByIdQuickPickStep } from '../FindByIdQuickPickStep'; -import { isWrapper } from '../../../registerCommandWithTreeNodeUnwrapping'; -import { AzureWizard } from '../../../wizard/AzureWizard'; -import { ResourceGroupsItem } from '../quickPickAzureResource/tempTypes'; - -export async function findByIdExperience(context: types.IActionContext, tdp: vscode.TreeDataProvider, id: string): Promise { - const promptSteps: types.AzureWizardPromptStep[] = [ - new FindByIdQuickPickStep(tdp, { - id: id, - }), - ]; - - // Fill in the `pickedNodes` property - const wizardContext = { ...context } as types.QuickPickWizardContext; - wizardContext.pickedNodes = []; - - const wizard = new AzureWizard(context, { - hideStepCount: true, - promptSteps: promptSteps, - }); - - await wizard.prompt(); - - const lastPickedItem = getLastNode(wizardContext); - - if (!lastPickedItem) { - throw new NoResourceFoundError(wizardContext); - } else { - return isWrapper(lastPickedItem) ? lastPickedItem.unwrap() : lastPickedItem as unknown as TPick; - } -} From 07f0e5ee092a26943bd771e8029115e28b67af38 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Wed, 30 Nov 2022 12:17:27 -0800 Subject: [PATCH 38/49] Add `isAzExtTreeItem` util (#1280) --- utils/index.d.ts | 3 +++ utils/src/index.ts | 1 + utils/src/tree/AzExtParentTreeItem.ts | 3 ++- utils/src/tree/AzExtTreeDataProvider.ts | 3 ++- utils/src/tree/AzExtTreeItem.ts | 7 +++++-- utils/src/tree/InternalInterfaces.ts | 12 +++--------- utils/src/tree/isAzExtTreeItem.ts | 19 +++++++++++++++++++ 7 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 utils/src/tree/isAzExtTreeItem.ts diff --git a/utils/index.d.ts b/utils/index.d.ts index 3125e36b09..020ae5985f 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -464,6 +464,9 @@ export declare abstract class AzExtTreeItem implements IAzExtTreeItem { public resolveTooltip?(): Promise; } +export declare function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is AzExtTreeItem; +export declare function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem; + export interface IGenericTreeItemOptions { id?: string; label: string; diff --git a/utils/src/index.ts b/utils/src/index.ts index 068bc7ab52..425125d328 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -37,4 +37,5 @@ export * from './utils/contextUtils'; export * from './activityLog/activities/ExecuteActivity'; export * from './getAzExtResourceType'; export * from './AzExtResourceType'; +export * from './tree/isAzExtTreeItem'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/tree/AzExtParentTreeItem.ts b/utils/src/tree/AzExtParentTreeItem.ts index dd351ea0d2..114721d8f3 100644 --- a/utils/src/tree/AzExtParentTreeItem.ts +++ b/utils/src/tree/AzExtParentTreeItem.ts @@ -11,7 +11,8 @@ import { localize } from '../localize'; import { randomUtils } from '../utils/randomUtils'; import { AzExtTreeItem } from './AzExtTreeItem'; import { GenericTreeItem } from './GenericTreeItem'; -import { IAzExtParentTreeItemInternal, isAzExtParentTreeItem } from './InternalInterfaces'; +import { IAzExtParentTreeItemInternal } from './InternalInterfaces'; +import { isAzExtParentTreeItem } from './isAzExtTreeItem'; import { runWithLoadingNotification } from './runWithLoadingNotification'; import { loadMoreLabel } from './treeConstants'; diff --git a/utils/src/tree/AzExtTreeDataProvider.ts b/utils/src/tree/AzExtTreeDataProvider.ts index 691a7bdb3a..91d326f9f1 100644 --- a/utils/src/tree/AzExtTreeDataProvider.ts +++ b/utils/src/tree/AzExtTreeDataProvider.ts @@ -14,7 +14,8 @@ import { AzExtParentTreeItem, InvalidTreeItem } from './AzExtParentTreeItem'; import { AzExtTreeItem } from './AzExtTreeItem'; import { CollapsibleStateTracker } from './CollapsibleStateTracker'; import { GenericTreeItem } from './GenericTreeItem'; -import { IAzExtTreeDataProviderInternal, isAzExtParentTreeItem } from './InternalInterfaces'; +import { IAzExtTreeDataProviderInternal } from './InternalInterfaces'; +import { isAzExtParentTreeItem } from './isAzExtTreeItem'; import { runWithLoadingNotification } from './runWithLoadingNotification'; import { loadMoreLabel } from './treeConstants'; diff --git a/utils/src/tree/AzExtTreeItem.ts b/utils/src/tree/AzExtTreeItem.ts index 2c5703768e..6eab7d0e57 100644 --- a/utils/src/tree/AzExtTreeItem.ts +++ b/utils/src/tree/AzExtTreeItem.ts @@ -8,11 +8,14 @@ import * as types from '../../index'; import { NotImplementedError } from '../errors'; import { localize } from '../localize'; import { nonNullProp } from '../utils/nonNull'; -import { IAzExtParentTreeItemInternal, IAzExtTreeDataProviderInternal, isAzExtParentTreeItem } from "./InternalInterfaces"; +import { IAzExtParentTreeItemInternal, IAzExtTreeDataProviderInternal } from "./InternalInterfaces"; import { settingUtils } from '../utils/settingUtils'; import { showContextValueSetting } from '../constants'; +import { isAzExtParentTreeItem } from './isAzExtTreeItem'; export abstract class AzExtTreeItem implements types.AzExtTreeItem { + public readonly _isAzExtTreeItem = true; + //#region Properties implemented by base class public abstract label: string; public abstract contextValue: string; @@ -120,7 +123,7 @@ export abstract class AzExtTreeItem implements types.AzExtTreeItem { } public get tooltip(): string | undefined { - if(process.env.DEBUGTELEMETRY === 'v' && !!settingUtils.getWorkspaceSetting(showContextValueSetting)) { + if (process.env.DEBUGTELEMETRY === 'v' && !!settingUtils.getWorkspaceSetting(showContextValueSetting)) { return `Context: "${this.contextValue}"`; } else { return this._tooltip; diff --git a/utils/src/tree/InternalInterfaces.ts b/utils/src/tree/InternalInterfaces.ts index e0318566f3..35fbaa74a9 100644 --- a/utils/src/tree/InternalInterfaces.ts +++ b/utils/src/tree/InternalInterfaces.ts @@ -5,13 +5,14 @@ import { EventEmitter } from 'vscode'; import * as types from '../../index'; -import { AzExtTreeItem } from './AzExtTreeItem'; +import { AzExtParentTreeItem } from './AzExtParentTreeItem'; +import type { AzExtTreeItem } from './AzExtTreeItem'; import { CollapsibleStateTracker } from './CollapsibleStateTracker'; // Interfaces for methods on the tree that aren't exposed outside of this package // We can't reference the classes directly because it would result in circular dependencies -export interface IAzExtParentTreeItemInternal extends types.AzExtParentTreeItem, AzExtTreeItem { +export interface IAzExtParentTreeItemInternal extends AzExtParentTreeItem { _isAzExtParentTreeItem: boolean; parent: IAzExtParentTreeItemInternal | undefined; treeDataProvider: IAzExtTreeDataProviderInternal; @@ -24,10 +25,3 @@ export interface IAzExtTreeDataProviderInternal extends types.AzExtTreeDataProvi refreshUIOnly(treeItem: AzExtTreeItem | undefined): void; readonly collapsibleStateTracker: CollapsibleStateTracker | undefined; } - -/** - * Using instanceof AzExtParentTreeItem causes issues whenever packages are linked for dev testing. Instead, check _isAzExtParentTreeItem - */ -export function isAzExtParentTreeItem(item: {}): boolean { - return !!(item)._isAzExtParentTreeItem; -} diff --git a/utils/src/tree/isAzExtTreeItem.ts b/utils/src/tree/isAzExtTreeItem.ts new file mode 100644 index 0000000000..e2ba870ce5 --- /dev/null +++ b/utils/src/tree/isAzExtTreeItem.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { AzExtParentTreeItem } from "./AzExtParentTreeItem"; +import type { AzExtTreeItem } from "./AzExtTreeItem"; +import type { IAzExtParentTreeItemInternal } from "./InternalInterfaces"; + +export function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is AzExtTreeItem { + return typeof maybeTreeItem === 'object' && (maybeTreeItem as AzExtTreeItem)._isAzExtTreeItem === true; +} + +/** + * Using instanceof AzExtParentTreeItem causes issues since each extension has their own version of the utils. Instead, check _isAzExtParentTreeItem + */ +export function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem { + return isAzExtTreeItem(maybeParentTreeItem) && (maybeParentTreeItem as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem === true; +} From adbb83bec41498b0f1b3aa6c7d62b4de1443d613 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:38:15 -0800 Subject: [PATCH 39/49] Delete isAzExtParentTreeItem.ts --- utils/src/index.ts | 2 +- utils/src/tree/isAzExtParentTreeItem.ts | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 utils/src/tree/isAzExtParentTreeItem.ts diff --git a/utils/src/index.ts b/utils/src/index.ts index f3e758ac57..648ae65706 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -40,6 +40,6 @@ export * from './AzExtResourceType'; export * from './treev2/quickPickWizard/experiences/appResourceExperience'; export * from './treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; -export * from './tree/isAzExtParentTreeItem'; export * from './utils/apiUtils'; +export * from './tree/isAzExtTreeItem'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/tree/isAzExtParentTreeItem.ts b/utils/src/tree/isAzExtParentTreeItem.ts deleted file mode 100644 index 0e6c52cc31..0000000000 --- a/utils/src/tree/isAzExtParentTreeItem.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ - -import type { AzExtParentTreeItem } from "./AzExtParentTreeItem"; -import type { AzExtTreeItem } from "./AzExtTreeItem"; -import type { IAzExtParentTreeItemInternal } from "./InternalInterfaces"; - -/** - * Using instanceof AzExtParentTreeItem causes issues since each extension has their own version of the utils. Instead, check _isAzExtParentTreeItem - */ -export function isAzExtParentTreeItem(maybeParentTreeItem: unknown): maybeParentTreeItem is AzExtParentTreeItem { - return typeof maybeParentTreeItem === 'object' && (maybeParentTreeItem as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem === true; -} - -export function isAzExtTreeItem(maybeTreeItem: unknown): maybeTreeItem is AzExtTreeItem { - return typeof maybeTreeItem === 'object' && (maybeTreeItem as AzExtTreeItem)._isAzExtTreeItem === true; -} From 5544e3b6891b8588b00831dce28f50bd72c93a3a Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:39:55 -0800 Subject: [PATCH 40/49] Reset changes to AzExtTreeItem* classes --- utils/src/tree/AzExtParentTreeItem.ts | 2 +- utils/src/tree/AzExtTreeDataProvider.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/src/tree/AzExtParentTreeItem.ts b/utils/src/tree/AzExtParentTreeItem.ts index dc1317f9e7..114721d8f3 100644 --- a/utils/src/tree/AzExtParentTreeItem.ts +++ b/utils/src/tree/AzExtParentTreeItem.ts @@ -247,7 +247,7 @@ export abstract class AzExtParentTreeItem extends AzExtTreeItem implements types // Just in case implementers of `loadMoreChildrenImpl` re-use the same child node, we want to clear those caches as well for (const child of this._cachedChildren) { if (isAzExtParentTreeItem(child)) { - child.clearCache(); + (child).clearCache(); } } this._cachedChildren = []; diff --git a/utils/src/tree/AzExtTreeDataProvider.ts b/utils/src/tree/AzExtTreeDataProvider.ts index ab4360a22d..c556c3e271 100644 --- a/utils/src/tree/AzExtTreeDataProvider.ts +++ b/utils/src/tree/AzExtTreeDataProvider.ts @@ -148,7 +148,7 @@ export class AzExtTreeDataProvider implements IAzExtTreeDataProviderInternal, ty } if (isAzExtParentTreeItem(treeItem)) { - treeItem.clearCache(); + (treeItem).clearCache(); } this.refreshUIOnly(treeItem); @@ -180,7 +180,7 @@ export class AzExtTreeDataProvider implements IAzExtTreeDataProviderInternal, ty while (!treeItem.matchesContextValue(expectedContextValues)) { if (isAzExtParentTreeItem(treeItem)) { - const pickedItems: AzExtTreeItem | AzExtTreeItem[] = await treeItem.pickChildTreeItem(expectedContextValues, context); + const pickedItems: AzExtTreeItem | AzExtTreeItem[] = await (treeItem).pickChildTreeItem(expectedContextValues, context); if (Array.isArray(pickedItems)) { // canPickMany is only supported at the last stage of the picker, so automatically return if this is an array return pickedItems; From bbebe06eb628062e45a2c17b41f6c3aed0281a24 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:41:38 -0800 Subject: [PATCH 41/49] Temporarily remove apiUtils --- utils/index.d.ts | 11 ----------- utils/src/index.ts | 1 - utils/src/utils/apiUtils.ts | 32 -------------------------------- 3 files changed, 44 deletions(-) delete mode 100644 utils/src/utils/apiUtils.ts diff --git a/utils/index.d.ts b/utils/index.d.ts index caa98849fc..703f55ebcf 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -1774,14 +1774,3 @@ type CreateOptions = { label?: string; callback: CreateCallback; } - -/** - * Gets the Azure Resource Groups API - */ -export declare function getResourceGroupsApi(apiVersionRange: AzureHostExtensionApi['apiVersion']): Promise; -export declare function getResourceGroupsApi(apiVersionRange: string): Promise; - -/** - * Get exported API from an extension - */ -export declare function getApiExport(extensionId: string): Promise; diff --git a/utils/src/index.ts b/utils/src/index.ts index 648ae65706..63f69ee51f 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -40,6 +40,5 @@ export * from './AzExtResourceType'; export * from './treev2/quickPickWizard/experiences/appResourceExperience'; export * from './treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility'; export * from './treev2/quickPickWizard/experiences/contextValueExperience'; -export * from './utils/apiUtils'; export * from './tree/isAzExtTreeItem'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/utils/src/utils/apiUtils.ts b/utils/src/utils/apiUtils.ts deleted file mode 100644 index 6031b2caa2..0000000000 --- a/utils/src/utils/apiUtils.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Extension, extensions } from "vscode"; -import { localize } from "vscode-nls"; -import type { AzureExtensionApi, AzureExtensionApiProvider } from "../../api"; -import { AzureHostExtensionApi } from "../../hostapi"; - -export async function getResourceGroupsApi(apiVersionRange: AzureExtensionApi['apiVersion']): Promise; -export async function getResourceGroupsApi(apiVersionRange: string): Promise { - const rgApiProvider = await getApiExport('ms-azuretools.vscode-azureresourcegroups'); - if (rgApiProvider) { - return rgApiProvider.getApi(apiVersionRange); - } else { - throw new Error(localize('noResourceGroupExt', 'Could not find the Azure Resource Groups extension')); - } -} - -export async function getApiExport(extensionId: string): Promise { - const extension: Extension | undefined = extensions.getExtension(extensionId); - if (extension) { - if (!extension.isActive) { - await extension.activate(); - } - - return extension.exports; - } - - return undefined; -} From 2390d1e9ea3de6e04a508e4c8326fc9bc1bd06bc Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:52:14 -0800 Subject: [PATCH 42/49] Fixups --- .../compatibility/CompatibilityContextValueQuickPickStep.ts | 2 +- .../compatibility/CompatibilityRecursiveQuickPickStep.ts | 2 +- .../experiences/compatibility/PickTreeItemWithCompatibility.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts index 34eb2e468b..6b33ba082b 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts @@ -9,7 +9,7 @@ import { getLastNode } from "../QuickPickWizardContext"; import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; import { AzExtParentTreeItem } from "../../../tree/AzExtParentTreeItem"; import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; -import { isAzExtParentTreeItem } from "../../../tree/isAzExtParentTreeItem"; +import { isAzExtParentTreeItem } from "../../../tree/isAzExtTreeItem"; /** * Provides compatability with {@link AzExtParentTreeItem.pickTreeItemImpl} diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts index 879c570732..7432ece5c3 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -10,7 +10,7 @@ import { localize } from "../../../localize"; import { NoResourceFoundError, UserCancelledError } from "../../../errors"; import type { ContextValueFilterQuickPickOptions } from "../ContextValueQuickPickStep"; import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; -import { isAzExtParentTreeItem, isAzExtTreeItem } from "../../../tree/isAzExtParentTreeItem"; +import { isAzExtParentTreeItem, isAzExtTreeItem } from "../../../tree/isAzExtTreeItem"; import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; export interface CompatibilityRecursiveQuickPickOptions extends ContextValueFilterQuickPickOptions { diff --git a/utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts b/utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts index a35b94b7b2..4e4f3365f7 100644 --- a/utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts +++ b/utils/src/treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import { ResourceGroupsItem } from '../../quickPickAzureResource/tempTypes'; import { appResourceExperience } from '../appResourceExperience'; import { subscriptionExperience } from '../subscriptionExperience'; -import { isAzExtTreeItem } from '../../../../tree/isAzExtParentTreeItem'; +import { isAzExtTreeItem } from '../../../../tree/isAzExtTreeItem'; import { createSubscriptionContext } from '../../../../utils/credentialUtils'; import { ISubscriptionContext } from '@microsoft/vscode-azext-dev'; From 6c36000a85ab2797de94351c256339a9349f08e0 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:53:44 -0800 Subject: [PATCH 43/49] Remove pick resource group step --- .../QuickPickAzureResourceGroupStep.ts | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts diff --git a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts b/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts deleted file mode 100644 index 9f5246ec5b..0000000000 --- a/utils/src/treev2/quickPickWizard/quickPickAzureResource/QuickPickAzureResourceGroupStep.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { TreeItem } from "vscode"; -import { AzureResourceQuickPickWizardContext } from "../../../../hostapi.v2"; -import * as types from "../../../../index"; -import { PickFilter } from "../common/PickFilter"; -import { GenericQuickPickOptions, GenericQuickPickStep } from "../GenericQuickPickStep"; - -// TODO: implement this for picking resource group -// The resource group may NOT be the grouping method used in the tree -export class QuickPickAzureResourceGroupStep extends GenericQuickPickStep { - protected override getPicks(_wizardContext: AzureResourceQuickPickWizardContext): Promise[]> { - throw new Error("Method not implemented."); - } - - pickFilter: PickFilter; -} From 0bbe7305ead21ef13836bb314b043ac4ff0fbc7c Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:55:38 -0800 Subject: [PATCH 44/49] Rename and move file --- utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts | 2 +- utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts | 2 +- utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts | 2 +- .../{QuickPickWizardContext.ts => common/getLastNode.ts} | 2 +- .../compatibility/CompatibilityContextValueQuickPickStep.ts | 2 +- .../compatibility/CompatibilityRecursiveQuickPickStep.ts | 2 +- .../treev2/quickPickWizard/experiences/appResourceExperience.ts | 2 +- .../quickPickWizard/experiences/contextValueExperience.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename utils/src/treev2/quickPickWizard/{QuickPickWizardContext.ts => common/getLastNode.ts} (90%) diff --git a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts index a9d6991bac..6f5609339d 100644 --- a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts @@ -5,7 +5,7 @@ import * as types from '../../../index'; import * as vscode from 'vscode'; -import { getLastNode } from './QuickPickWizardContext'; +import { getLastNode } from './common/getLastNode'; import { AzureWizardPromptStep } from '../../wizard/AzureWizardPromptStep'; import { PickFilter } from './common/PickFilter'; import { localize } from '../../localize'; diff --git a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts index c11fb76cee..d6af245cec 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts +++ b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as types from '../../../index'; -import { getLastNode } from './QuickPickWizardContext'; +import { getLastNode } from './common/getLastNode'; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; import { localize } from '../../localize'; import { NoResourceFoundError } from '../../errors'; diff --git a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts index e3d32015ad..67e4da32c1 100644 --- a/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts @@ -5,7 +5,7 @@ import * as types from '../../../index'; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; -import { getLastNode } from './QuickPickWizardContext'; +import { getLastNode } from './common/getLastNode'; export class RecursiveQuickPickStep extends ContextValueQuickPickStep { hideStepCount: boolean = true; diff --git a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts b/utils/src/treev2/quickPickWizard/common/getLastNode.ts similarity index 90% rename from utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts rename to utils/src/treev2/quickPickWizard/common/getLastNode.ts index 52a75732b0..e1ef19bcfa 100644 --- a/utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts +++ b/utils/src/treev2/quickPickWizard/common/getLastNode.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as types from '../../../index'; +import * as types from '../../../../index'; export function getLastNode(context: types.QuickPickWizardContext): TNode | undefined { return context.pickedNodes.at(-1) as TNode | undefined; diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts index 6b33ba082b..3df53643d8 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityContextValueQuickPickStep.ts @@ -5,7 +5,7 @@ import * as types from "../../../../index"; import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from "../ContextValueQuickPickStep"; -import { getLastNode } from "../QuickPickWizardContext"; +import { getLastNode } from "../common/getLastNode"; import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; import { AzExtParentTreeItem } from "../../../tree/AzExtParentTreeItem"; import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts index 7432ece5c3..38bfd752bc 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as types from "../../../../index"; -import { getLastNode } from "../QuickPickWizardContext"; +import { getLastNode } from "../common/getLastNode"; import { CompatibilityContextValueQuickPickStep } from './CompatibilityContextValueQuickPickStep'; import { localize } from "../../../localize"; import { NoResourceFoundError, UserCancelledError } from "../../../errors"; diff --git a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts index b524485ef9..5582ba5ae7 100644 --- a/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/appResourceExperience.ts @@ -9,7 +9,7 @@ import { QuickPickAzureSubscriptionStep } from '../quickPickAzureResource/QuickP import { QuickPickGroupStep } from '../quickPickAzureResource/QuickPickGroupStep'; import { QuickPickAppResourceStep } from '../quickPickAzureResource/QuickPickAppResourceStep'; import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; -import { getLastNode } from '../QuickPickWizardContext'; +import { getLastNode } from '../common/getLastNode'; import { NoResourceFoundError } from '../../../errors'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzExtResourceType } from '../../../AzExtResourceType'; diff --git a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts index d9ced1f4c5..b0db875ce0 100644 --- a/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts +++ b/utils/src/treev2/quickPickWizard/experiences/contextValueExperience.ts @@ -6,7 +6,7 @@ import * as types from '../../../../index'; import * as vscode from 'vscode'; import { RecursiveQuickPickStep } from '../RecursiveQuickPickStep'; -import { getLastNode } from '../QuickPickWizardContext'; +import { getLastNode } from '../common/getLastNode'; import { NoResourceFoundError } from '../../../errors'; import { AzureWizardPromptStep } from '../../../wizard/AzureWizardPromptStep'; import { AzureWizard } from '../../../wizard/AzureWizard'; From 92616e888872c176eace8f9fe810410ee74ec96c Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:56:20 -0800 Subject: [PATCH 45/49] Rename to final + ancestor naming --- .../treev2/quickPickWizard/GenericQuickPickStep.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts index 6f5609339d..77b6f3925d 100644 --- a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts @@ -67,18 +67,18 @@ export abstract class GenericQuickPickStep await this.treeDataProvider.getTreeItem(childElement))); const childPairs: [unknown, vscode.TreeItem][] = childNodes.map((childElement: unknown, i: number) => [childElement, childItems[i]]); - const directChoices = childPairs.filter(([, ti]) => this.pickFilter.isFinalPick(ti)); - const indirectChoices = childPairs.filter(([, ti]) => this.pickFilter.isAncestorPick(ti)); + const finalChoices = childPairs.filter(([, ti]) => this.pickFilter.isFinalPick(ti)); + const ancestorChoices = childPairs.filter(([, ti]) => this.pickFilter.isAncestorPick(ti)); let promptChoices: [unknown, vscode.TreeItem][] = []; - if (directChoices.length === 0) { - if (indirectChoices.length === 0) { + if (finalChoices.length === 0) { + if (ancestorChoices.length === 0) { // Don't throw and end the wizard, let user use back button instead } else { - promptChoices = indirectChoices; + promptChoices = ancestorChoices; } } else { - promptChoices = directChoices; + promptChoices = finalChoices; } const picks: types.IAzureQuickPickItem[] = []; From 619e6da34f46b7c768ee0a225b98af54ecef64e2 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Mon, 5 Dec 2022 17:58:29 -0800 Subject: [PATCH 46/49] Rename item to element --- .../src/treev2/quickPickWizard/GenericQuickPickStep.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts index 77b6f3925d..ff7fbc78bc 100644 --- a/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts @@ -63,9 +63,9 @@ export abstract class GenericQuickPickStep await this.treeDataProvider.getTreeItem(childElement))); - const childPairs: [unknown, vscode.TreeItem][] = childNodes.map((childElement: unknown, i: number) => [childElement, childItems[i]]); + const childElements = (await this.treeDataProvider.getChildren(lastPickedItem)) || []; + const childItems = await Promise.all(childElements.map(async (childElement: unknown) => await this.treeDataProvider.getTreeItem(childElement))); + const childPairs: [unknown, vscode.TreeItem][] = childElements.map((childElement: unknown, i: number) => [childElement, childItems[i]]); const finalChoices = childPairs.filter(([, ti]) => this.pickFilter.isFinalPick(ti)); const ancestorChoices = childPairs.filter(([, ti]) => this.pickFilter.isAncestorPick(ti)); @@ -89,11 +89,11 @@ export abstract class GenericQuickPickStep> { + private async getQuickPickItem(element: unknown, item: vscode.TreeItem): Promise> { return { label: ((item.label as vscode.TreeItemLabel)?.label || item.label) as string, description: item.description as string, - data: node, + data: element, }; } } From d02d8807bc927efe2eadc8123c9d871c0f18c065 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 6 Dec 2022 12:37:02 -0800 Subject: [PATCH 47/49] Add v2 hostapi typings (#1289) --- utils/hostapi.v2.d.ts | 327 ++++++++++++++++++++++++++++++++++++++++ utils/package-lock.json | 4 +- utils/package.json | 2 +- 3 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 utils/hostapi.v2.d.ts diff --git a/utils/hostapi.v2.d.ts b/utils/hostapi.v2.d.ts new file mode 100644 index 0000000000..1b4541d67f --- /dev/null +++ b/utils/hostapi.v2.d.ts @@ -0,0 +1,327 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import type { Environment } from '@azure/ms-rest-azure-env'; +import type { Activity } from './hostapi'; +import type { AzExtResourceType } from './index'; +import type { AzureExtensionApi } from './api'; + +/** + * Represents the base type for all Azure and workspace resources. + */ +export interface ResourceBase { + /** + * The ID of this resource. + * + * @remarks This value should be unique across all resources. + */ + readonly id: string; + + /** + * The display name of this resource. + */ + readonly name: string; +} + +/** + * Represents the base type for models of resources and their child items. + */ +export interface ResourceModelBase { + /** + * The ID of this model. + * + * @remarks This value should be unique across all models of its type. + */ + readonly id?: string; +} + +/** + * The base interface for providers of Azure and workspace resources. + */ +export interface ResourceProvider { + /** + * Fired when the provider's resources have changed. + */ + readonly onDidChangeResource?: vscode.Event; + + /** + * Called to supply the resources used as the basis for the resource views. + * + * @param source The source from which resources should be generated. + * + * @returns The resources to be displayed in the resource view. + */ + getResources(source: TResourceSource): vscode.ProviderResult; +} + +/** + * The base interface for visualizers of Azure and workspace resources. + */ +export interface BranchDataProvider extends vscode.TreeDataProvider { + /** + * Get the children of `element`. + * + * @param element The element from which the provider gets children. Unlike a traditional tree data provider, this will never be `undefined`. + * + * @return Children of `element`. + */ + getChildren(element: TModel): vscode.ProviderResult; + + /** + * Called to get the provider's model element for a specific resource. + * + * @remarks getChildren() assumes that the provider passes a known (TModel) model item, or undefined when getting the "root" children. + * However, branch data providers have no "root" so this function is called for each matching resource to obtain a starting branch item. + * + * @returns The provider's model element for `resource`. + */ + getResourceItem(element: TResource): TModel | Thenable; +} + +/** + * Represents a means of obtaining authentication data for an Azure subscription. + */ +export interface AzureAuthentication { + /** + * Gets a VS Code authentication session for an Azure subscription. + * + * @param scopes The scopes for which the authentication is needed. + * + * @returns A VS Code authentication session or undefined, if none could be obtained. + */ + getSession(scopes?: string[]): vscode.ProviderResult; +} + +/** + * Represents an Azure subscription. + */ +export interface AzureSubscription { + /** + * Access to the authentication session associated with this subscription. + */ + readonly authentication: AzureAuthentication; + + /** + * The Azure environment to which this subscription belongs. + */ + readonly environment: Environment; + + /** + * Whether this subscription belongs to a custom cloud. + */ + readonly isCustomCloud: boolean; + + /** + * The display name of this subscription. + */ + readonly name: string; + + /** + * The ID of this subscription. + */ + readonly subscriptionId: string; + + /** + * The tenant to which this subscription belongs or undefined, if not associated with a specific tenant. + */ + readonly tenantId: string; +} + +/** + * Represents a type of resource as designated by Azure. + */ +export interface AzureResourceType { + /** + * The kinds of resources that this type can represent. + */ + readonly kinds?: string[]; + + /** + * The (general) type of resource. + */ + readonly type: string; +} + +/** + * Represents an individual resource in Azure. + */ +export interface AzureResource extends ResourceBase { + /** + * The Azure-designated type of this resource. + */ + readonly azureResourceType: AzureResourceType; + + /** + * The location in which this resource exists. + */ + readonly location?: string; + + /** + * The resource group to which this resource belongs. + */ + readonly resourceGroup?: string; + + /** + * The type of this resource. + * + * @remarks This value is used to map resources to their associated branch data provider. + */ + readonly resourceType?: AzExtResourceType; + + /** + * The Azure subscription to which this resource belongs. + */ + readonly subscription: AzureSubscription; + + /** + * The tags associated with this resource. + */ + readonly tags?: { + [propertyName: string]: string; + }; + + /** + * A copy of the raw resource. + */ + readonly raw: {}; +} + +export interface ViewPropertiesModel { + /** + * File name displayed in VS Code. + */ + label: string; + + /** + * Raw data associated with the resource to populate the properties file. + */ + data: {}; +} + +/** + * Represents a model of an individual Azure resource or its child items. + */ +export interface AzureResourceModel extends ResourceModelBase { + /** + * The Azure ID of this resource. + * + * @remarks This property is expected to be implemented on "application-level" resources, but may also be applicable to its child items. + */ + readonly azureResourceId?: string; + + /** + * The URL of the area of Azure portal related to this item. + */ + readonly portalUrl?: vscode.Uri; + + /** + * Define to enable the "View Properties" command. + */ + readonly viewProperties?: ViewPropertiesModel; +} + +/** + * A provider for visualizing items in the Azure resource tree (e.g. Cosmos DB, Storage, etc.). + */ +export type AzureResourceBranchDataProvider = BranchDataProvider; + +/** + * Respresents a specific type of workspace resource. + * + * @remarks This value should be unique across all types of workspace resources. + */ +type WorkspaceResourceType = string; + +/** + * An indivdual root resource for a workspace. + */ +export interface WorkspaceResource extends ResourceBase { + /** + * The folder to which this resource belongs. + * Leave undefined if this resource is a global or system-level resource + * not associated with a specific workspace folder. + */ + readonly folder?: vscode.WorkspaceFolder; + + /** + * The type of this resource. + * + * @remarks This value is used to map resources to their associated branch data provider. + */ + readonly resourceType: WorkspaceResourceType; +} + +/** + * Represents a model of an individual workspace resource or its child items. + */ +export type WorkspaceResourceModel = ResourceModelBase; + +/** + * A provider for supplying items for the workspace resource tree (e.g., storage emulator, function apps in workspace, etc.). + * + * When a resources source is undefined, the resource is a global or system level resource not associated with a workspace folder. + * + */ +export type WorkspaceResourceProvider = ResourceProvider; + +/** + * A provider for visualizing items in the workspace resource tree (e.g., storage emulator, function apps in workspace, etc.). + */ +export type WorkspaceResourceBranchDataProvider = BranchDataProvider; + +// scope down vscode.TreeDataProvider to exactly what's allowed to be used +type ResourceGroupsTreeDataProvider = Pick, 'getChildren' | 'getTreeItem'>; + +/** + * The current (v2) Azure Resources extension API. + */ +export interface v2AzureResourcesApi extends AzureExtensionApi { + /** + * {@link vscode.TreeDataProvider} representing the Azure tree view. + */ + readonly azureResourceTreeDataProvider: ResourceGroupsTreeDataProvider; + + /** + * {@link vscode.TreeDataProvider} representing the Workspace tree view. + */ + readonly workspaceResourceTreeDataProvider: ResourceGroupsTreeDataProvider; + + /** + * Registers an activity to appear in the activity window. + * + * @param activity The activity information to show. + */ + registerActivity(activity: Activity): Promise; + + /** + * Registers an Azure resource branch data provider. + * + * @param type The Azure resource type associated with the provider. Must be unique. + * @param resolver The branch data provider for the resource type. + * + * @returns A disposable that unregisters the provider. + */ + registerAzureResourceBranchDataProvider(type: AzExtResourceType, provider: AzureResourceBranchDataProvider): vscode.Disposable; + + /** + * Registers a provider of workspace resources. + * + * @param provider The resource provider. + * + * @returns A disposable that unregisters the provider. + */ + registerWorkspaceResourceProvider(provider: WorkspaceResourceProvider): vscode.Disposable; + + /** + * Registers a workspace resource branch data provider. + * + * @param type The workspace resource type associated with the provider. Must be unique. + * @param provider The branch data provider for the resource type. + * + * @returns A disposable that unregisters the provider. + */ + registerWorkspaceResourceBranchDataProvider(type: WorkspaceResourceType, provider: WorkspaceResourceBranchDataProvider): vscode.Disposable; +} diff --git a/utils/package-lock.json b/utils/package-lock.json index 82accd5990..bd72910bd5 100644 --- a/utils/package-lock.json +++ b/utils/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/vscode-azext-utils", - "version": "0.3.16", + "version": "0.3.17", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@microsoft/vscode-azext-utils", - "version": "0.3.16", + "version": "0.3.17", "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", diff --git a/utils/package.json b/utils/package.json index c1abd25ccb..e2171ca583 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,7 +1,7 @@ { "name": "@microsoft/vscode-azext-utils", "author": "Microsoft Corporation", - "version": "0.3.16", + "version": "0.3.17", "description": "Common UI tools for developing Azure extensions for VS Code", "tags": [ "azure", From feb7cf06af3a2ed813191dd894f67823711b6c14 Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 6 Dec 2022 14:26:34 -0800 Subject: [PATCH 48/49] Remove unused QuickPickWithCreateStep for now Will add back when needed --- .../QuickPickWithCreateStep.ts | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts diff --git a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts b/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts deleted file mode 100644 index d6af245cec..0000000000 --- a/utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as types from '../../../index'; -import { getLastNode } from './common/getLastNode'; -import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep'; -import { localize } from '../../localize'; -import { NoResourceFoundError } from '../../errors'; - -type CreateCallback = () => TNode | Promise; -interface CreateQuickPickOptions extends ContextValueFilterQuickPickOptions { - skipIfOne?: never; // Not allowed in CreateQuickPickStep - createLabel?: string; - createCallback: CreateCallback; -} - -export class CreateQuickPickStep extends ContextValueQuickPickStep { - public override async prompt(wizardContext: TContext): Promise { - await super.prompt(wizardContext); - - const lastNode = getLastNode(wizardContext); - if (typeof lastNode === 'function') { - // If the last node is a function, pop it off the list and execute it - const callback = wizardContext.pickedNodes.pop() as unknown as CreateCallback; - wizardContext.pickedNodes.push(await callback()); - } - } - - protected override async getPicks(wizardContext: TContext): Promise[]> { - const picks: types.IAzureQuickPickItem[] = []; - try { - picks.push(...await super.getPicks(wizardContext)); - } catch (error) { - if (error instanceof NoResourceFoundError) { - // swallow NoResourceFoundError if create is defined, since we'll add a create pick - } else { - throw error; - } - } - - picks.push(this.getCreatePick()); - return picks as types.IAzureQuickPickItem[]; - } - - private getCreatePick(): types.IAzureQuickPickItem { - return { - label: this.pickOptions.createLabel || localize('createQuickPickLabel', '$(add) Create...'), - data: this.pickOptions.createCallback, - }; - } -} From 7c150b800841dc79e83af9c029c15142e4a2bb5b Mon Sep 17 00:00:00 2001 From: Alex Weininger Date: Tue, 6 Dec 2022 14:27:00 -0800 Subject: [PATCH 49/49] Make some types internal --- utils/index.d.ts | 7 ------- .../CompatibilityRecursiveQuickPickStep.ts | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/utils/index.d.ts b/utils/index.d.ts index 184081ba2b..62961e538e 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -1768,13 +1768,6 @@ export declare interface ContextValueFilter { exclude?: string | RegExp | (string | RegExp)[]; } -type CreateCallback = (context: IActionContext) => TNode | Promise; - -type CreateOptions = { - label?: string; - callback: CreateCallback; -} - /** * Get extension exports for the extension with the given id. Activates extension first if needed. * diff --git a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts index 38bfd752bc..7142f7abbc 100644 --- a/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts +++ b/utils/src/treev2/quickPickWizard/compatibility/CompatibilityRecursiveQuickPickStep.ts @@ -13,8 +13,15 @@ import { AzExtTreeItem } from "../../../tree/AzExtTreeItem"; import { isAzExtParentTreeItem, isAzExtTreeItem } from "../../../tree/isAzExtTreeItem"; import { isWrapper } from "../../../registerCommandWithTreeNodeUnwrapping"; -export interface CompatibilityRecursiveQuickPickOptions extends ContextValueFilterQuickPickOptions { - create?: types.CreateOptions; +type CreateCallback = (context: types.IActionContext) => TNode | Promise; + +type CreateOptions = { + label?: string; + callback: CreateCallback; +} + +interface CompatibilityRecursiveQuickPickOptions extends ContextValueFilterQuickPickOptions { + create?: CreateOptions; } /** @@ -36,7 +43,7 @@ export class CompatibilityRecursiveQuickPickStep; + const callback = selected.data as unknown as CreateCallback; // context passed to callback must have the same `ui` as the wizardContext // to prevent the wizard from being cancelled unexpectedly @@ -88,7 +95,7 @@ export class CompatibilityRecursiveQuickPickStep[]> { - const picks: types.IAzureQuickPickItem[] = []; + const picks: types.IAzureQuickPickItem[] = []; try { picks.push(...await super.getPicks(wizardContext)); } catch (error) { @@ -106,7 +113,7 @@ export class CompatibilityRecursiveQuickPickStep[]; } - private getCreatePick(options: types.CreateOptions): types.IAzureQuickPickItem { + private getCreatePick(options: CreateOptions): types.IAzureQuickPickItem { return { label: options.label || localize('createQuickPickLabel', '$(add) Create...'), data: options.callback,