diff --git a/package.json b/package.json index 20802fdf1..d38302501 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" ] } }, @@ -369,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": [ @@ -662,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..15709f06a 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 in Dashboard" } 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/commands/durableTaskScheduler/openTaskHubDashboard.ts b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts new file mode 100644 index 000000000..99b0fc1ba --- /dev/null +++ b/src/commands/durableTaskScheduler/openTaskHubDashboard.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * 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"; +import { localize } from '../../localize'; + +export async function openTaskHubDashboard(_: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise { + if (!taskHub) { + throw new Error(localize('noTaskHubSelectedErrorMessage', 'No task hub was selected.')); + } + + await openUrl(taskHub?.dashboardUrl.toString(/* skipEncoding: */ true)); +} 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/extension.ts b/src/extension.ts index 523ce3b13..fa0a35d2d 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,8 @@ 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'; +import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient'; export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { ext.context = context; @@ -104,6 +106,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(new HttpDurableTaskSchedulerClient())); }); return createApiProvider([{ diff --git a/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts new file mode 100644 index 000000000..dd3bd1290 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * 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"; +import { type ProviderResult, TreeItem, Uri } from "vscode"; +import { treeUtils } from "../../utils/treeUtils"; +import { localize } from '../../localize'; + +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(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.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/DurableTaskSchedulerClient.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts new file mode 100644 index 000000000..d37318fc8 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureAuthentication, type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { localize } from '../../localize'; + +export interface DurableTaskHubResource { + readonly id: string; + readonly name: string; + readonly properties: { + readonly dashboardUrl: string; + }; +} + +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 taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`; + + const taskHub = await this.getAsJson(taskHubsUrl, subscription.authentication); + + return taskHub; + } + + async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise { + 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) { + const provider = 'Microsoft.DurableTask'; + + return `${subscription.environment.resourceManagerEndpointUrl}/subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`; + } + + 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 request = new Request(versionedUrl); + + 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/DurableTaskSchedulerDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.ts new file mode 100644 index 000000000..742bc299e --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider.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 { 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"; + +export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider { + constructor(private readonly schedulerClient: DurableTaskSchedulerClient) { + } + + getChildren(element: DurableTaskSchedulerModel): ProviderResult { + return element.getChildren(); + } + + getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable { + return new DurableTaskSchedulerResourceModel(element, this.schedulerClient); + } + + 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..48ab2cf2a --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { 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..463a485b5 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { 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"; +import { localize } from '../../localize'; + +export class DurableTaskSchedulerResourceModel implements DurableTaskSchedulerModel, AzureResourceModel { + public constructor(private readonly resource: AzureResource, private readonly schedulerClient: DurableTaskSchedulerClient) { + } + + async getChildren(): Promise { + if (!this.resource.resourceGroup) { + 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); + + 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; } +}