Skip to content

Commit 02aeea2

Browse files
authored
Add basic support for Durable Task Scheduler resources (#4361)
* Scaffold BDP. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Refactor type hierarchy. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Sketch retrieval of task hubs. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Update task hub icon. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Enable "open in portal" command for task hubs. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Scaffold "open in dashboard" command. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Sketch "open in dashboard" implementation. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Move DTS management to separate client type. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Support viewing task hub properties. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Split apart types. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Add file headers. Signed-off-by: Phillip Hoff <phillip@orst.edu> * Consolidate client logic and add localizable strings. Signed-off-by: Phillip Hoff <phillip@orst.edu> --------- Signed-off-by: Phillip Hoff <phillip@orst.edu>
1 parent 51fb959 commit 02aeea2

File tree

11 files changed

+285
-4
lines changed

11 files changed

+285
-4
lines changed

package.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
"branches": [
4949
{
5050
"type": "FunctionApp"
51+
},
52+
{
53+
"type": "DurableTaskScheduler"
5154
}
5255
]
5356
},
@@ -69,10 +72,12 @@
6972
],
7073
"activation": {
7174
"onFetch": [
72-
"microsoft.web/sites"
75+
"microsoft.web/sites",
76+
"microsoft.durabletask/schedulers"
7377
],
7478
"onResolve": [
75-
"microsoft.web/sites"
79+
"microsoft.web/sites",
80+
"microsoft.durabletask/schedulers"
7681
]
7782
}
7883
},
@@ -369,6 +374,11 @@
369374
"title": "%azureFunctions.eventGrid.sendMockRequest%",
370375
"category": "Azure Functions",
371376
"icon": "$(notebook-execute)"
377+
},
378+
{
379+
"command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard",
380+
"title": "%azureFunctions.durableTaskScheduler.openTaskHubDashboard%",
381+
"category": "Azure Functions"
372382
}
373383
],
374384
"submenus": [
@@ -662,6 +672,10 @@
662672
"command": "azureResourceGroups.refresh",
663673
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.*folder/",
664674
"group": "1@1"
675+
},
676+
{
677+
"command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard",
678+
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/"
665679
}
666680
],
667681
"explorer/context": [

package.nls.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,7 @@
118118
"azureFunctions.walkthrough.functionsStart.initialize.title": "Initialize an existing project",
119119
"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).",
120120
"azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios",
121-
"azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions"
121+
"azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions",
122+
123+
"azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard"
122124
}
Lines changed: 30 additions & 0 deletions
Loading
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { openUrl, type IActionContext } from "@microsoft/vscode-azext-utils";
7+
import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel";
8+
import { localize } from '../../localize';
9+
10+
export async function openTaskHubDashboard(_: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise<void> {
11+
if (!taskHub) {
12+
throw new Error(localize('noTaskHubSelectedErrorMessage', 'No task hub was selected.'));
13+
}
14+
15+
await openUrl(taskHub?.dashboardUrl.toString(/* skipEncoding: */ true));
16+
}

src/commands/registerCommands.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { stopFunctionApp } from './stopFunctionApp';
6363
import { swapSlot } from './swapSlot';
6464
import { disableFunction, enableFunction } from './updateDisabledState';
6565
import { viewProperties } from './viewProperties';
66+
import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboard';
6667

6768
export function registerCommands(): void {
6869
commands.registerCommand('azureFunctions.agent.getCommands', getCommands);
@@ -154,4 +155,6 @@ export function registerCommands(): void {
154155
ext.eventGridProvider = new EventGridCodeLensProvider();
155156
ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider));
156157
registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest);
158+
159+
registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard);
157160
}

src/extension.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { registerAppServiceExtensionVariables } from '@microsoft/vscode-azext-azureappservice';
99
import { registerAzureUtilsExtensionVariables, type AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils';
1010
import { callWithTelemetryAndErrorHandling, createApiProvider, createAzExtOutputChannel, createExperimentationService, registerErrorHandler, registerEvent, registerReportIssueCommand, registerUIExtensionVariables, type IActionContext, type apiUtils } from '@microsoft/vscode-azext-utils';
11-
import { AzExtResourceType } from '@microsoft/vscode-azureresources-api';
11+
import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api';
1212
import * as vscode from 'vscode';
1313
import { FunctionAppResolver } from './FunctionAppResolver';
1414
import { FunctionsLocalResourceProvider } from './LocalResourceProvider';
@@ -38,6 +38,8 @@ import { verifyVSCodeConfigOnActivate } from './vsCodeConfig/verifyVSCodeConfigO
3838
import { type AzureFunctionsExtensionApi } from './vscode-azurefunctions.api';
3939
import { listLocalFunctions } from './workspace/listLocalFunctions';
4040
import { listLocalProjects } from './workspace/listLocalProjects';
41+
import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider';
42+
import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient';
4143

4244
export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise<apiUtils.AzureExtensionApiProvider> {
4345
ext.context = context;
@@ -104,6 +106,10 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta
104106
ext.azureAccountTreeItem = ext.rgApi.appResourceTree._rootTreeItem as AzureAccountTreeItemBase;
105107
ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.FunctionApp, new FunctionAppResolver());
106108
ext.rgApi.registerWorkspaceResourceProvider('func', new FunctionsLocalResourceProvider());
109+
110+
const azureResourcesApi = await getAzureResourcesExtensionApi(context, '2.0.0');
111+
112+
azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider(new HttpDurableTaskSchedulerClient()));
107113
});
108114

