From 1d32cd20c3c56c420962eff694fda744e694e746 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Jan 2025 15:02:09 -0800 Subject: [PATCH 01/58] Scaffold BDP. Signed-off-by: Phillip Hoff --- package.json | 9 +++++-- src/extension.ts | 7 +++++- .../DurableTaskSchedulerDataBranchProvider.ts | 25 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts diff --git a/package.json b/package.json index 741450029..83f10c6a4 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,9 @@ "branches": [ { "type": "FunctionApp" + }, + { + "type": "DurableTaskScheduler" } ] }, @@ -69,10 +72,12 @@ ], "activation": { "onFetch": [ - "microsoft.web/sites" + "microsoft.web/sites", + "microsoft.durabletask/schedulers" ], "onResolve": [ - "microsoft.web/sites" + "microsoft.web/sites", + "microsoft.durabletask/schedulers" ] } }, diff --git a/src/extension.ts b/src/extension.ts index 523ce3b13..81175cf75 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,7 @@ import { registerAppServiceExtensionVariables } from '@microsoft/vscode-azext-azureappservice'; import { registerAzureUtilsExtensionVariables, type AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils'; import { callWithTelemetryAndErrorHandling, createApiProvider, createAzExtOutputChannel, createExperimentationService, registerErrorHandler, registerEvent, registerReportIssueCommand, registerUIExtensionVariables, type IActionContext, type apiUtils } from '@microsoft/vscode-azext-utils'; -import { AzExtResourceType } from '@microsoft/vscode-azureresources-api'; +import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { FunctionAppResolver } from './FunctionAppResolver'; import { FunctionsLocalResourceProvider } from './LocalResourceProvider'; @@ -38,6 +38,7 @@ import { verifyVSCodeConfigOnActivate } from './vsCodeConfig/verifyVSCodeConfigO import { type AzureFunctionsExtensionApi } from './vscode-azurefunctions.api'; import { listLocalFunctions } from './workspace/listLocalFunctions'; import { listLocalProjects } from './workspace/listLocalProjects'; +import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { ext.context = context; @@ -104,6 +105,10 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta ext.azureAccountTreeItem = ext.rgApi.appResourceTree._rootTreeItem as AzureAccountTreeItemBase; ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.FunctionApp, new FunctionAppResolver()); ext.rgApi.registerWorkspaceResourceProvider('func', new FunctionsLocalResourceProvider()); + + const azureResourcesApi = await getAzureResourcesExtensionApi(context, '2.0.0'); + + azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider()); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts new file mode 100644 index 000000000..f8f818de9 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -0,0 +1,25 @@ +import { type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type ProviderResult, TreeItem } from "vscode"; + +export class DurableTaskSchedulerResourceModel implements AzureResourceModel { + public constructor(private readonly resource: AzureResource) { + } + + public get azureResourceId() { return this.resource.id; } + + public get name() { return this.resource.name; } +} + +export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { + getChildren(_: DurableTaskSchedulerResourceModel): ProviderResult { + return []; + } + + getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable { + return new DurableTaskSchedulerResourceModel(element); + } + + getTreeItem(element: DurableTaskSchedulerResourceModel): TreeItem | Thenable { + return new TreeItem(element.name); + } +} From 4d9864d54fbdc90a9918c798a49eead9d7745faa Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Jan 2025 15:27:02 -0800 Subject: [PATCH 02/58] Refactor type hierarchy. Signed-off-by: Phillip Hoff --- .../DurableTaskSchedulerDataBranchProvider.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index f8f818de9..998b7389a 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -1,18 +1,34 @@ -import { type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type ResourceModelBase, type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, TreeItem } from "vscode"; -export class DurableTaskSchedulerResourceModel implements AzureResourceModel { +interface DurableTaskSchedulerModelBase extends ResourceModelBase { + getChildren(_: DurableTaskSchedulerModelBase): ProviderResult; + + getTreeItem(element: DurableTaskSchedulerModelBase): TreeItem | Thenable; +} + +export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModelBase, AzureResourceModel { public constructor(private readonly resource: AzureResource) { } + getChildren(): ProviderResult { + return []; + } + + getTreeItem(): TreeItem | Thenable { + return new TreeItem(this.name); + } + + public get id(): string | undefined { return this.resource.id; } + public get azureResourceId() { return this.resource.id; } public get name() { return this.resource.name; } } export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { - getChildren(_: DurableTaskSchedulerResourceModel): ProviderResult { - return []; + getChildren(element: DurableTaskSchedulerResourceModel): ProviderResult { + return element.getChildren(); } getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable { @@ -20,6 +36,6 @@ export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBran } getTreeItem(element: DurableTaskSchedulerResourceModel): TreeItem | Thenable { - return new TreeItem(element.name); + return element.getTreeItem(); } } From 7df4864049f0bd5e93b2f1d6679bf850533ee714 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Jan 2025 16:15:28 -0800 Subject: [PATCH 03/58] Sketch retrieval of task hubs. Signed-off-by: Phillip Hoff --- .../DurableTaskSchedulerDataBranchProvider.ts | 77 ++++++++++++++++--- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 998b7389a..4d5545bfa 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -1,22 +1,77 @@ -import { type ResourceModelBase, type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; -import { type ProviderResult, TreeItem } from "vscode"; +import { type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type ProviderResult, TreeItem, TreeItemCollapsibleState } from "vscode"; -interface DurableTaskSchedulerModelBase extends ResourceModelBase { - getChildren(_: DurableTaskSchedulerModelBase): ProviderResult; +interface DurableTaskHubResource { + readonly id: string; + readonly name: string; + readonly properties: { + readonly dashboardUrl: string; + }; +} + +interface DurableTaskHubsResponse { + readonly value: DurableTaskHubResource[]; +} + +interface DurableTaskSchedulerModelBase extends AzureResourceModel { + getChildren(): ProviderResult; + + getTreeItem(): TreeItem | Thenable; +} + +export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBase { + constructor(private readonly resource: DurableTaskHubResource) { + } + + get id(): string { return this.resource.id; } + + getChildren(): ProviderResult + { + return []; + } - getTreeItem(element: DurableTaskSchedulerModelBase): TreeItem | Thenable; + getTreeItem(): TreeItem | Thenable + { + return new TreeItem(this.resource.name); + } } export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModelBase, AzureResourceModel { public constructor(private readonly resource: AzureResource) { } - getChildren(): ProviderResult { - return []; + async getChildren(): Promise { + const armEndpoint = 'https://management.azure.com'; + const apiVersion = '2024-10-01-preview'; + + const subscriptionId = this.resource.subscription.subscriptionId; + const resourceGroupName = this.resource.resourceGroup; + const provider = 'Microsoft.DurableTask'; + const schedulerName = this.resource.name; + + const authSession = await this.resource.subscription.authentication.getSession(); + + if (!authSession) { + return []; + } + + const accessToken = authSession.accessToken; + + const taskHubsUrl = `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}/taskHubs?api-version=${apiVersion}`; + + const request = new Request(taskHubsUrl); + + request.headers.append('Authorization', `Bearer ${accessToken}`); + + const response = await fetch(request); + + const taskHubs = await response.json() as DurableTaskHubsResponse; + + return taskHubs.value.map(resource => new DurableTaskHubResourceModel(resource)); } getTreeItem(): TreeItem | Thenable { - return new TreeItem(this.name); + return new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); } public get id(): string | undefined { return this.resource.id; } @@ -26,8 +81,8 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo public get name() { return this.resource.name; } } -export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { - getChildren(element: DurableTaskSchedulerResourceModel): ProviderResult { +export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { + getChildren(element: DurableTaskSchedulerModelBase): ProviderResult { return element.getChildren(); } @@ -35,7 +90,7 @@ export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBran return new DurableTaskSchedulerResourceModel(element); } - getTreeItem(element: DurableTaskSchedulerResourceModel): TreeItem | Thenable { + getTreeItem(element: DurableTaskSchedulerModelBase): TreeItem | Thenable { return element.getTreeItem(); } } From 9212ddc3c170a986ad957127d64b8331fd32bd4c Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Jan 2025 16:32:54 -0800 Subject: [PATCH 04/58] Update task hub icon. Signed-off-by: Phillip Hoff --- .../DurableTaskScheduler.svg | 30 +++++++++++++++++++ .../DurableTaskSchedulerDataBranchProvider.ts | 7 ++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 resources/durableTaskScheduler/DurableTaskScheduler.svg diff --git a/resources/durableTaskScheduler/DurableTaskScheduler.svg b/resources/durableTaskScheduler/DurableTaskScheduler.svg new file mode 100644 index 000000000..6a5efe2f4 --- /dev/null +++ b/resources/durableTaskScheduler/DurableTaskScheduler.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 4d5545bfa..e1e6922ee 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -1,5 +1,6 @@ import { type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { treeUtils } from "../../utils/treeUtils"; interface DurableTaskHubResource { readonly id: string; @@ -32,7 +33,11 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBas getTreeItem(): TreeItem | Thenable { - return new TreeItem(this.resource.name); + const treeItem = new TreeItem(this.resource.name) + + treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); + + return treeItem; } } From c59c09a1a4accd42751570c2a23644f7bc292340 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Jan 2025 16:50:14 -0800 Subject: [PATCH 05/58] Enable "open in portal" command for task hubs. Signed-off-by: Phillip Hoff --- .../DurableTaskSchedulerDataBranchProvider.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index e1e6922ee..b190d5bd9 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -1,5 +1,5 @@ -import { type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; -import { type ProviderResult, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { type AzureSubscription, type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type ProviderResult, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; import { treeUtils } from "../../utils/treeUtils"; interface DurableTaskHubResource { @@ -21,11 +21,20 @@ interface DurableTaskSchedulerModelBase extends AzureResourceModel { } export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBase { - constructor(private readonly resource: DurableTaskHubResource) { + constructor(private readonly subscription: AzureSubscription, private readonly resource: DurableTaskHubResource) { } + public get azureResourceId() { return this.resource.id; } + get id(): string { return this.resource.id; } + get portalUrl(): Uri { + const queryPrefix = ''; + const url: string = `${this.subscription.environment.portalUrl}/${queryPrefix}#@${this.subscription.tenantId}/resource${this.id}`; + + return Uri.parse(url); + } + getChildren(): ProviderResult { return []; @@ -72,7 +81,7 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo const taskHubs = await response.json() as DurableTaskHubsResponse; - return taskHubs.value.map(resource => new DurableTaskHubResourceModel(resource)); + return taskHubs.value.map(resource => new DurableTaskHubResourceModel(this.resource.subscription, resource)); } getTreeItem(): TreeItem | Thenable { From 97ab3c7fb9bb89966c768270da807fdc4016bb56 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Jan 2025 17:01:09 -0800 Subject: [PATCH 06/58] Scaffold "open in dashboard" command. Signed-off-by: Phillip Hoff --- package.json | 9 +++++++++ package.nls.json | 4 +++- .../durableTaskScheduler/openTaskHubDashboard.ts | 6 ++++++ src/commands/registerCommands.ts | 3 +++ .../DurableTaskSchedulerDataBranchProvider.ts | 1 + 5 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/commands/durableTaskScheduler/openTaskHubDashboard.ts diff --git a/package.json b/package.json index 83f10c6a4..24e72e94f 100644 --- a/package.json +++ b/package.json @@ -374,6 +374,11 @@ "title": "%azureFunctions.eventGrid.sendMockRequest%", "category": "Azure Functions", "icon": "$(notebook-execute)" + }, + { + "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", + "title": "%azureFunctions.durableTaskScheduler.openTaskHubDashboard%", + "category": "Azure Functions" } ], "submenus": [ @@ -667,6 +672,10 @@ "command": "azureResourceGroups.refresh", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.*folder/", "group": "1@1" + }, + { + "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/" } ], "explorer/context": [ diff --git a/package.nls.json b/package.nls.json index d8d5b78a0..85c0a60ff 100644 --- a/package.nls.json +++ b/package.nls.json @@ -118,5 +118,7 @@ "azureFunctions.walkthrough.functionsStart.initialize.title": "Initialize an existing project", "azureFunctions.walkthrough.functionsStart.scenarios.description": "Learn how you can use Azure Functions to build event-driven systems.\n\nIf you're just getting started with Azure Functions, you can [learn about the anatomy of an Azure Functions application](https://aka.ms/functions-getstarted-devguide).", "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", - "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions" + "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", + + "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open Dashboard" } diff --git a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts new file mode 100644 index 000000000..4831249fb --- /dev/null +++ b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts @@ -0,0 +1,6 @@ +import { type IActionContext } from "@microsoft/vscode-azext-utils"; +import {type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; + +export async function openTaskHubDashboard(_: IActionContext, __: DurableTaskHubResourceModel | undefined): Promise { + await Promise.resolve(); +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 2befbc369..0232d85cc 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -63,6 +63,7 @@ import { stopFunctionApp } from './stopFunctionApp'; import { swapSlot } from './swapSlot'; import { disableFunction, enableFunction } from './updateDisabledState'; import { viewProperties } from './viewProperties'; +import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboard'; export function registerCommands(): void { commands.registerCommand('azureFunctions.agent.getCommands', getCommands); @@ -154,4 +155,6 @@ export function registerCommands(): void { ext.eventGridProvider = new EventGridCodeLensProvider(); ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); + + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index b190d5bd9..e433a7c00 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -45,6 +45,7 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBas const treeItem = new TreeItem(this.resource.name) treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); + treeItem.contextValue = 'azFunc.dts.taskHub'; return treeItem; } From ad68a74907cad098af7bd7244ad26da8f705300a Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Jan 2025 17:13:31 -0800 Subject: [PATCH 07/58] Sketch "open in dashboard" implementation. Signed-off-by: Phillip Hoff --- package.nls.json | 2 +- src/commands/durableTaskScheduler/openTaskHubDashboard.ts | 8 ++++++-- .../DurableTaskSchedulerDataBranchProvider.ts | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.nls.json b/package.nls.json index 85c0a60ff..15709f06a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,5 +120,5 @@ "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", - "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open Dashboard" + "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" } diff --git a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts index 4831249fb..230eada56 100644 --- a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts +++ b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts @@ -1,6 +1,10 @@ -import { type IActionContext } from "@microsoft/vscode-azext-utils"; +import { openUrl, type IActionContext } from "@microsoft/vscode-azext-utils"; import {type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; export async function openTaskHubDashboard(_: IActionContext, __: DurableTaskHubResourceModel | undefined): Promise { - await Promise.resolve(); + if (!__) { + return; + } + + await openUrl(__?.dashboardUrl.toString(/* skipEncoding: */ true)); } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index e433a7c00..7385154e4 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -26,6 +26,8 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBas public get azureResourceId() { return this.resource.id; } + get dashboardUrl(): Uri { return Uri.parse(this.resource.properties.dashboardUrl); } + get id(): string { return this.resource.id; } get portalUrl(): Uri { From 9f457a5090daefd08bf9e0eace3171b2fe8f0411 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 14 Jan 2025 10:00:12 -0800 Subject: [PATCH 08/58] Move DTS management to separate client type. Signed-off-by: Phillip Hoff --- src/extension.ts | 3 +- .../DurableTaskSchedulerClient.ts | 47 +++++++++++++++++++ .../DurableTaskSchedulerDataBranchProvider.ts | 46 ++++-------------- 3 files changed, 58 insertions(+), 38 deletions(-) create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts diff --git a/src/extension.ts b/src/extension.ts index 81175cf75..fa0a35d2d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -39,6 +39,7 @@ import { type AzureFunctionsExtensionApi } from './vscode-azurefunctions.api'; import { listLocalFunctions } from './workspace/listLocalFunctions'; import { listLocalProjects } from './workspace/listLocalProjects'; import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; +import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient'; export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { ext.context = context; @@ -108,7 +109,7 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta const azureResourcesApi = await getAzureResourcesExtensionApi(context, '2.0.0'); - azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider()); + azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider(new HttpDurableTaskSchedulerClient())); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts new file mode 100644 index 000000000..dae617b48 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -0,0 +1,47 @@ +import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; + +export interface DurableTaskHubResource { + readonly id: string; + readonly name: string; + readonly properties: { + readonly dashboardUrl: string; + }; +} + +interface DurableTaskHubsResponse { + readonly value: DurableTaskHubResource[]; +} + +export interface DurableTaskSchedulerClient { + getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; +} + +export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClient { + async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { + const armEndpoint = subscription.environment.resourceManagerEndpointUrl; + const apiVersion = '2024-10-01-preview'; + + const subscriptionId = subscription.subscriptionId; + const provider = 'Microsoft.DurableTask'; + + const authSession = await subscription.authentication.getSession(); + + if (!authSession) { + throw new Error('Unable to obtain an authentication session.'); + } + + const accessToken = authSession.accessToken; + + const taskHubsUrl = `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}/taskHubs?api-version=${apiVersion}`; + + const request = new Request(taskHubsUrl); + + request.headers.append('Authorization', `Bearer ${accessToken}`); + + const response = await fetch(request); + + const taskHubs = await response.json() as DurableTaskHubsResponse; + + return taskHubs.value; + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 7385154e4..c884b5cb2 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -1,18 +1,7 @@ import { type AzureSubscription, type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; import { treeUtils } from "../../utils/treeUtils"; - -interface DurableTaskHubResource { - readonly id: string; - readonly name: string; - readonly properties: { - readonly dashboardUrl: string; - }; -} - -interface DurableTaskHubsResponse { - readonly value: DurableTaskHubResource[]; -} +import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; interface DurableTaskSchedulerModelBase extends AzureResourceModel { getChildren(): ProviderResult; @@ -54,37 +43,17 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBas } export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModelBase, AzureResourceModel { - public constructor(private readonly resource: AzureResource) { + public constructor(private readonly resource: AzureResource, private readonly schedulerClient: DurableTaskSchedulerClient) { } async getChildren(): Promise { - const armEndpoint = 'https://management.azure.com'; - const apiVersion = '2024-10-01-preview'; - - const subscriptionId = this.resource.subscription.subscriptionId; - const resourceGroupName = this.resource.resourceGroup; - const provider = 'Microsoft.DurableTask'; - const schedulerName = this.resource.name; - - const authSession = await this.resource.subscription.authentication.getSession(); - - if (!authSession) { + if (!this.resource.resourceGroup) { return []; } - const accessToken = authSession.accessToken; + const taskHubs = await this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup, this.resource.name); - const taskHubsUrl = `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}/taskHubs?api-version=${apiVersion}`; - - const request = new Request(taskHubsUrl); - - request.headers.append('Authorization', `Bearer ${accessToken}`); - - const response = await fetch(request); - - const taskHubs = await response.json() as DurableTaskHubsResponse; - - return taskHubs.value.map(resource => new DurableTaskHubResourceModel(this.resource.subscription, resource)); + return taskHubs.map(resource => new DurableTaskHubResourceModel(this.resource.subscription, resource)); } getTreeItem(): TreeItem | Thenable { @@ -99,12 +68,15 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo } export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { + constructor(private readonly schedulerClient: DurableTaskSchedulerClient) { + } + getChildren(element: DurableTaskSchedulerModelBase): ProviderResult { return element.getChildren(); } getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable { - return new DurableTaskSchedulerResourceModel(element); + return new DurableTaskSchedulerResourceModel(element, this.schedulerClient); } getTreeItem(element: DurableTaskSchedulerModelBase): TreeItem | Thenable { From 44c4d45c77241de89641b3337dbd4b483ba268ec Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 14 Jan 2025 10:46:57 -0800 Subject: [PATCH 09/58] Support viewing task hub properties. Signed-off-by: Phillip Hoff --- .../DurableTaskSchedulerClient.ts | 29 +++++++++++++++++ .../DurableTaskSchedulerDataBranchProvider.ts | 31 ++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index dae617b48..f1bbcd06c 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -13,10 +13,39 @@ interface DurableTaskHubsResponse { } export interface DurableTaskSchedulerClient { + getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; } export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClient { + async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { + const armEndpoint = subscription.environment.resourceManagerEndpointUrl; + const apiVersion = '2024-10-01-preview'; + + const subscriptionId = subscription.subscriptionId; + const provider = 'Microsoft.DurableTask'; + + const authSession = await subscription.authentication.getSession(); + + if (!authSession) { + throw new Error('Unable to obtain an authentication session.'); + } + + const accessToken = authSession.accessToken; + + const taskHubsUrl = `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}/taskHubs/${taskHubName}?api-version=${apiVersion}`; + + const request = new Request(taskHubsUrl); + + request.headers.append('Authorization', `Bearer ${accessToken}`); + + const response = await fetch(request); + + const taskHub = await response.json() as DurableTaskHubResource; + + return taskHub; + } + async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { const armEndpoint = subscription.environment.resourceManagerEndpointUrl; const apiVersion = '2024-10-01-preview'; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index c884b5cb2..9f24cfca5 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -1,4 +1,4 @@ -import { type AzureSubscription, type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; import { treeUtils } from "../../utils/treeUtils"; import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; @@ -10,7 +10,10 @@ interface DurableTaskSchedulerModelBase extends AzureResourceModel { } export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBase { - constructor(private readonly subscription: AzureSubscription, private readonly resource: DurableTaskHubResource) { + constructor( + private readonly schedulerResource: AzureResource, + private readonly resource: DurableTaskHubResource, + private readonly schedulerClient: DurableTaskSchedulerClient) { } public get azureResourceId() { return this.resource.id; } @@ -20,12 +23,30 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBas get id(): string { return this.resource.id; } get portalUrl(): Uri { - const queryPrefix = ''; - const url: string = `${this.subscription.environment.portalUrl}/${queryPrefix}#@${this.subscription.tenantId}/resource${this.id}`; + const url: string = `${this.schedulerResource.subscription.environment.portalUrl}/#@${this.schedulerResource.subscription.tenantId}/resource${this.id}`; return Uri.parse(url); } + get viewProperties(): ViewPropertiesModel { + return { + label: this.resource.name, + getData: async () => { + if (!this.schedulerResource.resourceGroup) { + throw new Error('Azure resource does not have a valid resource group name.'); + } + + const json = await this.schedulerClient.getSchedulerTaskHub( + this.schedulerResource.subscription, + this.schedulerResource.resourceGroup, + this.schedulerResource.name, + this.resource.name); + + return json; + } + }; + } + getChildren(): ProviderResult { return []; @@ -53,7 +74,7 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo const taskHubs = await this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup, this.resource.name); - return taskHubs.map(resource => new DurableTaskHubResourceModel(this.resource.subscription, resource)); + return taskHubs.map(resource => new DurableTaskHubResourceModel(this.resource, resource, this.schedulerClient)); } getTreeItem(): TreeItem | Thenable { From 045a889bdb33422b7fdcb44b64641c0922fe32cc Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 14 Jan 2025 11:02:11 -0800 Subject: [PATCH 10/58] Split apart types. Signed-off-by: Phillip Hoff --- .../openTaskHubDashboard.ts | 2 +- .../DurableTaskHubResourceModel.ts | 59 +++++++++++ .../DurableTaskSchedulerDataBranchProvider.ts | 100 ++---------------- .../DurableTaskSchedulerModel.ts | 8 ++ .../DurableTaskSchedulerResourceModel.ts | 30 ++++++ 5 files changed, 106 insertions(+), 93 deletions(-) create mode 100644 src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts diff --git a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts index 230eada56..696e0126e 100644 --- a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts +++ b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts @@ -1,5 +1,5 @@ import { openUrl, type IActionContext } from "@microsoft/vscode-azext-utils"; -import {type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; +import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel"; export async function openTaskHubDashboard(_: IActionContext, __: DurableTaskHubResourceModel | undefined): Promise { if (!__) { diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts new file mode 100644 index 000000000..5837c1621 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -0,0 +1,59 @@ +import { type AzureResource, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; +import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; +import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; +import { type ProviderResult, TreeItem, Uri } from "vscode"; +import { treeUtils } from "../../utils/treeUtils"; + +export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { + constructor( + private readonly schedulerResource: AzureResource, + private readonly resource: DurableTaskHubResource, + private readonly schedulerClient: DurableTaskSchedulerClient) { + } + + public get azureResourceId() { return this.resource.id; } + + get dashboardUrl(): Uri { return Uri.parse(this.resource.properties.dashboardUrl); } + + get id(): string { return this.resource.id; } + + get portalUrl(): Uri { + const url: string = `${this.schedulerResource.subscription.environment.portalUrl}/#@${this.schedulerResource.subscription.tenantId}/resource${this.id}`; + + return Uri.parse(url); + } + + get viewProperties(): ViewPropertiesModel { + return { + label: this.resource.name, + getData: async () => { + if (!this.schedulerResource.resourceGroup) { + throw new Error('Azure resource does not have a valid resource group name.'); + } + + const json = await this.schedulerClient.getSchedulerTaskHub( + this.schedulerResource.subscription, + this.schedulerResource.resourceGroup, + this.schedulerResource.name, + this.resource.name); + + return json; + } + }; + } + + getChildren(): ProviderResult + { + return []; + } + + getTreeItem(): TreeItem | Thenable + { + const treeItem = new TreeItem(this.resource.name) + + treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); + treeItem.contextValue = 'azFunc.dts.taskHub'; + + return treeItem; + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 9f24cfca5..4091c1968 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -1,98 +1,14 @@ -import { type AzureResource, type AzureResourceBranchDataProvider, type AzureResourceModel, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; -import { type ProviderResult, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; -import { treeUtils } from "../../utils/treeUtils"; -import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; +import { type AzureResource, type AzureResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api"; +import { type ProviderResult, type TreeItem } from "vscode"; +import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; +import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; +import { DurableTaskSchedulerResourceModel } from "./DurableTaskSchedulerResourceModel"; -interface DurableTaskSchedulerModelBase extends AzureResourceModel { - getChildren(): ProviderResult; - - getTreeItem(): TreeItem | Thenable; -} - -export class DurableTaskHubResourceModel implements DurableTaskSchedulerModelBase { - constructor( - private readonly schedulerResource: AzureResource, - private readonly resource: DurableTaskHubResource, - private readonly schedulerClient: DurableTaskSchedulerClient) { - } - - public get azureResourceId() { return this.resource.id; } - - get dashboardUrl(): Uri { return Uri.parse(this.resource.properties.dashboardUrl); } - - get id(): string { return this.resource.id; } - - get portalUrl(): Uri { - const url: string = `${this.schedulerResource.subscription.environment.portalUrl}/#@${this.schedulerResource.subscription.tenantId}/resource${this.id}`; - - return Uri.parse(url); - } - - get viewProperties(): ViewPropertiesModel { - return { - label: this.resource.name, - getData: async () => { - if (!this.schedulerResource.resourceGroup) { - throw new Error('Azure resource does not have a valid resource group name.'); - } - - const json = await this.schedulerClient.getSchedulerTaskHub( - this.schedulerResource.subscription, - this.schedulerResource.resourceGroup, - this.schedulerResource.name, - this.resource.name); - - return json; - } - }; - } - - getChildren(): ProviderResult - { - return []; - } - - getTreeItem(): TreeItem | Thenable - { - const treeItem = new TreeItem(this.resource.name) - - treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); - treeItem.contextValue = 'azFunc.dts.taskHub'; - - return treeItem; - } -} - -export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModelBase, AzureResourceModel { - public constructor(private readonly resource: AzureResource, private readonly schedulerClient: DurableTaskSchedulerClient) { - } - - async getChildren(): Promise { - if (!this.resource.resourceGroup) { - return []; - } - - const taskHubs = await this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup, this.resource.name); - - return taskHubs.map(resource => new DurableTaskHubResourceModel(this.resource, resource, this.schedulerClient)); - } - - getTreeItem(): TreeItem | Thenable { - return new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); - } - - public get id(): string | undefined { return this.resource.id; } - - public get azureResourceId() { return this.resource.id; } - - public get name() { return this.resource.name; } -} - -export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { +export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { constructor(private readonly schedulerClient: DurableTaskSchedulerClient) { } - getChildren(element: DurableTaskSchedulerModelBase): ProviderResult { + getChildren(element: DurableTaskSchedulerModel): ProviderResult { return element.getChildren(); } @@ -100,7 +16,7 @@ export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBran return new DurableTaskSchedulerResourceModel(element, this.schedulerClient); } - getTreeItem(element: DurableTaskSchedulerModelBase): TreeItem | Thenable { + getTreeItem(element: DurableTaskSchedulerModel): TreeItem | Thenable { return element.getTreeItem(); } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts new file mode 100644 index 000000000..e3ddd5f46 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts @@ -0,0 +1,8 @@ +import { type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type ProviderResult, type TreeItem } from "vscode"; + +export interface DurableTaskSchedulerModel extends AzureResourceModel { + getChildren(): ProviderResult; + + getTreeItem(): TreeItem | Thenable; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts new file mode 100644 index 000000000..af24000b6 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -0,0 +1,30 @@ +import { type AzureResource, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; +import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; +import { DurableTaskHubResourceModel } from "./DurableTaskHubResourceModel"; +import { TreeItem, TreeItemCollapsibleState } from "vscode"; + +export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModel, AzureResourceModel { + public constructor(private readonly resource: AzureResource, private readonly schedulerClient: DurableTaskSchedulerClient) { + } + + async getChildren(): Promise { + if (!this.resource.resourceGroup) { + return []; + } + + const taskHubs = await this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup, this.resource.name); + + return taskHubs.map(resource => new DurableTaskHubResourceModel(this.resource, resource, this.schedulerClient)); + } + + getTreeItem(): TreeItem | Thenable { + return new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); + } + + public get id(): string | undefined { return this.resource.id; } + + public get azureResourceId() { return this.resource.id; } + + public get name() { return this.resource.name; } +} From 56e0657fdb7aedfe86a591c6b1145c4c7b64ce2b Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 14 Jan 2025 11:05:43 -0800 Subject: [PATCH 11/58] Add file headers. Signed-off-by: Phillip Hoff --- .../durableTaskScheduler/openTaskHubDashboard.ts | 13 +++++++++---- .../DurableTaskHubResourceModel.ts | 5 +++++ .../DurableTaskSchedulerClient.ts | 5 +++++ .../DurableTaskSchedulerDataBranchProvider.ts | 5 +++++ .../DurableTaskSchedulerModel.ts | 5 +++++ .../DurableTaskSchedulerResourceModel.ts | 5 +++++ 6 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts index 696e0126e..07f7f0953 100644 --- a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts +++ b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts @@ -1,10 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { openUrl, type IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel"; -export async function openTaskHubDashboard(_: IActionContext, __: DurableTaskHubResourceModel | undefined): Promise { - if (!__) { - return; +export async function openTaskHubDashboard(_: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise { + if (!taskHub) { + throw new Error('No task hub was selected.'); } - await openUrl(__?.dashboardUrl.toString(/* skipEncoding: */ true)); + await openUrl(taskHub?.dashboardUrl.toString(/* skipEncoding: */ true)); } diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 5837c1621..4a57972bd 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -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 AzureResource, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index f1bbcd06c..54fd74b64 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -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 AzureSubscription } from "@microsoft/vscode-azureresources-api"; export interface DurableTaskHubResource { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 4091c1968..742bc299e 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -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 AzureResource, type AzureResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, type TreeItem } from "vscode"; import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts index e3ddd5f46..48ab2cf2a 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts @@ -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 AzureResourceModel } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, type TreeItem } from "vscode"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index af24000b6..75ac18270 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -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 AzureResource, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; From 872b8fb536622a06cad399be739b892f6a4c051e Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 14 Jan 2025 11:30:02 -0800 Subject: [PATCH 12/58] Consolidate client logic and add localizable strings. Signed-off-by: Phillip Hoff --- .../openTaskHubDashboard.ts | 3 +- .../DurableTaskHubResourceModel.ts | 3 +- .../DurableTaskSchedulerClient.ts | 58 ++++++++----------- .../DurableTaskSchedulerResourceModel.ts | 3 +- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts index 07f7f0953..99b0fc1ba 100644 --- a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts +++ b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts @@ -5,10 +5,11 @@ import { openUrl, type IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel"; +import { localize } from '../../localize'; export async function openTaskHubDashboard(_: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise { if (!taskHub) { - throw new Error('No task hub was selected.'); + throw new Error(localize('noTaskHubSelectedErrorMessage', 'No task hub was selected.')); } await openUrl(taskHub?.dashboardUrl.toString(/* skipEncoding: */ true)); diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 4a57972bd..dd3bd1290 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -8,6 +8,7 @@ import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { type ProviderResult, TreeItem, Uri } from "vscode"; import { treeUtils } from "../../utils/treeUtils"; +import { localize } from '../../localize'; export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { constructor( @@ -33,7 +34,7 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { label: this.resource.name, getData: async () => { if (!this.schedulerResource.resourceGroup) { - throw new Error('Azure resource does not have a valid resource group name.'); + throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); } const json = await this.schedulerClient.getSchedulerTaskHub( diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index 54fd74b64..d37318fc8 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { type AzureAuthentication, type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { localize } from '../../localize'; export interface DurableTaskHubResource { readonly id: string; @@ -13,10 +14,6 @@ export interface DurableTaskHubResource { }; } -interface DurableTaskHubsResponse { - readonly value: DurableTaskHubResource[]; -} - export interface DurableTaskSchedulerClient { getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; @@ -24,58 +21,49 @@ export interface DurableTaskSchedulerClient { export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClient { async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { - const armEndpoint = subscription.environment.resourceManagerEndpointUrl; - const apiVersion = '2024-10-01-preview'; - - const subscriptionId = subscription.subscriptionId; - const provider = 'Microsoft.DurableTask'; - - const authSession = await subscription.authentication.getSession(); + const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`; - if (!authSession) { - throw new Error('Unable to obtain an authentication session.'); - } - - const accessToken = authSession.accessToken; + const taskHub = await this.getAsJson(taskHubsUrl, subscription.authentication); - const taskHubsUrl = `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}/taskHubs/${taskHubName}?api-version=${apiVersion}`; + return taskHub; + } - const request = new Request(taskHubsUrl); + async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { + const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs`; - request.headers.append('Authorization', `Bearer ${accessToken}`); + const response = await this.getAsJson<{ value: DurableTaskHubResource[] }>(taskHubsUrl, subscription.authentication); - const response = await fetch(request); + return response.value; + } - const taskHub = await response.json() as DurableTaskHubResource; + private static getBaseUrl(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string) { + const provider = 'Microsoft.DurableTask'; - return taskHub; + return `${subscription.environment.resourceManagerEndpointUrl}/subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`; } - async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { - const armEndpoint = subscription.environment.resourceManagerEndpointUrl; + private async getAsJson(url: string, authentication: AzureAuthentication): Promise { const apiVersion = '2024-10-01-preview'; + const versionedUrl = `${url}?api-version=${apiVersion}`; - const subscriptionId = subscription.subscriptionId; - const provider = 'Microsoft.DurableTask'; - - const authSession = await subscription.authentication.getSession(); + const authSession = await authentication.getSession(); if (!authSession) { - throw new Error('Unable to obtain an authentication session.'); + throw new Error(localize('noAuthenticationSessionErrorMessage', 'Unable to obtain an authentication session.')); } const accessToken = authSession.accessToken; - const taskHubsUrl = `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}/taskHubs?api-version=${apiVersion}`; - - const request = new Request(taskHubsUrl); + const request = new Request(versionedUrl); request.headers.append('Authorization', `Bearer ${accessToken}`); const response = await fetch(request); - const taskHubs = await response.json() as DurableTaskHubsResponse; + if (!response.ok) { + throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.')); + } - return taskHubs.value; + return await response.json() as T; } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index 75ac18270..463a485b5 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -8,6 +8,7 @@ import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { DurableTaskHubResourceModel } from "./DurableTaskHubResourceModel"; import { TreeItem, TreeItemCollapsibleState } from "vscode"; +import { localize } from '../../localize'; export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModel, AzureResourceModel { public constructor(private readonly resource: AzureResource, private readonly schedulerClient: DurableTaskSchedulerClient) { @@ -15,7 +16,7 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo async getChildren(): Promise { if (!this.resource.resourceGroup) { - return []; + throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); } const taskHubs = await this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup, this.resource.name); From bb92c7699cf6dbe0bdc221a7d43a557259116aee Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 24 Jan 2025 16:40:07 -0800 Subject: [PATCH 13/58] Scaffold creation of task hub. Signed-off-by: Phillip Hoff --- package.json | 9 ++ package.nls.json | 1 + .../durableTaskScheduler/createTaskHub.ts | 78 +++++++++++++++ src/commands/registerCommands.ts | 5 +- src/extension.ts | 6 +- .../DurableTaskSchedulerClient.ts | 96 ++++++++++++++++++- .../DurableTaskSchedulerResourceModel.ts | 16 +++- 7 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 src/commands/durableTaskScheduler/createTaskHub.ts diff --git a/package.json b/package.json index 24e72e94f..4a6413de9 100644 --- a/package.json +++ b/package.json @@ -375,6 +375,11 @@ "category": "Azure Functions", "icon": "$(notebook-execute)" }, + { + "command": "azureFunctions.durableTaskScheduler.createTaskHub", + "title": "%azureFunctions.durableTaskScheduler.createTaskHub%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "title": "%azureFunctions.durableTaskScheduler.openTaskHubDashboard%", @@ -673,6 +678,10 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.*folder/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.createTaskHub", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/" + }, { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/" diff --git a/package.nls.json b/package.nls.json index 15709f06a..2aeff593a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,5 +120,6 @@ "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", + "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub", "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" } diff --git a/src/commands/durableTaskScheduler/createTaskHub.ts b/src/commands/durableTaskScheduler/createTaskHub.ts new file mode 100644 index 000000000..166e46fd7 --- /dev/null +++ b/src/commands/durableTaskScheduler/createTaskHub.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, type IActionContext } from "@microsoft/vscode-azext-utils"; +import { localize } from '../../localize'; +import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; +import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; +import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { type Progress } from "vscode"; + +interface ICreateTaskHubContext extends IActionContext { + readonly subscription: AzureSubscription; + readonly resourceGroup: string; + readonly schedulerName: string; + taskHubName?: string; +} + +class TaskHubNamingStep extends AzureWizardPromptStep { + async prompt(wizardContext: ICreateTaskHubContext): Promise { + wizardContext.taskHubName = await wizardContext.ui.showInputBox({ + prompt: localize('taskHubNamingStepPrompt', 'Enter a name for the new task hub') + }) + } + + shouldPrompt(wizardContext: ICreateTaskHubContext): boolean { + return !wizardContext.taskHubName; + } +} + +class TaskHubCreationStep extends AzureWizardExecuteStep { + priority: number = 1; + + constructor(private readonly schedulerClient: DurableTaskSchedulerClient) { + super(); + } + + async execute(wizardContext: ICreateTaskHubContext, _: Progress<{ message?: string; increment?: number; }>): Promise { + await this.schedulerClient.createTaskHub( + wizardContext.subscription, + wizardContext.resourceGroup, + wizardContext.schedulerName, + wizardContext.taskHubName as string + ); + } + + shouldExecute(wizardContext: ICreateTaskHubContext): boolean { + return wizardContext.taskHubName !== undefined; + } +} + +export function createTaskHubCommandFactory(schedulerClient: DurableTaskSchedulerClient) { + return async (actionContext: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { + if (!scheduler) { + throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); + } + + const wizardContext: ICreateTaskHubContext = + { + ...actionContext, + subscription: scheduler.subscription, + resourceGroup: scheduler.resourceGroup, + schedulerName: scheduler.name + }; + + const wizard = new AzureWizard( + wizardContext, + { + promptSteps: [new TaskHubNamingStep()], + executeSteps: [new TaskHubCreationStep(schedulerClient)], + title: localize('createTaskHubWizardTitle', 'Create Task Hub') + }); + + await wizard.prompt(); + await wizard.execute(); + } +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 0232d85cc..5ee4be6ff 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -64,8 +64,10 @@ import { swapSlot } from './swapSlot'; import { disableFunction, enableFunction } from './updateDisabledState'; import { viewProperties } from './viewProperties'; import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboard'; +import { createTaskHubCommandFactory } from './durableTaskScheduler/createTaskHub'; +import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerClient'; -export function registerCommands(): void { +export function registerCommands(schedulerClient: DurableTaskSchedulerClient): void { commands.registerCommand('azureFunctions.agent.getCommands', getCommands); commands.registerCommand('azureFunctions.agent.runWizardCommandWithoutExecution', runWizardCommandWithoutExecution); commands.registerCommand('azureFunctions.agent.runWizardCommandWithInputs', runWizardCommandWithInputs); @@ -156,5 +158,6 @@ export function registerCommands(): void { ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); } diff --git a/src/extension.ts b/src/extension.ts index fa0a35d2d..1badc6f93 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -75,7 +75,9 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta registerErrorHandler(c => c.errorHandling.suppressReportIssue = true); registerReportIssueCommand('azureFunctions.reportIssue'); - registerCommands(); + const schedulerClient = new HttpDurableTaskSchedulerClient(); + + registerCommands(schedulerClient); registerFuncHostTaskEvents(); @@ -109,7 +111,7 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta const azureResourcesApi = await getAzureResourcesExtensionApi(context, '2.0.0'); - azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider(new HttpDurableTaskSchedulerClient())); + azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider(schedulerClient)); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index d37318fc8..da9fbdd30 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -6,6 +6,27 @@ import { type AzureAuthentication, type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { localize } from '../../localize'; +interface DurableTaskSchedulerCreateRequest { + readonly location: string; + readonly properties: { + readonly ipAllowList: string[]; + readonly sku: { + readonly name: string; + readonly capacity: number; + }; + }; + readonly tags: unknown; +} + +interface DurableTaskHubCreateRequest { + readonly properties: unknown; +} + +export interface DurableTaskSchedulerResource { + readonly id: string; + readonly name: string; +} + export interface DurableTaskHubResource { readonly id: string; readonly name: string; @@ -15,11 +36,54 @@ export interface DurableTaskHubResource { } export interface DurableTaskSchedulerClient { + createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise; + createTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; + getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; } export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClient { + async createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise { + const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName); + + const request: DurableTaskSchedulerCreateRequest = { + location, + properties: { + ipAllowList: ['0.0.0.0/0'], + sku: { + name: 'Dedicated', + capacity: 1 + } + }, + tags: { + } + }; + + const scheduler = await this.putAsJson( + taskHubsUrl, + request, + subscription.authentication); + + return scheduler; + } + + async createTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { + const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskhubs/${taskHubName}`; + + const request: DurableTaskHubCreateRequest = { + properties: { + } + }; + + const taskHub = await this.putAsJson( + taskHubsUrl, + request, + subscription.authentication); + + return taskHub; + } + async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`; @@ -39,7 +103,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien private static getBaseUrl(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string) { const provider = 'Microsoft.DurableTask'; - return `${subscription.environment.resourceManagerEndpointUrl}/subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`; + return `${subscription.environment.resourceManagerEndpointUrl}subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`; } private async getAsJson(url: string, authentication: AzureAuthentication): Promise { @@ -66,4 +130,34 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return await response.json() as T; } + + private async putAsJson(url: string, body: unknown, authentication: AzureAuthentication): Promise { + const apiVersion = '2024-10-01-preview'; + const versionedUrl = `${url}?api-version=${apiVersion}`; + + const authSession = await authentication.getSession(); + + if (!authSession) { + throw new Error(localize('noAuthenticationSessionErrorMessage', 'Unable to obtain an authentication session.')); + } + + const accessToken = authSession.accessToken; + + const request = new Request( + versionedUrl, + { + body: JSON.stringify(body), + method: 'PUT' + }); + + request.headers.append('Authorization', `Bearer ${accessToken}`); + + const response = await fetch(request); + + if (!response.ok) { + throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.')); + } + + return await response.json() as T; + } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index 463a485b5..a961c3d4b 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -25,7 +25,11 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo } getTreeItem(): TreeItem | Thenable { - return new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); + const treeItem = new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); + + treeItem.contextValue = 'azFunc.dts.scheduler'; + + return treeItem; } public get id(): string | undefined { return this.resource.id; } @@ -33,4 +37,14 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo public get azureResourceId() { return this.resource.id; } public get name() { return this.resource.name; } + + get resourceGroup() { + if (!this.resource.resourceGroup) { + throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); + } + + return this.resource.resourceGroup; + } + + get subscription() { return this.resource.subscription; } } From f36b3c345652e95c5175f1a69ddbea2df8bbac3b Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 27 Jan 2025 14:39:30 -0800 Subject: [PATCH 14/58] Sketch creation of schedulers. Signed-off-by: Phillip Hoff --- package.json | 9 ++ package.nls.json | 1 + .../durableTaskScheduler/createScheduler.ts | 89 +++++++++++++++++++ src/commands/registerCommands.ts | 2 + src/extension.ts | 2 + src/extensionVariables.ts | 2 + .../DurableTaskSchedulerClient.ts | 7 +- 7 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/commands/durableTaskScheduler/createScheduler.ts diff --git a/package.json b/package.json index 4a6413de9..5be3e488d 100644 --- a/package.json +++ b/package.json @@ -375,6 +375,11 @@ "category": "Azure Functions", "icon": "$(notebook-execute)" }, + { + "command": "azureFunctions.durableTaskScheduler.createScheduler", + "title": "%azureFunctions.durableTaskScheduler.createScheduler%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.createTaskHub", "title": "%azureFunctions.durableTaskScheduler.createTaskHub%", @@ -678,6 +683,10 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.*folder/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.createScheduler", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i" + }, { "command": "azureFunctions.durableTaskScheduler.createTaskHub", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/" diff --git a/package.nls.json b/package.nls.json index 2aeff593a..eb0ba1432 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,6 +120,7 @@ "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", + "azureFunctions.durableTaskScheduler.createScheduler": "Create Scheduler", "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub", "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" } diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts new file mode 100644 index 000000000..9117aa849 --- /dev/null +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, type IActionContext, subscriptionExperience, createSubscriptionContext, type ExecuteActivityContext, type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; +import { localize } from '../../localize'; +import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; +import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { type Progress } from "vscode"; +import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; +import { ext } from '../../extensionVariables'; +import { createActivityContext } from "../../utils/activityUtils"; + +interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationWizardContext, IResourceGroupWizardContext, ExecuteActivityContext { + subscription?: AzureSubscription; + schedulerName?: string; +} + +class SchedulerNamingStep extends AzureWizardPromptStep { + async prompt(wizardContext: ICreateSchedulerContext): Promise { + wizardContext.schedulerName = await wizardContext.ui.showInputBox({ + prompt: localize('taskHubNamingStepPrompt', 'Enter a name for the new task hub') + }) + } + + shouldPrompt(wizardContext: ICreateSchedulerContext): boolean { + return !wizardContext.schedulerName; + } +} + +class SchedulerCreationStep extends AzureWizardExecuteStep { + priority: number = 1; + + constructor(private readonly schedulerClient: DurableTaskSchedulerClient) { + super(); + } + + async execute(wizardContext: ICreateSchedulerContext, _: Progress<{ message?: string; increment?: number; }>): Promise { + const location = await LocationListStep.getLocation(wizardContext); + + await this.schedulerClient.createScheduler( + wizardContext.subscription as AzureSubscription, + wizardContext.resourceGroup?.name as string, + location.name, + wizardContext.schedulerName as string + ); + } + + shouldExecute(wizardContext: ICreateSchedulerContext): boolean { + return wizardContext.subscription !== undefined + && wizardContext.resourceGroup !== undefined + && wizardContext.schedulerName !== undefined; + } +} + +export function createSchedulerCommandFactory(schedulerClient: DurableTaskSchedulerClient) { + return async (actionContext: IActionContext, node?: { subscription: AzureSubscription }): Promise => { + const subscription = node?.subscription ?? await subscriptionExperience(actionContext, ext.rgApiV2.resources.azureResourceTreeDataProvider); + + const wizardContext = + { + subscription, + + ...actionContext, + ...createSubscriptionContext(subscription), + ...await createActivityContext() + }; + + const promptSteps: AzureWizardPromptStep[] = [ new ResourceGroupListStep(), new SchedulerNamingStep() ]; + + LocationListStep.addStep(wizardContext, promptSteps); + + const wizard = new AzureWizard( + wizardContext, + { + hideStepCount: true, + promptSteps, + executeSteps: [ + new ResourceGroupCreateStep(), + new SchedulerCreationStep(schedulerClient) + ], + title: localize('createSchedulerWizardTitle', 'Create Scheduler') + }); + + await wizard.prompt(); + await wizard.execute(); + } +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 5ee4be6ff..060f715f9 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -66,6 +66,7 @@ import { viewProperties } from './viewProperties'; import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboard'; import { createTaskHubCommandFactory } from './durableTaskScheduler/createTaskHub'; import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerClient'; +import { createSchedulerCommandFactory } from './durableTaskScheduler/createScheduler'; export function registerCommands(schedulerClient: DurableTaskSchedulerClient): void { commands.registerCommand('azureFunctions.agent.getCommands', getCommands); @@ -158,6 +159,7 @@ export function registerCommands(schedulerClient: DurableTaskSchedulerClient): v ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); } diff --git a/src/extension.ts b/src/extension.ts index 1badc6f93..7e8620c5c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -111,6 +111,8 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta const azureResourcesApi = await getAzureResourcesExtensionApi(context, '2.0.0'); + ext.rgApiV2 = azureResourcesApi; + azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider(schedulerClient)); }); diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index b52cdfc2d..51c6094ad 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -12,6 +12,7 @@ import { type CentralTemplateProvider } from './templates/CentralTemplateProvide import { type AzureAccountTreeItemWithProjects } from './tree/AzureAccountTreeItemWithProjects'; import { type FunctionTreeItemBase } from './tree/FunctionTreeItemBase'; import { type IFunction } from './workspace/LocalFunction'; +import { type AzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; /** * Used for extensionVariables that can also be set per-action @@ -57,6 +58,7 @@ export namespace ext { export let experimentationService: IExperimentationServiceAdapter; export const templateProvider = new ActionVariable('_centralTemplateProvider'); export let rgApi: AzureHostExtensionApi; + export let rgApiV2: AzureResourcesExtensionApi; export let eventGridProvider: EventGridCodeLensProvider; export let currentExecutingFunctionNode: FunctionTreeItemBase | IFunction | undefined; export const fileToFunctionNodeMap: Map = new Map(); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index da9fbdd30..d7cad21c2 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -9,7 +9,7 @@ import { localize } from '../../localize'; interface DurableTaskSchedulerCreateRequest { readonly location: string; readonly properties: { - readonly ipAllowList: string[]; + readonly ipAllowlist: string[]; readonly sku: { readonly name: string; readonly capacity: number; @@ -50,7 +50,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien const request: DurableTaskSchedulerCreateRequest = { location, properties: { - ipAllowList: ['0.0.0.0/0'], + ipAllowlist: ['0.0.0.0/0'], sku: { name: 'Dedicated', capacity: 1 @@ -150,7 +150,8 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien method: 'PUT' }); - request.headers.append('Authorization', `Bearer ${accessToken}`); + request.headers.set('Authorization', `Bearer ${accessToken}`); + request.headers.set('Content-Type', 'application/json'); const response = await fetch(request); From 5008deff97d6039f42e78b8123f544576d41e98d Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 27 Jan 2025 15:33:39 -0800 Subject: [PATCH 15/58] Expose DTS creation from common new menu. Signed-off-by: Phillip Hoff --- package.json | 6 ++++++ package.nls.json | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 983b4fcdd..42bd2464e 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,12 @@ "resources": true }, "commands": [ + { + "command": "azureFunctions.durableTaskScheduler.createScheduler", + "title": "%azureFunctions.durableTaskScheduler.createScheduler%", + "type": "DurableTaskScheduler", + "detail": "%azureFunctions.durableTaskScheduler.createScheduler.detail%" + }, { "command": "azureFunctions.createFunctionApp", "title": "%azureFunctions.createFunctionApp%", diff --git a/package.nls.json b/package.nls.json index eb0ba1432..eb303cd72 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,7 +120,8 @@ "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", - "azureFunctions.durableTaskScheduler.createScheduler": "Create Scheduler", - "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub", + "azureFunctions.durableTaskScheduler.createScheduler": "Create Durable Task Scheduler...", + "azureFunctions.durableTaskScheduler.createScheduler.detail": "For long-running and reliable workflows.", + "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub...", "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" } From b22d97db0854c11d9de4ea55a0d26e2ba7e417c4 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 27 Jan 2025 16:21:29 -0800 Subject: [PATCH 16/58] Sketch deletion of task hubs. Signed-off-by: Phillip Hoff --- package.json | 16 +++++++- package.nls.json | 1 + .../durableTaskScheduler/deleteTaskHub.ts | 41 +++++++++++++++++++ src/commands/registerCommands.ts | 2 + .../DurableTaskHubResourceModel.ts | 16 +++++++- .../DurableTaskSchedulerClient.ts | 35 ++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/commands/durableTaskScheduler/deleteTaskHub.ts diff --git a/package.json b/package.json index 42bd2464e..0cb3d6153 100644 --- a/package.json +++ b/package.json @@ -391,6 +391,11 @@ "title": "%azureFunctions.durableTaskScheduler.createTaskHub%", "category": "Azure Functions" }, + { + "command": "azureFunctions.durableTaskScheduler.deleteTaskHub", + "title": "%azureFunctions.durableTaskScheduler.deleteTaskHub%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "title": "%azureFunctions.durableTaskScheduler.openTaskHubDashboard%", @@ -695,11 +700,18 @@ }, { "command": "azureFunctions.durableTaskScheduler.createTaskHub", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/" + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", + "group": "1@1" + }, + { + "command": "azureFunctions.durableTaskScheduler.deleteTaskHub", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/", + "group": "2@1" }, { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/" + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/", + "group": "1@1" } ], "explorer/context": [ diff --git a/package.nls.json b/package.nls.json index eb303cd72..0805d95de 100644 --- a/package.nls.json +++ b/package.nls.json @@ -123,5 +123,6 @@ "azureFunctions.durableTaskScheduler.createScheduler": "Create Durable Task Scheduler...", "azureFunctions.durableTaskScheduler.createScheduler.detail": "For long-running and reliable workflows.", "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub...", + "azureFunctions.durableTaskScheduler.deleteTaskHub": "Delete Task Hub...", "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" } diff --git a/src/commands/durableTaskScheduler/deleteTaskHub.ts b/src/commands/durableTaskScheduler/deleteTaskHub.ts new file mode 100644 index 000000000..76682739f --- /dev/null +++ b/src/commands/durableTaskScheduler/deleteTaskHub.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from "@microsoft/vscode-azext-utils"; +import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; +import { localize } from "../../localize"; +import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel"; +import { type MessageItem } from "vscode"; + +export function deleteTaskHubCommandFactory(schedulerClient: DurableTaskSchedulerClient) { + return async (actionContext: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise => { + if (!taskHub) { + throw new Error(localize('noTaskHubSelectedErrorMessage', 'No task hub was selected.')); + } + + const deleteItem: MessageItem = { + title: 'Delete' + }; + + const result = await actionContext.ui.showWarningMessage( + localize('deleteTaskHubConfirmationMessage', 'Are you sure you want to delete task hub \'{0}\'?', taskHub.name), + { + modal: true + }, + deleteItem + ); + + if (result !== deleteItem) { + return; + } + + await schedulerClient.deleteTaskHub( + taskHub.subscription, + taskHub.resourceGroup, + taskHub.schedulerName, + taskHub.name + ); + } +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 060f715f9..659385274 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -67,6 +67,7 @@ import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboar import { createTaskHubCommandFactory } from './durableTaskScheduler/createTaskHub'; import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerClient'; import { createSchedulerCommandFactory } from './durableTaskScheduler/createScheduler'; +import { deleteTaskHubCommandFactory } from './durableTaskScheduler/deleteTaskHub'; export function registerCommands(schedulerClient: DurableTaskSchedulerClient): void { commands.registerCommand('azureFunctions.agent.getCommands', getCommands); @@ -161,5 +162,6 @@ export function registerCommands(schedulerClient: DurableTaskSchedulerClient): v registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(schedulerClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteTaskHub', deleteTaskHubCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); } diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index dd3bd1290..53e01ccae 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -55,11 +55,25 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { getTreeItem(): TreeItem | Thenable { - const treeItem = new TreeItem(this.resource.name) + const treeItem = new TreeItem(this.name) treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); treeItem.contextValue = 'azFunc.dts.taskHub'; return treeItem; } + + get name() { return this.resource.name; } + + get resourceGroup() { + if (!this.schedulerResource.resourceGroup) { + throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); + } + + return this.schedulerResource.resourceGroup; + } + + get schedulerName() { return this.schedulerResource.name; } + + get subscription() { return this.schedulerResource.subscription; } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index d7cad21c2..116a92125 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -39,6 +39,8 @@ export interface DurableTaskSchedulerClient { createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise; createTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; + deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; + getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; } @@ -84,6 +86,12 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return taskHub; } + async deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { + const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskhubs/${taskHubName}`; + + await this.delete(taskHubsUrl, subscription.authentication); + } + async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`; @@ -106,6 +114,33 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return `${subscription.environment.resourceManagerEndpointUrl}subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`; } + private async delete(url: string, authentication: AzureAuthentication): Promise { + const apiVersion = '2024-10-01-preview'; + const versionedUrl = `${url}?api-version=${apiVersion}`; + + const authSession = await authentication.getSession(); + + if (!authSession) { + throw new Error(localize('noAuthenticationSessionErrorMessage', 'Unable to obtain an authentication session.')); + } + + const accessToken = authSession.accessToken; + + const request = new Request( + versionedUrl, + { + method: 'DELETE' + }); + + request.headers.append('Authorization', `Bearer ${accessToken}`); + + const response = await fetch(request); + + if (!response.ok) { + throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.')); + } + } + private async getAsJson(url: string, authentication: AzureAuthentication): Promise { const apiVersion = '2024-10-01-preview'; const versionedUrl = `${url}?api-version=${apiVersion}`; From 2bf4748f44378dc727ba714271edaa252fd6a21b Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 27 Jan 2025 16:38:14 -0800 Subject: [PATCH 17/58] Sketch deletion of schedulers. Signed-off-by: Phillip Hoff --- package.json | 10 +++++ package.nls.json | 1 + .../durableTaskScheduler/deleteScheduler.ts | 40 +++++++++++++++++++ src/commands/registerCommands.ts | 2 + .../DurableTaskSchedulerClient.ts | 7 ++++ 5 files changed, 60 insertions(+) create mode 100644 src/commands/durableTaskScheduler/deleteScheduler.ts diff --git a/package.json b/package.json index 0cb3d6153..e449a7202 100644 --- a/package.json +++ b/package.json @@ -391,6 +391,11 @@ "title": "%azureFunctions.durableTaskScheduler.createTaskHub%", "category": "Azure Functions" }, + { + "command": "azureFunctions.durableTaskScheduler.deleteScheduler", + "title": "%azureFunctions.durableTaskScheduler.deleteScheduler%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.deleteTaskHub", "title": "%azureFunctions.durableTaskScheduler.deleteTaskHub%", @@ -703,6 +708,11 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.deleteScheduler", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", + "group": "2@1" + }, { "command": "azureFunctions.durableTaskScheduler.deleteTaskHub", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/", diff --git a/package.nls.json b/package.nls.json index 0805d95de..d676dba15 100644 --- a/package.nls.json +++ b/package.nls.json @@ -123,6 +123,7 @@ "azureFunctions.durableTaskScheduler.createScheduler": "Create Durable Task Scheduler...", "azureFunctions.durableTaskScheduler.createScheduler.detail": "For long-running and reliable workflows.", "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub...", + "azureFunctions.durableTaskScheduler.deleteScheduler": "Delete Scheduler...", "azureFunctions.durableTaskScheduler.deleteTaskHub": "Delete Task Hub...", "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" } diff --git a/src/commands/durableTaskScheduler/deleteScheduler.ts b/src/commands/durableTaskScheduler/deleteScheduler.ts new file mode 100644 index 000000000..4477904ba --- /dev/null +++ b/src/commands/durableTaskScheduler/deleteScheduler.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 {type IActionContext } from "@microsoft/vscode-azext-utils"; +import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; +import { localize } from "../../localize"; +import { type MessageItem } from "vscode"; +import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; + +export function deleteSchedulerCommandFactory(schedulerClient: DurableTaskSchedulerClient) { + return async (actionContext: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { + if (!scheduler) { + throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); + } + + const deleteItem: MessageItem = { + title: 'Delete' + }; + + const result = await actionContext.ui.showWarningMessage( + localize('deleteSchedulerConfirmationMessage', 'Are you sure you want to delete scheduler \'{0}\'?', scheduler.name), + { + modal: true + }, + deleteItem + ); + + if (result !== deleteItem) { + return; + } + + await schedulerClient.deleteScheduler( + scheduler.subscription, + scheduler.resourceGroup, + scheduler.name + ); + } +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 659385274..7cf388bbd 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -68,6 +68,7 @@ import { createTaskHubCommandFactory } from './durableTaskScheduler/createTaskHu import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerClient'; import { createSchedulerCommandFactory } from './durableTaskScheduler/createScheduler'; import { deleteTaskHubCommandFactory } from './durableTaskScheduler/deleteTaskHub'; +import { deleteSchedulerCommandFactory } from './durableTaskScheduler/deleteScheduler'; export function registerCommands(schedulerClient: DurableTaskSchedulerClient): void { commands.registerCommand('azureFunctions.agent.getCommands', getCommands); @@ -162,6 +163,7 @@ export function registerCommands(schedulerClient: DurableTaskSchedulerClient): v registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(schedulerClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteScheduler', deleteSchedulerCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteTaskHub', deleteTaskHubCommandFactory(schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index 116a92125..83c3925bb 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -39,6 +39,7 @@ export interface DurableTaskSchedulerClient { createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise; createTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; + deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; @@ -86,6 +87,12 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return taskHub; } + async deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { + const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}`; + + await this.delete(taskHubsUrl, subscription.authentication); + } + async deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskhubs/${taskHubName}`; From c7412645927d164f697f425ecb3b8e6a0626acbb Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 30 Jan 2025 13:37:15 -0800 Subject: [PATCH 18/58] Sketch refreshing models post-creation. Signed-off-by: Phillip Hoff --- src/commands/durableTaskScheduler/createTaskHub.ts | 8 +++++++- .../DurableTaskSchedulerDataBranchProvider.ts | 11 +++++++++-- .../DurableTaskSchedulerResourceModel.ts | 9 ++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/commands/durableTaskScheduler/createTaskHub.ts b/src/commands/durableTaskScheduler/createTaskHub.ts index 166e46fd7..3bf59a218 100644 --- a/src/commands/durableTaskScheduler/createTaskHub.ts +++ b/src/commands/durableTaskScheduler/createTaskHub.ts @@ -73,6 +73,12 @@ export function createTaskHubCommandFactory(schedulerClient: DurableTaskSchedule }); await wizard.prompt(); - await wizard.execute(); + + try { + await wizard.execute(); + } + finally { + scheduler.refresh(); + } } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 742bc299e..477cc84cd 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -4,21 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import { type AzureResource, type AzureResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api"; -import { type ProviderResult, type TreeItem } from "vscode"; +import { EventEmitter, type ProviderResult, type TreeItem } from "vscode"; import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { DurableTaskSchedulerResourceModel } from "./DurableTaskSchedulerResourceModel"; export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { + private readonly onDidChangeTreeDataEventEmitter = new EventEmitter(); + constructor(private readonly schedulerClient: DurableTaskSchedulerClient) { } + get onDidChangeTreeData() { return this.onDidChangeTreeDataEventEmitter.event; } + getChildren(element: DurableTaskSchedulerModel): ProviderResult { return element.getChildren(); } getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable { - return new DurableTaskSchedulerResourceModel(element, this.schedulerClient); + return new DurableTaskSchedulerResourceModel( + element, + this.schedulerClient, + model => this.onDidChangeTreeDataEventEmitter.fire(model)); } getTreeItem(element: DurableTaskSchedulerModel): TreeItem | Thenable { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index a961c3d4b..d8f3e73c2 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -11,7 +11,10 @@ import { TreeItem, TreeItemCollapsibleState } from "vscode"; import { localize } from '../../localize'; export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModel, AzureResourceModel { - public constructor(private readonly resource: AzureResource, private readonly schedulerClient: DurableTaskSchedulerClient) { + public constructor( + private readonly resource: AzureResource, + private readonly schedulerClient: DurableTaskSchedulerClient, + private readonly refreshModel: (model: DurableTaskSchedulerModel | undefined) => void) { } async getChildren(): Promise { @@ -32,6 +35,10 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo return treeItem; } + refresh() { + this.refreshModel(this); + } + public get id(): string | undefined { return this.resource.id; } public get azureResourceId() { return this.resource.id; } From e3f11252ca179ca5bc97e25ccfc987a54a77f078 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 30 Jan 2025 14:06:44 -0800 Subject: [PATCH 19/58] Add tree refresh to remainder of DTS commands. Signed-off-by: Phillip Hoff --- .../durableTaskScheduler/createScheduler.ts | 11 ++++++-- .../durableTaskScheduler/deleteScheduler.ts | 18 ++++++++----- .../durableTaskScheduler/deleteTaskHub.ts | 17 +++++++----- src/commands/registerCommands.ts | 17 ++++++++---- src/extension.ts | 10 +++++-- .../DurableTaskHubResourceModel.ts | 27 ++++++------------- .../DurableTaskSchedulerDataBranchProvider.ts | 4 +++ .../DurableTaskSchedulerResourceModel.ts | 2 +- 8 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index 9117aa849..12387b46e 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -11,6 +11,7 @@ import { type Progress } from "vscode"; import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; import { ext } from '../../extensionVariables'; import { createActivityContext } from "../../utils/activityUtils"; +import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationWizardContext, IResourceGroupWizardContext, ExecuteActivityContext { subscription?: AzureSubscription; @@ -54,7 +55,7 @@ class SchedulerCreationStep extends AzureWizardExecuteStep => { const subscription = node?.subscription ?? await subscriptionExperience(actionContext, ext.rgApiV2.resources.azureResourceTreeDataProvider); @@ -84,6 +85,12 @@ export function createSchedulerCommandFactory(schedulerClient: DurableTaskSchedu }); await wizard.prompt(); - await wizard.execute(); + + try { + await wizard.execute(); + } + finally { + dataBranchProvider.refresh(); + } } } diff --git a/src/commands/durableTaskScheduler/deleteScheduler.ts b/src/commands/durableTaskScheduler/deleteScheduler.ts index 4477904ba..1f2b90fa3 100644 --- a/src/commands/durableTaskScheduler/deleteScheduler.ts +++ b/src/commands/durableTaskScheduler/deleteScheduler.ts @@ -8,8 +8,9 @@ import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler import { localize } from "../../localize"; import { type MessageItem } from "vscode"; import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; +import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; -export function deleteSchedulerCommandFactory(schedulerClient: DurableTaskSchedulerClient) { +export function deleteSchedulerCommandFactory(dataBranchProvider: DurableTaskSchedulerDataBranchProvider, schedulerClient: DurableTaskSchedulerClient) { return async (actionContext: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { if (!scheduler) { throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); @@ -31,10 +32,15 @@ export function deleteSchedulerCommandFactory(schedulerClient: DurableTaskSchedu return; } - await schedulerClient.deleteScheduler( - scheduler.subscription, - scheduler.resourceGroup, - scheduler.name - ); + try { + await schedulerClient.deleteScheduler( + scheduler.subscription, + scheduler.resourceGroup, + scheduler.name + ); + } + finally { + dataBranchProvider.refresh(); + } } } diff --git a/src/commands/durableTaskScheduler/deleteTaskHub.ts b/src/commands/durableTaskScheduler/deleteTaskHub.ts index 76682739f..0954fb9a4 100644 --- a/src/commands/durableTaskScheduler/deleteTaskHub.ts +++ b/src/commands/durableTaskScheduler/deleteTaskHub.ts @@ -31,11 +31,16 @@ export function deleteTaskHubCommandFactory(schedulerClient: DurableTaskSchedule return; } - await schedulerClient.deleteTaskHub( - taskHub.subscription, - taskHub.resourceGroup, - taskHub.schedulerName, - taskHub.name - ); + try { + await schedulerClient.deleteTaskHub( + taskHub.scheduler.subscription, + taskHub.scheduler.resourceGroup, + taskHub.scheduler.name, + taskHub.name + ); + } + finally { + taskHub.scheduler.refresh(); + } } } diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 7cf388bbd..13c5d32f7 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -69,8 +69,15 @@ import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/Du import { createSchedulerCommandFactory } from './durableTaskScheduler/createScheduler'; import { deleteTaskHubCommandFactory } from './durableTaskScheduler/deleteTaskHub'; import { deleteSchedulerCommandFactory } from './durableTaskScheduler/deleteScheduler'; +import { type DurableTaskSchedulerDataBranchProvider } from '../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; -export function registerCommands(schedulerClient: DurableTaskSchedulerClient): void { +export function registerCommands( + services: { + dts: { + dataBranchProvider: DurableTaskSchedulerDataBranchProvider, + schedulerClient: DurableTaskSchedulerClient + } + }): void { commands.registerCommand('azureFunctions.agent.getCommands', getCommands); commands.registerCommand('azureFunctions.agent.runWizardCommandWithoutExecution', runWizardCommandWithoutExecution); commands.registerCommand('azureFunctions.agent.runWizardCommandWithInputs', runWizardCommandWithInputs); @@ -161,9 +168,9 @@ export function registerCommands(schedulerClient: DurableTaskSchedulerClient): v ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); - registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(schedulerClient)); - registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(schedulerClient)); - registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteScheduler', deleteSchedulerCommandFactory(schedulerClient)); - registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteTaskHub', deleteTaskHubCommandFactory(schedulerClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(services.dts.schedulerClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteScheduler', deleteSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteTaskHub', deleteTaskHubCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); } diff --git a/src/extension.ts b/src/extension.ts index 7e8620c5c..885aa65d5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -76,8 +76,14 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta registerReportIssueCommand('azureFunctions.reportIssue'); const schedulerClient = new HttpDurableTaskSchedulerClient(); + const dataBranchProvider = new DurableTaskSchedulerDataBranchProvider(schedulerClient); - registerCommands(schedulerClient); + registerCommands({ + dts: { + dataBranchProvider, + schedulerClient + } + }); registerFuncHostTaskEvents(); @@ -113,7 +119,7 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta ext.rgApiV2 = azureResourcesApi; - azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider(schedulerClient)); + azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, dataBranchProvider); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 53e01ccae..0c8714a3b 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzureResource, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; +import { type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { type ProviderResult, TreeItem, Uri } from "vscode"; import { treeUtils } from "../../utils/treeUtils"; import { localize } from '../../localize'; +import { type DurableTaskSchedulerResourceModel } from "./DurableTaskSchedulerResourceModel"; export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { constructor( - private readonly schedulerResource: AzureResource, + public readonly scheduler: DurableTaskSchedulerResourceModel, private readonly resource: DurableTaskHubResource, private readonly schedulerClient: DurableTaskSchedulerClient) { } @@ -24,7 +25,7 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { get id(): string { return this.resource.id; } get portalUrl(): Uri { - const url: string = `${this.schedulerResource.subscription.environment.portalUrl}/#@${this.schedulerResource.subscription.tenantId}/resource${this.id}`; + const url: string = `${this.scheduler.subscription.environment.portalUrl}/#@${this.scheduler.subscription.tenantId}/resource${this.id}`; return Uri.parse(url); } @@ -33,14 +34,14 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { return { label: this.resource.name, getData: async () => { - if (!this.schedulerResource.resourceGroup) { + if (!this.scheduler.resourceGroup) { throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); } const json = await this.schedulerClient.getSchedulerTaskHub( - this.schedulerResource.subscription, - this.schedulerResource.resourceGroup, - this.schedulerResource.name, + this.scheduler.subscription, + this.scheduler.resourceGroup, + this.scheduler.name, this.resource.name); return json; @@ -64,16 +65,4 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { } get name() { return this.resource.name; } - - get resourceGroup() { - if (!this.schedulerResource.resourceGroup) { - throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); - } - - return this.schedulerResource.resourceGroup; - } - - get schedulerName() { return this.schedulerResource.name; } - - get subscription() { return this.schedulerResource.subscription; } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 477cc84cd..6a3aaca19 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -31,4 +31,8 @@ export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBran getTreeItem(element: DurableTaskSchedulerModel): TreeItem | Thenable { return element.getTreeItem(); } + + refresh() { + this.onDidChangeTreeDataEventEmitter.fire(); + } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index d8f3e73c2..ef9b476d0 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -24,7 +24,7 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo const taskHubs = await this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup, this.resource.name); - return taskHubs.map(resource => new DurableTaskHubResourceModel(this.resource, resource, this.schedulerClient)); + return taskHubs.map(resource => new DurableTaskHubResourceModel(this, resource, this.schedulerClient)); } getTreeItem(): TreeItem | Thenable { From 0fe25f87dff19ae007c6529eb0eb50b0397f1954 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 30 Jan 2025 14:19:32 -0800 Subject: [PATCH 20/58] Provide extended schduler properties. Signed-off-by: Phillip Hoff --- .../DurableTaskSchedulerClient.ts | 15 ++++++++++++++ .../DurableTaskSchedulerResourceModel.ts | 20 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index 83c3925bb..56a7a7fd5 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -25,6 +25,10 @@ interface DurableTaskHubCreateRequest { export interface DurableTaskSchedulerResource { readonly id: string; readonly name: string; + readonly properties: { + readonly endpoint: string; + readonly provisioningState: string; + }; } export interface DurableTaskHubResource { @@ -32,6 +36,7 @@ export interface DurableTaskHubResource { readonly name: string; readonly properties: { readonly dashboardUrl: string; + readonly provisioningState: string; }; } @@ -42,6 +47,8 @@ export interface DurableTaskSchedulerClient { deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; + getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; + getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; } @@ -99,6 +106,14 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien await this.delete(taskHubsUrl, subscription.authentication); } + async getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { + const schedulerUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName); + + const scheduler = await this.getAsJson(schedulerUrl, subscription.authentication); + + return scheduler; + } + async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index ef9b476d0..b3396ebd8 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzureResource, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type ViewPropertiesModel, type AzureResource, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { DurableTaskHubResourceModel } from "./DurableTaskHubResourceModel"; @@ -54,4 +54,22 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo } get subscription() { return this.resource.subscription; } + + get viewProperties(): ViewPropertiesModel { + return { + label: this.resource.name, + getData: async () => { + if (!this.resource.resourceGroup) { + throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); + } + + const json = await this.schedulerClient.getScheduler( + this.resource.subscription, + this.resource.resourceGroup, + this.resource.name); + + return json; + } + }; + } } From 18fca77d8e28b20e5079956605476c9cb7c9b514 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 30 Jan 2025 15:08:04 -0800 Subject: [PATCH 21/58] Add provisioning state when not "normal". Signed-off-by: Phillip Hoff --- .../DurableTaskHubResourceModel.ts | 12 +++++++++++- .../DurableTaskSchedulerResourceModel.ts | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 0c8714a3b..052812e36 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -54,13 +54,23 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { return []; } - getTreeItem(): TreeItem | Thenable + async getTreeItem(): Promise { const treeItem = new TreeItem(this.name) treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); treeItem.contextValue = 'azFunc.dts.taskHub'; + const json = await this.schedulerClient.getSchedulerTaskHub( + this.scheduler.subscription, + this.scheduler.resourceGroup, + this.scheduler.name, + this.name); + + if (json.properties.provisioningState !== 'Succeeded') { + treeItem.description = localize('taskHubDescription', '({0})', json.properties.provisioningState); + } + return treeItem; } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index b3396ebd8..5b3f7eb34 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -27,11 +27,22 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo return taskHubs.map(resource => new DurableTaskHubResourceModel(this, resource, this.schedulerClient)); } - getTreeItem(): TreeItem | Thenable { + async getTreeItem(): Promise { const treeItem = new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); treeItem.contextValue = 'azFunc.dts.scheduler'; + if (this.resource.resourceGroup) { + const json = await this.schedulerClient.getScheduler( + this.resource.subscription, + this.resource.resourceGroup, + this.resource.name); + + if (json.properties.provisioningState !== 'Succeeded') { + treeItem.description = localize('schedulerDescription', '({0})', json.properties.provisioningState); + } + } + return treeItem; } From 19f70c2a7e1478cdf27d34661ac360a11305e056 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 30 Jan 2025 15:13:28 -0800 Subject: [PATCH 22/58] Move creation command to top of context menu. Signed-off-by: Phillip Hoff --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e449a7202..d371d6556 100644 --- a/package.json +++ b/package.json @@ -701,7 +701,8 @@ }, { "command": "azureFunctions.durableTaskScheduler.createScheduler", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i" + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i", + "group": "1@1" }, { "command": "azureFunctions.durableTaskScheduler.createTaskHub", From db2d957ec942574b306d1b848b17636dd03ceeea Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 30 Jan 2025 15:34:00 -0800 Subject: [PATCH 23/58] Add copy scheduler endpoint command. Signed-off-by: Phillip Hoff --- package.json | 10 ++++++ package.nls.json | 1 + .../copySchedulerEndpoint.ts | 31 +++++++++++++++++++ src/commands/registerCommands.ts | 2 ++ 4 files changed, 44 insertions(+) create mode 100644 src/commands/durableTaskScheduler/copySchedulerEndpoint.ts diff --git a/package.json b/package.json index d371d6556..d392f710a 100644 --- a/package.json +++ b/package.json @@ -381,6 +381,11 @@ "category": "Azure Functions", "icon": "$(notebook-execute)" }, + { + "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", + "title": "%azureFunctions.durableTaskScheduler.copySchedulerEndpoint%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.createScheduler", "title": "%azureFunctions.durableTaskScheduler.createScheduler%", @@ -699,6 +704,11 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.*folder/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", + "group": "3@1" + }, { "command": "azureFunctions.durableTaskScheduler.createScheduler", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i", diff --git a/package.nls.json b/package.nls.json index d676dba15..662d1b384 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,6 +120,7 @@ "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", + "azureFunctions.durableTaskScheduler.copySchedulerEndpoint": "Copy Scheduler Endpoint", "azureFunctions.durableTaskScheduler.createScheduler": "Create Durable Task Scheduler...", "azureFunctions.durableTaskScheduler.createScheduler.detail": "For long-running and reliable workflows.", "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub...", diff --git a/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts b/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts new file mode 100644 index 000000000..e4176c751 --- /dev/null +++ b/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from "@microsoft/vscode-azext-utils"; +import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; +import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; +import { localize } from "../../localize"; +import { ext } from "../../extensionVariables"; +import { env } from "vscode"; + +export function copySchedulerEndpointCommandFactory(schedulerClient: DurableTaskSchedulerClient) { + return async (_: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { + if (!scheduler) { + throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); + } + + const schedulerJson = schedulerClient.getScheduler( + scheduler.subscription, + scheduler.resourceGroup, + scheduler.name); + + const { endpoint } = (await schedulerJson).properties; + + await env.clipboard.writeText(endpoint); + + ext.outputChannel.show(); + ext.outputChannel.appendLog(localize('schedulerEndpointCopiedMessage', 'Endpoint copied to clipboard: {0}', endpoint)); + } +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 13c5d32f7..2f482bb9f 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -70,6 +70,7 @@ import { createSchedulerCommandFactory } from './durableTaskScheduler/createSche import { deleteTaskHubCommandFactory } from './durableTaskScheduler/deleteTaskHub'; import { deleteSchedulerCommandFactory } from './durableTaskScheduler/deleteScheduler'; import { type DurableTaskSchedulerDataBranchProvider } from '../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; +import { copySchedulerEndpointCommandFactory } from './durableTaskScheduler/copySchedulerEndpoint'; export function registerCommands( services: { @@ -168,6 +169,7 @@ export function registerCommands( ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerEndpoint', copySchedulerEndpointCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteScheduler', deleteSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); From 5e7d1ea7c33dd2903558c3017ab86872d498930c Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 30 Jan 2025 23:13:14 -0800 Subject: [PATCH 24/58] Sketch copy connections string command. Signed-off-by: Phillip Hoff --- package.json | 12 +++- package.nls.json | 3 +- .../copySchedulerConnectionString.ts | 70 +++++++++++++++++++ src/commands/registerCommands.ts | 2 + 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/commands/durableTaskScheduler/copySchedulerConnectionString.ts diff --git a/package.json b/package.json index d392f710a..620981215 100644 --- a/package.json +++ b/package.json @@ -381,6 +381,11 @@ "category": "Azure Functions", "icon": "$(notebook-execute)" }, + { + "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", + "title": "%azureFunctions.durableTaskScheduler.copySchedulerConnectionString%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", "title": "%azureFunctions.durableTaskScheduler.copySchedulerEndpoint%", @@ -705,10 +710,15 @@ "group": "1@1" }, { - "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", + "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", "group": "3@1" }, + { + "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", + "group": "3@2" + }, { "command": "azureFunctions.durableTaskScheduler.createScheduler", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i", diff --git a/package.nls.json b/package.nls.json index 662d1b384..f37c2b978 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,7 +120,8 @@ "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", - "azureFunctions.durableTaskScheduler.copySchedulerEndpoint": "Copy Scheduler Endpoint", + "azureFunctions.durableTaskScheduler.copySchedulerConnectionString": "Copy Connection String", + "azureFunctions.durableTaskScheduler.copySchedulerEndpoint": "Copy Endpoint", "azureFunctions.durableTaskScheduler.createScheduler": "Create Durable Task Scheduler...", "azureFunctions.durableTaskScheduler.createScheduler.detail": "For long-running and reliable workflows.", "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub...", diff --git a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts new file mode 100644 index 000000000..a8291f3d2 --- /dev/null +++ b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from "@microsoft/vscode-azext-utils"; +import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; +import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; +import { localize } from "../../localize"; +import { ext } from "../../extensionVariables"; +import { env, type QuickPickItem } from "vscode"; + +export function copySchedulerConnectionStringCommandFactory(schedulerClient: DurableTaskSchedulerClient) { + return async (actionContext: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { + if (!scheduler) { + throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); + } + + // TODO: Prompt for type of connection string (local development, user-assigned managed identity, server-assigned managed identity) + + const localDevelopment: QuickPickItem = { + label: localize('localDevelopmentLabel', 'Local development') + }; + + const userAssignedManagedIdentity: QuickPickItem = { + label: localize('userAssignedManagedIdentityLabel', 'User-assigned managed identity') + } + + const systemAssignedManagedIdentity: QuickPickItem = { + label: localize('systemAssignedManagedIdentityLabel', 'System-assigned managed identity') + } + + const result = await actionContext.ui.showQuickPick( + [ + localDevelopment, + userAssignedManagedIdentity, + systemAssignedManagedIdentity + ], + { + canPickMany: false + }); + + // TODO: Prompt for (optional) task hub + + const schedulerJson = schedulerClient.getScheduler( + scheduler.subscription, + scheduler.resourceGroup, + scheduler.name); + + const { endpoint } = (await schedulerJson).properties; + + let connectionString = `Endpoint=${endpoint};Authentication=` + + if (result === localDevelopment) { + connectionString += 'DefaultAzure'; + } + else { + connectionString += 'ManagedIdentity'; + + if (result === userAssignedManagedIdentity) { + connectionString += ';ClientID='; + } + } + + await env.clipboard.writeText(connectionString); + + ext.outputChannel.show(); + ext.outputChannel.appendLog(localize('schedulerConnectionStringCopiedMessage', 'Connection string copied to clipboard: {0}', connectionString)); + } +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 2f482bb9f..ecc36821e 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -71,6 +71,7 @@ import { deleteTaskHubCommandFactory } from './durableTaskScheduler/deleteTaskHu import { deleteSchedulerCommandFactory } from './durableTaskScheduler/deleteScheduler'; import { type DurableTaskSchedulerDataBranchProvider } from '../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; import { copySchedulerEndpointCommandFactory } from './durableTaskScheduler/copySchedulerEndpoint'; +import { copySchedulerConnectionStringCommandFactory } from './durableTaskScheduler/copySchedulerConnectionString'; export function registerCommands( services: { @@ -169,6 +170,7 @@ export function registerCommands( ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerConnectionString', copySchedulerConnectionStringCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerEndpoint', copySchedulerEndpointCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(services.dts.schedulerClient)); From b61e5f5f207d87cd296c08c72e540f5086ba4a62 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 31 Jan 2025 00:03:39 -0800 Subject: [PATCH 25/58] Add task hub selection for connection string. Signed-off-by: Phillip Hoff --- .../copySchedulerConnectionString.ts | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts index a8291f3d2..ff53f0255 100644 --- a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts +++ b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts @@ -8,7 +8,7 @@ import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; import { localize } from "../../localize"; import { ext } from "../../extensionVariables"; -import { env, type QuickPickItem } from "vscode"; +import { env, QuickPickItemKind, type QuickPickItem } from "vscode"; export function copySchedulerConnectionStringCommandFactory(schedulerClient: DurableTaskSchedulerClient) { return async (actionContext: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { @@ -16,7 +16,12 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); } - // TODO: Prompt for type of connection string (local development, user-assigned managed identity, server-assigned managed identity) + const schedulerJson = schedulerClient.getScheduler( + scheduler.subscription, + scheduler.resourceGroup, + scheduler.name); + + const { endpoint } = (await schedulerJson).properties; const localDevelopment: QuickPickItem = { label: localize('localDevelopmentLabel', 'Local development') @@ -37,18 +42,10 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur systemAssignedManagedIdentity ], { - canPickMany: false + canPickMany: false, + placeHolder: localize('authenticationTypePlaceholder', 'Select the type of authentication to be used') }); - // TODO: Prompt for (optional) task hub - - const schedulerJson = schedulerClient.getScheduler( - scheduler.subscription, - scheduler.resourceGroup, - scheduler.name); - - const { endpoint } = (await schedulerJson).properties; - let connectionString = `Endpoint=${endpoint};Authentication=` if (result === localDevelopment) { @@ -62,6 +59,41 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur } } + // TODO: Prompt for (optional) task hub + + const taskHubs = await schedulerClient.getSchedulerTaskHubs( + scheduler.subscription, + scheduler.resourceGroup, + scheduler.name); + + if (taskHubs.length > 0) { + + const noTaskHubItem: QuickPickItem = { + label: localize('noTaskHubLabel', 'No task hub') + } + + const taskHubItems: QuickPickItem[] = + taskHubs.map(taskHub => ({ label: taskHub.name })); + + const taskHubResult = await actionContext.ui.showQuickPick( + [ + noTaskHubItem, + { + kind: QuickPickItemKind.Separator, + label: localize('taskHubSepratorLabel', 'Task Hubs') + }, + ...taskHubItems + ], + { + canPickMany: false, + placeHolder: localize('taskHubSelectionPlaceholder', 'Select the task hub') + }); + + if (taskHubResult && taskHubResult !== noTaskHubItem) { + connectionString += `;TaskHub=${taskHubResult.label}`; + } + } + await env.clipboard.writeText(connectionString); ext.outputChannel.show(); From abe8d2923334b2eff22992c76a097ec011964cf1 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 31 Jan 2025 15:39:36 -0800 Subject: [PATCH 26/58] Add async waits. Signed-off-by: Phillip Hoff --- .../copySchedulerConnectionString.ts | 2 - .../durableTaskScheduler/createScheduler.ts | 52 ++++-- src/constants.ts | 3 + .../DurableTaskSchedulerClient.ts | 170 +++++++++++------- 4 files changed, 145 insertions(+), 82 deletions(-) diff --git a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts index ff53f0255..08e547783 100644 --- a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts +++ b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts @@ -59,8 +59,6 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur } } - // TODO: Prompt for (optional) task hub - const taskHubs = await schedulerClient.getSchedulerTaskHubs( scheduler.subscription, scheduler.resourceGroup, diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index 12387b46e..0e48e3df4 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, type IActionContext, subscriptionExperience, createSubscriptionContext, type ExecuteActivityContext, type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; -import { localize } from '../../localize'; -import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; +import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, createSubscriptionContext, type ExecuteActivityContext, type IActionContext, type ISubscriptionActionContext, subscriptionExperience } from "@microsoft/vscode-azext-utils"; import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { type Progress } from "vscode"; -import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; +import { DurableTaskProvider, DurableTaskSchedulersResourceType } from "../../constants"; import { ext } from '../../extensionVariables'; -import { createActivityContext } from "../../utils/activityUtils"; +import { localize } from '../../localize'; +import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; +import { createActivityContext } from "../../utils/activityUtils"; interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationWizardContext, IResourceGroupWizardContext, ExecuteActivityContext { subscription?: AzureSubscription; @@ -21,7 +22,7 @@ interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationW class SchedulerNamingStep extends AzureWizardPromptStep { async prompt(wizardContext: ICreateSchedulerContext): Promise { wizardContext.schedulerName = await wizardContext.ui.showInputBox({ - prompt: localize('taskHubNamingStepPrompt', 'Enter a name for the new task hub') + prompt: localize('schedulerNamingStepPrompt', 'Enter a name for the new scheduler') }) } @@ -40,12 +41,29 @@ class SchedulerCreationStep extends AzureWizardExecuteStep): Promise { const location = await LocationListStep.getLocation(wizardContext); - await this.schedulerClient.createScheduler( + const response = await this.schedulerClient.createScheduler( wizardContext.subscription as AzureSubscription, wizardContext.resourceGroup?.name as string, location.name, wizardContext.schedulerName as string ); + + const delay = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); + + while (true) { + await delay(1000); + + const status = await response.status.get(); + + if (status === true) + { + break; + } + + if (status === false) { + throw new Error(localize('schedulerCreationFailed', 'The scheduler could not be created.')); + } + } } shouldExecute(wizardContext: ICreateSchedulerContext): boolean { @@ -60,16 +78,20 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch const subscription = node?.subscription ?? await subscriptionExperience(actionContext, ext.rgApiV2.resources.azureResourceTreeDataProvider); const wizardContext = - { - subscription, + { + subscription, - ...actionContext, - ...createSubscriptionContext(subscription), - ...await createActivityContext() - }; + ...actionContext, + ...createSubscriptionContext(subscription), + ...await createActivityContext() + }; - const promptSteps: AzureWizardPromptStep[] = [ new ResourceGroupListStep(), new SchedulerNamingStep() ]; + const promptSteps: AzureWizardPromptStep[] = [ + new SchedulerNamingStep(), + new ResourceGroupListStep() + ]; + LocationListStep.addProviderForFiltering(wizardContext, DurableTaskProvider, DurableTaskSchedulersResourceType); LocationListStep.addStep(wizardContext, promptSteps); const wizard = new AzureWizard( @@ -81,7 +103,7 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch new ResourceGroupCreateStep(), new SchedulerCreationStep(schedulerClient) ], - title: localize('createSchedulerWizardTitle', 'Create Scheduler') + title: localize('createSchedulerWizardTitle', 'Create Durable Task Scheduler') }); await wizard.prompt(); diff --git a/src/constants.ts b/src/constants.ts index d208d9094..1980839b5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -207,3 +207,6 @@ export enum EventGridExecuteFunctionEntryPoint { // Originally from the Docker extension: https://github.com/microsoft/vscode-docker/blob/main/src/constants.ts export const dockerfileGlobPattern = '{*.[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE],[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE],[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE].*}'; + +export const DurableTaskProvider: string = 'Microsoft.DurableTask'; +export const DurableTaskSchedulersResourceType: string = 'schedulers'; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index 56a7a7fd5..451ccb15a 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -6,6 +6,14 @@ import { type AzureAuthentication, type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { localize } from '../../localize'; +interface FetchOptions { + authentication: AzureAuthentication; + contentType?: string | undefined; + body?: string | undefined; + method: string; + url: string; +} + interface DurableTaskSchedulerCreateRequest { readonly location: string; readonly properties: { @@ -18,6 +26,10 @@ interface DurableTaskSchedulerCreateRequest { readonly tags: unknown; } +interface AzureAsyncOperationStatus { + status: string; +} + interface DurableTaskHubCreateRequest { readonly properties: unknown; } @@ -40,11 +52,20 @@ export interface DurableTaskHubResource { }; } +export interface DurableTaskStatus { + get: () => Promise; +} + +export interface DurableTaskSchedulerCreateResponse { + scheduler: DurableTaskSchedulerResource; + status: DurableTaskStatus; +} + export interface DurableTaskSchedulerClient { - createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise; + createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise; createTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; - deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; + deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; @@ -54,7 +75,7 @@ export interface DurableTaskSchedulerClient { } export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClient { - async createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise { + async createScheduler(subscription: AzureSubscription, resourceGroupName: string, location: string, schedulerName: string): Promise { const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName); const request: DurableTaskSchedulerCreateRequest = { @@ -70,16 +91,37 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien } }; - const scheduler = await this.putAsJson( + const response = await this.putAsJson( taskHubsUrl, request, subscription.authentication); - return scheduler; + return { + scheduler: response.value, + status: { + get: async () => { + if (!response.asyncOperation) { + return true; + } + + const status = await this.getAsJson( + response.asyncOperation, + subscription.authentication + ); + + switch (status.status) { + case 'Succeeded': return true; + case 'Failed': return false; + case 'Canceled': return false; + default: return undefined; + } + } + } + }; } async createTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { - const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskhubs/${taskHubName}`; + const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName, `/taskhubs/${taskHubName}`); const request: DurableTaskHubCreateRequest = { properties: { @@ -91,17 +133,21 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien request, subscription.authentication); - return taskHub; + return taskHub.value; } - async deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { - const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}`; + async deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { + const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName); await this.delete(taskHubsUrl, subscription.authentication); + + return { + get: () => Promise.resolve(true) + } } async deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { - const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskhubs/${taskHubName}`; + const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName, `/taskhubs/${taskHubName}`); await this.delete(taskHubsUrl, subscription.authentication); } @@ -115,82 +161,80 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien } async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { - const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`; + const taskHubUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName, `/taskHubs/${taskHubName}`); - const taskHub = await this.getAsJson(taskHubsUrl, subscription.authentication); + const taskHub = await this.getAsJson(taskHubUrl, subscription.authentication); return taskHub; } async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { - const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs`; + const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName, '/taskHubs'); const response = await this.getAsJson<{ value: DurableTaskHubResource[] }>(taskHubsUrl, subscription.authentication); return response.value; } - private static getBaseUrl(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string) { + private static getBaseUrl(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, relativeUrl?: string | undefined) { const provider = 'Microsoft.DurableTask'; - - return `${subscription.environment.resourceManagerEndpointUrl}subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`; - } - - private async delete(url: string, authentication: AzureAuthentication): Promise { const apiVersion = '2024-10-01-preview'; - const versionedUrl = `${url}?api-version=${apiVersion}`; - const authSession = await authentication.getSession(); + let url = `${subscription.environment.resourceManagerEndpointUrl}subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`; - if (!authSession) { - throw new Error(localize('noAuthenticationSessionErrorMessage', 'Unable to obtain an authentication session.')); + if (relativeUrl) { + url += relativeUrl; } - const accessToken = authSession.accessToken; - - const request = new Request( - versionedUrl, - { - method: 'DELETE' - }); - - request.headers.append('Authorization', `Bearer ${accessToken}`); + url += `?api-version=${apiVersion}`; - const response = await fetch(request); + return url; + } - if (!response.ok) { - throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.')); - } + private async delete(url: string, authentication: AzureAuthentication): Promise { + await this.fetch({ + authentication, + method: 'DELETE', + url + }); } private async getAsJson(url: string, authentication: AzureAuthentication): Promise { - const apiVersion = '2024-10-01-preview'; - const versionedUrl = `${url}?api-version=${apiVersion}`; - - const authSession = await authentication.getSession(); - - if (!authSession) { - throw new Error(localize('noAuthenticationSessionErrorMessage', 'Unable to obtain an authentication session.')); - } - - const accessToken = authSession.accessToken; + const response = await this.fetch({ + authentication, + method: 'GET', + url + }); - const request = new Request(versionedUrl); + return await response.json() as T; + } - request.headers.append('Authorization', `Bearer ${accessToken}`); + private async putAsJson(url: string, body: unknown, authentication: AzureAuthentication): Promise<{ asyncOperation?: string, value: T }> { + const response = await this.fetch({ + authentication, + contentType: 'application/json', + body: JSON.stringify(body), + method: 'PUT', + url + }); - const response = await fetch(request); + const value = await response.json() as T; - if (!response.ok) { - throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.')); + return { + asyncOperation: response.headers.get('Azure-AsyncOperation') ?? undefined, + value } - - return await response.json() as T; } - private async putAsJson(url: string, body: unknown, authentication: AzureAuthentication): Promise { - const apiVersion = '2024-10-01-preview'; - const versionedUrl = `${url}?api-version=${apiVersion}`; + private async fetch(options: FetchOptions): Promise { + const { authentication, body, contentType, method, url } = options; + + const request = new Request( + url, + { + body, + method + }); const authSession = await authentication.getSession(); @@ -200,15 +244,11 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien const accessToken = authSession.accessToken; - const request = new Request( - versionedUrl, - { - body: JSON.stringify(body), - method: 'PUT' - }); - request.headers.set('Authorization', `Bearer ${accessToken}`); - request.headers.set('Content-Type', 'application/json'); + + if (contentType) { + request.headers.set('Content-Type', contentType); + } const response = await fetch(request); @@ -216,6 +256,6 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.')); } - return await response.json() as T; + return response; } } From 92c3f1246f0db53bdd3961ecda2099a22fe70f4a Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 09:52:39 -0800 Subject: [PATCH 27/58] Wrap deletion in an activity. Signed-off-by: Phillip Hoff --- .../durableTaskScheduler/createScheduler.ts | 20 ++--- .../durableTaskScheduler/deleteScheduler.ts | 82 ++++++++++++++++++- .../DurableTaskSchedulerClient.ts | 81 ++++++++++++------ src/utils/cancellation.ts | 27 ++++++ 4 files changed, 166 insertions(+), 44 deletions(-) create mode 100644 src/utils/cancellation.ts diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index 0e48e3df4..201e2b762 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -6,13 +6,14 @@ import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, createSubscriptionContext, type ExecuteActivityContext, type IActionContext, type ISubscriptionActionContext, subscriptionExperience } from "@microsoft/vscode-azext-utils"; import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; -import { type Progress } from "vscode"; import { DurableTaskProvider, DurableTaskSchedulersResourceType } from "../../constants"; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; import { createActivityContext } from "../../utils/activityUtils"; +import { withCancellation } from "../../utils/cancellation"; +import { type Progress } from "vscode"; interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationWizardContext, IResourceGroupWizardContext, ExecuteActivityContext { subscription?: AzureSubscription; @@ -48,21 +49,10 @@ class SchedulerCreationStep extends AzureWizardExecuteStep => new Promise(resolve => setTimeout(resolve, ms)); - - while (true) { - await delay(1000); - - const status = await response.status.get(); - - if (status === true) - { - break; - } + const status = await withCancellation(token => response.status.waitForCompletion(token), 1000 * 60 * 30); - if (status === false) { - throw new Error(localize('schedulerCreationFailed', 'The scheduler could not be created.')); - } + if (status !== true) { + throw new Error(localize('schedulerCreationFailed', 'The scheduler could not be created.')); } } diff --git a/src/commands/durableTaskScheduler/deleteScheduler.ts b/src/commands/durableTaskScheduler/deleteScheduler.ts index 1f2b90fa3..f9a214607 100644 --- a/src/commands/durableTaskScheduler/deleteScheduler.ts +++ b/src/commands/durableTaskScheduler/deleteScheduler.ts @@ -6,18 +6,65 @@ import {type IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; import { localize } from "../../localize"; -import { type MessageItem } from "vscode"; +import { type CancellationTokenSource, Disposable, type Event, EventEmitter, type MessageItem } from "vscode"; import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; +import { type Activity, type ActivityTreeItemOptions, type OnErrorActivityData, type OnProgressActivityData } from "@microsoft/vscode-azext-utils/hostapi"; +import { randomUUID } from "crypto"; +import { withCancellation } from "../../utils/cancellation"; +import { ext } from "../../extensionVariables"; -export function deleteSchedulerCommandFactory(dataBranchProvider: DurableTaskSchedulerDataBranchProvider, schedulerClient: DurableTaskSchedulerClient) { +export class AzureActivity extends Disposable implements Activity { + private readonly onStartEmitter = new EventEmitter; + private readonly onProgressEmitter = new EventEmitter; + private readonly onSuccessEmitter = new EventEmitter; + private readonly onErrorEmitter = new EventEmitter; + + private readonly uniqueId: string; + + constructor() { + super( + () => { + this.onStartEmitter.dispose(); + this.onProgressEmitter.dispose(); + this.onSuccessEmitter.dispose(); + this.onErrorEmitter.dispose(); + }); + + this.uniqueId = randomUUID(); + } + + get id(): string { return this.uniqueId; } + + cancellationTokenSource?: CancellationTokenSource | undefined; + + get onStart(): Event { return this.onStartEmitter.event; } + + get onProgress(): Event { return this.onProgressEmitter.event; } + + get onSuccess(): Event { return this.onSuccessEmitter.event; } + + get onError(): Event { return this.onErrorEmitter.event; } + + start(options: ActivityTreeItemOptions): void { this.onStartEmitter.fire(options); } + + progress(data: OnProgressActivityData): void { this.onProgressEmitter.fire(data); } + + succeed(options: ActivityTreeItemOptions): void { this.onSuccessEmitter.fire(options); } + + fail(data: OnErrorActivityData): void { this.onErrorEmitter.fire(data); } +} + +export function deleteSchedulerCommandFactory( + dataBranchProvider: DurableTaskSchedulerDataBranchProvider, + schedulerClient: DurableTaskSchedulerClient) { return async (actionContext: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { if (!scheduler) { throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); } const deleteItem: MessageItem = { - title: 'Delete' + title: localize('deleteSchedulerLabel', 'Delete') }; const result = await actionContext.ui.showWarningMessage( @@ -32,15 +79,42 @@ export function deleteSchedulerCommandFactory(dataBranchProvider: DurableTaskSch return; } + const activity = new AzureActivity(); + + await ext.rgApi.registerActivity(activity); + + activity.start({ + label: localize('deletingSchedulerActivityLabel', 'Deleting scheduler \'{0}\'', scheduler.name) + }); + try { - await schedulerClient.deleteScheduler( + const response = await schedulerClient.deleteScheduler( scheduler.subscription, scheduler.resourceGroup, scheduler.name ); + + const result = await withCancellation(token => response.waitForCompletion(token), 1000 * 60 * 30); + + if (result === true) { + activity.succeed({ + label: localize('deleteSucceededActivityLabel', 'Deleted scheduler \'{0}\'', scheduler.name) + }); + } + else { + throw new Error(localize('deleteFailureMessage', 'The scheduler failed to be deleted in the allotted time.')); + } + } + catch (error: unknown) { + activity.fail({ + error, + label: localize('deleteFailureActivityLabel', 'Failed to delete scheduler \'{0}\'', scheduler.name) + }); } finally { dataBranchProvider.refresh(); + + activity.dispose(); } } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index 451ccb15a..bd38af50f 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -5,6 +5,8 @@ import { type AzureAuthentication, type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { localize } from '../../localize'; +import { type CancellationToken } from "vscode"; +import { delay } from "../../utils/delay"; interface FetchOptions { authentication: AzureAuthentication; @@ -54,6 +56,7 @@ export interface DurableTaskHubResource { export interface DurableTaskStatus { get: () => Promise; + waitForCompletion: (token: CancellationToken) => Promise; } export interface DurableTaskSchedulerCreateResponse { @@ -98,25 +101,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return { scheduler: response.value, - status: { - get: async () => { - if (!response.asyncOperation) { - return true; - } - - const status = await this.getAsJson( - response.asyncOperation, - subscription.authentication - ); - - switch (status.status) { - case 'Succeeded': return true; - case 'Failed': return false; - case 'Canceled': return false; - default: return undefined; - } - } - } + status: this.createStatus(response.asyncOperation, subscription.authentication) }; } @@ -139,11 +124,9 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien async deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName); - await this.delete(taskHubsUrl, subscription.authentication); + const response = await this.delete(taskHubsUrl, subscription.authentication); - return { - get: () => Promise.resolve(true) - } + return this.createStatus(response.asyncOperation, subscription.authentication); } async deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { @@ -191,12 +174,16 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return url; } - private async delete(url: string, authentication: AzureAuthentication): Promise { - await this.fetch({ + private async delete(url: string, authentication: AzureAuthentication): Promise<{ asyncOperation?: string }> { + const response = await this.fetch({ authentication, method: 'DELETE', url }); + + return { + asyncOperation: response.headers.get('Azure-AsyncOperation') ?? undefined + } } private async getAsJson(url: string, authentication: AzureAuthentication): Promise { @@ -258,4 +245,48 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return response; } + + private createStatus(asyncOperation: string | undefined, authentication: AzureAuthentication): DurableTaskStatus { + const get = async () => { + if (!asyncOperation) { + return true; + } + + const status = await this.getAsJson( + asyncOperation, + authentication + ); + + switch (status.status) { + case 'Succeeded': return true; + case 'Failed': return false; + case 'Canceled': return false; + default: return undefined; + } + }; + + return { + get, + waitForCompletion: async token => { + while (true) { + await delay(1000); + + if (token.isCancellationRequested) { + return undefined; + } + + const status = await get(); + + if (status === true) + { + return true; + } + + if (status === false) { + return false; + } + } + } + }; + } } diff --git a/src/utils/cancellation.ts b/src/utils/cancellation.ts new file mode 100644 index 000000000..7c4bf327b --- /dev/null +++ b/src/utils/cancellation.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource, type CancellationToken } from "vscode"; +import { delay } from "./delay"; + +export async function withCancellation(action: (token: CancellationToken) => Promise, timeoutMs: number): Promise { + + const cts = new CancellationTokenSource(); + + try { + + const asyncWait = action(cts.token); + const delayWait = delay(timeoutMs); + + await Promise.race([asyncWait, delayWait]); + + cts.cancel(); + + return await asyncWait; + } + finally { + cts.dispose(); + } +} From bbc47c08e4571a918adff67cd6af340f7f73ef44 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 11:40:47 -0800 Subject: [PATCH 28/58] Add some robustness to API call failures. Signed-off-by: Phillip Hoff --- .../copySchedulerConnectionString.ts | 8 ++++-- .../copySchedulerEndpoint.ts | 8 ++++-- .../durableTaskScheduler/createScheduler.ts | 4 ++- .../durableTaskScheduler/deleteScheduler.ts | 10 ++++--- .../DurableTaskHubResourceModel.ts | 6 ++--- .../DurableTaskSchedulerClient.ts | 26 ++++++++++++------- .../DurableTaskSchedulerResourceModel.ts | 6 ++--- 7 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts index 08e547783..23ae3994b 100644 --- a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts +++ b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts @@ -16,12 +16,16 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); } - const schedulerJson = schedulerClient.getScheduler( + const schedulerJson = await schedulerClient.getScheduler( scheduler.subscription, scheduler.resourceGroup, scheduler.name); - const { endpoint } = (await schedulerJson).properties; + if (!schedulerJson) { + throw new Error(localize('schedulerNotFoundErrorMessage', 'Scheduler does not exist.')); + } + + const { endpoint } = schedulerJson.properties; const localDevelopment: QuickPickItem = { label: localize('localDevelopmentLabel', 'Local development') diff --git a/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts b/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts index e4176c751..4dded714a 100644 --- a/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts +++ b/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts @@ -16,12 +16,16 @@ export function copySchedulerEndpointCommandFactory(schedulerClient: DurableTask throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); } - const schedulerJson = schedulerClient.getScheduler( + const schedulerJson = await schedulerClient.getScheduler( scheduler.subscription, scheduler.resourceGroup, scheduler.name); - const { endpoint } = (await schedulerJson).properties; + if (!schedulerJson) { + throw new Error(localize('schedulerNotFoundErrorMessage', 'Scheduler does not exist.')); + } + + const { endpoint } = schedulerJson.properties; await env.clipboard.writeText(endpoint); diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index 201e2b762..bf301a28b 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -67,7 +67,7 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch return async (actionContext: IActionContext, node?: { subscription: AzureSubscription }): Promise => { const subscription = node?.subscription ?? await subscriptionExperience(actionContext, ext.rgApiV2.resources.azureResourceTreeDataProvider); - const wizardContext = + const wizardContext: ICreateSchedulerContext = { subscription, @@ -98,6 +98,8 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch await wizard.prompt(); + wizardContext.activityTitle = localize('createSchedulerActivityTitle', 'Create Durable Task Scheduler \'{0}\'', wizardContext.schedulerName); + try { await wizard.execute(); } diff --git a/src/commands/durableTaskScheduler/deleteScheduler.ts b/src/commands/durableTaskScheduler/deleteScheduler.ts index f9a214607..76491ad9c 100644 --- a/src/commands/durableTaskScheduler/deleteScheduler.ts +++ b/src/commands/durableTaskScheduler/deleteScheduler.ts @@ -83,8 +83,10 @@ export function deleteSchedulerCommandFactory( await ext.rgApi.registerActivity(activity); + const label = localize('deletingSchedulerActivityLabel', 'Delete Durable Task Scheduler \'{0}\'', scheduler.name); + activity.start({ - label: localize('deletingSchedulerActivityLabel', 'Deleting scheduler \'{0}\'', scheduler.name) + label }); try { @@ -98,17 +100,17 @@ export function deleteSchedulerCommandFactory( if (result === true) { activity.succeed({ - label: localize('deleteSucceededActivityLabel', 'Deleted scheduler \'{0}\'', scheduler.name) + label }); } else { - throw new Error(localize('deleteFailureMessage', 'The scheduler failed to be deleted in the allotted time.')); + throw new Error(localize('deleteFailureMessage', 'The scheduler failed to delete within the allotted time.')); } } catch (error: unknown) { activity.fail({ error, - label: localize('deleteFailureActivityLabel', 'Failed to delete scheduler \'{0}\'', scheduler.name) + label }); } finally { diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 052812e36..34e9f1d24 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -44,7 +44,7 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { this.scheduler.name, this.resource.name); - return json; + return json ?? ''; } }; } @@ -67,8 +67,8 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { this.scheduler.name, this.name); - if (json.properties.provisioningState !== 'Succeeded') { - treeItem.description = localize('taskHubDescription', '({0})', json.properties.provisioningState); + if (json?.properties.provisioningState !== 'Succeeded') { + treeItem.description = localize('taskHubDescription', '({0})', json?.properties.provisioningState || 'Deleted'); } return treeItem; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index bd38af50f..c1b3d6390 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -9,6 +9,7 @@ import { type CancellationToken } from "vscode"; import { delay } from "../../utils/delay"; interface FetchOptions { + allowNotFound?: boolean; authentication: AzureAuthentication; contentType?: string | undefined; body?: string | undefined; @@ -71,9 +72,9 @@ export interface DurableTaskSchedulerClient { deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; - getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; + getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; - getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; + getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; } @@ -135,7 +136,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien await this.delete(taskHubsUrl, subscription.authentication); } - async getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { + async getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { const schedulerUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName); const scheduler = await this.getAsJson(schedulerUrl, subscription.authentication); @@ -143,7 +144,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return scheduler; } - async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { + async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { const taskHubUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName, `/taskHubs/${taskHubName}`); const taskHub = await this.getAsJson(taskHubUrl, subscription.authentication); @@ -156,7 +157,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien const response = await this.getAsJson<{ value: DurableTaskHubResource[] }>(taskHubsUrl, subscription.authentication); - return response.value; + return response?.value ?? []; } private static getBaseUrl(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, relativeUrl?: string | undefined) { @@ -186,13 +187,18 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien } } - private async getAsJson(url: string, authentication: AzureAuthentication): Promise { + private async getAsJson(url: string, authentication: AzureAuthentication): Promise { const response = await this.fetch({ + allowNotFound: true, authentication, method: 'GET', url }); + if (response.status === 404) { + return undefined; + } + return await response.json() as T; } @@ -239,8 +245,8 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien const response = await fetch(request); - if (!response.ok) { - throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.')); + if (!response.ok && (response.status !== 404 || options.allowNotFound !== true)) { + throw new Error(localize('failureInvokingArmErrorMessage', 'The Azure management API returned an unsuccessful response ({0}): {1}', response.status, response.statusText)); } return response; @@ -257,7 +263,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien authentication ); - switch (status.status) { + switch (status?.status) { case 'Succeeded': return true; case 'Failed': return false; case 'Canceled': return false; @@ -269,7 +275,7 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien get, waitForCompletion: async token => { while (true) { - await delay(1000); + await delay(2500); if (token.isCancellationRequested) { return undefined; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index 5b3f7eb34..08e1cf304 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -38,8 +38,8 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo this.resource.resourceGroup, this.resource.name); - if (json.properties.provisioningState !== 'Succeeded') { - treeItem.description = localize('schedulerDescription', '({0})', json.properties.provisioningState); + if (json?.properties.provisioningState !== 'Succeeded') { + treeItem.description = localize('schedulerDescription', '({0})', json?.properties.provisioningState ?? 'Deleted'); } } @@ -79,7 +79,7 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo this.resource.resourceGroup, this.resource.name); - return json; + return json ?? ''; } }; } From 992c2f811130b2a82a1926257d356f929c885c39 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 12:35:32 -0800 Subject: [PATCH 29/58] Tweak strings for task hub selection. Signed-off-by: Phillip Hoff --- .../copySchedulerConnectionString.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts index 23ae3994b..96607fad0 100644 --- a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts +++ b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts @@ -27,32 +27,44 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur const { endpoint } = schedulerJson.properties; + const noAuthentication: QuickPickItem = { + detail: localize('noAuthenticationDetail', 'No credentials will be used.'), + label: localize('noAuthenticationLabel', 'None') + } + const localDevelopment: QuickPickItem = { + detail: localize('localDevelopmentDetail', 'Use the credentials of the local developer.'), label: localize('localDevelopmentLabel', 'Local development') }; const userAssignedManagedIdentity: QuickPickItem = { + detail: localize('userAssignedManagedIdentityDetail', 'Use managed identity credentials for a specific client.'), label: localize('userAssignedManagedIdentityLabel', 'User-assigned managed identity') } const systemAssignedManagedIdentity: QuickPickItem = { + detail: localize('systemAssignedManagedIdentityDetail', 'Use managed identity credentials for a client assigned to a specific Azure resource.'), label: localize('systemAssignedManagedIdentityLabel', 'System-assigned managed identity') } const result = await actionContext.ui.showQuickPick( [ + noAuthentication, localDevelopment, userAssignedManagedIdentity, systemAssignedManagedIdentity ], { canPickMany: false, - placeHolder: localize('authenticationTypePlaceholder', 'Select the type of authentication to be used') + placeHolder: localize('authenticationTypePlaceholder', 'Select the credentials to be used to connect to the scheduler') }); let connectionString = `Endpoint=${endpoint};Authentication=` - if (result === localDevelopment) { + if (result === noAuthentication) { + connectionString += 'None'; + } + else if (result === localDevelopment) { connectionString += 'DefaultAzure'; } else { @@ -71,7 +83,8 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur if (taskHubs.length > 0) { const noTaskHubItem: QuickPickItem = { - label: localize('noTaskHubLabel', 'No task hub') + detail: localize('noTaskHubDetail', 'Do not connect to a specific task hub.'), + label: localize('noTaskHubLabel', 'None') } const taskHubItems: QuickPickItem[] = @@ -88,7 +101,7 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur ], { canPickMany: false, - placeHolder: localize('taskHubSelectionPlaceholder', 'Select the task hub') + placeHolder: localize('taskHubSelectionPlaceholder', 'Select a task hub to connect to') }); if (taskHubResult && taskHubResult !== noTaskHubItem) { From b3f202236e7d229d5e2c2b866ccf5c1845b022a6 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 12:45:34 -0800 Subject: [PATCH 30/58] Wrap task hub creation with activity. Signed-off-by: Phillip Hoff --- src/commands/durableTaskScheduler/createTaskHub.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/commands/durableTaskScheduler/createTaskHub.ts b/src/commands/durableTaskScheduler/createTaskHub.ts index 3bf59a218..6df8c4aa2 100644 --- a/src/commands/durableTaskScheduler/createTaskHub.ts +++ b/src/commands/durableTaskScheduler/createTaskHub.ts @@ -3,14 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, type IActionContext } from "@microsoft/vscode-azext-utils"; +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, type ExecuteActivityContext, type IActionContext } from "@microsoft/vscode-azext-utils"; import { localize } from '../../localize'; import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { type Progress } from "vscode"; +import { createActivityContext } from "../../utils/activityUtils"; -interface ICreateTaskHubContext extends IActionContext { +interface ICreateTaskHubContext extends IActionContext, ExecuteActivityContext { readonly subscription: AzureSubscription; readonly resourceGroup: string; readonly schedulerName: string; @@ -58,10 +59,11 @@ export function createTaskHubCommandFactory(schedulerClient: DurableTaskSchedule const wizardContext: ICreateTaskHubContext = { - ...actionContext, subscription: scheduler.subscription, resourceGroup: scheduler.resourceGroup, - schedulerName: scheduler.name + schedulerName: scheduler.name, + ...actionContext, + ...await createActivityContext() }; const wizard = new AzureWizard( @@ -69,11 +71,13 @@ export function createTaskHubCommandFactory(schedulerClient: DurableTaskSchedule { promptSteps: [new TaskHubNamingStep()], executeSteps: [new TaskHubCreationStep(schedulerClient)], - title: localize('createTaskHubWizardTitle', 'Create Task Hub') + title: localize('createTaskHubWizardTitle', 'Create Durable Task Hub') }); await wizard.prompt(); + wizardContext.activityTitle = localize('createTaskHubActivityTitle', 'Create Durable Task Hub \'{0}\'', wizardContext.taskHubName); + try { await wizard.execute(); } From 0d5ee1a76ccc7708aee935579913ab3155c09ccb Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 13:11:09 -0800 Subject: [PATCH 31/58] Wrap task hub deletion with activity. Signed-off-by: Phillip Hoff --- .../durableTaskScheduler/deleteScheduler.ts | 94 ++++--------------- .../durableTaskScheduler/deleteTaskHub.ts | 25 +++-- .../DurableTaskSchedulerClient.ts | 8 +- src/utils/AzureActivity.ts | 80 ++++++++++++++++ 4 files changed, 119 insertions(+), 88 deletions(-) create mode 100644 src/utils/AzureActivity.ts diff --git a/src/commands/durableTaskScheduler/deleteScheduler.ts b/src/commands/durableTaskScheduler/deleteScheduler.ts index 76491ad9c..cc4575bec 100644 --- a/src/commands/durableTaskScheduler/deleteScheduler.ts +++ b/src/commands/durableTaskScheduler/deleteScheduler.ts @@ -6,54 +6,11 @@ import {type IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; import { localize } from "../../localize"; -import { type CancellationTokenSource, Disposable, type Event, EventEmitter, type MessageItem } from "vscode"; +import { type MessageItem } from "vscode"; import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; -import { type Activity, type ActivityTreeItemOptions, type OnErrorActivityData, type OnProgressActivityData } from "@microsoft/vscode-azext-utils/hostapi"; -import { randomUUID } from "crypto"; import { withCancellation } from "../../utils/cancellation"; -import { ext } from "../../extensionVariables"; - -export class AzureActivity extends Disposable implements Activity { - private readonly onStartEmitter = new EventEmitter; - private readonly onProgressEmitter = new EventEmitter; - private readonly onSuccessEmitter = new EventEmitter; - private readonly onErrorEmitter = new EventEmitter; - - private readonly uniqueId: string; - - constructor() { - super( - () => { - this.onStartEmitter.dispose(); - this.onProgressEmitter.dispose(); - this.onSuccessEmitter.dispose(); - this.onErrorEmitter.dispose(); - }); - - this.uniqueId = randomUUID(); - } - - get id(): string { return this.uniqueId; } - - cancellationTokenSource?: CancellationTokenSource | undefined; - - get onStart(): Event { return this.onStartEmitter.event; } - - get onProgress(): Event { return this.onProgressEmitter.event; } - - get onSuccess(): Event { return this.onSuccessEmitter.event; } - - get onError(): Event { return this.onErrorEmitter.event; } - - start(options: ActivityTreeItemOptions): void { this.onStartEmitter.fire(options); } - - progress(data: OnProgressActivityData): void { this.onProgressEmitter.fire(data); } - - succeed(options: ActivityTreeItemOptions): void { this.onSuccessEmitter.fire(options); } - - fail(data: OnErrorActivityData): void { this.onErrorEmitter.fire(data); } -} +import { withAzureActivity } from "../../utils/AzureActivity"; export function deleteSchedulerCommandFactory( dataBranchProvider: DurableTaskSchedulerDataBranchProvider, @@ -79,44 +36,25 @@ export function deleteSchedulerCommandFactory( return; } - const activity = new AzureActivity(); - - await ext.rgApi.registerActivity(activity); - - const label = localize('deletingSchedulerActivityLabel', 'Delete Durable Task Scheduler \'{0}\'', scheduler.name); - - activity.start({ - label - }); - try { - const response = await schedulerClient.deleteScheduler( - scheduler.subscription, - scheduler.resourceGroup, - scheduler.name - ); - - const result = await withCancellation(token => response.waitForCompletion(token), 1000 * 60 * 30); - - if (result === true) { - activity.succeed({ - label + await withAzureActivity( + localize('deletingSchedulerActivityLabel', 'Delete Durable Task Scheduler \'{0}\'', scheduler.name), + async () => { + const response = await schedulerClient.deleteScheduler( + scheduler.subscription, + scheduler.resourceGroup, + scheduler.name + ); + + const result = await withCancellation(token => response.waitForCompletion(token), 1000 * 60 * 30); + + if (result !== true) { + throw new Error(localize('deleteFailureMessage', 'The scheduler failed to delete within the allotted time.')); + } }); - } - else { - throw new Error(localize('deleteFailureMessage', 'The scheduler failed to delete within the allotted time.')); - } - } - catch (error: unknown) { - activity.fail({ - error, - label - }); } finally { dataBranchProvider.refresh(); - - activity.dispose(); } } } diff --git a/src/commands/durableTaskScheduler/deleteTaskHub.ts b/src/commands/durableTaskScheduler/deleteTaskHub.ts index 0954fb9a4..6b6b0d2ce 100644 --- a/src/commands/durableTaskScheduler/deleteTaskHub.ts +++ b/src/commands/durableTaskScheduler/deleteTaskHub.ts @@ -8,6 +8,8 @@ import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler import { localize } from "../../localize"; import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel"; import { type MessageItem } from "vscode"; +import { withAzureActivity } from "../../utils/AzureActivity"; +import { withCancellation } from "../../utils/cancellation"; export function deleteTaskHubCommandFactory(schedulerClient: DurableTaskSchedulerClient) { return async (actionContext: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise => { @@ -16,7 +18,7 @@ export function deleteTaskHubCommandFactory(schedulerClient: DurableTaskSchedule } const deleteItem: MessageItem = { - title: 'Delete' + title: localize('deleteTaskHubLabel', 'Delete') }; const result = await actionContext.ui.showWarningMessage( @@ -32,12 +34,21 @@ export function deleteTaskHubCommandFactory(schedulerClient: DurableTaskSchedule } try { - await schedulerClient.deleteTaskHub( - taskHub.scheduler.subscription, - taskHub.scheduler.resourceGroup, - taskHub.scheduler.name, - taskHub.name - ); + await withAzureActivity( + localize('deletingTaskHubActivityLabel', 'Delete Durable Task Hub \'{0}\'', taskHub.name), + async () => { + const response = await schedulerClient.deleteTaskHub( + taskHub.scheduler.subscription, + taskHub.scheduler.resourceGroup, + taskHub.scheduler.name, + taskHub.name); + + const result = await withCancellation(token => response.waitForCompletion(token), 1000 * 60 * 5); + + if (result !== true) { + throw new Error(localize('deleteFailureMessage', 'The scheduler failed to delete within the allotted time.')); + } + }); } finally { taskHub.scheduler.refresh(); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts index c1b3d6390..a3d54f178 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -70,7 +70,7 @@ export interface DurableTaskSchedulerClient { createTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; deleteScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; - deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; + deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise; getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise; @@ -130,10 +130,12 @@ export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClien return this.createStatus(response.asyncOperation, subscription.authentication); } - async deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { + async deleteTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise { const taskHubsUrl = HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName, `/taskhubs/${taskHubName}`); - await this.delete(taskHubsUrl, subscription.authentication); + const response = await this.delete(taskHubsUrl, subscription.authentication); + + return this.createStatus(response.asyncOperation, subscription.authentication); } async getScheduler(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { diff --git a/src/utils/AzureActivity.ts b/src/utils/AzureActivity.ts new file mode 100644 index 000000000..3fdf6cf60 --- /dev/null +++ b/src/utils/AzureActivity.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Activity, type ActivityTreeItemOptions, type OnErrorActivityData, type OnProgressActivityData } from "@microsoft/vscode-azext-utils/hostapi"; +import { randomUUID } from "crypto"; +import { type CancellationTokenSource, Disposable, type Event, EventEmitter } from "vscode"; +import { ext } from "../extensionVariables"; + +export class AzureActivity extends Disposable implements Activity { + private readonly onStartEmitter = new EventEmitter; + private readonly onProgressEmitter = new EventEmitter; + private readonly onSuccessEmitter = new EventEmitter; + private readonly onErrorEmitter = new EventEmitter; + + private readonly uniqueId: string; + + constructor() { + super( + () => { + this.onStartEmitter.dispose(); + this.onProgressEmitter.dispose(); + this.onSuccessEmitter.dispose(); + this.onErrorEmitter.dispose(); + }); + + this.uniqueId = randomUUID(); + } + + get id(): string { return this.uniqueId; } + + cancellationTokenSource?: CancellationTokenSource | undefined; + + get onStart(): Event { return this.onStartEmitter.event; } + + get onProgress(): Event { return this.onProgressEmitter.event; } + + get onSuccess(): Event { return this.onSuccessEmitter.event; } + + get onError(): Event { return this.onErrorEmitter.event; } + + start(options: ActivityTreeItemOptions): void { this.onStartEmitter.fire(options); } + + progress(data: OnProgressActivityData): void { this.onProgressEmitter.fire(data); } + + succeed(options: ActivityTreeItemOptions): void { this.onSuccessEmitter.fire(options); } + + fail(data: OnErrorActivityData): void { this.onErrorEmitter.fire(data); } +} + +export async function withAzureActivity( + label: string, + action: () => Promise): Promise { + + const activity = new AzureActivity(); + + await ext.rgApi.registerActivity(activity); + + activity.start({ + label + }); + + try { + await action(); + + activity.succeed({ + label + }); + } + catch (error: unknown) { + activity.fail({ + error, + label + }); + } + finally { + activity.dispose(); + } +} From 3b5226965b561dc99e6a9d7e857ecb972d5311a4 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 14:06:10 -0800 Subject: [PATCH 32/58] Add retry for task hub retrieval. Signed-off-by: Phillip Hoff --- .../DurableTaskSchedulerResourceModel.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index 08e1cf304..53c63a7db 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -9,6 +9,7 @@ import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { DurableTaskHubResourceModel } from "./DurableTaskHubResourceModel"; import { TreeItem, TreeItemCollapsibleState } from "vscode"; import { localize } from '../../localize'; +import * as retry from 'p-retry'; export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModel, AzureResourceModel { public constructor( @@ -22,7 +23,15 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.')); } - const taskHubs = await this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup, this.resource.name); + // NOTE: The DTS RP may return a 500 when getting task hubs for a just-deleted scheduler. + // In the case of such a failure, just wait a moment and try again. + + const taskHubs = await retry( + () => this.schedulerClient.getSchedulerTaskHubs(this.resource.subscription, this.resource.resourceGroup as string, this.resource.name), + { + retries: 3, + minTimeout: 2500 + }); return taskHubs.map(resource => new DurableTaskHubResourceModel(this, resource, this.schedulerClient)); } From 1eb351e6418741672a74a6bf18347ec2e3e8b590 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 15:26:26 -0800 Subject: [PATCH 33/58] Scaffold DTS emulators workspace tree. Signed-off-by: Phillip Hoff --- package.json | 3 +++ src/extension.ts | 5 +++++ ...TaskSchedulerEmulatorsWorkspaceResource.ts | 9 +++++++++ ...chedulerEmulatorsWorkspaceResourceModel.ts | 20 +++++++++++++++++++ ...askSchedulerWorkspaceDataBranchProvider.ts | 20 +++++++++++++++++++ ...ableTaskSchedulerWorkspaceResourceModel.ts | 8 ++++++++ ...eTaskSchedulerWorkspaceResourceProvider.ts | 11 ++++++++++ 7 files changed, 76 insertions(+) create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts diff --git a/package.json b/package.json index 620981215..c555f58a9 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,9 @@ "branches": [ { "type": "func" + }, + { + "type": "DurableTaskSchedulerEmulator" } ], "resources": true diff --git a/src/extension.ts b/src/extension.ts index 885aa65d5..c0f50a155 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -40,6 +40,8 @@ import { listLocalFunctions } from './workspace/listLocalFunctions'; import { listLocalProjects } from './workspace/listLocalProjects'; import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient'; +import { DurableTaskSchedulerWorkspaceDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider'; +import { DurableTaskSchedulerWorkspaceResourceProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider'; export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { ext.context = context; @@ -120,6 +122,9 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta ext.rgApiV2 = azureResourcesApi; azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, dataBranchProvider); + + azureResourcesApi.resources.registerWorkspaceResourceProvider(new DurableTaskSchedulerWorkspaceResourceProvider()); + azureResourcesApi.resources.registerWorkspaceResourceBranchDataProvider('DurableTaskSchedulerEmulator', new DurableTaskSchedulerWorkspaceDataBranchProvider()); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts new file mode 100644 index 000000000..c2e0048cd --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts @@ -0,0 +1,9 @@ +import { type WorkspaceResource } from "@microsoft/vscode-azureresources-api"; + +export class DurableTaskSchedulerEmulatorsWorkspaceResource implements WorkspaceResource { + resourceType: string = 'DurableTaskSchedulerEmulator'; + + id: string = 'DurableTaskSchedulerEmulator'; + + name: string = 'DurableTaskSchedulerEmulator'; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts new file mode 100644 index 000000000..decb3129a --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -0,0 +1,20 @@ +import { TreeItem, TreeItemCollapsibleState, type ProviderResult } from "vscode"; +import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; +import { localize } from "../../localize"; +import { treeUtils } from "../../utils/treeUtils"; + +export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { + getChildren(): ProviderResult { + return []; + } + + getTreeItem(): TreeItem | Thenable { + const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), TreeItemCollapsibleState.Collapsed); + + treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); + + return treeItem; + } + + id?: string | undefined; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts new file mode 100644 index 000000000..730ea1899 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts @@ -0,0 +1,20 @@ +import { type WorkspaceResource, type WorkspaceResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api"; +import { type ProviderResult, type Event, type TreeItem } from "vscode"; +import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; +import { DurableTaskSchedulerEmulatorsWorkspaceResourceModel } from "./DurableTaskSchedulerEmulatorsWorkspaceResourceModel"; + +export class DurableTaskSchedulerWorkspaceDataBranchProvider implements WorkspaceResourceBranchDataProvider { + getChildren(element: DurableTaskSchedulerWorkspaceResourceModel): ProviderResult { + return element.getChildren(); + } + + getResourceItem(_: WorkspaceResource): DurableTaskSchedulerWorkspaceResourceModel | Thenable { + return new DurableTaskSchedulerEmulatorsWorkspaceResourceModel(); + } + + onDidChangeTreeData?: Event | undefined; + + getTreeItem(element: DurableTaskSchedulerWorkspaceResourceModel): TreeItem | Thenable { + return element.getTreeItem(); + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts new file mode 100644 index 000000000..99cb3728b --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts @@ -0,0 +1,8 @@ +import { type WorkspaceResourceModel } from "@microsoft/vscode-azureresources-api"; +import { type ProviderResult, type TreeItem } from "vscode"; + +export interface DurableTaskSchedulerWorkspaceResourceModel extends WorkspaceResourceModel { + getChildren(): ProviderResult; + + getTreeItem(): TreeItem | Thenable; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts new file mode 100644 index 000000000..16adaebd0 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts @@ -0,0 +1,11 @@ +import { type WorkspaceResource, type WorkspaceResourceProvider } from "@microsoft/vscode-azureresources-api"; +import { type Event, type ProviderResult } from "vscode"; +import { DurableTaskSchedulerEmulatorsWorkspaceResource } from "./DurableTaskSchedulerEmulatorsWorkspaceResource"; + +export class DurableTaskSchedulerWorkspaceResourceProvider implements WorkspaceResourceProvider { + onDidChangeResource?: Event | undefined; + + getResources(): ProviderResult { + return [ new DurableTaskSchedulerEmulatorsWorkspaceResource() ]; + } +} From 7f292651b9b8a5aeecd41381519664ee0e700cf8 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 16:36:35 -0800 Subject: [PATCH 34/58] Sketch display of running DTS emulators. Signed-off-by: Phillip Hoff --- package-lock.json | 28 +++++++++++++ package.json | 2 + src/extension.ts | 8 +++- src/tree/durableTaskScheduler/DockerClient.ts | 40 +++++++++++++++++++ .../DurableTaskSchedulerEmulatorClient.ts | 26 ++++++++++++ ...SchedulerEmulatorWorkspaceResourceModel.ts | 24 +++++++++++ ...chedulerEmulatorsWorkspaceResourceModel.ts | 13 ++++-- ...askSchedulerWorkspaceDataBranchProvider.ts | 6 ++- 8 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/tree/durableTaskScheduler/DockerClient.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts diff --git a/package-lock.json b/package-lock.json index 02331ccd3..0c9ac3223 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@microsoft/vscode-azext-serviceconnector": "^0.1.3", "@microsoft/vscode-azext-utils": "^2.5.7", "@microsoft/vscode-azureresources-api": "^2.0.4", + "@microsoft/vscode-container-client": "^0.1.2", "cross-fetch": "^4.0.0", "escape-string-regexp": "^4.0.0", "extract-zip": "^2.0.1", @@ -75,6 +76,7 @@ "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", "vscode-azurekudu": "^0.2.0", + "vscode-jsonrpc": "^8.2.1", "webpack": "^5.76.0", "webpack-cli": "^4.6.0" }, @@ -1426,6 +1428,15 @@ "@azure/ms-rest-azure-env": "^2.0.0" } }, + "node_modules/@microsoft/vscode-container-client": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-container-client/-/vscode-container-client-0.1.2.tgz", + "integrity": "sha512-R90cDc2ggweJqc/jD85/rRSAHigRfKbqLWiOaKz8tpMtS+najxTSsNY5XWgg2/QhBjv4xgeWON/R0VRe0w8yiw==", + "dependencies": { + "dayjs": "^1.11.2", + "tree-kill": "^1.2.2" + } + }, "node_modules/@nevware21/ts-async": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.2.tgz", @@ -10525,6 +10536,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -11392,6 +11411,15 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/vscode-nls": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.2.tgz", diff --git a/package.json b/package.json index c555f58a9..21c89a143 100644 --- a/package.json +++ b/package.json @@ -1294,6 +1294,7 @@ "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", "vscode-azurekudu": "^0.2.0", + "vscode-jsonrpc": "^8.2.1", "webpack": "^5.76.0", "webpack-cli": "^4.6.0" }, @@ -1314,6 +1315,7 @@ "@microsoft/vscode-azext-serviceconnector": "^0.1.3", "@microsoft/vscode-azext-utils": "^2.5.7", "@microsoft/vscode-azureresources-api": "^2.0.4", + "@microsoft/vscode-container-client": "^0.1.2", "cross-fetch": "^4.0.0", "escape-string-regexp": "^4.0.0", "extract-zip": "^2.0.1", diff --git a/src/extension.ts b/src/extension.ts index c0f50a155..69a11d9ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -42,6 +42,8 @@ import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskSchedu import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient'; import { DurableTaskSchedulerWorkspaceDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider'; import { DurableTaskSchedulerWorkspaceResourceProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider'; +import { DockerDurableTaskSchedulerEmulatorClient } from './tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; +import { CliDockerClient } from './tree/durableTaskScheduler/DockerClient'; export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { ext.context = context; @@ -124,7 +126,11 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, dataBranchProvider); azureResourcesApi.resources.registerWorkspaceResourceProvider(new DurableTaskSchedulerWorkspaceResourceProvider()); - azureResourcesApi.resources.registerWorkspaceResourceBranchDataProvider('DurableTaskSchedulerEmulator', new DurableTaskSchedulerWorkspaceDataBranchProvider()); + azureResourcesApi.resources.registerWorkspaceResourceBranchDataProvider( + 'DurableTaskSchedulerEmulator', + new DurableTaskSchedulerWorkspaceDataBranchProvider( + new DockerDurableTaskSchedulerEmulatorClient( + new CliDockerClient()))); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DockerClient.ts b/src/tree/durableTaskScheduler/DockerClient.ts new file mode 100644 index 000000000..7a11e8655 --- /dev/null +++ b/src/tree/durableTaskScheduler/DockerClient.ts @@ -0,0 +1,40 @@ +import * as ContainerClient from '@microsoft/vscode-container-client'; + +export interface DockerContainer { + id: string; + image: string; + name: string; + ports: { [key: number]: number }; +} + +export interface DockerContainerInternal { + ID: string; + Image: string; + Names: string; + Ports: string; +} + +export interface DockerClient { + getContainers(): Promise; +} + +export class CliDockerClient implements DockerClient { + async getContainers(): Promise { + + const dockerClient = new ContainerClient.DockerClient(); + const factory = new ContainerClient.ShellStreamCommandRunnerFactory({ + }); + + const commandRunner = factory.getCommandRunner(); + + const containers = await commandRunner(dockerClient.listContainers({})); + + return containers.map( + container => ({ + id: container.id, + image: container.image.image as string, + name: container.name, + ports: [] + })); + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts new file mode 100644 index 000000000..b043a8072 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -0,0 +1,26 @@ +import { type DockerClient } from "./DockerClient"; + +export interface DurableTaskSchedulerEmulator { + id: string; + name: string; +} + +export interface DurableTaskSchedulerEmulatorClient { + getEmulators(): Promise; +} + +export class DockerDurableTaskSchedulerEmulatorClient implements DurableTaskSchedulerEmulatorClient { + constructor(private readonly dockerClient: DockerClient) { + } + + async getEmulators(): Promise { + const containers = await this.dockerClient.getContainers(); + + const emulatorContainers = containers.filter(container => container.image.toLowerCase().startsWith('durable-task-scheduler/emulator')); + + return emulatorContainers.map(container => ({ + id: container.id, + name: container.name + })); + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts new file mode 100644 index 000000000..9389388b0 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -0,0 +1,24 @@ +import { TreeItem, type ProviderResult } from "vscode"; +import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; +import { type DurableTaskSchedulerEmulator } from "./DurableTaskSchedulerEmulatorClient"; +import { treeUtils } from "../../utils/treeUtils"; + +export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { + constructor(private readonly emulator: DurableTaskSchedulerEmulator) { + } + + getChildren(): ProviderResult { + throw new Error("Method not implemented."); + } + + getTreeItem(): TreeItem | Thenable { + const treeItem = new TreeItem(this.emulator.name); + + treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); + treeItem.id = this.emulator.id; + + return treeItem; + } + + id?: string | undefined; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts index decb3129a..5758181ee 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -1,11 +1,18 @@ -import { TreeItem, TreeItemCollapsibleState, type ProviderResult } from "vscode"; +import { TreeItem, TreeItemCollapsibleState } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { localize } from "../../localize"; import { treeUtils } from "../../utils/treeUtils"; +import {type DurableTaskSchedulerEmulatorClient } from "./DurableTaskSchedulerEmulatorClient"; +import { DurableTaskSchedulerEmulatorWorkspaceResourceModel } from "./DurableTaskSchedulerEmulatorWorkspaceResourceModel"; export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { - getChildren(): ProviderResult { - return []; + constructor(private readonly emulatorClient: DurableTaskSchedulerEmulatorClient) { + } + + async getChildren(): Promise { + const emulators = await this.emulatorClient.getEmulators(); + + return emulators.map(emulator => new DurableTaskSchedulerEmulatorWorkspaceResourceModel(emulator)); } getTreeItem(): TreeItem | Thenable { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts index 730ea1899..efa5d8b3d 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts @@ -2,14 +2,18 @@ import { type WorkspaceResource, type WorkspaceResourceBranchDataProvider } from import { type ProviderResult, type Event, type TreeItem } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { DurableTaskSchedulerEmulatorsWorkspaceResourceModel } from "./DurableTaskSchedulerEmulatorsWorkspaceResourceModel"; +import { type DurableTaskSchedulerEmulatorClient } from "./DurableTaskSchedulerEmulatorClient"; export class DurableTaskSchedulerWorkspaceDataBranchProvider implements WorkspaceResourceBranchDataProvider { + constructor(private readonly emulatorClient: DurableTaskSchedulerEmulatorClient) { + } + getChildren(element: DurableTaskSchedulerWorkspaceResourceModel): ProviderResult { return element.getChildren(); } getResourceItem(_: WorkspaceResource): DurableTaskSchedulerWorkspaceResourceModel | Thenable { - return new DurableTaskSchedulerEmulatorsWorkspaceResourceModel(); + return new DurableTaskSchedulerEmulatorsWorkspaceResourceModel(this.emulatorClient); } onDidChangeTreeData?: Event | undefined; From 9678041dbeb27a5fe8b44ebbf2513b7505d78fa8 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 21:22:28 -0800 Subject: [PATCH 35/58] Scaffold start/stop emulator commands. Signed-off-by: Phillip Hoff --- package.json | 20 ++++++++++++++++ package.nls.json | 4 +++- .../durableTaskScheduler/startEmulator.ts | 8 +++++++ .../durableTaskScheduler/stopEmulator.ts | 9 +++++++ src/commands/registerCommands.ts | 6 +++++ src/extension.ts | 2 ++ .../DurableTaskSchedulerEmulatorClient.ts | 24 ++++++++++++++++++- ...SchedulerEmulatorWorkspaceResourceModel.ts | 1 + ...chedulerEmulatorsWorkspaceResourceModel.ts | 1 + 9 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/commands/durableTaskScheduler/startEmulator.ts create mode 100644 src/commands/durableTaskScheduler/stopEmulator.ts diff --git a/package.json b/package.json index 21c89a143..2e2346d7e 100644 --- a/package.json +++ b/package.json @@ -418,6 +418,16 @@ "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "title": "%azureFunctions.durableTaskScheduler.openTaskHubDashboard%", "category": "Azure Functions" + }, + { + "command": "azureFunctions.durableTaskScheduler.startEmulator", + "title": "%azureFunctions.durableTaskScheduler.startEmulator%", + "category": "Azure Functions" + }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "title": "%azureFunctions.durableTaskScheduler.stopEmulator%", + "category": "Azure Functions" } ], "submenus": [ @@ -746,6 +756,16 @@ "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/", "group": "1@1" + }, + { + "command": "azureFunctions.durableTaskScheduler.startEmulator", + "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulators/", + "group": "1@1" + }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulatorInstance/", + "group": "1@1" } ], "explorer/context": [ diff --git a/package.nls.json b/package.nls.json index f37c2b978..77182306b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -127,5 +127,7 @@ "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub...", "azureFunctions.durableTaskScheduler.deleteScheduler": "Delete Scheduler...", "azureFunctions.durableTaskScheduler.deleteTaskHub": "Delete Task Hub...", - "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" + "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard", + "azureFunctions.durableTaskScheduler.startEmulator": "Start Durable Task Emulator", + "azureFunctions.durableTaskScheduler.stopEmulator": "Stop Emulator" } diff --git a/src/commands/durableTaskScheduler/startEmulator.ts b/src/commands/durableTaskScheduler/startEmulator.ts new file mode 100644 index 000000000..af36c3e87 --- /dev/null +++ b/src/commands/durableTaskScheduler/startEmulator.ts @@ -0,0 +1,8 @@ +import { type IActionContext } from "@microsoft/vscode-azext-utils"; +import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; + +export function startEmulatorCommandFactory(_: DurableTaskSchedulerEmulatorClient) { + return async (_: IActionContext) => { + return Promise.resolve(); + }; +} diff --git a/src/commands/durableTaskScheduler/stopEmulator.ts b/src/commands/durableTaskScheduler/stopEmulator.ts new file mode 100644 index 000000000..74e10df88 --- /dev/null +++ b/src/commands/durableTaskScheduler/stopEmulator.ts @@ -0,0 +1,9 @@ +import { type IActionContext } from "@microsoft/vscode-azext-utils"; +import { type DurableTaskSchedulerEmulatorWorkspaceResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel"; +import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; + +export function stopEmulatorCommandFactory(_: DurableTaskSchedulerEmulatorClient) { + return async (__: IActionContext, ___: DurableTaskSchedulerEmulatorWorkspaceResourceModel | undefined) => { + return Promise.resolve(); + }; +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index ecc36821e..5628c07ba 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -72,11 +72,15 @@ import { deleteSchedulerCommandFactory } from './durableTaskScheduler/deleteSche import { type DurableTaskSchedulerDataBranchProvider } from '../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; import { copySchedulerEndpointCommandFactory } from './durableTaskScheduler/copySchedulerEndpoint'; import { copySchedulerConnectionStringCommandFactory } from './durableTaskScheduler/copySchedulerConnectionString'; +import { startEmulatorCommandFactory } from './durableTaskScheduler/startEmulator'; +import { stopEmulatorCommandFactory } from './durableTaskScheduler/stopEmulator'; +import { type DurableTaskSchedulerEmulatorClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; export function registerCommands( services: { dts: { dataBranchProvider: DurableTaskSchedulerDataBranchProvider, + emulatorClient: DurableTaskSchedulerEmulatorClient, schedulerClient: DurableTaskSchedulerClient } }): void { @@ -177,4 +181,6 @@ export function registerCommands( registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteScheduler', deleteSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteTaskHub', deleteTaskHubCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.startEmulator', startEmulatorCommandFactory(services.dts.emulatorClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.stopEmulator', stopEmulatorCommandFactory(services.dts.emulatorClient)); } diff --git a/src/extension.ts b/src/extension.ts index 69a11d9ac..def0be770 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -81,10 +81,12 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta const schedulerClient = new HttpDurableTaskSchedulerClient(); const dataBranchProvider = new DurableTaskSchedulerDataBranchProvider(schedulerClient); + const emulatorClient = new DockerDurableTaskSchedulerEmulatorClient(new CliDockerClient()); registerCommands({ dts: { dataBranchProvider, + emulatorClient, schedulerClient } }); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index b043a8072..71a5a8076 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -1,4 +1,6 @@ +import { type Event, EventEmitter } from "vscode"; import { type DockerClient } from "./DockerClient"; +import { Disposable } from "vscode"; export interface DurableTaskSchedulerEmulator { id: string; @@ -6,13 +8,25 @@ export interface DurableTaskSchedulerEmulator { } export interface DurableTaskSchedulerEmulatorClient { + readonly onEmulatorsChanged: Event; + getEmulators(): Promise; + startEmulator(): Promise; + stopEmulator(emulator: DurableTaskSchedulerEmulator): Promise; } -export class DockerDurableTaskSchedulerEmulatorClient implements DurableTaskSchedulerEmulatorClient { +export class DockerDurableTaskSchedulerEmulatorClient extends Disposable implements DurableTaskSchedulerEmulatorClient { + private readonly onEmulatorsChangedEmitter = new EventEmitter(); + constructor(private readonly dockerClient: DockerClient) { + super( + () => { + this.onEmulatorsChangedEmitter.dispose(); + }); } + get onEmulatorsChanged(): Event { return this.onEmulatorsChangedEmitter.event; } + async getEmulators(): Promise { const containers = await this.dockerClient.getContainers(); @@ -23,4 +37,12 @@ export class DockerDurableTaskSchedulerEmulatorClient implements DurableTaskSche name: container.name })); } + + startEmulator(): Promise { + return Promise.resolve({} as DurableTaskSchedulerEmulator); + } + + stopEmulator(_: DurableTaskSchedulerEmulator): Promise { + return Promise.resolve(); + } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts index 9389388b0..652ebfc85 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -14,6 +14,7 @@ export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements Durab getTreeItem(): TreeItem | Thenable { const treeItem = new TreeItem(this.emulator.name); + treeItem.contextValue = 'azFunc.dts.emulatorInstance'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); treeItem.id = this.emulator.id; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts index 5758181ee..f96ea62e7 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -18,6 +18,7 @@ export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements Dura getTreeItem(): TreeItem | Thenable { const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), TreeItemCollapsibleState.Collapsed); + treeItem.contextValue = 'azFunc.dts.emulators'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); return treeItem; From 59eb3e5edc71600691820403064af3960c4c7324 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 22:42:36 -0800 Subject: [PATCH 36/58] Implement stop emulator command. Signed-off-by: Phillip Hoff --- .../durableTaskScheduler/stopEmulator.ts | 11 +++++++--- src/extension.ts | 4 +--- src/tree/durableTaskScheduler/DockerClient.ts | 20 ++++++++++++------- .../DurableTaskSchedulerEmulatorClient.ts | 13 ++++++++---- ...SchedulerEmulatorWorkspaceResourceModel.ts | 2 +- ...askSchedulerWorkspaceDataBranchProvider.ts | 19 +++++++++++++++--- 6 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/commands/durableTaskScheduler/stopEmulator.ts b/src/commands/durableTaskScheduler/stopEmulator.ts index 74e10df88..74ce0b020 100644 --- a/src/commands/durableTaskScheduler/stopEmulator.ts +++ b/src/commands/durableTaskScheduler/stopEmulator.ts @@ -1,9 +1,14 @@ import { type IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskSchedulerEmulatorWorkspaceResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel"; import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; +import { localize } from "../../localize"; -export function stopEmulatorCommandFactory(_: DurableTaskSchedulerEmulatorClient) { - return async (__: IActionContext, ___: DurableTaskSchedulerEmulatorWorkspaceResourceModel | undefined) => { - return Promise.resolve(); +export function stopEmulatorCommandFactory(emulatorClient: DurableTaskSchedulerEmulatorClient) { + return async (__: IActionContext, emulator: DurableTaskSchedulerEmulatorWorkspaceResourceModel | undefined) => { + if (!emulator) { + throw new Error(localize('noEmulatorSelected', 'No emulator was selected.')); + } + + await emulatorClient.stopEmulator(emulator.id); }; } diff --git a/src/extension.ts b/src/extension.ts index def0be770..a46cb010b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -130,9 +130,7 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta azureResourcesApi.resources.registerWorkspaceResourceProvider(new DurableTaskSchedulerWorkspaceResourceProvider()); azureResourcesApi.resources.registerWorkspaceResourceBranchDataProvider( 'DurableTaskSchedulerEmulator', - new DurableTaskSchedulerWorkspaceDataBranchProvider( - new DockerDurableTaskSchedulerEmulatorClient( - new CliDockerClient()))); + new DurableTaskSchedulerWorkspaceDataBranchProvider(emulatorClient)); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DockerClient.ts b/src/tree/durableTaskScheduler/DockerClient.ts index 7a11e8655..b88a9664b 100644 --- a/src/tree/durableTaskScheduler/DockerClient.ts +++ b/src/tree/durableTaskScheduler/DockerClient.ts @@ -7,15 +7,9 @@ export interface DockerContainer { ports: { [key: number]: number }; } -export interface DockerContainerInternal { - ID: string; - Image: string; - Names: string; - Ports: string; -} - export interface DockerClient { getContainers(): Promise; + stopContainer(id: string): Promise; } export class CliDockerClient implements DockerClient { @@ -37,4 +31,16 @@ export class CliDockerClient implements DockerClient { ports: [] })); } + + async stopContainer(id: string): Promise { + const dockerClient = new ContainerClient.DockerClient(); + const factory = new ContainerClient.ShellStreamCommandRunnerFactory({ + }); + + const commandRunner = factory.getCommandRunner(); + + await commandRunner(dockerClient.stopContainers({ + container: [id] + })); + } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index 71a5a8076..badbfd399 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -12,7 +12,7 @@ export interface DurableTaskSchedulerEmulatorClient { getEmulators(): Promise; startEmulator(): Promise; - stopEmulator(emulator: DurableTaskSchedulerEmulator): Promise; + stopEmulator(id: string): Promise; } export class DockerDurableTaskSchedulerEmulatorClient extends Disposable implements DurableTaskSchedulerEmulatorClient { @@ -25,7 +25,7 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme }); } - get onEmulatorsChanged(): Event { return this.onEmulatorsChangedEmitter.event; } + readonly onEmulatorsChanged: Event = this.onEmulatorsChangedEmitter.event; async getEmulators(): Promise { const containers = await this.dockerClient.getContainers(); @@ -42,7 +42,12 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme return Promise.resolve({} as DurableTaskSchedulerEmulator); } - stopEmulator(_: DurableTaskSchedulerEmulator): Promise { - return Promise.resolve(); + async stopEmulator(id: string): Promise { + try { + await this.dockerClient.stopContainer(id); + } + finally { + this.onEmulatorsChangedEmitter.fire(); + } } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts index 652ebfc85..6d51f4a81 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -21,5 +21,5 @@ export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements Durab return treeItem; } - id?: string | undefined; + get id(): string { return this.emulator.id; } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts index efa5d8b3d..9a5d4585a 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts @@ -1,11 +1,24 @@ import { type WorkspaceResource, type WorkspaceResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api"; -import { type ProviderResult, type Event, type TreeItem } from "vscode"; +import { type ProviderResult, type Event, type TreeItem, Disposable, EventEmitter } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { DurableTaskSchedulerEmulatorsWorkspaceResourceModel } from "./DurableTaskSchedulerEmulatorsWorkspaceResourceModel"; import { type DurableTaskSchedulerEmulatorClient } from "./DurableTaskSchedulerEmulatorClient"; -export class DurableTaskSchedulerWorkspaceDataBranchProvider implements WorkspaceResourceBranchDataProvider { +export class DurableTaskSchedulerWorkspaceDataBranchProvider extends Disposable implements WorkspaceResourceBranchDataProvider { + private readonly onDidChangeTreeDataEmitter = new EventEmitter; + private readonly onEmulatorsChangedSubscription: Disposable; + constructor(private readonly emulatorClient: DurableTaskSchedulerEmulatorClient) { + super( + () => { + this.onEmulatorsChangedSubscription.dispose(); + this.onDidChangeTreeDataEmitter.dispose(); + }); + + this.onEmulatorsChangedSubscription = this.emulatorClient.onEmulatorsChanged( + () => { + this.onDidChangeTreeDataEmitter.fire(); + }); } getChildren(element: DurableTaskSchedulerWorkspaceResourceModel): ProviderResult { @@ -16,7 +29,7 @@ export class DurableTaskSchedulerWorkspaceDataBranchProvider implements Workspac return new DurableTaskSchedulerEmulatorsWorkspaceResourceModel(this.emulatorClient); } - onDidChangeTreeData?: Event | undefined; + get onDidChangeTreeData(): Event { return this.onDidChangeTreeDataEmitter.event; } getTreeItem(element: DurableTaskSchedulerWorkspaceResourceModel): TreeItem | Thenable { return element.getTreeItem(); From e34cddedfc24b1ec992c24df5a2c0ed6231e8618 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 3 Feb 2025 23:03:50 -0800 Subject: [PATCH 37/58] Implement start emulator command. Signed-off-by: Phillip Hoff --- .../durableTaskScheduler/startEmulator.ts | 4 ++-- src/tree/durableTaskScheduler/DockerClient.ts | 23 +++++++++++++++++++ .../DurableTaskSchedulerEmulatorClient.ts | 13 ++++++++--- ...chedulerEmulatorsWorkspaceResourceModel.ts | 2 +- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/commands/durableTaskScheduler/startEmulator.ts b/src/commands/durableTaskScheduler/startEmulator.ts index af36c3e87..8e28d2266 100644 --- a/src/commands/durableTaskScheduler/startEmulator.ts +++ b/src/commands/durableTaskScheduler/startEmulator.ts @@ -1,8 +1,8 @@ import { type IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; -export function startEmulatorCommandFactory(_: DurableTaskSchedulerEmulatorClient) { +export function startEmulatorCommandFactory(emulatorClient: DurableTaskSchedulerEmulatorClient) { return async (_: IActionContext) => { - return Promise.resolve(); + await emulatorClient.startEmulator(); }; } diff --git a/src/tree/durableTaskScheduler/DockerClient.ts b/src/tree/durableTaskScheduler/DockerClient.ts index b88a9664b..90700aab6 100644 --- a/src/tree/durableTaskScheduler/DockerClient.ts +++ b/src/tree/durableTaskScheduler/DockerClient.ts @@ -1,4 +1,5 @@ import * as ContainerClient from '@microsoft/vscode-container-client'; +import { localize } from '../../localize'; export interface DockerContainer { id: string; @@ -9,6 +10,8 @@ export interface DockerContainer { export interface DockerClient { getContainers(): Promise; + + startContainer(image: string): Promise; stopContainer(id: string): Promise; } @@ -32,6 +35,26 @@ export class CliDockerClient implements DockerClient { })); } + async startContainer(image: string): Promise { + const dockerClient = new ContainerClient.DockerClient(); + const factory = new ContainerClient.ShellStreamCommandRunnerFactory({ + }); + + const commandRunner = factory.getCommandRunner(); + + const id = await commandRunner(dockerClient.runContainer({ + detached: true, + imageRef: image, + publishAllPorts: true + })); + + if (!id) { + throw new Error(localize('startContainerFailed', 'Unable to start the container.')); + } + + return id; + } + async stopContainer(id: string): Promise { const dockerClient = new ContainerClient.DockerClient(); const factory = new ContainerClient.ShellStreamCommandRunnerFactory({ diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index badbfd399..a15320ba9 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -11,7 +11,7 @@ export interface DurableTaskSchedulerEmulatorClient { readonly onEmulatorsChanged: Event; getEmulators(): Promise; - startEmulator(): Promise; + startEmulator(): Promise; stopEmulator(id: string): Promise; } @@ -38,8 +38,15 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme })); } - startEmulator(): Promise { - return Promise.resolve({} as DurableTaskSchedulerEmulator); + async startEmulator(): Promise { + try { + const id = await this.dockerClient.startContainer('durabletasks.azurecr.io/durable-task-scheduler/emulator:latest-linux-arm64'); + + return id; + } + finally { + this.onEmulatorsChangedEmitter.fire(); + } } async stopEmulator(id: string): Promise { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts index f96ea62e7..e73bed270 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -16,7 +16,7 @@ export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements Dura } getTreeItem(): TreeItem | Thenable { - const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), TreeItemCollapsibleState.Collapsed); + const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), TreeItemCollapsibleState.Expanded); treeItem.contextValue = 'azFunc.dts.emulators'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); From 02092f9809ba9d0bb3fd06322187247b2ebc135a Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 00:48:44 -0800 Subject: [PATCH 38/58] Add open dashboard for emulator task hub. Signed-off-by: Phillip Hoff --- package.json | 2 +- .../openTaskHubDashboard.ts | 4 +-- src/tree/durableTaskScheduler/DockerClient.ts | 11 ++++++-- .../DurableTaskHubResourceModel.ts | 5 ++-- .../DurableTaskSchedulerDashboardModel.ts | 5 ++++ .../DurableTaskSchedulerEmulatorClient.ts | 8 ++++-- ...SchedulerEmulatorWorkspaceResourceModel.ts | 11 ++++++-- ...kSchedulerTaskHubWorkspaceResourceModel.ts | 28 +++++++++++++++++++ 8 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts diff --git a/package.json b/package.json index 2e2346d7e..e0176c0a1 100644 --- a/package.json +++ b/package.json @@ -754,7 +754,7 @@ }, { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/", + "when": "view =~ /(azureResourceGroups|azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHubDashboard/", "group": "1@1" }, { diff --git a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts index 99b0fc1ba..75613b247 100644 --- a/src/commands/durableTaskScheduler/openTaskHubDashboard.ts +++ b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { openUrl, type IActionContext } from "@microsoft/vscode-azext-utils"; -import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel"; import { localize } from '../../localize'; +import { type DurableTaskSchedulerDashboardModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel"; -export async function openTaskHubDashboard(_: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise { +export async function openTaskHubDashboard(_: IActionContext, taskHub: DurableTaskSchedulerDashboardModel | undefined): Promise { if (!taskHub) { throw new Error(localize('noTaskHubSelectedErrorMessage', 'No task hub was selected.')); } diff --git a/src/tree/durableTaskScheduler/DockerClient.ts b/src/tree/durableTaskScheduler/DockerClient.ts index 90700aab6..d808ee760 100644 --- a/src/tree/durableTaskScheduler/DockerClient.ts +++ b/src/tree/durableTaskScheduler/DockerClient.ts @@ -27,11 +27,18 @@ export class CliDockerClient implements DockerClient { const containers = await commandRunner(dockerClient.listContainers({})); return containers.map( - container => ({ + container => + ({ id: container.id, image: container.image.image as string, name: container.name, - ports: [] + ports: container.ports.reduce( + (previous, port) => { + previous[port.containerPort] = port.hostPort; + + return previous; + }, + {}) })); } diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 34e9f1d24..5b0d26ac5 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -10,8 +10,9 @@ import { type ProviderResult, TreeItem, Uri } from "vscode"; import { treeUtils } from "../../utils/treeUtils"; import { localize } from '../../localize'; import { type DurableTaskSchedulerResourceModel } from "./DurableTaskSchedulerResourceModel"; +import { type DurableTaskSchedulerDashboardModel } from "./DurableTaskSchedulerDashboardModel"; -export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { +export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel, DurableTaskSchedulerDashboardModel { constructor( public readonly scheduler: DurableTaskSchedulerResourceModel, private readonly resource: DurableTaskHubResource, @@ -59,7 +60,7 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel { const treeItem = new TreeItem(this.name) treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); - treeItem.contextValue = 'azFunc.dts.taskHub'; + treeItem.contextValue = 'azFunc.dts.taskHub azFunc.dts.taskHubDashboard'; const json = await this.schedulerClient.getSchedulerTaskHub( this.scheduler.subscription, diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts new file mode 100644 index 000000000..5792ebc30 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts @@ -0,0 +1,5 @@ +import { type Uri } from "vscode"; + +export interface DurableTaskSchedulerDashboardModel { + dashboardUrl: Uri; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index a15320ba9..2ca0a594b 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -1,10 +1,12 @@ -import { type Event, EventEmitter } from "vscode"; +import { type Event, EventEmitter, Uri } from "vscode"; import { type DockerClient } from "./DockerClient"; import { Disposable } from "vscode"; export interface DurableTaskSchedulerEmulator { + dashboardEndpoint: Uri; id: string; name: string; + schedulerEndpoint: Uri; } export interface DurableTaskSchedulerEmulatorClient { @@ -33,8 +35,10 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme const emulatorContainers = containers.filter(container => container.image.toLowerCase().startsWith('durable-task-scheduler/emulator')); return emulatorContainers.map(container => ({ + dashboardEndpoint: Uri.parse(`http://localhost:${container.ports[8082]}`), id: container.id, - name: container.name + name: container.name, + schedulerEndpoint: Uri.parse(`http://localhost:${container.ports[8080]}`) })); } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts index 6d51f4a81..6c74489e6 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -1,18 +1,23 @@ -import { TreeItem, type ProviderResult } from "vscode"; +import { TreeItem, TreeItemCollapsibleState, type ProviderResult } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { type DurableTaskSchedulerEmulator } from "./DurableTaskSchedulerEmulatorClient"; import { treeUtils } from "../../utils/treeUtils"; +import { DurableTaskSchedulerTaskHubWorkspaceResourceModel } from "./DurableTaskSchedulerTaskHubWorkspaceResourceModel"; export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { constructor(private readonly emulator: DurableTaskSchedulerEmulator) { } getChildren(): ProviderResult { - throw new Error("Method not implemented."); + return [ + new DurableTaskSchedulerTaskHubWorkspaceResourceModel( + 'default', + this.emulator.dashboardEndpoint) + ] } getTreeItem(): TreeItem | Thenable { - const treeItem = new TreeItem(this.emulator.name); + const treeItem = new TreeItem(this.emulator.name, TreeItemCollapsibleState.Expanded); treeItem.contextValue = 'azFunc.dts.emulatorInstance'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts new file mode 100644 index 000000000..8245df468 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts @@ -0,0 +1,28 @@ +import { type ProviderResult, TreeItem, Uri } from "vscode"; +import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; +import { treeUtils } from "../../utils/treeUtils"; +import { type DurableTaskSchedulerDashboardModel } from "./DurableTaskSchedulerDashboardModel"; + +export class DurableTaskSchedulerTaskHubWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel, DurableTaskSchedulerDashboardModel { + constructor( + private readonly name: string, + private readonly dashboardEndpoint: Uri) { + } + + getChildren(): ProviderResult { + return []; + } + + getTreeItem(): TreeItem | Thenable { + const treeItem = new TreeItem(this.name); + + treeItem.contextValue = 'azFunc.dts.emulatorTaskHub azFunc.dts.taskHubDashboard'; + treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); + + return treeItem; + } + + get dashboardUrl(): Uri { return Uri.joinPath(this.dashboardEndpoint, 'subscriptions/default/schedulers/default/taskhubs', this.name); } + + id?: string | undefined; +} From 2c9cd5f0b3bfdc87afcf6ebddfeca4b0e3608b21 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 12:29:32 -0800 Subject: [PATCH 39/58] Hide DTS commands from palette. Signed-off-by: Phillip Hoff --- package.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/package.json b/package.json index 620981215..707c5cef6 100644 --- a/package.json +++ b/package.json @@ -795,6 +795,34 @@ { "command": "azureFunctions.viewProperties", "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", + "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", + "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.createScheduler", + "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.createTaskHub", + "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.deleteScheduler", + "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.deleteTaskHub", + "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", + "when": "never" } ], "editor/context": [ From 85fe2bc2a2adf71d5c23db0508d833779b2d184d Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 12:35:45 -0800 Subject: [PATCH 40/58] Add verify providers step to scheduler creation. Signed-off-by: Phillip Hoff --- src/commands/durableTaskScheduler/createScheduler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index bf301a28b..8292bcb50 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; +import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, createSubscriptionContext, type ExecuteActivityContext, type IActionContext, type ISubscriptionActionContext, subscriptionExperience } from "@microsoft/vscode-azext-utils"; import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { DurableTaskProvider, DurableTaskSchedulersResourceType } from "../../constants"; @@ -90,6 +90,7 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch hideStepCount: true, promptSteps, executeSteps: [ + new VerifyProvidersStep([DurableTaskProvider]), new ResourceGroupCreateStep(), new SchedulerCreationStep(schedulerClient) ], From dc569e8036a942706d467fd4a634fe131516fea1 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 13:58:21 -0800 Subject: [PATCH 41/58] Un-hide create scheduler command. Signed-off-by: Phillip Hoff --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 707c5cef6..7f6d93df7 100644 --- a/package.json +++ b/package.json @@ -804,10 +804,6 @@ "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", "when": "never" }, - { - "command": "azureFunctions.durableTaskScheduler.createScheduler", - "when": "never" - }, { "command": "azureFunctions.durableTaskScheduler.createTaskHub", "when": "never" From 6407f4178e4d590b84d58b833bf4c6e7ece4f92a Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 15:16:24 -0800 Subject: [PATCH 42/58] Sketch DTS setting. Signed-off-by: Phillip Hoff --- package-lock.json | 1 + package.json | 17 +++++++- package.nls.json | 4 +- .../durableTaskScheduler/createScheduler.ts | 43 ++++++++++++++++++- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02331ccd3..3fb421475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@azure/arm-appservice": "^15.0.0", "@azure/arm-cosmosdb": "^15.0.0", "@azure/arm-eventhub": "^5.1.0", + "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0", "@azure/arm-servicebus": "^5.0.0", "@azure/arm-sql": "^9.1.0", "@azure/arm-storage": "^18.1.0", diff --git a/package.json b/package.json index 7f6d93df7..73aff1c6a 100644 --- a/package.json +++ b/package.json @@ -721,7 +721,7 @@ }, { "command": "azureFunctions.durableTaskScheduler.createScheduler", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i && config.azureFunctions.durableTaskScheduler.enableCreation == true", "group": "1@1" }, { @@ -804,6 +804,10 @@ "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", "when": "never" }, + { + "command": "azureFunctions.durableTaskScheduler.createScheduler", + "when": "config.azureFunctions.durableTaskScheduler.enableCreation == true" + }, { "command": "azureFunctions.durableTaskScheduler.createTaskHub", "when": "never" @@ -926,6 +930,16 @@ } ], "configuration": [ + { + "title": "Durable Task Scheduler", + "properties": { + "azureFunctions.durableTaskScheduler.enableCreation": { + "type": "boolean", + "default": false, + "description": "%azureFunctions.durableTaskScheduler.enableCreation%" + } + } + }, { "title": "Azure Functions", "properties": { @@ -1323,6 +1337,7 @@ "@azure/arm-appservice": "^15.0.0", "@azure/arm-cosmosdb": "^15.0.0", "@azure/arm-eventhub": "^5.1.0", + "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0", "@azure/arm-servicebus": "^5.0.0", "@azure/arm-sql": "^9.1.0", "@azure/arm-storage": "^18.1.0", diff --git a/package.nls.json b/package.nls.json index f37c2b978..bc77f335a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -127,5 +127,7 @@ "azureFunctions.durableTaskScheduler.createTaskHub": "Create Task Hub...", "azureFunctions.durableTaskScheduler.deleteScheduler": "Delete Scheduler...", "azureFunctions.durableTaskScheduler.deleteTaskHub": "Delete Task Hub...", - "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard" + "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard", + + "azureFunctions.durableTaskScheduler.enableCreation": "Enable Durable Task Scheduler creation" } diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index 8292bcb50..7ac3d1eb8 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; +import { type AzExtClientContext, createAzureClient, type ILocationWizardContext, type IResourceGroupWizardContext, LocationListStep, parseClientContext, ResourceGroupCreateStep, ResourceGroupListStep, VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, createSubscriptionContext, type ExecuteActivityContext, type IActionContext, type ISubscriptionActionContext, subscriptionExperience } from "@microsoft/vscode-azext-utils"; import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { DurableTaskProvider, DurableTaskSchedulersResourceType } from "../../constants"; @@ -13,7 +13,8 @@ import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; import { createActivityContext } from "../../utils/activityUtils"; import { withCancellation } from "../../utils/cancellation"; -import { type Progress } from "vscode"; +import { workspace, type Progress } from "vscode"; +import { type ResourceManagementClient } from '@azure/arm-resources'; interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationWizardContext, IResourceGroupWizardContext, ExecuteActivityContext { subscription?: AzureSubscription; @@ -63,6 +64,30 @@ class SchedulerCreationStep extends AzureWizardExecuteStep { + if (parseClientContext(context).isCustomCloud) { + return createAzureClient(context, (await import('@azure/arm-resources-profile-2020-09-01-hybrid')).ResourceManagementClient); + } else { + return createAzureClient(context, (await import('@azure/arm-resources')).ResourceManagementClient); + } +} + +export async function isDtsCreationEnabled(): Promise { + const configuration = workspace.getConfiguration('azureFunctions'); + + const enableCreation = configuration.get('durableTaskScheduler.enableCreation'); + + return Promise.resolve(enableCreation === true); +} + +export async function isDtsProviderRegistered(context: AzExtClientContext): Promise { + const resourcesClient = await createResourcesClient(context); + + const provider = await resourcesClient.providers.get(DurableTaskProvider); + + return provider.registrationState?.toLocaleLowerCase() === "registered"; +} + export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSchedulerDataBranchProvider, schedulerClient: DurableTaskSchedulerClient) { return async (actionContext: IActionContext, node?: { subscription: AzureSubscription }): Promise => { const subscription = node?.subscription ?? await subscriptionExperience(actionContext, ext.rgApiV2.resources.azureResourceTreeDataProvider); @@ -76,6 +101,20 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch ...await createActivityContext() }; + if (!await isDtsCreationEnabled()) { + throw new Error('Creation is not enabled!'); + } + + if (!await isDtsProviderRegistered(wizardContext)) { + await actionContext.ui.showWarningMessage( + localize('dtsProviderNotRegistered', 'The Durable Task Scheduler provider is not registered for the subscription.'), + { + learnMoreLink: 'https://aka.ms/' + }); + + return; + } + const promptSteps: AzureWizardPromptStep[] = [ new SchedulerNamingStep(), new ResourceGroupListStep() From 0370550cdd4db724b38ac401524d79c78ab67c08 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 15:27:27 -0800 Subject: [PATCH 43/58] Refactor DTS preview setting name. Signed-off-by: Phillip Hoff --- package.json | 8 ++++---- package.nls.json | 2 +- .../durableTaskScheduler/createScheduler.ts | 18 +++--------------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 73aff1c6a..e1b926d5b 100644 --- a/package.json +++ b/package.json @@ -721,7 +721,7 @@ }, { "command": "azureFunctions.durableTaskScheduler.createScheduler", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i && config.azureFunctions.durableTaskScheduler.enableCreation == true", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /DurableTaskScheduler/i && viewItem =~ /azureResourceTypeGroup/i && config.azureFunctions.durableTaskScheduler.enablePreviewFeatures == true", "group": "1@1" }, { @@ -806,7 +806,7 @@ }, { "command": "azureFunctions.durableTaskScheduler.createScheduler", - "when": "config.azureFunctions.durableTaskScheduler.enableCreation == true" + "when": "config.azureFunctions.durableTaskScheduler.enablePreviewFeatures == true" }, { "command": "azureFunctions.durableTaskScheduler.createTaskHub", @@ -933,10 +933,10 @@ { "title": "Durable Task Scheduler", "properties": { - "azureFunctions.durableTaskScheduler.enableCreation": { + "azureFunctions.durableTaskScheduler.enablePreviewFeatures": { "type": "boolean", "default": false, - "description": "%azureFunctions.durableTaskScheduler.enableCreation%" + "description": "%azureFunctions.durableTaskScheduler.enablePreviewFeatures%" } } }, diff --git a/package.nls.json b/package.nls.json index bc77f335a..29619393e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -129,5 +129,5 @@ "azureFunctions.durableTaskScheduler.deleteTaskHub": "Delete Task Hub...", "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard", - "azureFunctions.durableTaskScheduler.enableCreation": "Enable Durable Task Scheduler creation" + "azureFunctions.durableTaskScheduler.enablePreviewFeatures": "Enable Durable Task Scheduler preview features" } diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index 7ac3d1eb8..0859aa4d3 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -13,7 +13,7 @@ import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; import { createActivityContext } from "../../utils/activityUtils"; import { withCancellation } from "../../utils/cancellation"; -import { workspace, type Progress } from "vscode"; +import { type Progress } from "vscode"; import { type ResourceManagementClient } from '@azure/arm-resources'; interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationWizardContext, IResourceGroupWizardContext, ExecuteActivityContext { @@ -72,14 +72,6 @@ export async function createResourcesClient(context: AzExtClientContext): Promis } } -export async function isDtsCreationEnabled(): Promise { - const configuration = workspace.getConfiguration('azureFunctions'); - - const enableCreation = configuration.get('durableTaskScheduler.enableCreation'); - - return Promise.resolve(enableCreation === true); -} - export async function isDtsProviderRegistered(context: AzExtClientContext): Promise { const resourcesClient = await createResourcesClient(context); @@ -101,15 +93,11 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch ...await createActivityContext() }; - if (!await isDtsCreationEnabled()) { - throw new Error('Creation is not enabled!'); - } - if (!await isDtsProviderRegistered(wizardContext)) { await actionContext.ui.showWarningMessage( - localize('dtsProviderNotRegistered', 'The Durable Task Scheduler provider is not registered for the subscription.'), + localize('dtsProviderNotRegistered', 'The Durable Task Scheduler provider ({0}) is not registered for the subscription ({1}).', DurableTaskProvider, subscription.subscriptionId), { - learnMoreLink: 'https://aka.ms/' + learnMoreLink: 'https://aka.ms/dts-preview-info' }); return; From b8ae76e734db05d9de21e33453461473d8e8f612 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 15:42:40 -0800 Subject: [PATCH 44/58] Add preview features enabled check during create. Signed-off-by: Phillip Hoff --- src/commands/durableTaskScheduler/createScheduler.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/commands/durableTaskScheduler/createScheduler.ts b/src/commands/durableTaskScheduler/createScheduler.ts index 0859aa4d3..b236e2a0d 100644 --- a/src/commands/durableTaskScheduler/createScheduler.ts +++ b/src/commands/durableTaskScheduler/createScheduler.ts @@ -13,7 +13,7 @@ import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler import { type DurableTaskSchedulerDataBranchProvider } from "../../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider"; import { createActivityContext } from "../../utils/activityUtils"; import { withCancellation } from "../../utils/cancellation"; -import { type Progress } from "vscode"; +import { workspace, type Progress } from "vscode"; import { type ResourceManagementClient } from '@azure/arm-resources'; interface ICreateSchedulerContext extends ISubscriptionActionContext, ILocationWizardContext, IResourceGroupWizardContext, ExecuteActivityContext { @@ -72,6 +72,12 @@ export async function createResourcesClient(context: AzExtClientContext): Promis } } +export function isDtsPreviewFeaturesEnabled(): boolean { + const configuration = workspace.getConfiguration('azureFunctions'); + + return configuration.get('durableTaskScheduler.enablePreviewFeatures') === true; +} + export async function isDtsProviderRegistered(context: AzExtClientContext): Promise { const resourcesClient = await createResourcesClient(context); @@ -93,6 +99,10 @@ export function createSchedulerCommandFactory(dataBranchProvider: DurableTaskSch ...await createActivityContext() }; + if (!isDtsPreviewFeaturesEnabled()) { + throw new Error(localize('dtsPreviewFeaturesNotEnabled', 'Durable Task Scheduler preview features have not been enabled in settings.')); + } + if (!await isDtsProviderRegistered(wizardContext)) { await actionContext.ui.showWarningMessage( localize('dtsProviderNotRegistered', 'The Durable Task Scheduler provider ({0}) is not registered for the subscription ({1}).', DurableTaskProvider, subscription.subscriptionId), From e0f438ca8915f26cd70c516969ab6883ff6970e3 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 4 Feb 2025 23:54:23 -0800 Subject: [PATCH 45/58] Stop local emulators on shutdown. Signed-off-by: Phillip Hoff --- src/extension.ts | 19 +++---- .../{DockerClient.ts => ContainerClient.ts} | 49 ++++++++++--------- .../DurableTaskSchedulerEmulatorClient.ts | 30 ++++++++++-- 3 files changed, 60 insertions(+), 38 deletions(-) rename src/tree/durableTaskScheduler/{DockerClient.ts => ContainerClient.ts} (50%) diff --git a/src/extension.ts b/src/extension.ts index a46cb010b..42352bbbd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -33,17 +33,19 @@ import { validateFuncCoreToolsInstalled } from './funcCoreTools/validateFuncCore import { validateFuncCoreToolsIsLatest } from './funcCoreTools/validateFuncCoreToolsIsLatest'; import { getResourceGroupsApi } from './getExtensionApi'; import { CentralTemplateProvider } from './templates/CentralTemplateProvider'; +import { ShellContainerClient } from './tree/durableTaskScheduler/ContainerClient'; +import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient'; +import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; +import { DockerDurableTaskSchedulerEmulatorClient } from './tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; +import { DurableTaskSchedulerWorkspaceDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider'; +import { DurableTaskSchedulerWorkspaceResourceProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider'; import { registerContentProvider } from './utils/textUtils'; import { verifyVSCodeConfigOnActivate } from './vsCodeConfig/verifyVSCodeConfigOnActivate'; import { type AzureFunctionsExtensionApi } from './vscode-azurefunctions.api'; import { listLocalFunctions } from './workspace/listLocalFunctions'; import { listLocalProjects } from './workspace/listLocalProjects'; -import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; -import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient'; -import { DurableTaskSchedulerWorkspaceDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider'; -import { DurableTaskSchedulerWorkspaceResourceProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider'; -import { DockerDurableTaskSchedulerEmulatorClient } from './tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; -import { CliDockerClient } from './tree/durableTaskScheduler/DockerClient'; + +const emulatorClient = new DockerDurableTaskSchedulerEmulatorClient(new ShellContainerClient()); export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { ext.context = context; @@ -81,7 +83,6 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta const schedulerClient = new HttpDurableTaskSchedulerClient(); const dataBranchProvider = new DurableTaskSchedulerDataBranchProvider(schedulerClient); - const emulatorClient = new DockerDurableTaskSchedulerEmulatorClient(new CliDockerClient()); registerCommands({ dts: { @@ -150,6 +151,6 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta }]); } -// eslint-disable-next-line @typescript-eslint/no-empty-function -export function deactivateInternal(): void { +export async function deactivateInternal(): Promise { + await emulatorClient.disposeAsync(); } diff --git a/src/tree/durableTaskScheduler/DockerClient.ts b/src/tree/durableTaskScheduler/ContainerClient.ts similarity index 50% rename from src/tree/durableTaskScheduler/DockerClient.ts rename to src/tree/durableTaskScheduler/ContainerClient.ts index d808ee760..89975488c 100644 --- a/src/tree/durableTaskScheduler/DockerClient.ts +++ b/src/tree/durableTaskScheduler/ContainerClient.ts @@ -1,4 +1,4 @@ -import * as ContainerClient from '@microsoft/vscode-container-client'; +import * as CodeContainerClient from '@microsoft/vscode-container-client'; import { localize } from '../../localize'; export interface DockerContainer { @@ -8,27 +8,28 @@ export interface DockerContainer { ports: { [key: number]: number }; } -export interface DockerClient { +export interface ContainerClient { getContainers(): Promise; - startContainer(image: string): Promise; + removeContainer(id: string): Promise; + + runContainer(image: string): Promise; + stopContainer(id: string): Promise; } -export class CliDockerClient implements DockerClient { - async getContainers(): Promise { +export class ShellContainerClient implements ContainerClient { + private readonly dockerClient = new CodeContainerClient.DockerClient(); + private readonly factory = new CodeContainerClient.ShellStreamCommandRunnerFactory({}); - const dockerClient = new ContainerClient.DockerClient(); - const factory = new ContainerClient.ShellStreamCommandRunnerFactory({ - }); - - const commandRunner = factory.getCommandRunner(); + async getContainers(): Promise { + const commandRunner = this.factory.getCommandRunner(); - const containers = await commandRunner(dockerClient.listContainers({})); + const containers = await commandRunner(this.dockerClient.listContainers({})); return containers.map( container => - ({ + ({ id: container.id, image: container.image.image as string, name: container.name, @@ -42,14 +43,18 @@ export class CliDockerClient implements DockerClient { })); } - async startContainer(image: string): Promise { - const dockerClient = new ContainerClient.DockerClient(); - const factory = new ContainerClient.ShellStreamCommandRunnerFactory({ - }); + async removeContainer(id: string): Promise { + const commandRunner = this.factory.getCommandRunner(); - const commandRunner = factory.getCommandRunner(); + await commandRunner(this.dockerClient.removeContainers({ + containers: [id] + })); + } - const id = await commandRunner(dockerClient.runContainer({ + async runContainer(image: string): Promise { + const commandRunner = this.factory.getCommandRunner(); + + const id = await commandRunner(this.dockerClient.runContainer({ detached: true, imageRef: image, publishAllPorts: true @@ -63,13 +68,9 @@ export class CliDockerClient implements DockerClient { } async stopContainer(id: string): Promise { - const dockerClient = new ContainerClient.DockerClient(); - const factory = new ContainerClient.ShellStreamCommandRunnerFactory({ - }); - - const commandRunner = factory.getCommandRunner(); + const commandRunner = this.factory.getCommandRunner(); - await commandRunner(dockerClient.stopContainers({ + await commandRunner(this.dockerClient.stopContainers({ container: [id] })); } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index 2ca0a594b..46611dc61 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -1,6 +1,5 @@ -import { type Event, EventEmitter, Uri } from "vscode"; -import { type DockerClient } from "./DockerClient"; -import { Disposable } from "vscode"; +import { Disposable, type Event, EventEmitter, Uri } from "vscode"; +import { type ContainerClient } from "./ContainerClient"; export interface DurableTaskSchedulerEmulator { dashboardEndpoint: Uri; @@ -20,7 +19,9 @@ export interface DurableTaskSchedulerEmulatorClient { export class DockerDurableTaskSchedulerEmulatorClient extends Disposable implements DurableTaskSchedulerEmulatorClient { private readonly onEmulatorsChangedEmitter = new EventEmitter(); - constructor(private readonly dockerClient: DockerClient) { + private localEmulatorIds = new Set(); + + constructor(private readonly dockerClient: ContainerClient) { super( () => { this.onEmulatorsChangedEmitter.dispose(); @@ -29,6 +30,17 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme readonly onEmulatorsChanged: Event = this.onEmulatorsChangedEmitter.event; + async disposeAsync(): Promise { + for (const id of this.localEmulatorIds) { + await this.dockerClient.stopContainer(id); + await this.dockerClient.removeContainer(id); + } + + this.localEmulatorIds.clear(); + + this.dispose(); + } + async getEmulators(): Promise { const containers = await this.dockerClient.getContainers(); @@ -44,7 +56,9 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme async startEmulator(): Promise { try { - const id = await this.dockerClient.startContainer('durabletasks.azurecr.io/durable-task-scheduler/emulator:latest-linux-arm64'); + const id = await this.dockerClient.runContainer('durabletasks.azurecr.io/durable-task-scheduler/emulator:latest-linux-arm64'); + + this.localEmulatorIds.add(id); return id; } @@ -56,6 +70,12 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme async stopEmulator(id: string): Promise { try { await this.dockerClient.stopContainer(id); + + if (this.localEmulatorIds.has(id)) { + await this.dockerClient.removeContainer(id); + + this.localEmulatorIds.delete(id); + } } finally { this.onEmulatorsChangedEmitter.fire(); From 837a90c0f10dc304038fc08fd91df60736ebc7b5 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 5 Feb 2025 15:38:19 -0800 Subject: [PATCH 46/58] Add copy endpoint command to emulator items. Signed-off-by: Phillip Hoff --- package.json | 2 +- .../copySchedulerEndpoint.ts | 22 +++++-------------- src/commands/registerCommands.ts | 2 +- .../DurableTaskSchedulerDataBranchProvider.ts | 17 +++++++++++--- .../DurableTaskSchedulerEmulatorClient.ts | 4 +++- ...SchedulerEmulatorWorkspaceResourceModel.ts | 19 +++++++++------- .../DurableTaskSchedulerEndpointModel.ts | 5 +++++ .../DurableTaskSchedulerResourceModel.ts | 18 ++++++--------- 8 files changed, 48 insertions(+), 41 deletions(-) create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts diff --git a/package.json b/package.json index 686a8921c..a304f0939 100644 --- a/package.json +++ b/package.json @@ -729,7 +729,7 @@ }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerEndpoint", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", + "when": "view =~ /(azureResourceGroups|azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.schedulerEndpoint/", "group": "3@2" }, { diff --git a/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts b/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts index 4dded714a..175cf31a5 100644 --- a/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts +++ b/src/commands/durableTaskScheduler/copySchedulerEndpoint.ts @@ -4,32 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { type IActionContext } from "@microsoft/vscode-azext-utils"; -import { type DurableTaskSchedulerClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerClient"; -import { type DurableTaskSchedulerResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerResourceModel"; import { localize } from "../../localize"; import { ext } from "../../extensionVariables"; import { env } from "vscode"; +import { type DurableTaskSchedulerEndpointModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel"; -export function copySchedulerEndpointCommandFactory(schedulerClient: DurableTaskSchedulerClient) { - return async (_: IActionContext, scheduler: DurableTaskSchedulerResourceModel | undefined): Promise => { +export function copySchedulerEndpointCommandFactory() { + return async (_: IActionContext, scheduler: DurableTaskSchedulerEndpointModel | undefined): Promise => { if (!scheduler) { throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); } - const schedulerJson = await schedulerClient.getScheduler( - scheduler.subscription, - scheduler.resourceGroup, - scheduler.name); + const { endpointUrl } = scheduler; - if (!schedulerJson) { - throw new Error(localize('schedulerNotFoundErrorMessage', 'Scheduler does not exist.')); - } - - const { endpoint } = schedulerJson.properties; - - await env.clipboard.writeText(endpoint); + await env.clipboard.writeText(endpointUrl.toString()); ext.outputChannel.show(); - ext.outputChannel.appendLog(localize('schedulerEndpointCopiedMessage', 'Endpoint copied to clipboard: {0}', endpoint)); + ext.outputChannel.appendLog(localize('schedulerEndpointCopiedMessage', 'Endpoint copied to clipboard: {0}', endpointUrl.toString())); } } diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 5628c07ba..aaf9d988f 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -175,7 +175,7 @@ export function registerCommands( registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerConnectionString', copySchedulerConnectionStringCommandFactory(services.dts.schedulerClient)); - registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerEndpoint', copySchedulerEndpointCommandFactory(services.dts.schedulerClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerEndpoint', copySchedulerEndpointCommandFactory()); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createTaskHub', createTaskHubCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.deleteScheduler', deleteSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts index 6a3aaca19..fa7cb9f88 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts @@ -5,7 +5,7 @@ import { type AzureResource, type AzureResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api"; import { EventEmitter, type ProviderResult, type TreeItem } from "vscode"; -import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; +import { type DurableTaskSchedulerResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; import { DurableTaskSchedulerResourceModel } from "./DurableTaskSchedulerResourceModel"; @@ -21,9 +21,20 @@ export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBran return element.getChildren(); } - getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable { + async getResourceItem(azureResource: AzureResource): Promise { + + let schedulerResource: DurableTaskSchedulerResource | undefined; + + if (azureResource.resourceGroup) { + schedulerResource = await this.schedulerClient.getScheduler( + azureResource.subscription, + azureResource.resourceGroup, + azureResource.name); + } + return new DurableTaskSchedulerResourceModel( - element, + azureResource, + schedulerResource, this.schedulerClient, model => this.onDidChangeTreeDataEventEmitter.fire(model)); } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index 46611dc61..e921312c8 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -6,6 +6,7 @@ export interface DurableTaskSchedulerEmulator { id: string; name: string; schedulerEndpoint: Uri; + taskHubs: string[]; } export interface DurableTaskSchedulerEmulatorClient { @@ -50,7 +51,8 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme dashboardEndpoint: Uri.parse(`http://localhost:${container.ports[8082]}`), id: container.id, name: container.name, - schedulerEndpoint: Uri.parse(`http://localhost:${container.ports[8080]}`) + schedulerEndpoint: Uri.parse(`http://localhost:${container.ports[8080]}`), + taskHubs: ['default'] })); } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts index 6c74489e6..4983e0bc9 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -1,30 +1,33 @@ -import { TreeItem, TreeItemCollapsibleState, type ProviderResult } from "vscode"; +import { TreeItem, TreeItemCollapsibleState, type Uri, type ProviderResult } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { type DurableTaskSchedulerEmulator } from "./DurableTaskSchedulerEmulatorClient"; import { treeUtils } from "../../utils/treeUtils"; import { DurableTaskSchedulerTaskHubWorkspaceResourceModel } from "./DurableTaskSchedulerTaskHubWorkspaceResourceModel"; +import { type DurableTaskSchedulerEndpointModel } from "./DurableTaskSchedulerEndpointModel"; -export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { +export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel, DurableTaskSchedulerEndpointModel { constructor(private readonly emulator: DurableTaskSchedulerEmulator) { } getChildren(): ProviderResult { - return [ - new DurableTaskSchedulerTaskHubWorkspaceResourceModel( - 'default', - this.emulator.dashboardEndpoint) - ] + return this.emulator.taskHubs.map( + taskHub => + new DurableTaskSchedulerTaskHubWorkspaceResourceModel( + taskHub, + this.emulator.dashboardEndpoint)); } getTreeItem(): TreeItem | Thenable { const treeItem = new TreeItem(this.emulator.name, TreeItemCollapsibleState.Expanded); - treeItem.contextValue = 'azFunc.dts.emulatorInstance'; + treeItem.contextValue = 'azFunc.dts.emulatorInstance azFunc.dts.schedulerEndpoint'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); treeItem.id = this.emulator.id; return treeItem; } + get endpointUrl(): Uri { return this.emulator.schedulerEndpoint; } + get id(): string { return this.emulator.id; } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts new file mode 100644 index 000000000..41d9878fd --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts @@ -0,0 +1,5 @@ +import { type Uri } from "vscode"; + +export interface DurableTaskSchedulerEndpointModel { + endpointUrl: Uri; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index 53c63a7db..60033be41 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -5,7 +5,7 @@ import { type ViewPropertiesModel, type AzureResource, type AzureResourceModel } from "@microsoft/vscode-azureresources-api"; import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel"; -import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; +import { type DurableTaskSchedulerResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient"; import { DurableTaskHubResourceModel } from "./DurableTaskHubResourceModel"; import { TreeItem, TreeItemCollapsibleState } from "vscode"; import { localize } from '../../localize'; @@ -14,6 +14,7 @@ import * as retry from 'p-retry'; export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModel, AzureResourceModel { public constructor( private readonly resource: AzureResource, + private readonly schedulerResource: DurableTaskSchedulerResource | undefined, private readonly schedulerClient: DurableTaskSchedulerClient, private readonly refreshModel: (model: DurableTaskSchedulerModel | undefined) => void) { } @@ -39,17 +40,10 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo async getTreeItem(): Promise { const treeItem = new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); - treeItem.contextValue = 'azFunc.dts.scheduler'; + treeItem.contextValue = 'azFunc.dts.scheduler azFunc.dts.schedulerEndpoint'; - if (this.resource.resourceGroup) { - const json = await this.schedulerClient.getScheduler( - this.resource.subscription, - this.resource.resourceGroup, - this.resource.name); - - if (json?.properties.provisioningState !== 'Succeeded') { - treeItem.description = localize('schedulerDescription', '({0})', json?.properties.provisioningState ?? 'Deleted'); - } + if (this.schedulerResource?.properties.provisioningState !== 'Succeeded') { + treeItem.description = localize('schedulerDescription', '({0})', this.schedulerResource?.properties.provisioningState ?? 'Deleted'); } return treeItem; @@ -73,6 +67,8 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo return this.resource.resourceGroup; } + get endpointUrl() { return this.schedulerResource?.properties.endpoint; } + get subscription() { return this.resource.subscription; } get viewProperties(): ViewPropertiesModel { From 9061445321f5ae355a8f2f10671a2f7f0fd0a7e2 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 7 Feb 2025 00:00:00 -0800 Subject: [PATCH 47/58] Add copy connection string for emulators. Signed-off-by: Phillip Hoff --- package.json | 14 +++++ package.nls.json | 1 + .../copyEmulatorConnectionString.ts | 58 +++++++++++++++++++ .../copySchedulerConnectionString.ts | 13 +---- src/commands/registerCommands.ts | 2 + ...SchedulerEmulatorWorkspaceResourceModel.ts | 2 + 6 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 src/commands/durableTaskScheduler/copyEmulatorConnectionString.ts diff --git a/package.json b/package.json index a304f0939..d386b0143 100644 --- a/package.json +++ b/package.json @@ -384,6 +384,11 @@ "category": "Azure Functions", "icon": "$(notebook-execute)" }, + { + "command": "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString", + "title": "%azureFunctions.durableTaskScheduler.copyEmulatorConnectionString%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", "title": "%azureFunctions.durableTaskScheduler.copySchedulerConnectionString%", @@ -722,6 +727,11 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.*folder/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString", + "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulatorInstance/", + "group": "3@1" + }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", @@ -819,6 +829,10 @@ "command": "azureFunctions.viewProperties", "when": "never" }, + { + "command": "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString", + "when": "never" + }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", "when": "never" diff --git a/package.nls.json b/package.nls.json index 676d3b4ae..f9d53dc85 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,6 +120,7 @@ "azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios", "azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions", + "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString": "Copy Connection String", "azureFunctions.durableTaskScheduler.copySchedulerConnectionString": "Copy Connection String", "azureFunctions.durableTaskScheduler.copySchedulerEndpoint": "Copy Endpoint", "azureFunctions.durableTaskScheduler.createScheduler": "Create Durable Task Scheduler...", diff --git a/src/commands/durableTaskScheduler/copyEmulatorConnectionString.ts b/src/commands/durableTaskScheduler/copyEmulatorConnectionString.ts new file mode 100644 index 000000000..ba94b0d2e --- /dev/null +++ b/src/commands/durableTaskScheduler/copyEmulatorConnectionString.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 { type IActionContext } from "@microsoft/vscode-azext-utils"; +import { localize } from "../../localize"; +import { ext } from "../../extensionVariables"; +import { env, QuickPickItemKind, type QuickPickItem } from "vscode"; +import { type DurableTaskSchedulerEmulatorWorkspaceResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel"; + +export function copyEmulatorConnectionStringCommandFactory() { + return async (actionContext: IActionContext, scheduler: DurableTaskSchedulerEmulatorWorkspaceResourceModel | undefined): Promise => { + if (!scheduler) { + throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); + } + + const { endpointUrl } = scheduler; + + let connectionString = `Endpoint=${endpointUrl};Authentication=None`; + + const taskHubs = scheduler.taskHubs; + + if (taskHubs.length > 0) { + + const noTaskHubItem: QuickPickItem = { + detail: localize('noTaskHubDetail', 'Do not connect to a specific task hub.'), + label: localize('noTaskHubLabel', 'None') + } + + const taskHubItems: QuickPickItem[] = + taskHubs.map(taskHub => ({ label: taskHub })); + + const taskHubResult = await actionContext.ui.showQuickPick( + [ + noTaskHubItem, + { + kind: QuickPickItemKind.Separator, + label: localize('taskHubSepratorLabel', 'Task Hubs') + }, + ...taskHubItems + ], + { + canPickMany: false, + placeHolder: localize('taskHubSelectionPlaceholder', 'Select a task hub to connect to') + }); + + if (taskHubResult && taskHubResult !== noTaskHubItem) { + connectionString += `;TaskHub=${taskHubResult.label}`; + } + } + + await env.clipboard.writeText(connectionString); + + ext.outputChannel.show(); + ext.outputChannel.appendLog(localize('schedulerConnectionStringCopiedMessage', 'Connection string copied to clipboard: {0}', connectionString)); + } +} diff --git a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts index 96607fad0..603e9541c 100644 --- a/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts +++ b/src/commands/durableTaskScheduler/copySchedulerConnectionString.ts @@ -16,16 +16,7 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur throw new Error(localize('noSchedulerSelectedErrorMessage', 'No scheduler was selected.')); } - const schedulerJson = await schedulerClient.getScheduler( - scheduler.subscription, - scheduler.resourceGroup, - scheduler.name); - - if (!schedulerJson) { - throw new Error(localize('schedulerNotFoundErrorMessage', 'Scheduler does not exist.')); - } - - const { endpoint } = schedulerJson.properties; + const { endpointUrl } = scheduler; const noAuthentication: QuickPickItem = { detail: localize('noAuthenticationDetail', 'No credentials will be used.'), @@ -59,7 +50,7 @@ export function copySchedulerConnectionStringCommandFactory(schedulerClient: Dur placeHolder: localize('authenticationTypePlaceholder', 'Select the credentials to be used to connect to the scheduler') }); - let connectionString = `Endpoint=${endpoint};Authentication=` + let connectionString = `Endpoint=${endpointUrl};Authentication=` if (result === noAuthentication) { connectionString += 'None'; diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index aaf9d988f..d394aa623 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -75,6 +75,7 @@ import { copySchedulerConnectionStringCommandFactory } from './durableTaskSchedu import { startEmulatorCommandFactory } from './durableTaskScheduler/startEmulator'; import { stopEmulatorCommandFactory } from './durableTaskScheduler/stopEmulator'; import { type DurableTaskSchedulerEmulatorClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; +import { copyEmulatorConnectionStringCommandFactory } from './durableTaskScheduler/copyEmulatorConnectionString'; export function registerCommands( services: { @@ -174,6 +175,7 @@ export function registerCommands( ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copyEmulatorConnectionString', copyEmulatorConnectionStringCommandFactory()); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerConnectionString', copySchedulerConnectionStringCommandFactory(services.dts.schedulerClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copySchedulerEndpoint', copySchedulerEndpointCommandFactory()); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.createScheduler', createSchedulerCommandFactory(services.dts.dataBranchProvider, services.dts.schedulerClient)); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts index 4983e0bc9..464914552 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -30,4 +30,6 @@ export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements Durab get endpointUrl(): Uri { return this.emulator.schedulerEndpoint; } get id(): string { return this.emulator.id; } + + get taskHubs(): string[] { return this.emulator.taskHubs; } } From 5f3e223c4ddc0f5c8ffc10facd4ea7e2856be6fc Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 7 Feb 2025 08:44:11 -0800 Subject: [PATCH 48/58] Move emulator image details/defaults to configuration. Signed-off-by: Phillip Hoff --- package.json | 15 ++++++++++ package.nls.json | 5 +++- .../DurableTaskSchedulerEmulatorClient.ts | 29 +++++++++++++++++-- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d386b0143..ff0a2b255 100644 --- a/package.json +++ b/package.json @@ -974,6 +974,21 @@ "type": "boolean", "default": false, "description": "%azureFunctions.durableTaskScheduler.enablePreviewFeatures%" + }, + "azureFunctions.durableTaskScheduler.emulatorRegistry": { + "type": "string", + "description": "%azureFunctions.durableTaskScheduler.emulatorRegistry%", + "default": "durabletasks.azurecr.io" + }, + "azureFunctions.durableTaskScheduler.emulatorImage": { + "type": "string", + "description": "%azureFunctions.durableTaskScheduler.emulatorImage%", + "default": "durable-task-scheduler/emulator" + }, + "azureFunctions.durableTaskScheduler.emulatorTag": { + "type": "string", + "description": "%azureFunctions.durableTaskScheduler.emulatorTag%", + "default": "latest-linux-amd64" } } }, diff --git a/package.nls.json b/package.nls.json index f9d53dc85..ac99b7fb4 100644 --- a/package.nls.json +++ b/package.nls.json @@ -132,5 +132,8 @@ "azureFunctions.durableTaskScheduler.startEmulator": "Start Durable Task Emulator", "azureFunctions.durableTaskScheduler.stopEmulator": "Stop Emulator", - "azureFunctions.durableTaskScheduler.enablePreviewFeatures": "Enable Durable Task Scheduler preview features" + "azureFunctions.durableTaskScheduler.enablePreviewFeatures": "Enable Durable Task Scheduler preview features", + "azureFunctions.durableTaskScheduler.emulatorRegistry": "The registry of the Durable Task Scheduler emulator image.", + "azureFunctions.durableTaskScheduler.emulatorImage": "The name of the Durable Task Scheduler emulator image.", + "azureFunctions.durableTaskScheduler.emulatorTag": "The tag of the Durable Task Scheduler emulator image." } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index e921312c8..3dfcc8391 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -1,4 +1,4 @@ -import { Disposable, type Event, EventEmitter, Uri } from "vscode"; +import { Disposable, type Event, EventEmitter, Uri, workspace } from "vscode"; import { type ContainerClient } from "./ContainerClient"; export interface DurableTaskSchedulerEmulator { @@ -17,6 +17,27 @@ export interface DurableTaskSchedulerEmulatorClient { stopEmulator(id: string): Promise; } +interface ImageFullTag { + registry: string; + image: string; + tag: string; +} + +function getEmulatorFullTag(): ImageFullTag { + const configuration = workspace.getConfiguration('azureFunctions'); + + // NOTE: Defaults should be specified in package.json. + const registry = configuration.get('durableTaskScheduler.emulatorRegistry') as string; + const image = configuration.get('durableTaskScheduler.emulatorImage') as string; + const tag = configuration.get('durableTaskScheduler.emulatorTag') as string; + + return { + registry, + image, + tag + }; +} + export class DockerDurableTaskSchedulerEmulatorClient extends Disposable implements DurableTaskSchedulerEmulatorClient { private readonly onEmulatorsChangedEmitter = new EventEmitter(); @@ -43,9 +64,10 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme } async getEmulators(): Promise { + const { image } = getEmulatorFullTag(); const containers = await this.dockerClient.getContainers(); - const emulatorContainers = containers.filter(container => container.image.toLowerCase().startsWith('durable-task-scheduler/emulator')); + const emulatorContainers = containers.filter(container => container.image.toLowerCase().startsWith(image)); return emulatorContainers.map(container => ({ dashboardEndpoint: Uri.parse(`http://localhost:${container.ports[8082]}`), @@ -58,7 +80,8 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme async startEmulator(): Promise { try { - const id = await this.dockerClient.runContainer('durabletasks.azurecr.io/durable-task-scheduler/emulator:latest-linux-arm64'); + const { registry, image, tag } = getEmulatorFullTag(); + const id = await this.dockerClient.runContainer(`${registry}/${image}:${tag}`); this.localEmulatorIds.add(id); From d5b686769a18c01a0ce48ed208e44fb0ac9dd89d Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 7 Feb 2025 08:51:33 -0800 Subject: [PATCH 49/58] Exclude stopped/paused emulator containers. Signed-off-by: Phillip Hoff --- src/tree/durableTaskScheduler/ContainerClient.ts | 2 +- .../durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tree/durableTaskScheduler/ContainerClient.ts b/src/tree/durableTaskScheduler/ContainerClient.ts index 89975488c..5034fd4c1 100644 --- a/src/tree/durableTaskScheduler/ContainerClient.ts +++ b/src/tree/durableTaskScheduler/ContainerClient.ts @@ -25,7 +25,7 @@ export class ShellContainerClient implements ContainerClient { async getContainers(): Promise { const commandRunner = this.factory.getCommandRunner(); - const containers = await commandRunner(this.dockerClient.listContainers({})); + const containers = await commandRunner(this.dockerClient.listContainers({ running: true })); return containers.map( container => diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index 3dfcc8391..fc96be167 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -67,7 +67,7 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme const { image } = getEmulatorFullTag(); const containers = await this.dockerClient.getContainers(); - const emulatorContainers = containers.filter(container => container.image.toLowerCase().startsWith(image)); + const emulatorContainers = containers.filter(container => container.image.toLowerCase() === image.toLowerCase()); return emulatorContainers.map(container => ({ dashboardEndpoint: Uri.parse(`http://localhost:${container.ports[8082]}`), From ee0288ff2fa4724d358d1d230c5a074c4829dc93 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Sat, 8 Mar 2025 15:38:52 -0800 Subject: [PATCH 50/58] Tweak emulator shutdown. Signed-off-by: Phillip Hoff --- package.json | 6 +++--- .../DurableTaskSchedulerEmulatorClient.ts | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 3e6ed0a6b..52498a412 100644 --- a/package.json +++ b/package.json @@ -978,17 +978,17 @@ "azureFunctions.durableTaskScheduler.emulatorRegistry": { "type": "string", "description": "%azureFunctions.durableTaskScheduler.emulatorRegistry%", - "default": "durabletasks.azurecr.io" + "default": "mcr.microsoft.com" }, "azureFunctions.durableTaskScheduler.emulatorImage": { "type": "string", "description": "%azureFunctions.durableTaskScheduler.emulatorImage%", - "default": "durable-task-scheduler/emulator" + "default": "dts/dts-emulator" }, "azureFunctions.durableTaskScheduler.emulatorTag": { "type": "string", "description": "%azureFunctions.durableTaskScheduler.emulatorTag%", - "default": "latest-linux-amd64" + "default": "v0.0.4" } } }, diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index fc96be167..b0e5c485a 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -53,10 +53,18 @@ export class DockerDurableTaskSchedulerEmulatorClient extends Disposable impleme readonly onEmulatorsChanged: Event = this.onEmulatorsChangedEmitter.event; async disposeAsync(): Promise { - for (const id of this.localEmulatorIds) { - await this.dockerClient.stopContainer(id); - await this.dockerClient.removeContainer(id); - } + + // NOTE: Stopping containers may take more time than allowed during disposal. + // Hence, it's done in parallel in the hope that the stop commands are + // at least issued before VS Code is shutdown. + + const tasks = Array.from(this.localEmulatorIds).map( + async (id) => { + await this.dockerClient.stopContainer(id); + await this.dockerClient.removeContainer(id); + }); + + await Promise.all(tasks); this.localEmulatorIds.clear(); From 49d5100880780472d944bf039ffb119eec07fcd0 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Sat, 8 Mar 2025 15:55:12 -0800 Subject: [PATCH 51/58] Hide emulator commands from palette. Signed-off-by: Phillip Hoff --- package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/package.json b/package.json index 6400e48b8..d03f48719 100644 --- a/package.json +++ b/package.json @@ -860,6 +860,14 @@ { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.startEmulator", + "when": "never" + }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "when": "never" } ], "editor/context": [ From 8446d46448d2b628f181f0235b1f160704dd058f Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Sat, 8 Mar 2025 16:04:29 -0800 Subject: [PATCH 52/58] Add missing headers. Signed-off-by: Phillip Hoff --- src/commands/durableTaskScheduler/startEmulator.ts | 5 +++++ src/commands/durableTaskScheduler/stopEmulator.ts | 5 +++++ src/tree/durableTaskScheduler/ContainerClient.ts | 5 +++++ .../DurableTaskSchedulerDashboardModel.ts | 5 +++++ .../DurableTaskSchedulerEmulatorClient.ts | 5 +++++ .../DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts | 5 +++++ .../DurableTaskSchedulerEmulatorsWorkspaceResource.ts | 5 +++++ .../DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts | 5 +++++ .../DurableTaskSchedulerEndpointModel.ts | 5 +++++ .../DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts | 5 +++++ .../DurableTaskSchedulerWorkspaceDataBranchProvider.ts | 5 +++++ .../DurableTaskSchedulerWorkspaceResourceModel.ts | 5 +++++ .../DurableTaskSchedulerWorkspaceResourceProvider.ts | 5 +++++ 13 files changed, 65 insertions(+) diff --git a/src/commands/durableTaskScheduler/startEmulator.ts b/src/commands/durableTaskScheduler/startEmulator.ts index 8e28d2266..31fb87bb9 100644 --- a/src/commands/durableTaskScheduler/startEmulator.ts +++ b/src/commands/durableTaskScheduler/startEmulator.ts @@ -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 IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; diff --git a/src/commands/durableTaskScheduler/stopEmulator.ts b/src/commands/durableTaskScheduler/stopEmulator.ts index 74ce0b020..a37cf0ade 100644 --- a/src/commands/durableTaskScheduler/stopEmulator.ts +++ b/src/commands/durableTaskScheduler/stopEmulator.ts @@ -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 IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskSchedulerEmulatorWorkspaceResourceModel } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel"; import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; diff --git a/src/tree/durableTaskScheduler/ContainerClient.ts b/src/tree/durableTaskScheduler/ContainerClient.ts index 5034fd4c1..61e01c8f6 100644 --- a/src/tree/durableTaskScheduler/ContainerClient.ts +++ b/src/tree/durableTaskScheduler/ContainerClient.ts @@ -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 * as CodeContainerClient from '@microsoft/vscode-container-client'; import { localize } from '../../localize'; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts index 5792ebc30..f2720c3db 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts @@ -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 Uri } from "vscode"; export interface DurableTaskSchedulerDashboardModel { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index b0e5c485a..0cf41deae 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -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 { Disposable, type Event, EventEmitter, Uri, workspace } from "vscode"; import { type ContainerClient } from "./ContainerClient"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts index 464914552..5299489aa 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -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 { TreeItem, TreeItemCollapsibleState, type Uri, type ProviderResult } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { type DurableTaskSchedulerEmulator } from "./DurableTaskSchedulerEmulatorClient"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts index c2e0048cd..eb3989c67 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts @@ -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 WorkspaceResource } from "@microsoft/vscode-azureresources-api"; export class DurableTaskSchedulerEmulatorsWorkspaceResource implements WorkspaceResource { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts index e73bed270..050cdfd53 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -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 { TreeItem, TreeItemCollapsibleState } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { localize } from "../../localize"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts index 41d9878fd..27ca74daa 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts @@ -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 Uri } from "vscode"; export interface DurableTaskSchedulerEndpointModel { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts index 8245df468..54e38c018 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts @@ -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 ProviderResult, TreeItem, Uri } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { treeUtils } from "../../utils/treeUtils"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts index 9a5d4585a..11d4bbb4a 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts @@ -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 WorkspaceResource, type WorkspaceResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, type Event, type TreeItem, Disposable, EventEmitter } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts index 99cb3728b..dd7a68356 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts @@ -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 WorkspaceResourceModel } from "@microsoft/vscode-azureresources-api"; import { type ProviderResult, type TreeItem } from "vscode"; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts index 16adaebd0..ad3af8b95 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.ts @@ -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 WorkspaceResource, type WorkspaceResourceProvider } from "@microsoft/vscode-azureresources-api"; import { type Event, type ProviderResult } from "vscode"; import { DurableTaskSchedulerEmulatorsWorkspaceResource } from "./DurableTaskSchedulerEmulatorsWorkspaceResource"; From b14fd226e1c13ccaa86f2e645b00797ab4a0f010 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 13 Mar 2025 23:23:31 -0700 Subject: [PATCH 53/58] Improve container command error handling. Signed-off-by: Phillip Hoff --- .../durableTaskScheduler/ContainerClient.ts | 87 ++++++++++++------- ...chedulerEmulatorsWorkspaceResourceModel.ts | 15 +++- ...askSchedulerErrorWorkspaceResourceModel.ts | 26 ++++++ 3 files changed, 91 insertions(+), 37 deletions(-) create mode 100644 src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts diff --git a/src/tree/durableTaskScheduler/ContainerClient.ts b/src/tree/durableTaskScheduler/ContainerClient.ts index 61e01c8f6..02886d24f 100644 --- a/src/tree/durableTaskScheduler/ContainerClient.ts +++ b/src/tree/durableTaskScheduler/ContainerClient.ts @@ -28,42 +28,51 @@ export class ShellContainerClient implements ContainerClient { private readonly factory = new CodeContainerClient.ShellStreamCommandRunnerFactory({}); async getContainers(): Promise { - const commandRunner = this.factory.getCommandRunner(); - - const containers = await commandRunner(this.dockerClient.listContainers({ running: true })); - - return containers.map( - container => - ({ - id: container.id, - image: container.image.image as string, - name: container.name, - ports: container.ports.reduce( - (previous, port) => { - previous[port.containerPort] = port.hostPort; - - return previous; - }, - {}) - })); + return await this.withErrorHandling( + async () => { + const commandRunner = this.factory.getCommandRunner(); + + const containers = await commandRunner(this.dockerClient.listContainers({ running: true })); + + return containers.map( + container => + ({ + id: container.id, + image: container.image.image as string, + name: container.name, + ports: container.ports.reduce( + (previous, port) => { + previous[port.containerPort] = port.hostPort; + + return previous; + }, + {}) + })); + }); } async removeContainer(id: string): Promise { - const commandRunner = this.factory.getCommandRunner(); - - await commandRunner(this.dockerClient.removeContainers({ - containers: [id] - })); + await this.withErrorHandling( + async () => { + const commandRunner = this.factory.getCommandRunner(); + + return await commandRunner(this.dockerClient.removeContainers({ + containers: [id] + })); + }); } async runContainer(image: string): Promise { - const commandRunner = this.factory.getCommandRunner(); + const id = await this.withErrorHandling( + async () => { + const commandRunner = this.factory.getCommandRunner(); - const id = await commandRunner(this.dockerClient.runContainer({ - detached: true, - imageRef: image, - publishAllPorts: true - })); + return await commandRunner(this.dockerClient.runContainer({ + detached: true, + imageRef: image, + publishAllPorts: true + })); + }); if (!id) { throw new Error(localize('startContainerFailed', 'Unable to start the container.')); @@ -73,10 +82,22 @@ export class ShellContainerClient implements ContainerClient { } async stopContainer(id: string): Promise { - const commandRunner = this.factory.getCommandRunner(); + await this.withErrorHandling( + async () => { + const commandRunner = this.factory.getCommandRunner(); + + await commandRunner(this.dockerClient.stopContainers({ + container: [id] + })); + }); + } - await commandRunner(this.dockerClient.stopContainers({ - container: [id] - })); + private async withErrorHandling(command: () => Promise): Promise { + try { + return await command(); + } + catch (error) { + throw new Error(localize('containerRuntimeError', 'Unable to perform container command. Is a container runtime installed and running?')); + } } } diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts index 050cdfd53..840167d35 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -9,19 +9,26 @@ import { localize } from "../../localize"; import { treeUtils } from "../../utils/treeUtils"; import {type DurableTaskSchedulerEmulatorClient } from "./DurableTaskSchedulerEmulatorClient"; import { DurableTaskSchedulerEmulatorWorkspaceResourceModel } from "./DurableTaskSchedulerEmulatorWorkspaceResourceModel"; +import { DurableTaskSchedulerErrorWorkspaceResourceModel } from "./DurableTaskSchedulerErrorWorkspaceResourceModel"; +import { parseError } from "@microsoft/vscode-azext-utils"; export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { constructor(private readonly emulatorClient: DurableTaskSchedulerEmulatorClient) { } async getChildren(): Promise { - const emulators = await this.emulatorClient.getEmulators(); - - return emulators.map(emulator => new DurableTaskSchedulerEmulatorWorkspaceResourceModel(emulator)); + try { + const emulators = await this.emulatorClient.getEmulators(); + + return emulators.map(emulator => new DurableTaskSchedulerEmulatorWorkspaceResourceModel(emulator)); + } + catch (error) { + return [new DurableTaskSchedulerErrorWorkspaceResourceModel(parseError(error).message)]; + } } getTreeItem(): TreeItem | Thenable { - const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), TreeItemCollapsibleState.Expanded); + const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), TreeItemCollapsibleState.Collapsed); treeItem.contextValue = 'azFunc.dts.emulators'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts new file mode 100644 index 000000000..387abc709 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.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 { type ProviderResult, ThemeIcon, TreeItem } from "vscode"; +import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; + +export class DurableTaskSchedulerErrorWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { + constructor( + private readonly error: string) { + } + + getChildren(): ProviderResult { + return []; + } + + getTreeItem(): TreeItem | Thenable { + const treeItem = new TreeItem(this.error); + + treeItem.contextValue = 'azFunc.dts.emulatorError'; + treeItem.iconPath = new ThemeIcon('warning') + + return treeItem; + } +} From 3aa7ba154e8fdddc413111d9044ff1bc538c61e1 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 13 Mar 2025 23:50:46 -0700 Subject: [PATCH 54/58] Auto-expand when emulators are running. Signed-off-by: Phillip Hoff --- ...chedulerEmulatorsWorkspaceResourceModel.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts index 840167d35..4509ace1d 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -7,18 +7,22 @@ import { TreeItem, TreeItemCollapsibleState } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { localize } from "../../localize"; import { treeUtils } from "../../utils/treeUtils"; -import {type DurableTaskSchedulerEmulatorClient } from "./DurableTaskSchedulerEmulatorClient"; +import {type DurableTaskSchedulerEmulator, type DurableTaskSchedulerEmulatorClient } from "./DurableTaskSchedulerEmulatorClient"; import { DurableTaskSchedulerEmulatorWorkspaceResourceModel } from "./DurableTaskSchedulerEmulatorWorkspaceResourceModel"; import { DurableTaskSchedulerErrorWorkspaceResourceModel } from "./DurableTaskSchedulerErrorWorkspaceResourceModel"; import { parseError } from "@microsoft/vscode-azext-utils"; export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { + private getEmulatorsTask: Promise; + constructor(private readonly emulatorClient: DurableTaskSchedulerEmulatorClient) { } async getChildren(): Promise { + this.getEmulatorsTask = this.emulatorClient.getEmulators(); + try { - const emulators = await this.emulatorClient.getEmulators(); + const emulators = await this.getEmulatorsTask return emulators.map(emulator => new DurableTaskSchedulerEmulatorWorkspaceResourceModel(emulator)); } @@ -27,8 +31,14 @@ export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements Dura } } - getTreeItem(): TreeItem | Thenable { - const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), TreeItemCollapsibleState.Collapsed); + async getTreeItem(): Promise { + this.getEmulatorsTask ??= this.emulatorClient.getEmulators(); + + const collapsibleState = await this.emulatorsExist() + ? TreeItemCollapsibleState.Expanded + : TreeItemCollapsibleState.Collapsed; + + const treeItem = new TreeItem(localize('dtsEmulatorsLabel', 'Durable Task Scheduler Emulators'), collapsibleState); treeItem.contextValue = 'azFunc.dts.emulators'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); @@ -37,4 +47,24 @@ export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements Dura } id?: string | undefined; + + private async emulatorsExist(): Promise { + // Used any cached result that might exist... + this.getEmulatorsTask ??= this.emulatorClient.getEmulators(); + + try { + await this.getEmulatorsTask; + + const emulators = await this.getEmulatorsTask; + + if (emulators.length) { + return true; + } + } + catch { + // NOTE: No-op. + } + + return false; + } } From 7c2d12b1338ab3902f5ac1402ac391fb61678214 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 9 Apr 2025 09:33:13 -0700 Subject: [PATCH 55/58] Make primary commands more prominent. --- package.json | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d03f48719..76a09ca71 100644 --- a/package.json +++ b/package.json @@ -422,17 +422,20 @@ { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "title": "%azureFunctions.durableTaskScheduler.openTaskHubDashboard%", - "category": "Azure Functions" + "category": "Azure Functions", + "icon": "$(dashboard)" }, { "command": "azureFunctions.durableTaskScheduler.startEmulator", "title": "%azureFunctions.durableTaskScheduler.startEmulator%", - "category": "Azure Functions" + "category": "Azure Functions", + "icon": "$(debug-start)" }, { "command": "azureFunctions.durableTaskScheduler.stopEmulator", "title": "%azureFunctions.durableTaskScheduler.stopEmulator%", - "category": "Azure Functions" + "category": "Azure Functions", + "icon": "$(debug-stop)" } ], "submenus": [ @@ -767,15 +770,30 @@ "when": "view =~ /(azureResourceGroups|azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHubDashboard/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", + "when": "view =~ /(azureResourceGroups|azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHubDashboard/", + "group": "inline" + }, { "command": "azureFunctions.durableTaskScheduler.startEmulator", "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulators/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.startEmulator", + "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulators/", + "group": "inline" + }, { "command": "azureFunctions.durableTaskScheduler.stopEmulator", "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulatorInstance/", "group": "1@1" + }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulatorInstance/", + "group": "inline" } ], "explorer/context": [ From 068f32600c55cfe7992b4a724222f5bd32ea038d Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 9 Apr 2025 09:42:19 -0700 Subject: [PATCH 56/58] Fixup bad merge. --- src/commands/registerCommands.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index ae6d4a12f..d68c9c0b2 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -79,15 +79,6 @@ import { stopFunctionApp } from './stopFunctionApp'; import { swapSlot } from './swapSlot'; import { disableFunction, enableFunction } from './updateDisabledState'; import { viewProperties } from './viewProperties'; -import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboard'; -import { createTaskHubCommandFactory } from './durableTaskScheduler/createTaskHub'; -import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerClient'; -import { createSchedulerCommandFactory } from './durableTaskScheduler/createScheduler'; -import { deleteTaskHubCommandFactory } from './durableTaskScheduler/deleteTaskHub'; -import { deleteSchedulerCommandFactory } from './durableTaskScheduler/deleteScheduler'; -import { type DurableTaskSchedulerDataBranchProvider } from '../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; -import { copySchedulerEndpointCommandFactory } from './durableTaskScheduler/copySchedulerEndpoint'; -import { copySchedulerConnectionStringCommandFactory } from './durableTaskScheduler/copySchedulerConnectionString'; import { startEmulatorCommandFactory } from './durableTaskScheduler/startEmulator'; import { stopEmulatorCommandFactory } from './durableTaskScheduler/stopEmulator'; import { type DurableTaskSchedulerEmulatorClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; From 5320cc1be85e843902ea221db45fc2c87e8ab630 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 14 May 2025 12:59:50 -0700 Subject: [PATCH 57/58] Updates per PR feedback. --- package.json | 8 ++------ package.nls.json | 1 - src/commands/durableTaskScheduler/stopEmulator.ts | 2 +- .../DurableTaskHubResourceModel.ts | 2 +- .../DurableTaskSchedulerEmulatorClient.ts | 11 +++++------ ...ableTaskSchedulerEmulatorWorkspaceResourceModel.ts | 2 +- ...bleTaskSchedulerEmulatorsWorkspaceResourceModel.ts | 2 -- .../DurableTaskSchedulerResourceModel.ts | 2 +- ...rableTaskSchedulerTaskHubWorkspaceResourceModel.ts | 8 ++------ ...DurableTaskSchedulerWorkspaceDataBranchProvider.ts | 2 +- .../DurableTaskSchedulerWorkspaceResourceModel.ts | 2 +- 11 files changed, 15 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 58a04f29c..51fdfba14 100644 --- a/package.json +++ b/package.json @@ -833,7 +833,7 @@ }, { "command": "azureFunctions.durableTaskScheduler.startEmulator", - "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulators/", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulators/", "group": "1@1" }, { @@ -935,10 +935,6 @@ "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "when": "never" }, - { - "command": "azureFunctions.durableTaskScheduler.startEmulator", - "when": "never" - }, { "command": "azureFunctions.durableTaskScheduler.stopEmulator", "when": "never" @@ -1074,7 +1070,7 @@ "azureFunctions.durableTaskScheduler.emulatorTag": { "type": "string", "description": "%azureFunctions.durableTaskScheduler.emulatorTag%", - "default": "v0.0.4" + "default": "latest" } } }, diff --git a/package.nls.json b/package.nls.json index d4379f577..4d3c70a81 100644 --- a/package.nls.json +++ b/package.nls.json @@ -140,5 +140,4 @@ "azureFunctions.durableTaskScheduler.emulatorRegistry": "The registry of the Durable Task Scheduler emulator image.", "azureFunctions.durableTaskScheduler.emulatorImage": "The name of the Durable Task Scheduler emulator image.", "azureFunctions.durableTaskScheduler.emulatorTag": "The tag of the Durable Task Scheduler emulator image." - "azureFunctions.durableTaskScheduler.enablePreviewFeatures": "Enable Durable Task Scheduler preview features" } diff --git a/src/commands/durableTaskScheduler/stopEmulator.ts b/src/commands/durableTaskScheduler/stopEmulator.ts index a37cf0ade..c5fa16a7d 100644 --- a/src/commands/durableTaskScheduler/stopEmulator.ts +++ b/src/commands/durableTaskScheduler/stopEmulator.ts @@ -9,7 +9,7 @@ import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskS import { localize } from "../../localize"; export function stopEmulatorCommandFactory(emulatorClient: DurableTaskSchedulerEmulatorClient) { - return async (__: IActionContext, emulator: DurableTaskSchedulerEmulatorWorkspaceResourceModel | undefined) => { + return async (_: IActionContext, emulator: DurableTaskSchedulerEmulatorWorkspaceResourceModel | undefined) => { if (!emulator) { throw new Error(localize('noEmulatorSelected', 'No emulator was selected.')); } diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 5b0d26ac5..66d670e53 100644 --- a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -60,7 +60,7 @@ export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel, D const treeItem = new TreeItem(this.name) treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); - treeItem.contextValue = 'azFunc.dts.taskHub azFunc.dts.taskHubDashboard'; + treeItem.contextValue = 'azFunc.dts.taskHub;azFunc.dts.taskHubDashboard'; const json = await this.schedulerClient.getSchedulerTaskHub( this.scheduler.subscription, diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts index 0cf41deae..6c5a80689 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, type Event, EventEmitter, Uri, workspace } from "vscode"; +import { Disposable, type Event, EventEmitter, Uri } from "vscode"; import { type ContainerClient } from "./ContainerClient"; +import { getWorkspaceSetting } from "../../vsCodeConfig/settings"; export interface DurableTaskSchedulerEmulator { dashboardEndpoint: Uri; @@ -29,12 +30,10 @@ interface ImageFullTag { } function getEmulatorFullTag(): ImageFullTag { - const configuration = workspace.getConfiguration('azureFunctions'); - // NOTE: Defaults should be specified in package.json. - const registry = configuration.get('durableTaskScheduler.emulatorRegistry') as string; - const image = configuration.get('durableTaskScheduler.emulatorImage') as string; - const tag = configuration.get('durableTaskScheduler.emulatorTag') as string; + const registry = getWorkspaceSetting('durableTaskScheduler.emulatorRegistry') as string; + const image = getWorkspaceSetting('durableTaskScheduler.emulatorImage') as string; + const tag = getWorkspaceSetting('durableTaskScheduler.emulatorTag') as string; return { registry, diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts index 5299489aa..ad125543a 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts @@ -25,7 +25,7 @@ export class DurableTaskSchedulerEmulatorWorkspaceResourceModel implements Durab getTreeItem(): TreeItem | Thenable { const treeItem = new TreeItem(this.emulator.name, TreeItemCollapsibleState.Expanded); - treeItem.contextValue = 'azFunc.dts.emulatorInstance azFunc.dts.schedulerEndpoint'; + treeItem.contextValue = 'azFunc.dts.emulatorInstance;azFunc.dts.schedulerEndpoint'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); treeItem.id = this.emulator.id; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts index 4509ace1d..ee2f0c9e3 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -53,8 +53,6 @@ export class DurableTaskSchedulerEmulatorsWorkspaceResourceModel implements Dura this.getEmulatorsTask ??= this.emulatorClient.getEmulators(); try { - await this.getEmulatorsTask; - const emulators = await this.getEmulatorsTask; if (emulators.length) { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index 60033be41..afc5f1f7d 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -40,7 +40,7 @@ export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerMo async getTreeItem(): Promise { const treeItem = new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); - treeItem.contextValue = 'azFunc.dts.scheduler azFunc.dts.schedulerEndpoint'; + treeItem.contextValue = 'azFunc.dts.scheduler;azFunc.dts.schedulerEndpoint'; if (this.schedulerResource?.properties.provisioningState !== 'Succeeded') { treeItem.description = localize('schedulerDescription', '({0})', this.schedulerResource?.properties.provisioningState ?? 'Deleted'); diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts index 54e38c018..511fbd7ee 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type ProviderResult, TreeItem, Uri } from "vscode"; +import { TreeItem, Uri } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; import { treeUtils } from "../../utils/treeUtils"; import { type DurableTaskSchedulerDashboardModel } from "./DurableTaskSchedulerDashboardModel"; @@ -14,14 +14,10 @@ export class DurableTaskSchedulerTaskHubWorkspaceResourceModel implements Durabl private readonly dashboardEndpoint: Uri) { } - getChildren(): ProviderResult { - return []; - } - getTreeItem(): TreeItem | Thenable { const treeItem = new TreeItem(this.name); - treeItem.contextValue = 'azFunc.dts.emulatorTaskHub azFunc.dts.taskHubDashboard'; + treeItem.contextValue = 'azFunc.dts.emulatorTaskHub;azFunc.dts.taskHubDashboard'; treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler'); return treeItem; diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts index 11d4bbb4a..6717fb0f6 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts @@ -27,7 +27,7 @@ export class DurableTaskSchedulerWorkspaceDataBranchProvider extends Disposable } getChildren(element: DurableTaskSchedulerWorkspaceResourceModel): ProviderResult { - return element.getChildren(); + return element.getChildren?.() ?? []; } getResourceItem(_: WorkspaceResource): DurableTaskSchedulerWorkspaceResourceModel | Thenable { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts index dd7a68356..9d1be2836 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.ts @@ -7,7 +7,7 @@ import { type WorkspaceResourceModel } from "@microsoft/vscode-azureresources-ap import { type ProviderResult, type TreeItem } from "vscode"; export interface DurableTaskSchedulerWorkspaceResourceModel extends WorkspaceResourceModel { - getChildren(): ProviderResult; + getChildren?(): ProviderResult; getTreeItem(): TreeItem | Thenable; } From 2cc041d06f3aacaec40e764045cfd5aab08887a8 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 16 May 2025 08:58:38 -0700 Subject: [PATCH 58/58] More updates per PR feedback. --- package.json | 8 ++++---- .../DurableTaskSchedulerErrorWorkspaceResourceModel.ts | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 51fdfba14..5800d528e 100644 --- a/package.json +++ b/package.json @@ -788,7 +788,7 @@ }, { "command": "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString", - "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulatorInstance/", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulatorInstance/", "group": "3@1" }, { @@ -838,17 +838,17 @@ }, { "command": "azureFunctions.durableTaskScheduler.startEmulator", - "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulators/", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulators/", "group": "inline" }, { "command": "azureFunctions.durableTaskScheduler.stopEmulator", - "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulatorInstance/", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulatorInstance/", "group": "1@1" }, { "command": "azureFunctions.durableTaskScheduler.stopEmulator", - "when": "view =~ /(azureWorkspace|azureFocusView)/ && viewItem =~ /azFunc.dts.emulatorInstance/", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulatorInstance/", "group": "inline" } ], diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts index 387abc709..5aadecf50 100644 --- a/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type ProviderResult, ThemeIcon, TreeItem } from "vscode"; +import { ThemeIcon, TreeItem } from "vscode"; import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; export class DurableTaskSchedulerErrorWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { @@ -11,10 +11,6 @@ export class DurableTaskSchedulerErrorWorkspaceResourceModel implements DurableT private readonly error: string) { } - getChildren(): ProviderResult { - return []; - } - getTreeItem(): TreeItem | Thenable { const treeItem = new TreeItem(this.error);