diff --git a/package-lock.json b/package-lock.json index 6cffc925e..289752112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@microsoft/vscode-azext-azureutils": "^3.1.7", "@microsoft/vscode-azext-utils": "^2.6.6", "@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", @@ -76,6 +77,7 @@ "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", "vscode-azurekudu": "^0.2.0", + "vscode-jsonrpc": "^8.2.1", "webpack": "^5.96.1", "webpack-cli": "^4.6.0" }, @@ -1452,6 +1454,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.4", "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", @@ -11138,6 +11149,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.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", @@ -12021,6 +12040,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 6a30b152b..5800d528e 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,9 @@ "branches": [ { "type": "func" + }, + { + "type": "DurableTaskSchedulerEmulator" } ], "resources": true @@ -407,6 +410,11 @@ "title": "%azureFunctions.enableSystemIdentity%", "category": "Azure Functions" }, + { + "command": "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString", + "title": "%azureFunctions.durableTaskScheduler.copyEmulatorConnectionString%", + "category": "Azure Functions" + }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", "title": "%azureFunctions.durableTaskScheduler.copySchedulerConnectionString%", @@ -440,7 +448,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", + "icon": "$(debug-start)" + }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "title": "%azureFunctions.durableTaskScheduler.stopEmulator%", + "category": "Azure Functions", + "icon": "$(debug-stop)" } ], "submenus": [ @@ -765,6 +786,11 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /userAssignedIdentity/", "group": "1@1" }, + { + "command": "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulatorInstance/", + "group": "3@1" + }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.scheduler/", @@ -772,7 +798,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" }, { @@ -797,8 +823,33 @@ }, { "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/", + "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 && viewItem =~ /azFunc.dts.emulators/", + "group": "1@1" + }, + { + "command": "azureFunctions.durableTaskScheduler.startEmulator", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulators/", + "group": "inline" + }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulatorInstance/", "group": "1@1" + }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "when": "view == azureWorkspace && viewItem =~ /azFunc.dts.emulatorInstance/", + "group": "inline" } ], "explorer/context": [ @@ -852,6 +903,10 @@ "command": "azureFunctions.viewProperties", "when": "never" }, + { + "command": "azureFunctions.durableTaskScheduler.copyEmulatorConnectionString", + "when": "never" + }, { "command": "azureFunctions.durableTaskScheduler.copySchedulerConnectionString", "when": "never" @@ -880,6 +935,10 @@ "command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard", "when": "never" }, + { + "command": "azureFunctions.durableTaskScheduler.stopEmulator", + "when": "never" + }, { "command": "azureFunctions.unassignManagedIdentity", "when": "never" @@ -997,6 +1056,21 @@ "type": "boolean", "default": false, "description": "%azureFunctions.durableTaskScheduler.enablePreviewFeatures%" + }, + "azureFunctions.durableTaskScheduler.emulatorRegistry": { + "type": "string", + "description": "%azureFunctions.durableTaskScheduler.emulatorRegistry%", + "default": "mcr.microsoft.com" + }, + "azureFunctions.durableTaskScheduler.emulatorImage": { + "type": "string", + "description": "%azureFunctions.durableTaskScheduler.emulatorImage%", + "default": "dts/dts-emulator" + }, + "azureFunctions.durableTaskScheduler.emulatorTag": { + "type": "string", + "description": "%azureFunctions.durableTaskScheduler.emulatorTag%", + "default": "latest" } } }, @@ -1395,6 +1469,7 @@ "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", "vscode-azurekudu": "^0.2.0", + "vscode-jsonrpc": "^8.2.1", "webpack": "^5.96.1", "webpack-cli": "^4.6.0" }, @@ -1415,6 +1490,7 @@ "@microsoft/vscode-azext-azureutils": "^3.1.7", "@microsoft/vscode-azext-utils": "^2.6.6", "@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/package.nls.json b/package.nls.json index 1c33d9669..4d3c70a81 100644 --- a/package.nls.json +++ b/package.nls.json @@ -125,6 +125,7 @@ "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.durableTaskScheduler.copyEmulatorConnectionString": "Copy Connection String", "azureFunctions.durableTaskScheduler.copySchedulerConnectionString": "Copy Connection String", "azureFunctions.durableTaskScheduler.copySchedulerEndpoint": "Copy Endpoint", "azureFunctions.durableTaskScheduler.createScheduler": "Create Durable Task Scheduler...", @@ -133,5 +134,10 @@ "azureFunctions.durableTaskScheduler.deleteScheduler": "Delete Scheduler...", "azureFunctions.durableTaskScheduler.deleteTaskHub": "Delete Task Hub...", "azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard", - "azureFunctions.durableTaskScheduler.enablePreviewFeatures": "Enable Durable Task Scheduler preview features" + "azureFunctions.durableTaskScheduler.startEmulator": "Start Durable Task Emulator", + "azureFunctions.durableTaskScheduler.stopEmulator": "Stop Emulator", + "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/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/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/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/commands/durableTaskScheduler/startEmulator.ts b/src/commands/durableTaskScheduler/startEmulator.ts new file mode 100644 index 000000000..31fb87bb9 --- /dev/null +++ b/src/commands/durableTaskScheduler/startEmulator.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 IActionContext } from "@microsoft/vscode-azext-utils"; +import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; + +export function startEmulatorCommandFactory(emulatorClient: DurableTaskSchedulerEmulatorClient) { + return async (_: IActionContext) => { + await emulatorClient.startEmulator(); + }; +} diff --git a/src/commands/durableTaskScheduler/stopEmulator.ts b/src/commands/durableTaskScheduler/stopEmulator.ts new file mode 100644 index 000000000..c5fa16a7d --- /dev/null +++ b/src/commands/durableTaskScheduler/stopEmulator.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type 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(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/commands/registerCommands.ts b/src/commands/registerCommands.ts index be4ada1ab..d68c9c0b2 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -79,11 +79,16 @@ import { stopFunctionApp } from './stopFunctionApp'; import { swapSlot } from './swapSlot'; import { disableFunction, enableFunction } from './updateDisabledState'; import { viewProperties } from './viewProperties'; +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: { dts: { dataBranchProvider: DurableTaskSchedulerDataBranchProvider, + emulatorClient: DurableTaskSchedulerEmulatorClient, schedulerClient: DurableTaskSchedulerClient } }): void { @@ -189,11 +194,14 @@ export function registerCommands( registerCommandWithTreeNodeUnwrapping('azureFunctions.unassignManagedIdentity', unassignManagedIdentity); registerCommandWithTreeNodeUnwrapping('azureFunctions.enableSystemIdentity', enableSystemIdentity); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.copyEmulatorConnectionString', copyEmulatorConnectionStringCommandFactory()); 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)); 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 885aa65d5..42352bbbd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -33,13 +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'; + +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,6 +87,7 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta registerCommands({ dts: { dataBranchProvider, + emulatorClient, schedulerClient } }); @@ -120,6 +127,11 @@ 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(emulatorClient)); }); return createApiProvider([{ @@ -139,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/ContainerClient.ts b/src/tree/durableTaskScheduler/ContainerClient.ts new file mode 100644 index 000000000..02886d24f --- /dev/null +++ b/src/tree/durableTaskScheduler/ContainerClient.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export interface DockerContainer { + id: string; + image: string; + name: string; + ports: { [key: number]: number }; +} + +export interface ContainerClient { + getContainers(): Promise; + + removeContainer(id: string): Promise; + + runContainer(image: string): Promise; + + stopContainer(id: string): Promise; +} + +export class ShellContainerClient implements ContainerClient { + private readonly dockerClient = new CodeContainerClient.DockerClient(); + private readonly factory = new CodeContainerClient.ShellStreamCommandRunnerFactory({}); + + async getContainers(): Promise { + 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 { + await this.withErrorHandling( + async () => { + const commandRunner = this.factory.getCommandRunner(); + + return await commandRunner(this.dockerClient.removeContainers({ + containers: [id] + })); + }); + } + + async runContainer(image: string): Promise { + const id = await this.withErrorHandling( + async () => { + const commandRunner = this.factory.getCommandRunner(); + + return await commandRunner(this.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 { + await this.withErrorHandling( + async () => { + const commandRunner = this.factory.getCommandRunner(); + + 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/DurableTaskHubResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts index 34e9f1d24..66d670e53 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..f2720c3db --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerDashboardModel.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + dashboardUrl: Uri; +} 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 new file mode 100644 index 000000000..6c5a80689 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from "vscode"; +import { type ContainerClient } from "./ContainerClient"; +import { getWorkspaceSetting } from "../../vsCodeConfig/settings"; + +export interface DurableTaskSchedulerEmulator { + dashboardEndpoint: Uri; + id: string; + name: string; + schedulerEndpoint: Uri; + taskHubs: string[]; +} + +export interface DurableTaskSchedulerEmulatorClient { + readonly onEmulatorsChanged: Event; + + getEmulators(): Promise; + startEmulator(): Promise; + stopEmulator(id: string): Promise; +} + +interface ImageFullTag { + registry: string; + image: string; + tag: string; +} + +function getEmulatorFullTag(): ImageFullTag { + // NOTE: Defaults should be specified in package.json. + const registry = getWorkspaceSetting('durableTaskScheduler.emulatorRegistry') as string; + const image = getWorkspaceSetting('durableTaskScheduler.emulatorImage') as string; + const tag = getWorkspaceSetting('durableTaskScheduler.emulatorTag') as string; + + return { + registry, + image, + tag + }; +} + +export class DockerDurableTaskSchedulerEmulatorClient extends Disposable implements DurableTaskSchedulerEmulatorClient { + private readonly onEmulatorsChangedEmitter = new EventEmitter(); + + private localEmulatorIds = new Set(); + + constructor(private readonly dockerClient: ContainerClient) { + super( + () => { + this.onEmulatorsChangedEmitter.dispose(); + }); + } + + readonly onEmulatorsChanged: Event = this.onEmulatorsChangedEmitter.event; + + async disposeAsync(): Promise { + + // 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(); + + this.dispose(); + } + + async getEmulators(): Promise { + const { image } = getEmulatorFullTag(); + const containers = await this.dockerClient.getContainers(); + + const emulatorContainers = containers.filter(container => container.image.toLowerCase() === image.toLowerCase()); + + return emulatorContainers.map(container => ({ + dashboardEndpoint: Uri.parse(`http://localhost:${container.ports[8082]}`), + id: container.id, + name: container.name, + schedulerEndpoint: Uri.parse(`http://localhost:${container.ports[8080]}`), + taskHubs: ['default'] + })); + } + + async startEmulator(): Promise { + try { + const { registry, image, tag } = getEmulatorFullTag(); + const id = await this.dockerClient.runContainer(`${registry}/${image}:${tag}`); + + this.localEmulatorIds.add(id); + + return id; + } + finally { + this.onEmulatorsChangedEmitter.fire(); + } + } + + 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(); + } + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.ts new file mode 100644 index 000000000..ad125543a --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorWorkspaceResourceModel.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 { 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, DurableTaskSchedulerEndpointModel { + constructor(private readonly emulator: DurableTaskSchedulerEmulator) { + } + + getChildren(): ProviderResult { + 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;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; } + + get taskHubs(): string[] { return this.emulator.taskHubs; } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts new file mode 100644 index 000000000..eb3989c67 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResource.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type 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..ee2f0c9e3 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEmulatorsWorkspaceResourceModel.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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"; +import { treeUtils } from "../../utils/treeUtils"; +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.getEmulatorsTask + + return emulators.map(emulator => new DurableTaskSchedulerEmulatorWorkspaceResourceModel(emulator)); + } + catch (error) { + return [new DurableTaskSchedulerErrorWorkspaceResourceModel(parseError(error).message)]; + } + } + + 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'); + + return treeItem; + } + + id?: string | undefined; + + private async emulatorsExist(): Promise { + // Used any cached result that might exist... + this.getEmulatorsTask ??= this.emulatorClient.getEmulators(); + + try { + const emulators = await this.getEmulatorsTask; + + if (emulators.length) { + return true; + } + } + catch { + // NOTE: No-op. + } + + return false; + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts new file mode 100644 index 000000000..27ca74daa --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerEndpointModel.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + endpointUrl: Uri; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts new file mode 100644 index 000000000..5aadecf50 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerErrorWorkspaceResourceModel.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ThemeIcon, TreeItem } from "vscode"; +import { type DurableTaskSchedulerWorkspaceResourceModel } from "./DurableTaskSchedulerWorkspaceResourceModel"; + +export class DurableTaskSchedulerErrorWorkspaceResourceModel implements DurableTaskSchedulerWorkspaceResourceModel { + constructor( + private readonly error: string) { + } + + getTreeItem(): TreeItem | Thenable { + const treeItem = new TreeItem(this.error); + + treeItem.contextValue = 'azFunc.dts.emulatorError'; + treeItem.iconPath = new ThemeIcon('warning') + + return treeItem; + } +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerResourceModel.ts index 53c63a7db..afc5f1f7d 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 { diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts new file mode 100644 index 000000000..511fbd7ee --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerTaskHubWorkspaceResourceModel.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { 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) { + } + + 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; +} diff --git a/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts new file mode 100644 index 000000000..6717fb0f6 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceDataBranchProvider.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { 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"; +import { DurableTaskSchedulerEmulatorsWorkspaceResourceModel } from "./DurableTaskSchedulerEmulatorsWorkspaceResourceModel"; +import { type DurableTaskSchedulerEmulatorClient } from "./DurableTaskSchedulerEmulatorClient"; + +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 { + return element.getChildren?.() ?? []; + } + + getResourceItem(_: WorkspaceResource): DurableTaskSchedulerWorkspaceResourceModel | Thenable { + return new DurableTaskSchedulerEmulatorsWorkspaceResourceModel(this.emulatorClient); + } + + get onDidChangeTreeData(): Event { return this.onDidChangeTreeDataEmitter.event; } + + 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..9d1be2836 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceModel.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 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..ad3af8b95 --- /dev/null +++ b/src/tree/durableTaskScheduler/DurableTaskSchedulerWorkspaceResourceProvider.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 { 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() ]; + } +}