diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 8ca1cb0a..64b615f9 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -182,6 +182,7 @@ "Inactive Sites": "Inactive Sites", "No sites found": "No sites found", "No environments found": "No environments found", + "Active Environment is expired or deleted. Please select a new environment.": "Active Environment is expired or deleted. Please select a new environment.", "PAC Telemetry enabled": "PAC Telemetry enabled", "Failed to enable PAC telemetry.": "Failed to enable PAC telemetry.", "PAC Telemetry disabled": "PAC Telemetry disabled", diff --git a/loc/translations-export/vscode-powerplatform.xlf b/loc/translations-export/vscode-powerplatform.xlf index 62c771cb..9c6d83c9 100644 --- a/loc/translations-export/vscode-powerplatform.xlf +++ b/loc/translations-export/vscode-powerplatform.xlf @@ -22,6 +22,9 @@ AI-generated content can contain mistakes + + Active Environment is expired or deleted. Please select a new environment. + Active Sites @@ -693,6 +696,9 @@ The second line should be '[TRANSLATION HERE](command:powerplatform-walkthrough. Preview Site + + Refresh + Refresh diff --git a/package.json b/package.json index 8058db7b..29532a4d 100644 --- a/package.json +++ b/package.json @@ -375,6 +375,11 @@ "command": "microsoft.powerplatform.pages.preview-site", "category": "Powerpages", "title": "%powerplatform.pages.previewSite.title%" + }, + { + "command": "powerpages.actionsHub.refresh", + "title": "%powerpages.actionsHub.refresh.title%", + "icon": "$(refresh)" } ], "configuration": { @@ -850,6 +855,11 @@ "command": "powerpages.copilot.clearConversation", "when": "view == powerpages.copilot", "group": "navigation" + }, + { + "command": "powerpages.actionsHub.refresh", + "when": "view == powerpages.actionsHub", + "group": "navigation" } ], "view/item/context": [ diff --git a/package.nls.json b/package.nls.json index cba41a2f..7b76055f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -99,5 +99,6 @@ }, "microsoft-powerplatform-portals.navigation-loop.powerPagesFileExplorer.title": "POWER PAGES ACTIONS", "powerplatform.pages.previewSite.title": "Preview Site", - "microsoft.powerplatform.pages.actionsHub.title": "POWER PAGES ACTIONS" + "microsoft.powerplatform.pages.actionsHub.title": "POWER PAGES ACTIONS", + "powerpages.actionsHub.refresh.title": "Refresh" } diff --git a/src/client/OrgChangeNotifier.ts b/src/client/OrgChangeNotifier.ts index ef6dc9eb..39f217b1 100644 --- a/src/client/OrgChangeNotifier.ts +++ b/src/client/OrgChangeNotifier.ts @@ -50,6 +50,7 @@ export class OrgChangeNotifier { this._orgDetails = pacActiveOrg.Results; orgChangeEventEmitter.fire(this._orgDetails); } else { + //If org/env is expired or deleted, this event will be fired orgChangeErrorEventEmitter.fire(); } } diff --git a/src/client/extension.ts b/src/client/extension.ts index 744ab694..4aab7099 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -34,7 +34,7 @@ import { CopilotNotificationShown } from "../common/copilot/telemetry/telemetryC import { copilotNotificationPanel, disposeNotificationPanel } from "../common/copilot/welcome-notification/CopilotNotificationPanel"; import { COPILOT_NOTIFICATION_DISABLED, EXTENSION_VERSION_KEY } from "../common/copilot/constants"; import { oneDSLoggerWrapper } from "../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; -import { OrgChangeNotifier, orgChangeEvent } from "./OrgChangeNotifier"; +import { OrgChangeNotifier, orgChangeErrorEvent, orgChangeEvent } from "./OrgChangeNotifier"; import { ActiveOrgOutput } from "./pac/PacTypes"; import { desktopTelemetryEventNames } from "../common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames"; import { ArtemisService } from "../common/services/ArtemisService"; @@ -48,6 +48,10 @@ import { getECSOrgLocationValue, getWorkspaceFolders } from "../common/utilities import { CliAcquisitionContext } from "./lib/CliAcquisitionContext"; import { PreviewSite } from "./power-pages/preview-site/PreviewSite"; import { ActionsHub } from "./power-pages/actions-hub/ActionsHub"; +import { pacAuthManager } from "./pac/PacAuthManager"; +import { showErrorDialog } from "../common/utilities/errorHandlerUtil"; +import { ENVIRONMENT_EXPIRED } from "./power-pages/actions-hub/Constants"; +import { extractAuthInfo } from "./power-pages/commonUtility"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -205,6 +209,8 @@ export async function activate( AadIdObject = pacActiveAuth.Results?.filter(obj => obj.Key === AadIdKey); EnvID = pacActiveAuth.Results?.filter(obj => obj.Key === EnvIdKey); TenantID = pacActiveAuth.Results?.filter(obj => obj.Key === TenantIdKey); + const authInfo = extractAuthInfo(pacActiveAuth.Results); + pacAuthManager.setAuthInfo(authInfo); } if (EnvID?.[0]?.Value && TenantID?.[0]?.Value && AadIdObject?.[0]?.Value) { @@ -251,6 +257,10 @@ export async function activate( await PreviewSite.initialize(artemisResponse, workspaceFolders, orgDetails, pacTerminal, context, _telemetry); await ActionsHub.initialize(context, pacTerminal); + }), + + orgChangeErrorEvent(() => { + showErrorDialog(ENVIRONMENT_EXPIRED); }) ); diff --git a/src/client/pac/PacAuthManager.ts b/src/client/pac/PacAuthManager.ts index a612001c..f9931338 100644 --- a/src/client/pac/PacAuthManager.ts +++ b/src/client/pac/PacAuthManager.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ +import { EventEmitter, Event } from "vscode"; import { AuthInfo } from "./PacTypes"; - - class PacAuthManager { private static instance: PacAuthManager; private authInfo: AuthInfo | null = null; + private _onDidChangeEnvironment: EventEmitter = new EventEmitter(); + public readonly onDidChangeEnvironment: Event = this._onDidChangeEnvironment.event; public static getInstance(): PacAuthManager { if (!PacAuthManager.instance) { @@ -20,6 +21,7 @@ class PacAuthManager { public setAuthInfo(authInfo: AuthInfo): void { this.authInfo = authInfo; + this._onDidChangeEnvironment.fire(); } public getAuthInfo(): AuthInfo | null { diff --git a/src/client/power-pages/actions-hub/ActionsHub.ts b/src/client/power-pages/actions-hub/ActionsHub.ts index ff625cf6..21643c8e 100644 --- a/src/client/power-pages/actions-hub/ActionsHub.ts +++ b/src/client/power-pages/actions-hub/ActionsHub.ts @@ -9,11 +9,6 @@ import { EnableActionsHub } from "../../../common/ecs-features/ecsFeatureGates"; import { ActionsHubTreeDataProvider } from "./ActionsHubTreeDataProvider"; import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; import { PacTerminal } from "../../lib/PacTerminal"; -import { SUCCESS } from "../../../common/constants"; -import { extractAuthInfo } from "../commonUtility"; -import { pacAuthManager } from "../../pac/PacAuthManager"; -import { Constants } from "./Constants"; - export class ActionsHub { static isEnabled(): boolean { const enableActionsHub = ECSFeaturesClient.getConfig(EnableActionsHub).enableActionsHub @@ -38,16 +33,6 @@ export class ActionsHub { return; } - try { - const pacActiveAuth = await pacTerminal.getWrapper()?.activeAuth(); - if (pacActiveAuth && pacActiveAuth.Status === SUCCESS) { - const authInfo = extractAuthInfo(pacActiveAuth.Results); - pacAuthManager.setAuthInfo(authInfo); - } - - ActionsHubTreeDataProvider.initialize(context); - } catch (error) { - oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.ACTIONS_HUB_INITIALIZATION_FAILED, error as string, error as Error, { methodName: ActionsHub.initialize.name }, {}); - } + ActionsHubTreeDataProvider.initialize(context, pacTerminal); } } diff --git a/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts b/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts index 15bb606a..40d72fa6 100644 --- a/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts +++ b/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts @@ -11,21 +11,31 @@ import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLo import { EnvironmentGroupTreeItem } from "./tree-items/EnvironmentGroupTreeItem"; import { IEnvironmentInfo } from "./models/IEnvironmentInfo"; import { pacAuthManager } from "../../pac/PacAuthManager"; +import { SUCCESS } from "../../../common/constants"; +import { extractAuthInfo } from "../commonUtility"; +import { PacTerminal } from "../../lib/PacTerminal"; export class ActionsHubTreeDataProvider implements vscode.TreeDataProvider { private readonly _disposables: vscode.Disposable[] = []; private readonly _context: vscode.ExtensionContext; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - private constructor(context: vscode.ExtensionContext) { + + private constructor(context: vscode.ExtensionContext, private readonly _pacTerminal: PacTerminal) { this._disposables.push( vscode.window.registerTreeDataProvider("powerpages.actionsHub", this) ); this._context = context; + + // Register an event listener for environment changes + pacAuthManager.onDidChangeEnvironment(() => this.refresh()); + this._disposables.push(...this.registerPanel(this._pacTerminal)); } - public static initialize(context: vscode.ExtensionContext): ActionsHubTreeDataProvider { - const actionsHubTreeDataProvider = new ActionsHubTreeDataProvider(context); + public static initialize(context: vscode.ExtensionContext, pacTerminal: PacTerminal): ActionsHubTreeDataProvider { + const actionsHubTreeDataProvider = new ActionsHubTreeDataProvider(context, pacTerminal); oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.ACTIONS_HUB_INITIALIZED); return actionsHubTreeDataProvider; @@ -61,7 +71,28 @@ export class ActionsHubTreeDataProvider implements vscode.TreeDataProvider d.dispose()); } + + private registerPanel(pacTerminal: PacTerminal): vscode.Disposable[] { + const pacWrapper = pacTerminal.getWrapper(); + return [ + vscode.commands.registerCommand("powerpages.actionsHub.refresh", async () => { + try { + const pacActiveAuth = await pacWrapper.activeAuth(); + if (pacActiveAuth && pacActiveAuth.Status === SUCCESS) { + const authInfo = extractAuthInfo(pacActiveAuth.Results); + pacAuthManager.setAuthInfo(authInfo); + } + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.ACTIONS_HUB_REFRESH_FAILED, error as string, error as Error, { methodName: this.refresh.name }, {}); + } + }) + ]; + } } diff --git a/src/client/power-pages/actions-hub/Constants.ts b/src/client/power-pages/actions-hub/Constants.ts index 8f40165a..dedd9910 100644 --- a/src/client/power-pages/actions-hub/Constants.ts +++ b/src/client/power-pages/actions-hub/Constants.ts @@ -32,6 +32,8 @@ export const Constants = { ACTIONS_HUB_INITIALIZED: "actionsHubInitialized", ACTIONS_HUB_INITIALIZATION_FAILED: "actionsHubInitializationFailed", ACTIONS_HUB_CURRENT_ENV_FETCH_FAILED: "actionsHubCurrentEnvFetchFailed", + ACTIONS_HUB_REFRESH_FAILED: "actionsHubRefreshFailed" } }; +export const ENVIRONMENT_EXPIRED = vscode.l10n.t("Active Environment is expired or deleted. Please select a new environment.") diff --git a/src/client/test/Integration/power-pages/actions-hub/ActionsHub.test.ts b/src/client/test/Integration/power-pages/actions-hub/ActionsHub.test.ts index 95072ac5..02459fa1 100644 --- a/src/client/test/Integration/power-pages/actions-hub/ActionsHub.test.ts +++ b/src/client/test/Integration/power-pages/actions-hub/ActionsHub.test.ts @@ -6,17 +6,12 @@ import { expect } from "chai"; import * as sinon from "sinon"; import * as vscode from "vscode"; -import * as CommonUtils from "../../../../power-pages/commonUtility"; import { ActionsHub } from "../../../../power-pages/actions-hub/ActionsHub"; import { PacTerminal } from "../../../../lib/PacTerminal"; import { ECSFeaturesClient } from "../../../../../common/ecs-features/ecsFeatureClient"; import { oneDSLoggerWrapper } from "../../../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; import { ActionsHubTreeDataProvider } from "../../../../power-pages/actions-hub/ActionsHubTreeDataProvider"; -import { SUCCESS } from "../../../../../common/constants"; -import { AUTH_KEYS } from "../../../../../common/OneDSLoggerTelemetry/telemetryConstants"; import { PacWrapper } from "../../../../pac/PacWrapper"; -import { AuthInfo, PacAuthWhoOutput } from "../../../../pac/PacTypes"; -import { pacAuthManager } from "../../../../pac/PacAuthManager"; describe("ActionsHub", () => { let getConfigStub: sinon.SinonStub; @@ -72,31 +67,21 @@ describe("ActionsHub", () => { expect(executeCommandStub.calledWith("setContext", "microsoft.powerplatform.pages.actionsHubEnabled", true)).to.be.true; }); - it("should set Auth Info", async () => { - getConfigStub.returns({ enableActionsHub: true }); - const data = { organizationFriendlyName: 'Foo' } as AuthInfo; - sinon.stub(CommonUtils, 'extractAuthInfo').returns(data); - const pacAuthSetInfoStub = sinon.stub(pacAuthManager, 'setAuthInfo'); - fakePacWrapper.activeAuth.returns(Promise.resolve({ Status: SUCCESS, Results: [{ Key: AUTH_KEYS.ORGANIZATION_FRIENDLY_NAME, Value: 'Foo' }] }) as Promise); - - await ActionsHub.initialize(fakeContext, fakePacTerminal); - expect(pacAuthSetInfoStub.calledWith(data)).to.be.true; - }); - it("should initialize ActionsHubTreeDataProvider", async () => { getConfigStub.returns({ enableActionsHub: true }); const actionsHubTreeDataProviderStub = sinon.stub(ActionsHubTreeDataProvider, 'initialize'); - fakePacWrapper.activeAuth.returns(Promise.resolve({ Status: SUCCESS, Results: [{ Key: AUTH_KEYS.ORGANIZATION_FRIENDLY_NAME, Value: 'Foo' }] }) as Promise); await ActionsHub.initialize(fakeContext, fakePacTerminal); expect(actionsHubTreeDataProviderStub.calledWith(fakeContext)).to.be.true; + + actionsHubTreeDataProviderStub.restore(); }); }); describe("when ActionsHub is disabled", () => { - it("should set context to false and return early if isEnabled returns false", () => { + it("should set context to false and return early if isEnabled returns false", async () => { getConfigStub.returns({ enableActionsHub: false }); - ActionsHub.initialize(fakeContext, fakePacTerminal); + await ActionsHub.initialize(fakeContext, fakePacTerminal); expect(traceInfoStub.calledWith("EnableActionsHub", { isEnabled: "false" })).to.be.true; expect(executeCommandStub.calledWith("setContext", "microsoft.powerplatform.pages.actionsHubEnabled", false)).to.be.true; }); @@ -104,6 +89,7 @@ describe("ActionsHub", () => { it("should not initialize ActionsHubTreeDataProvider", () => { getConfigStub.returns({ enableActionsHub: false }); const actionsHubTreeDataProviderStub = sinon.stub(ActionsHubTreeDataProvider, 'initialize'); + ActionsHub.initialize(fakeContext, fakePacTerminal); expect(actionsHubTreeDataProviderStub.called).to.be.false; }); diff --git a/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts b/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts index 51109ec5..a164fdeb 100644 --- a/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts +++ b/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts @@ -14,14 +14,21 @@ import { EnvironmentGroupTreeItem } from "../../../../power-pages/actions-hub/tr import { OtherSitesGroupTreeItem } from "../../../../power-pages/actions-hub/tree-items/OtherSitesGroupTreeItem"; import { ActionsHubTreeItem } from "../../../../power-pages/actions-hub/tree-items/ActionsHubTreeItem"; import { pacAuthManager } from "../../../../pac/PacAuthManager"; +import { PacTerminal } from "../../../../lib/PacTerminal"; +import { PacWrapper } from "../../../../pac/PacWrapper"; +import { SUCCESS } from "../../../../../common/constants"; describe("ActionsHubTreeDataProvider", () => { let context: vscode.ExtensionContext; let traceInfoStub: sinon.SinonStub; let traceErrorStub: sinon.SinonStub; let authInfoStub: sinon.SinonStub; + let pacTerminal: PacTerminal; + let pacWrapperStub: sinon.SinonStubbedInstance; + let registerCommandStub: sinon.SinonStub; beforeEach(() => { + registerCommandStub = sinon.stub(vscode.commands, "registerCommand"); context = { extensionUri: vscode.Uri.parse("https://localhost:3000") } as vscode.ExtensionContext; @@ -34,6 +41,10 @@ describe("ActionsHubTreeDataProvider", () => { featureUsage: sinon.stub() }); authInfoStub = sinon.stub(pacAuthManager, "getAuthInfo"); + pacTerminal = sinon.createStubInstance(PacTerminal); + pacWrapperStub = sinon.createStubInstance(PacWrapper); + pacWrapperStub.activeAuth.resolves({ Status: SUCCESS, Results: [], Errors: [], Information: [] }); + (pacTerminal.getWrapper as sinon.SinonStub).returns(pacWrapperStub); }); afterEach(() => { @@ -42,15 +53,24 @@ describe("ActionsHubTreeDataProvider", () => { describe('initialize', () => { it("should initialize and log initialization event", () => { - ActionsHubTreeDataProvider.initialize(context); + ActionsHubTreeDataProvider.initialize(context, pacTerminal); expect(traceInfoStub.calledWith(Constants.EventNames.ACTIONS_HUB_INITIALIZED)).to.be.true; }); + + it("should register refresh command", () => { + // Initialize + const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal); + actionsHubTreeDataProvider["registerPanel"](pacTerminal); + + // Assert that the command was registered + expect(registerCommandStub.calledWith("powerpages.actionsHub.refresh")).to.be.true; + }); }); describe('getTreeItem', () => { it("should return the element in getTreeItem", () => { const element = {} as ActionsHubTreeItem; - const result = ActionsHubTreeDataProvider.initialize(context).getTreeItem(element); + const result = ActionsHubTreeDataProvider.initialize(context, pacTerminal).getTreeItem(element); expect(result).to.equal(element); }); }); @@ -75,7 +95,7 @@ describe("ActionsHubTreeDataProvider", () => { organizationId: "", organizationUniqueName: "" }); - const provider = ActionsHubTreeDataProvider.initialize(context); + const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal); const result = await provider.getChildren(); expect(result).to.not.be.null; @@ -87,7 +107,7 @@ describe("ActionsHubTreeDataProvider", () => { it("should return environment group tree item with default name when no auth info is available", async () => { authInfoStub.returns(null); - const provider = ActionsHubTreeDataProvider.initialize(context); + const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal); const result = await provider.getChildren(); expect(result).to.not.be.null; @@ -101,7 +121,7 @@ describe("ActionsHubTreeDataProvider", () => { it("should return null in getChildren when an error occurs", async () => { authInfoStub.throws(new Error("Test Error")); - const provider = ActionsHubTreeDataProvider.initialize(context); + const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal); const result = await provider.getChildren(); expect(result).to.be.null; @@ -112,23 +132,11 @@ describe("ActionsHubTreeDataProvider", () => { it("should return an empty array in getChildren when an element is passed", async () => { const element = {} as ActionsHubTreeItem; - const provider = ActionsHubTreeDataProvider.initialize(context); + const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal); const result = await provider.getChildren(element); expect(result).to.be.an("array").that.is.empty; }); }); - describe('dispose', () => { - it("should dispose all disposables", () => { - const disposable1 = { dispose: sinon.spy() }; - const disposable2 = { dispose: sinon.spy() }; - const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context); - actionsHubTreeDataProvider["_disposables"].push(disposable1 as vscode.Disposable, disposable2 as vscode.Disposable); - - actionsHubTreeDataProvider.dispose(); - expect(disposable1.dispose.calledOnce).to.be.true; - expect(disposable2.dispose.calledOnce).to.be.true; - }); - }); });