Skip to content

Commit 02dbf35

Browse files
amitjoshi438amitjoshi
and
amitjoshi
authoredFeb 3, 2025··
Enhance Actions Hub with environment support and unit tests (#1098)
* Enhance Actions Hub: Add support for environment retrieval and localization updates * Add unit tests for ActionsHubTreeDataProvider functionality --------- Co-authored-by: amitjoshi <[email protected]>
1 parent 346bc7c commit 02dbf35

File tree

11 files changed

+274
-20
lines changed

11 files changed

+274
-20
lines changed
 

‎l10n/bundle.l10n.json

+1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
"Active Sites": "Active Sites",
182182
"Inactive Sites": "Inactive Sites",
183183
"No sites found": "No sites found",
184+
"No environments found": "No environments found",
184185
"PAC Telemetry enabled": "PAC Telemetry enabled",
185186
"Failed to enable PAC telemetry.": "Failed to enable PAC telemetry.",
186187
"PAC Telemetry disabled": "PAC Telemetry disabled",

‎loc/translations-export/vscode-powerplatform.xlf

+3
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID)</note>
324324
<trans-unit id="++CODE++b1061a7adf836619e40617bbbbe42d04a9c167b5756a2b4926c02d88e780a7f9">
325325
<source xml:lang="en">No Website Data Found in Current Directory. Please switch to a directory that contains the PAC downloaded website data to continue.</source>
326326
</trans-unit>
327+
<trans-unit id="++CODE++80266a1ea7e6671d65c98a0624c254f5b9acfacddd05d69524b7f034eeba3268">
328+
<source xml:lang="en">No environments found</source>
329+
</trans-unit>
327330
<trans-unit id="++CODE++e0d2b29e6060c3d50691e6961835418fe6a24723c227dacf93250b94e21ed1bb">
328331
<source xml:lang="en">No page templates found</source>
329332
</trans-unit>

‎src/client/extension.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ import { getECSOrgLocationValue, getWorkspaceFolders } from "../common/utilities
4848
import { CliAcquisitionContext } from "./lib/CliAcquisitionContext";
4949
import { PreviewSite, SITE_PREVIEW_COMMAND_ID } from "./power-pages/preview-site/PreviewSite";
5050
import { ActionsHubTreeDataProvider } from "./power-pages/actions-hub/ActionsHubTreeDataProvider";
51+
import { authManager } from "./pac/PacAuthManager";
52+
import { extractAuthInfo } from "./power-pages/commonUtility";
53+
import { Constants } from "./power-pages/actions-hub/Constants";
5154
import { IArtemisServiceResponse } from "../common/services/Interfaces";
5255

5356
let client: LanguageClient;
@@ -273,7 +276,7 @@ export async function activate(
273276
const workspaceFolderWatcher = vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFolderChange);
274277
_context.subscriptions.push(workspaceFolderWatcher);
275278

276-
initializeActionsHub(context);
279+
initializeActionsHub(context, pacTerminal);
277280

278281
if (shouldEnableDebugger()) {
279282
activateDebugger(context, _telemetry);
@@ -309,17 +312,26 @@ async function initializeSiteRuntimePreview(
309312
}
310313
}
311314