109115
return createApiProvider([<AzureFunctionsExtensionApi>{
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type AzureResource, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api";
7+
import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel";
8+
import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient";
9+
import { type ProviderResult, TreeItem, Uri } from "vscode";
10+
import { treeUtils } from "../../utils/treeUtils";
11+
import { localize } from '../../localize';
12+
13+
export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel {
14+
constructor(
15+
private readonly schedulerResource: AzureResource,
16+
private readonly resource: DurableTaskHubResource,
17+
private readonly schedulerClient: DurableTaskSchedulerClient) {
18+
}
19+
20+
public get azureResourceId() { return this.resource.id; }
21+
22+
get dashboardUrl(): Uri { return Uri.parse(this.resource.properties.dashboardUrl); }
23+
24+
get id(): string { return this.resource.id; }
25+
26+
get portalUrl(): Uri {
27+
const url: string = `${this.schedulerResource.subscription.environment.portalUrl}/#@${this.schedulerResource.subscription.tenantId}/resource${this.id}`;
28+
29+
return Uri.parse(url);
30+
}
31+
32+
get viewProperties(): ViewPropertiesModel {
33+
return {
34+
label: this.resource.name,
35+
getData: async () => {
36+
if (!this.schedulerResource.resourceGroup) {
37+
throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.'));
38+
}
39+
40+
const json = await this.schedulerClient.getSchedulerTaskHub(
41+
this.schedulerResource.subscription,
42+
this.schedulerResource.resourceGroup,
43+
this.schedulerResource.name,
44+
this.resource.name);
45+
46+
return json;
47+
}
48+
};
49+
}
50+
51+
getChildren(): ProviderResult<DurableTaskSchedulerModel[]>
52+
{
53+
return [];
54+
}
55+
56+
getTreeItem(): TreeItem | Thenable<TreeItem>
57+
{
58+
const treeItem = new TreeItem(this.resource.name)
59+
60+
treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler');
61+
treeItem.contextValue = 'azFunc.dts.taskHub';
62+
63+
return treeItem;
64+
}
65+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type AzureAuthentication, type AzureSubscription } from "@microsoft/vscode-azureresources-api";
7+
import { localize } from '../../localize';
8+
9+
export interface DurableTaskHubResource {
10+
readonly id: string;
11+
readonly name: string;
12+
readonly properties: {
13+
readonly dashboardUrl: string;
14+
};
15+
}
16+
17+
export interface DurableTaskSchedulerClient {
18+
getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise<DurableTaskHubResource>;
19+
getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise<DurableTaskHubResource[]>;
20+
}
21+
22+
export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClient {
23+
async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise<DurableTaskHubResource> {
24+
const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`;
25+
26+
const taskHub = await this.getAsJson<DurableTaskHubResource>(taskHubsUrl, subscription.authentication);
27+
28+
return taskHub;
29+
}
30+
31+
async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise<DurableTaskHubResource[]> {
32+
const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs`;
33+
34+
const response = await this.getAsJson<{ value: DurableTaskHubResource[] }>(taskHubsUrl, subscription.authentication);
35+
36+
return response.value;
37+
}
38+
39+
private static getBaseUrl(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string) {
40+
const provider = 'Microsoft.DurableTask';
41+
42+
return `${subscription.environment.resourceManagerEndpointUrl}/subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`;
43+
}
44+
45+
private async getAsJson<T>(url: string, authentication: AzureAuthentication): Promise<T> {
46+
const apiVersion = '2024-10-01-preview';
47+
const versionedUrl = `${url}?api-version=${apiVersion}`;
48+
49+
const authSession = await authentication.getSession();
50+
51+
if (!authSession) {
52+
throw new Error(localize('noAuthenticationSessionErrorMessage', 'Unable to obtain an authentication session.'));
53+
}
54+
55+
const accessToken = authSession.accessToken;
56+
57+
const request = new Request(versionedUrl);
58+
59+
request.headers.append('Authorization', `Bearer ${accessToken}`);
60+
61+
const response = await fetch(request);
62+
63+
if (!response.ok) {
64+
throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.'));
65+
}
66+
67+
return await response.json() as T;
68+
}
69+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type AzureResource, type AzureResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api";
7+
import { type ProviderResult, type TreeItem } from "vscode";
8+
import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient";
9+
import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel";
10+
import { DurableTaskSchedulerResourceModel } from "./DurableTaskSchedulerResourceModel";
11+
12+
export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider<DurableTaskSchedulerModel> {
13+
constructor(private readonly schedulerClient: DurableTaskSchedulerClient) {
14+
}
15+
16+
getChildren(element: DurableTaskSchedulerModel): ProviderResult<DurableTaskSchedulerModel[]> {
17+
return element.getChildren();
18+
}
19+
20+
getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable<DurableTaskSchedulerResourceModel> {
21+
return new DurableTaskSchedulerResourceModel(element, this.schedulerClient);
22+
}
23+
24+
getTreeItem(element: DurableTaskSchedulerModel): TreeItem | Thenable<TreeItem> {
25+
return element.getTreeItem();
26+
}
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type AzureResourceModel } from "@microsoft/vscode-azureresources-api";
7+
import { type ProviderResult, type TreeItem } from "vscode";
8+
9+
export interface DurableTaskSchedulerModel extends AzureResourceModel {
10+
getChildren(): ProviderResult<DurableTaskSchedulerModel[]>;
11+
12+
getTreeItem(): TreeItem | Thenable<TreeItem>;
13+
}

0 commit comments

Comments
 (0)