Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add V2 workspace tree view #388

Merged
merged 26 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bf88108
Skeleton workspace tree.
philliphoff Sep 6, 2022
59da0a4
Sketch workspace tree implementation.
philliphoff Sep 7, 2022
b508e38
Hookup workspace BDP registration with API and add refresh.
philliphoff Sep 7, 2022
24a1daf
Added treeview description.
philliphoff Sep 8, 2022
2fd740c
Subscribe to workspace BDP changes.
philliphoff Sep 9, 2022
978d81e
Extract common resource provider management.
philliphoff Sep 9, 2022
620b500
Refactor application resource provider to use common implementation.
philliphoff Sep 14, 2022
12e6f8b
Consolidate provider map with listeners.
philliphoff Sep 14, 2022
1d0c0ee
Extract common logic from workspace BDP manager.
philliphoff Sep 14, 2022
6bc7abe
Move application resource BDP management to common logic.
philliphoff Sep 14, 2022
2b44ad1
Use common RG item model.
philliphoff Sep 14, 2022
011844c
Start extraction of common TDP logic.
philliphoff Sep 14, 2022
639d9e7
Further simplification of base TDP implementation.
philliphoff Sep 15, 2022
fe048a3
Refactor workspace TDP to derive from base.
philliphoff Sep 15, 2022
f9aba91
Tweak file/type names.
philliphoff Sep 15, 2022
57b5a81
Eliminate unnecessary base model type.
philliphoff Sep 15, 2022
9f13863
More renaming.
philliphoff Sep 15, 2022
8231ccd
More moves and renames.
philliphoff Sep 15, 2022
2d91192
Yet more renames.
philliphoff Sep 15, 2022
ec889c4
Move getParent() to base type.
philliphoff Sep 15, 2022
11fd03c
Add missing headers.
philliphoff Sep 15, 2022
5ebffdc
Updates per PR feedback.
philliphoff Sep 28, 2022
8c17203
Consolidate resource provider manager.
philliphoff Sep 28, 2022
757ecce
Refactoring out base class functions.
philliphoff Sep 28, 2022
c8ea46e
Cleanup API definitions.
philliphoff Sep 28, 2022
fd0186f
More updates per PR feedback.
philliphoff Sep 29, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionDevelopmentPath=${workspaceFolder}/../vscode-azurestorage",
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@
"name": "Workspace",
"visibility": "visible"
},
{
"id": "azureWorkspaceV2",
"name": "Workspace (V2)",
"visibility": "visible"
},
{
"id": "ms-azuretools.helpAndFeedback",
"name": "%ms-azuretools.helpAndFeedback%",
Expand Down Expand Up @@ -281,6 +286,11 @@
"when": "view == azureWorkspace",
"group": "navigation@10"
},
{
"command": "azureWorkspace.refresh",
"when": "view == azureWorkspaceV2",
"group": "navigation@10"
},
{
"command": "azureResourceGroups.clearActivities",
"when": "view == azureActivityLog",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { GenericResource, ResourceGroup } from '@azure/arm-resources';
import { getResourceGroupFromId, uiUtils } from "@microsoft/vscode-azext-azureutils";
import { callWithTelemetryAndErrorHandling, getAzExtResourceType, IActionContext, nonNullProp } from '@microsoft/vscode-azext-utils';
import * as vscode from 'vscode';
import { createResourceClient } from '../../../utils/azureClients';
import { createSubscriptionContext } from '../../../utils/v2/credentialsUtils';
import { ApplicationResource, ApplicationResourceProvider, ApplicationSubscription, ProvideResourceOptions } from '../v2AzureResourcesApi';
import { createResourceClient } from '../../utils/azureClients';
import { createSubscriptionContext } from '../../utils/v2/credentialsUtils';
import { ApplicationResource, ApplicationResourceProvider, ApplicationSubscription, ProvideResourceOptions } from './v2AzureResourcesApi';

export class BuiltInApplicationResourceProvider implements ApplicationResourceProvider {
export class DefaultApplicationResourceProvider implements ApplicationResourceProvider {
private readonly onDidChangeResourceEmitter = new vscode.EventEmitter<ApplicationResource | undefined>();

getResources(subscription: ApplicationSubscription, _options?: ProvideResourceOptions | undefined): Promise<ApplicationResource[] | undefined> {
Expand Down
72 changes: 65 additions & 7 deletions src/api/v2/ResourceGroupsExtensionManager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +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 vscode from 'vscode';

interface ResourceGroupsContribution {
readonly activation?: {
readonly onFetch?: string[];
readonly application: {
readonly branches?: { type: string }[];
readonly resources?: { type: string }[];
}
readonly workspace: {
readonly branches?: { type: string }[];
readonly resources?: { type: string }[];
}
}

Expand All @@ -12,19 +22,67 @@ interface ExtensionPackage {
};
}

const v2ResourceContributionsKey = 'x-azResourcesV2';

function getV2ResourceContributions(extension: vscode.Extension<unknown>): ResourceGroupsContribution | undefined {
const packageJson = extension.packageJSON as ExtensionPackage;

return packageJson?.contributes?.[v2ResourceContributionsKey];
}

const builtInExtensionIdRegex = /^vscode\./i;

function getInactiveExtensions(): vscode.Extension<unknown>[] {
return vscode.extensions
.all
// We don't need to activate extensions that are already active
.filter(extension => !extension.isActive)
// We don't need to look at any built-in extensions (often the majority of them)
.filter(extension => !builtInExtensionIdRegex.test(extension.id));
}

export class ResourceGroupsExtensionManager {
async activateApplicationResourceBranchDataProvider(type: string): Promise<void> {
type = type.toLowerCase();

const extensionAndContributions =
vscode.extensions.all
.map(extension => ({ extension, contributions: (extension.packageJSON as ExtensionPackage)?.contributes?.['x-azResourcesV2']?.activation?.onFetch?.map(type => type.toLowerCase()) ?? [] }))
getInactiveExtensions()
.map(extension => ({ extension, contributions: getV2ResourceContributions(extension)?.application?.branches?.map(resource => resource.type.toLowerCase()) ?? [] }))
.find(extensionAndContributions => extensionAndContributions.contributions.find(contribution => contribution === type) !== undefined);

if (extensionAndContributions) {
await extensionAndContributions.extension.activate();
}
}

async activateApplicationResourceProviders(): Promise<void> {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
const inactiveResourceContributors =
getInactiveExtensions()
.map(extension => ({ extension, resources: getV2ResourceContributions(extension)?.application?.resources ?? [] }))
.filter(extensionAndContributions => extensionAndContributions.resources.length > 0);

await Promise.all(inactiveResourceContributors.map(contributor => contributor.extension.activate()));
}

async activateWorkspaceResourceProviders(): Promise<void> {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
const inactiveResourceContributors =
getInactiveExtensions()
.map(extension => ({ extension, resources: getV2ResourceContributions(extension)?.workspace?.resources ?? [] }))
.filter(extensionAndContributions => extensionAndContributions.resources.length > 0);

await Promise.all(inactiveResourceContributors.map(contributor => contributor.extension.activate()));
}

async activateWorkspaceResourceBranchDataProvider(type: string): Promise<void> {
type = type.toLowerCase();

const extensionAndContributions =
getInactiveExtensions()
.map(extension => ({ extension, contributions: getV2ResourceContributions(extension)?.workspace?.branches?.map(resources => resources.type.toLowerCase()) ?? [] }))
.find(extensionAndContributions => extensionAndContributions.contributions.find(contribution => contribution === type) !== undefined);

if (extensionAndContributions) {
if (!extensionAndContributions.extension.isActive) {
await extensionAndContributions.extension.activate();
}
await extensionAndContributions.extension.activate();
}
}
}
84 changes: 84 additions & 0 deletions src/api/v2/ResourceProviderManagers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* 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 { ApplicationResource, ApplicationResourceProvider, ApplicationSubscription, ProvideResourceOptions, ResourceBase, ResourceProvider, WorkspaceResource, WorkspaceResourceProvider } from './v2AzureResourcesApi';

export function isArray<T>(maybeArray: T[] | null | undefined): maybeArray is T[] {
return Array.isArray(maybeArray);
}

class ResourceProviderManager<TResourceSource, TResource extends ResourceBase, TResourceProvider extends ResourceProvider<TResourceSource, TResource>> extends vscode.Disposable {
private readonly onDidChangeResourceEventEmitter = new vscode.EventEmitter<TResource | undefined>();
private readonly providers = new Map<TResourceProvider, { listener: vscode.Disposable | undefined }>();

private isActivating = false;

public readonly onDidChangeResourceChange: vscode.Event<TResource | undefined>;

constructor(private readonly extensionActivator: () => Promise<void>) {
super(
() => {
for (const context of this.providers.values()) {
context.listener?.dispose();
}

this.onDidChangeResourceEventEmitter.dispose();
});

this.onDidChangeResourceChange = this.onDidChangeResourceEventEmitter.event;
}

addResourceProvider(resourceProvider: TResourceProvider): void {
this.providers.set(resourceProvider, { listener: resourceProvider.onDidChangeResource?.(resource => this.onDidChangeResourceEventEmitter.fire(resource)) });

if (!this.isActivating) {
this.onDidChangeResourceEventEmitter.fire(undefined);
}
}

removeResourceProvider(resourceProvider: TResourceProvider): void {
const context = this.providers.get(resourceProvider);

if (context) {
context.listener?.dispose();

this.providers.delete(resourceProvider);
}

if (!this.isActivating) {
this.onDidChangeResourceEventEmitter.fire(undefined);
}
}

async getResources(source: TResourceSource, options?: ProvideResourceOptions): Promise<TResource[]> {
await this.activateExtensions();

const resourceProviders = Array.from(this.providers.keys());

const resources = await Promise.all(resourceProviders.map(resourceProvider => resourceProvider.getResources(source, options)));

return resources.filter(isArray).reduce((acc, result) => acc?.concat(result ?? []), []);
}

private async activateExtensions(): Promise<void> {
this.isActivating = true;

try {
await this.extensionActivator();
} finally {
this.isActivating = false;
}
}
}

// NOTE: TS doesn't seem to like exporting a type alias (i.e. you cannot instantiate it),
// so we still have to extend the class.

export class ApplicationResourceProviderManager extends ResourceProviderManager<ApplicationSubscription, ApplicationResource, ApplicationResourceProvider> {
}

export class WorkspaceResourceProviderManager extends ResourceProviderManager<vscode.WorkspaceFolder, WorkspaceResource, WorkspaceResourceProvider> {
}
20 changes: 0 additions & 20 deletions src/api/v2/providers/ApplicationResourceProviderManager.ts

This file was deleted.

39 changes: 28 additions & 11 deletions src/api/v2/v2AzureResourcesApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { Environment } from '@azure/ms-rest-azure-env';
import { AzExtResourceType } from '@microsoft/vscode-azext-utils';
import { AppResourceFilter } from '@microsoft/vscode-azext-utils/hostapi';
Expand Down Expand Up @@ -45,18 +50,22 @@ export interface ApplicationResource extends ResourceBase {
/* add more properties from GenericResource if needed */
}

export interface ResourceProviderBase<TResource extends ResourceBase> {
export interface ResourceProvider<TResourceSource, TResource extends ResourceBase> {
readonly onDidChangeResource?: vscode.Event<TResource | undefined>;

/**
* Called to supply the resources used as the basis for the resource group views.
* @param source The source from which resources should be generated.
*/
getResources(source: TResourceSource, options?: ProvideResourceOptions): vscode.ProviderResult<TResource[]>;
}

export interface ProvideResourceOptions {
readonly startAt?: number;
readonly maxResults?: number;
}

export interface ApplicationResourceProvider extends ResourceProviderBase<ApplicationResource> {
getResources(subscription: ApplicationSubscription, options?: ProvideResourceOptions): vscode.ProviderResult<ApplicationResource[]>;
}
export type ApplicationResourceProvider = ResourceProvider<ApplicationSubscription, ApplicationResource>;

export interface ResourceQuickPickOptions {
readonly contexts?: string[];
Expand Down Expand Up @@ -84,6 +93,19 @@ export interface WrappedResourceModel {
* The interface that resource resolvers must implement
*/
export interface BranchDataProvider<TResource extends ResourceBase, TModel extends ResourceModelBase> extends vscode.TreeDataProvider<TModel> {
/**
* Get the children of `element`.
*
* @param element The element from which the provider gets children. Unlike a traditional TreeDataProvider, this will never be `undefined`.
* @return Children of `element`.
*/
getChildren(element: TModel): vscode.ProviderResult<TModel[]>;

/**
philliphoff marked this conversation as resolved.
Show resolved Hide resolved
* A BranchDataProvider need not (and should not) implement this function.
*/
getParent?: never;

/**
* Called to get the provider's model element for a specific resource.
* @remarks getChildren() assumes that the provider passes a known <T> model item, or undefined when getting the root children.
Expand All @@ -102,18 +124,13 @@ export interface BranchDataProvider<TResource extends ResourceBase, TModel exten
*/
export interface WorkspaceResource extends ResourceBase {
readonly folder: vscode.WorkspaceFolder;
readonly type: string;
}

/**
* A provider for supplying items for the workspace resource tree (e.g., storage emulator, function apps in workspace, etc.)
*/
export interface WorkspaceResourceProvider extends ResourceProviderBase<WorkspaceResource> {
/**
* Called to supply the tree nodes to the workspace resource tree
* @param folder A folder in the opened workspace
*/
provideResources(folder: vscode.WorkspaceFolder, options?: ProvideResourceOptions): vscode.ProviderResult<WorkspaceResource[]>;
}
export type WorkspaceResourceProvider = ResourceProvider<vscode.WorkspaceFolder, WorkspaceResource>;

export interface ResourcePickOptions {
/**
Expand Down
40 changes: 26 additions & 14 deletions src/api/v2/v2AzureResourcesApiImplementation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/*---------------------------------------------------------------------------------------------
* 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 { BranchDataProviderManager } from '../../tree/v2/providers/BranchDataProviderManager';
import { ApplicationResourceProviderManager } from './providers/ApplicationResourceProviderManager';
import { ApplicationResource, ApplicationResourceProvider, BranchDataProvider, ResourcePickOptions, V2AzureResourcesApi, WorkspaceResource, WorkspaceResourceProvider } from './v2AzureResourcesApi';
import { ApplicationResourceBranchDataProviderManager } from '../../tree/v2/application/ApplicationResourceBranchDataProviderManager';
import { WorkspaceResourceBranchDataProviderManager } from '../../tree/v2/workspace/WorkspaceResourceBranchDataProviderManager';
import { ApplicationResourceProviderManager, WorkspaceResourceProviderManager } from './ResourceProviderManagers';
import { ApplicationResource, ApplicationResourceProvider, BranchDataProvider, ResourceModelBase, ResourcePickOptions, V2AzureResourcesApi, WorkspaceResource, WorkspaceResourceProvider } from './v2AzureResourcesApi';

export class V2AzureResourcesApiImplementation implements V2AzureResourcesApi {
public static apiVersion: string = '2.0.0';

constructor(
private readonly branchDataProviderManager: BranchDataProviderManager,
private readonly resourceProviderManager: ApplicationResourceProviderManager) {
private readonly applicationResourceProviderManager: ApplicationResourceProviderManager,
private readonly applicationResourceBranchDataProviderManager: ApplicationResourceBranchDataProviderManager,
private readonly workspaceResourceProviderManager: WorkspaceResourceProviderManager,
private readonly workspaceResourceBranchDataProviderManager: WorkspaceResourceBranchDataProviderManager) {
}

get apiVersion(): string {
Expand All @@ -24,22 +32,26 @@ export class V2AzureResourcesApiImplementation implements V2AzureResourcesApi {
}

registerApplicationResourceProvider(_id: string, provider: ApplicationResourceProvider): vscode.Disposable {
this.resourceProviderManager.addResourceProvider(provider);
this.applicationResourceProviderManager.addResourceProvider(provider);

return new vscode.Disposable(() => this.resourceProviderManager.removeResourceProvider(provider));
return new vscode.Disposable(() => this.applicationResourceProviderManager.removeResourceProvider(provider));
}

registerApplicationResourceBranchDataProvider<T>(id: string, provider: BranchDataProvider<ApplicationResource, T>): vscode.Disposable {
this.branchDataProviderManager.addApplicationResourceBranchDataProvider(id, provider);
registerApplicationResourceBranchDataProvider<T extends ResourceModelBase>(id: string, provider: BranchDataProvider<ApplicationResource, T>): vscode.Disposable {
this.applicationResourceBranchDataProviderManager.addProvider(id, provider);

return new vscode.Disposable(() => this.branchDataProviderManager.removeApplicationResourceBranchDataProvider(id));
return new vscode.Disposable(() => this.applicationResourceBranchDataProviderManager.removeProvider(id));
}

registerWorkspaceResourceProvider(_id: string, _provider: WorkspaceResourceProvider): vscode.Disposable {
throw new Error("Method not implemented.");
registerWorkspaceResourceProvider(_id: string, provider: WorkspaceResourceProvider): vscode.Disposable {
this.workspaceResourceProviderManager.addResourceProvider(provider);

return new vscode.Disposable(() => this.workspaceResourceProviderManager.removeResourceProvider(provider));
}

registerWorkspaceResourceBranchDataProvider<T>(_id: string, _provider: BranchDataProvider<WorkspaceResource, T>): vscode.Disposable {
throw new Error("Method not implemented.");
registerWorkspaceResourceBranchDataProvider<T extends ResourceModelBase>(type: string, provider: BranchDataProvider<WorkspaceResource, T>): vscode.Disposable {
this.workspaceResourceBranchDataProviderManager.addProvider(type, provider);

return new vscode.Disposable(() => this.workspaceResourceBranchDataProviderManager.removeProvider(type));
}
}
Loading