312-
function initializeActionsHub(context: vscode.ExtensionContext) {
315+
async function initializeActionsHub(context: vscode.ExtensionContext, pacTerminal: PacTerminal) {
313316
//TODO: Initialize this based on ECS feature flag
314317
const actionsHubEnabled = false;
315318

316-
vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.actionsHubEnabled", actionsHubEnabled);
319+
try {
320+
const pacActiveAuth = await pacTerminal.getWrapper()?.activeAuth();
321+
if (pacActiveAuth && pacActiveAuth.Status === SUCCESS) {
322+
const authInfo = extractAuthInfo(pacActiveAuth.Results);
323+
authManager.setAuthInfo(authInfo);
324+
}
325+
326+
vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.actionsHubEnabled", actionsHubEnabled);
317327

318-
if (actionsHubEnabled) {
319-
ActionsHubTreeDataProvider.initialize(context, _telemetry);
328+
if (actionsHubEnabled) {
329+
ActionsHubTreeDataProvider.initialize(context, pacTerminal);
330+
}
331+
} catch (error) {
332+
oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.ACTIONS_HUB_INITIALIZATION_FAILED, error as string, error as Error, { methodName: initializeActionsHub.name }, {});
320333
}
321334
}
322-
323335
export async function deactivate(): Promise<void> {
324336
if (_telemetry) {
325337
_telemetry.sendTelemetryEvent("End");

‎src/client/pac/PacAuthManager.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 { AuthInfo } from "./PacTypes";
7+
8+
9+
10+
class PacAuthManager {
11+
private static instance: PacAuthManager;
12+
private authInfo: AuthInfo | null = null;
13+
14+
public static getInstance(): PacAuthManager {
15+
if (!PacAuthManager.instance) {
16+
PacAuthManager.instance = new PacAuthManager();
17+
}
18+
return PacAuthManager.instance;
19+
}
20+
21+
public setAuthInfo(authInfo: AuthInfo): void {
22+
this.authInfo = authInfo;
23+
}
24+
25+
public getAuthInfo(): AuthInfo | null {
26+
return this.authInfo;
27+
}
28+
}
29+
30+
export const authManager = PacAuthManager.getInstance();

‎src/client/pac/PacTypes.ts

+19
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,22 @@ export type ActiveAuthOutput = {
8080
}
8181

8282
export type PacAuthWhoOutput = PacOutputWithResultList<ActiveAuthOutput>;
83+
84+
export interface AuthInfo {
85+
userType: string;
86+
cloud: string;
87+
tenantId: string;
88+
tenantCountry: string;
89+
user: string;
90+
entraIdObjectId: string;
91+
puid: string;
92+
userCountryRegion: string;
93+
tokenExpires: string;
94+
authority: string;
95+
environmentGeo: string;
96+
environmentId: string;
97+
environmentType: string;
98+
organizationId: string;
99+
organizationUniqueName: string;
100+
organizationFriendlyName: string;
101+
}

‎src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts

+31-12
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,59 @@
66
import * as vscode from "vscode";
77
import { ActionsHubTreeItem } from "./tree-items/ActionsHubTreeItem";
88
import { OtherSitesGroupTreeItem } from "./tree-items/OtherSitesGroupTreeItem";
9-
import { ITelemetry } from "../../../common/OneDSLoggerTelemetry/telemetry/ITelemetry";
109
import { Constants } from "./Constants";
1110
import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper";
11+
import { PacTerminal } from "../../lib/PacTerminal";
12+
import { EnvironmentGroupTreeItem } from "./tree-items/EnvironmentGroupTreeItem";
13+
import { IEnvironmentInfo } from "./models/IEnvironmentInfo";
14+
import { authManager } from "../../pac/PacAuthManager";
1215

1316
export class ActionsHubTreeDataProvider implements vscode.TreeDataProvider<ActionsHubTreeItem> {
1417
private readonly _disposables: vscode.Disposable[] = [];
1518
private readonly _context: vscode.ExtensionContext;
16-
private readonly _telemetry: ITelemetry;
19+
private readonly _pacTerminal: PacTerminal;
1720

18-
private constructor(context: vscode.ExtensionContext, telemetry: ITelemetry) {
21+
private constructor(context: vscode.ExtensionContext, pacTerminal: PacTerminal) {
1922
this._disposables.push(
2023
vscode.window.registerTreeDataProvider("powerpages.actionsHub", this)
2124
);
2225

2326
this._context = context;
24-
this._telemetry = telemetry;
27+
this._pacTerminal = pacTerminal;
2528
}
2629

27-
public static initialize(context: vscode.ExtensionContext, telemetry: ITelemetry): void {
28-
new ActionsHubTreeDataProvider(context, telemetry);
29-
30-
telemetry.sendTelemetryEvent(Constants.EventNames.ACTIONS_HUB_INITIALIZED);
30+
public static initialize(context: vscode.ExtensionContext, pacTerminal: PacTerminal): ActionsHubTreeDataProvider {
31+
const actionsHubTreeDataProvider = new ActionsHubTreeDataProvider(context, pacTerminal);
3132
oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.ACTIONS_HUB_INITIALIZED);
33+
34+
return actionsHubTreeDataProvider;
3235
}
3336

3437
getTreeItem(element: ActionsHubTreeItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
3538
return element;
3639
}
3740

38-
getChildren(element?: ActionsHubTreeItem | undefined): vscode.ProviderResult<ActionsHubTreeItem[]> {
41+
async getChildren(element?: ActionsHubTreeItem): Promise<ActionsHubTreeItem[] | null | undefined> {
3942
if (!element) {
40-
return [
41-
new OtherSitesGroupTreeItem()
42-
];
43+
try {
44+
45+
const orgFriendlyName = Constants.Strings.NO_ENVIRONMENTS_FOUND; // Login experience scenario
46+
let currentEnvInfo: IEnvironmentInfo = { currentEnvironmentName: orgFriendlyName };
47+
const authInfo = authManager.getAuthInfo();
48+
if (authInfo) {
49+
currentEnvInfo = { currentEnvironmentName: authInfo.organizationFriendlyName };
50+
}
51+
52+
//TODO: Handle the case when the user is not logged in
53+
54+
return [
55+
new EnvironmentGroupTreeItem(currentEnvInfo, this._context),
56+
new OtherSitesGroupTreeItem()
57+
];
58+
} catch (error) {
59+
oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.ACTIONS_HUB_CURRENT_ENV_FETCH_FAILED, error as string, error as Error, { methodName: this.getChildren }, {});
60+
return null;
61+
}
4362
} else {
4463
return [];
4564
}

‎src/client/power-pages/actions-hub/Constants.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ export const Constants = {
2525
OTHER_SITES: vscode.l10n.t("Other Sites"),
2626
ACTIVE_SITES: vscode.l10n.t("Active Sites"),
2727
INACTIVE_SITES: vscode.l10n.t("Inactive Sites"),
28-
NO_SITES_FOUND: vscode.l10n.t("No sites found")
28+
NO_SITES_FOUND: vscode.l10n.t("No sites found"),
29+
NO_ENVIRONMENTS_FOUND: vscode.l10n.t("No environments found")
2930
},
3031
EventNames: {
31-
ACTIONS_HUB_INITIALIZED: "actionsHubInitialized"
32+
ACTIONS_HUB_INITIALIZED: "actionsHubInitialized",
33+
ACTIONS_HUB_INITIALIZATION_FAILED: "actionsHubInitializationFailed",
34+
ACTIONS_HUB_CURRENT_ENV_FETCH_FAILED: "actionsHubCurrentEnvFetchFailed",
3235
}
3336
};
37+

‎src/client/power-pages/actions-hub/tree-items/EnvironmentGroupTreeItem.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IEnvironmentInfo } from "../models/IEnvironmentInfo";
99
import { Constants } from "../Constants";
1010

1111
export class EnvironmentGroupTreeItem extends ActionsHubTreeItem {
12+
environmentInfo: IEnvironmentInfo = {} as IEnvironmentInfo;
1213
constructor(environmentInfo: IEnvironmentInfo, context: vscode.ExtensionContext) {
1314
super(
1415
environmentInfo.currentEnvironmentName,
@@ -18,5 +19,6 @@ export class EnvironmentGroupTreeItem extends ActionsHubTreeItem {
1819
dark: vscode.Uri.joinPath(context.extensionUri, 'src', 'client', 'assets', 'environment-icon', 'dark', 'environment.svg')
1920
},
2021
Constants.ContextValues.ENVIRONMENT_GROUP);
22+
this.environmentInfo = environmentInfo;
2123
}
2224
}

‎src/client/power-pages/commonUtility.ts

+29
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import path from "path";
77
import * as vscode from "vscode";
88
import { removeTrailingSlash } from "../../debugger/utils";
99
import * as Constants from "./constants";
10+
import { AUTH_KEYS } from "../../common/OneDSLoggerTelemetry/telemetryConstants";
11+
import { AuthInfo } from "../pac/PacTypes";
1012

1113
export interface IFileProperties {
1214
fileCompleteName?: string,
@@ -197,3 +199,30 @@ export function getRegExPattern(fileNameArray: string[]): RegExp[] {
197199

198200
return patterns;
199201
}
202+
203+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
204+
export function extractAuthInfo(results: any[]): AuthInfo {
205+
return {
206+
userType: findAuthValue(results, AUTH_KEYS.USER_TYPE),
207+
cloud: findAuthValue(results, AUTH_KEYS.CLOUD),
208+
tenantId: findAuthValue(results, AUTH_KEYS.TENANT_ID),
209+
tenantCountry: findAuthValue(results, AUTH_KEYS.TENANT_COUNTRY),
210+
user: findAuthValue(results, AUTH_KEYS.USER),
211+
entraIdObjectId: findAuthValue(results, AUTH_KEYS.ENTRA_ID_OBJECT_ID),
212+
puid: findAuthValue(results, AUTH_KEYS.PUID),
213+
userCountryRegion: findAuthValue(results, AUTH_KEYS.USER_COUNTRY_REGION),
214+
tokenExpires: findAuthValue(results, AUTH_KEYS.TOKEN_EXPIRES),
215+
authority: findAuthValue(results, AUTH_KEYS.AUTHORITY),
216+
environmentGeo: findAuthValue(results, AUTH_KEYS.ENVIRONMENT_GEO),
217+
environmentId: findAuthValue(results, AUTH_KEYS.ENVIRONMENT_ID),
218+
environmentType: findAuthValue(results, AUTH_KEYS.ENVIRONMENT_TYPE),
219+
organizationId: findAuthValue(results, AUTH_KEYS.ORGANIZATION_ID),
220+
organizationUniqueName: findAuthValue(results, AUTH_KEYS.ORGANIZATION_UNIQUE_NAME),
221+
organizationFriendlyName: findAuthValue(results, AUTH_KEYS.ORGANIZATION_FRIENDLY_NAME)
222+
};
223+
}
224+
225+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
226+
export function findAuthValue(results: any[], key: string): string {
227+
return results?.find(obj => obj.Key === key)?.Value ?? '';
228+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
7+
import { expect } from "chai";
8+
import * as sinon from "sinon";
9+
import * as vscode from "vscode";
10+
import { ActionsHubTreeDataProvider } from "../../../../power-pages/actions-hub/ActionsHubTreeDataProvider";
11+
import { oneDSLoggerWrapper } from "../../../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper";
12+
import { Constants } from "../../../../power-pages/actions-hub/Constants";
13+
import { EnvironmentGroupTreeItem } from "../../../../power-pages/actions-hub/tree-items/EnvironmentGroupTreeItem";
14+
import { OtherSitesGroupTreeItem } from "../../../../power-pages/actions-hub/tree-items/OtherSitesGroupTreeItem";
15+
import { ActionsHubTreeItem } from "../../../../power-pages/actions-hub/tree-items/ActionsHubTreeItem";
16+
import { PacTerminal } from "../../../../lib/PacTerminal";
17+
import { authManager } from "../../../../pac/PacAuthManager";
18+
19+
describe("ActionsHubTreeDataProvider", () => {
20+
let context: vscode.ExtensionContext;
21+
let pacTerminal: PacTerminal;
22+
let actionsHubTreeDataProvider: ActionsHubTreeDataProvider;
23+
24+
beforeEach(() => {
25+
context = {} as vscode.ExtensionContext;
26+
pacTerminal = {} as PacTerminal;
27+
actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
28+
});
29+
30+
afterEach(() => {
31+
sinon.restore();
32+
});
33+
34+
it("should initialize and log initialization event", () => {
35+
const traceInfoStub = sinon.stub(oneDSLoggerWrapper.getLogger(), "traceInfo");
36+
ActionsHubTreeDataProvider.initialize(context, pacTerminal);
37+
expect(traceInfoStub.calledWith(Constants.EventNames.ACTIONS_HUB_INITIALIZED)).to.be.true;
38+
});
39+
40+
it("should return the element in getTreeItem", () => {
41+
const element = {} as ActionsHubTreeItem;
42+
const result = actionsHubTreeDataProvider.getTreeItem(element);
43+
expect(result).to.equal(element);
44+
});
45+
46+
it("should return environment and other sites group tree items in getChildren when no element is passed", async () => {
47+
const authInfoStub = sinon.stub(authManager, "getAuthInfo").returns({
48+
organizationFriendlyName: "TestOrg",
49+
userType: "",
50+
cloud: "",
51+
tenantId: "",
52+
tenantCountry: "",
53+
user: "",
54+
entraIdObjectId: "",
55+
puid: "",
56+
userCountryRegion: "",
57+
tokenExpires: "",
58+
authority: "",
59+
environmentGeo: "",
60+
environmentId: "",
61+
environmentType: "",
62+
organizationId: "",
63+
organizationUniqueName: ""
64+
});
65+
const result = await actionsHubTreeDataProvider.getChildren();
66+
67+
expect(result).to.not.be.null;
68+
expect(result).to.not.be.undefined;
69+
expect(result).to.have.lengthOf(2);
70+
expect(result![0]).to.be.instanceOf(EnvironmentGroupTreeItem);
71+
expect(result![1]).to.be.instanceOf(OtherSitesGroupTreeItem);
72+
73+
authInfoStub.restore();
74+
});
75+
76+
it("should return environment group tree item with default name when no auth info is available", async () => {
77+
const authInfoStub = sinon.stub(authManager, "getAuthInfo").returns(null);
78+
const result = await actionsHubTreeDataProvider.getChildren();
79+
80+
expect(result).to.not.be.null;
81+
expect(result).to.not.be.undefined;
82+
expect(result).to.have.lengthOf(2);
83+
expect(result![0]).to.be.instanceOf(EnvironmentGroupTreeItem);
84+
expect((result![0] as EnvironmentGroupTreeItem).environmentInfo.currentEnvironmentName).to.equal(Constants.Strings.NO_ENVIRONMENTS_FOUND);
85+
expect(result![1]).to.be.instanceOf(OtherSitesGroupTreeItem);
86+
87+
authInfoStub.restore();
88+
});
89+
90+
it("should return null in getChildren when an error occurs", async () => {
91+
const authInfoStub = sinon.stub(authManager, "getAuthInfo").throws(new Error("Test Error"));
92+
const traceErrorStub = sinon.stub(oneDSLoggerWrapper.getLogger(), "traceError");
93+
94+
const result = await actionsHubTreeDataProvider.getChildren();
95+
expect(result).to.be.null;
96+
expect(traceErrorStub.calledWith(Constants.EventNames.ACTIONS_HUB_CURRENT_ENV_FETCH_FAILED)).to.be.true;
97+
98+
authInfoStub.restore();
99+
traceErrorStub.restore();
100+
});
101+
102+
it("should return an empty array in getChildren when an element is passed", async () => {
103+
const element = {} as ActionsHubTreeItem;
104+
const result = await actionsHubTreeDataProvider.getChildren(element);
105+
expect(result).to.be.an("array").that.is.empty;
106+
});
107+
108+
it("should dispose all disposables", () => {
109+
const disposable1 = { dispose: sinon.spy() };
110+
const disposable2 = { dispose: sinon.spy() };
111+
actionsHubTreeDataProvider["_disposables"].push(disposable1 as vscode.Disposable, disposable2 as vscode.Disposable);
112+
113+
actionsHubTreeDataProvider.dispose();
114+
expect(disposable1.dispose.calledOnce).to.be.true;
115+
expect(disposable2.dispose.calledOnce).to.be.true;
116+
});
117+
});

‎src/common/OneDSLoggerTelemetry/telemetryConstants.ts

+18
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,21 @@ export const CUSTOM_TELEMETRY_FOR_POWER_PAGES_SETTING_NAME = 'enableTelemetry';
4242
export const AadIdKey= 'Entra ID Object Id:';
4343
export const EnvIdKey = "Environment Id:";
4444
export const TenantIdKey = "Tenant Id:";
45+
export const AUTH_KEYS = {
46+
USER_TYPE: 'Type:',
47+
CLOUD: 'Cloud:',
48+
TENANT_ID: 'Tenant Id:',
49+
TENANT_COUNTRY: 'Tenant Country:',
50+
USER: 'User:',
51+
ENTRA_ID_OBJECT_ID: 'Entra ID Object Id:',
52+
PUID: 'PUID:',
53+
USER_COUNTRY_REGION: 'User Country/Region:',
54+
TOKEN_EXPIRES: 'Token Expires:',
55+
AUTHORITY: 'Authority:',
56+
ENVIRONMENT_GEO: 'Environment Geo:',
57+
ENVIRONMENT_ID: 'Environment Id:',
58+
ENVIRONMENT_TYPE: 'Environment Type:',
59+
ORGANIZATION_ID: 'Organization Id:',
60+
ORGANIZATION_UNIQUE_NAME: 'Organization Unique Name:',
61+
ORGANIZATION_FRIENDLY_NAME: 'Organization Friendly Name:'
62+
};

0 commit comments

Comments
 (0)
Please sign in to comment.