From b0b973b21ac82985d4e0babb049ae736a69d8b35 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Mon, 25 Jan 2021 13:56:15 -0600 Subject: [PATCH] Refactor explorer config into useKnockoutExplorer hook (#397) Co-authored-by: Steve Faulkner --- src/ConfigContext.ts | 2 +- src/Contracts/ExplorerContracts.ts | 1 - src/Explorer/Explorer.ts | 55 +---- src/HostedExplorerChildFrame.ts | 11 +- src/Main.tsx | 207 +--------------- src/applyExplorerBindings.ts | 2 - src/hooks/useConfig.ts | 14 ++ src/hooks/useKnockoutExplorer.ts | 298 ++++++++++++++++++++++++ test/testExplorer/TestExplorer.ts | 85 +++---- test/testExplorer/TestExplorerParams.ts | 4 +- test/testExplorer/TestExplorerUtils.ts | 22 +- test/testExplorer/testExplorer.html | 10 +- 12 files changed, 368 insertions(+), 343 deletions(-) create mode 100644 src/hooks/useConfig.ts create mode 100644 src/hooks/useKnockoutExplorer.ts diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 2ca4b9ada..18a1f1fa0 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -4,7 +4,7 @@ export enum Platform { Emulator = "Emulator", } -interface ConfigContext { +export interface ConfigContext { platform: Platform; allowedParentFrameOrigins: string[]; gitSha?: string; diff --git a/src/Contracts/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts index e9ed8a322..1689a96b5 100644 --- a/src/Contracts/ExplorerContracts.ts +++ b/src/Contracts/ExplorerContracts.ts @@ -33,7 +33,6 @@ export enum MessageTypes { CreateWorkspace, CreateSparkPool, RefreshDatabaseAccount, - InitTestExplorer, } export { Versions, ActionContracts, Diagnostics }; diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 8c7ebd2e3..33c557106 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -1718,58 +1718,7 @@ export default class Explorer { this._addSynapseLinkDialogProps.valueHasMutated(); }; - private _shouldProcessMessage(event: MessageEvent): boolean { - if (typeof event.data !== "object") { - return false; - } - if (event.data["signature"] !== "pcIframe") { - return false; - } - if (!("data" in event.data)) { - return false; - } - if (typeof event.data["data"] !== "object") { - return false; - } - - // before initialization completed give exception - const message = event.data.data; - if (!this._importExplorerConfigComplete && message && message.type) { - const messageType = message.type; - switch (messageType) { - case MessageTypes.SendNotification: - case MessageTypes.ClearNotification: - case MessageTypes.LoadingStatus: - case MessageTypes.InitTestExplorer: - return true; - } - } - if (!("inputs" in event.data["data"]) && !this._importExplorerConfigComplete) { - return false; - } - return true; - } - - public handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (!this._shouldProcessMessage(event)) { - return; - } - - const message: any = event.data.data; - const inputs: ViewModels.DataExplorerInputsFrame = message.inputs; - - const isRunningInPortal = configContext.platform === Platform.Portal; - const isRunningInDevMode = process.env.NODE_ENV === "development"; - if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) { - inputs.extensionEndpoint = configContext.PROXY_PATH; - } - - this.initDataExplorerWithFrameInputs(inputs); - + public handleMessage(message: any) { const openAction: ActionContracts.DataExplorerAction = message.openAction; if (!!openAction) { if (this.isRefreshingExplorer()) { @@ -1874,7 +1823,7 @@ export default class Explorer { } } - public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void { + public configure(inputs: ViewModels.DataExplorerInputsFrame): void { if (inputs != null) { // In development mode, save the iframe message from the portal in session storage. // This allows webpack hot reload to funciton properly diff --git a/src/HostedExplorerChildFrame.ts b/src/HostedExplorerChildFrame.ts index 70497fb2c..2cff6c862 100644 --- a/src/HostedExplorerChildFrame.ts +++ b/src/HostedExplorerChildFrame.ts @@ -1,17 +1,18 @@ import { AuthType } from "./AuthType"; import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels"; +type HostedConfig = AAD | ConnectionString | EncryptedToken | ResourceToken; export interface HostedExplorerChildFrame extends Window { - hostedConfig: AAD | ConnectionString | EncryptedToken | ResourceToken; + hostedConfig: HostedConfig; } -interface AAD { +export interface AAD { authType: AuthType.AAD; databaseAccount: DatabaseAccount; authorizationToken: string; } -interface ConnectionString { +export interface ConnectionString { authType: AuthType.ConnectionString; // Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy encryptedToken: string; @@ -20,13 +21,13 @@ interface ConnectionString { masterKey?: string; } -interface EncryptedToken { +export interface EncryptedToken { authType: AuthType.EncryptedToken; encryptedToken: string; encryptedTokenMetadata: AccessInputMetadata; } -interface ResourceToken { +export interface ResourceToken { authType: AuthType.ResourceToken; resourceToken: string; } diff --git a/src/Main.tsx b/src/Main.tsx index 955287fd9..ad4a9caaa 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -53,215 +53,22 @@ import "object.entries/auto"; import "./Libs/is-integer-polyfill"; import "url-polyfill/url-polyfill.min"; -initializeIcons(); - -import { AuthType } from "./AuthType"; - import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; -import { applyExplorerBindings } from "./applyExplorerBindings"; -import { configContext, initializeConfiguration, Platform } from "./ConfigContext"; -import Explorer from "./Explorer/Explorer"; -import React, { useEffect } from "react"; +import React from "react"; import ReactDOM from "react-dom"; import copyImage from "../images/Copy.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import refreshImg from "../images/refresh-cosmos.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg"; import { KOCommentEnd, KOCommentIfStart } from "./koComment"; -import { updateUserContext } from "./UserContext"; -import { CollectionCreation } from "./Shared/Constants"; -import { extractFeatures } from "./Platform/Hosted/extractFeatures"; -import { emulatorAccount } from "./Platform/Emulator/emulatorAccount"; -import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; -import { - getDatabaseAccountKindFromExperience, - getDatabaseAccountPropertiesFromMetadata, -} from "./Platform/Hosted/HostedUtils"; -import { DefaultExperienceUtility } from "./Shared/DefaultExperienceUtility"; -import { parseResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils"; -import { AccountKind, DefaultAccountExperience, ServerIds } from "./Common/Constants"; -import { listKeys } from "./Utils/arm/generatedClients/2020-04-01/databaseAccounts"; -import { SelfServeType } from "./SelfServe/SelfServeUtils"; +import { useConfig } from "./hooks/useConfig"; +import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; + +initializeIcons(); const App: React.FunctionComponent = () => { - useEffect(() => { - initializeConfiguration().then(async (config) => { - let explorer: Explorer; - if (config.platform === Platform.Hosted) { - const win = (window as unknown) as HostedExplorerChildFrame; - explorer = new Explorer(); - explorer.selfServeType(SelfServeType.none); - if (win.hostedConfig.authType === AuthType.EncryptedToken) { - // TODO: Remove window.authType - window.authType = AuthType.EncryptedToken; - // Impossible to tell if this is a try cosmos sub using an encrypted token - explorer.isTryCosmosDBSubscription(false); - updateUserContext({ - accessToken: encodeURIComponent(win.hostedConfig.encryptedToken), - }); - - const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( - win.hostedConfig.encryptedTokenMetadata.apiKind - ); - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: "", - // id: Main._databaseAccountId, - name: win.hostedConfig.encryptedTokenMetadata.accountName, - kind: getDatabaseAccountKindFromExperience(apiExperience), - properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), - tags: [], - }, - subscriptionId: undefined, - resourceGroup: undefined, - masterKey: undefined, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), - }); - explorer.isAccountReady(true); - } else if (win.hostedConfig.authType === AuthType.ResourceToken) { - window.authType = AuthType.ResourceToken; - // Resource tokens can only be used with SQL API - const apiExperience: string = DefaultAccountExperience.DocumentDB; - const parsedResourceToken = parseResourceTokenConnectionString(win.hostedConfig.resourceToken); - updateUserContext({ - resourceToken: parsedResourceToken.resourceToken, - endpoint: parsedResourceToken.accountEndpoint, - }); - explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); - explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); - if (parsedResourceToken.partitionKey) { - explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); - } - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: "", - name: parsedResourceToken.accountEndpoint, - kind: AccountKind.GlobalDocumentDB, - properties: { documentEndpoint: parsedResourceToken.accountEndpoint }, - tags: { defaultExperience: apiExperience }, - }, - subscriptionId: undefined, - resourceGroup: undefined, - masterKey: undefined, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), - isAuthWithresourceToken: true, - }); - explorer.isAccountReady(true); - explorer.isRefreshingExplorer(false); - } else if (win.hostedConfig.authType === AuthType.ConnectionString) { - // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login - window.authType = AuthType.EncryptedToken; - // Impossible to tell if this is a try cosmos sub using an encrypted token - explorer.isTryCosmosDBSubscription(false); - updateUserContext({ - accessToken: encodeURIComponent(win.hostedConfig.encryptedToken), - }); - - const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( - win.hostedConfig.encryptedTokenMetadata.apiKind - ); - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: "", - // id: Main._databaseAccountId, - name: win.hostedConfig.encryptedTokenMetadata.accountName, - kind: getDatabaseAccountKindFromExperience(apiExperience), - properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), - tags: [], - }, - subscriptionId: undefined, - resourceGroup: undefined, - masterKey: win.hostedConfig.masterKey, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), - }); - explorer.isAccountReady(true); - } else if (win.hostedConfig.authType === AuthType.AAD) { - window.authType = AuthType.AAD; - const account = win.hostedConfig.databaseAccount; - const accountResourceId = account.id; - const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; - const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; - updateUserContext({ - authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, - databaseAccount: win.hostedConfig.databaseAccount, - }); - const keys = await listKeys(subscriptionId, resourceGroup, account.name); - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: account, - subscriptionId, - resourceGroup, - masterKey: keys.primaryMasterKey, - hasWriteAccess: true, //TODO: 425017 - support read access - authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), - }); - explorer.isAccountReady(true); - } - } else if (config.platform === Platform.Emulator) { - window.authType = AuthType.MasterKey; - explorer = new Explorer(); - explorer.selfServeType(SelfServeType.none); - explorer.databaseAccount(emulatorAccount); - explorer.isAccountReady(true); - } else if (config.platform === Platform.Portal) { - window.authType = AuthType.AAD; - explorer = new Explorer(); - - // In development mode, try to load the iframe message from session storage. - // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { - const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); - if (initMessage) { - const message = JSON.parse(initMessage); - console.warn("Loaded cached portal iframe message from session storage"); - console.dir(message); - explorer.initDataExplorerWithFrameInputs(message); - } - } - - window.addEventListener("message", explorer.handleMessage.bind(explorer), false); - } - applyExplorerBindings(explorer); - }); - }, []); + const config = useConfig(); + useKnockoutExplorer(config); return (
diff --git a/src/applyExplorerBindings.ts b/src/applyExplorerBindings.ts index 56114f1dc..ca28aea5c 100644 --- a/src/applyExplorerBindings.ts +++ b/src/applyExplorerBindings.ts @@ -1,5 +1,4 @@ import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer"; -import { sendMessage } from "./Common/MessageHandler"; import * as ko from "knockout"; import Explorer from "./Explorer/Explorer"; @@ -10,7 +9,6 @@ export const applyExplorerBindings = (explorer: Explorer) => { ko.applyBindings(explorer); // This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times. // TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal - sendMessage("ready"); $("#divExplorer").show(); } }; diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts new file mode 100644 index 000000000..27cce3c2d --- /dev/null +++ b/src/hooks/useConfig.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from "react"; +import { ConfigContext, initializeConfiguration } from "../ConfigContext"; + +// This hook initializes global configuration from a config.json file that is injected at deploy time +// This allows the same main Data Explorer build to be exactly the same in all clouds/platforms, +// but override some of the configuration as nesssary +export function useConfig(): Readonly { + const [state, setState] = useState(); + + useEffect(() => { + initializeConfiguration().then((response) => setState(response)); + }, []); + return state; +} diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts new file mode 100644 index 000000000..3dce2a113 --- /dev/null +++ b/src/hooks/useKnockoutExplorer.ts @@ -0,0 +1,298 @@ +import { useEffect } from "react"; +import { applyExplorerBindings } from "../applyExplorerBindings"; +import { AuthType } from "../AuthType"; +import { AccountKind, DefaultAccountExperience, ServerIds } from "../Common/Constants"; +import { sendMessage } from "../Common/MessageHandler"; +import { configContext, ConfigContext, Platform } from "../ConfigContext"; +import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; +import { MessageTypes } from "../Contracts/ExplorerContracts"; +import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; +import Explorer from "../Explorer/Explorer"; +import { + AAD, + ConnectionString, + EncryptedToken, + HostedExplorerChildFrame, + ResourceToken, +} from "../HostedExplorerChildFrame"; +import { emulatorAccount } from "../Platform/Emulator/emulatorAccount"; +import { extractFeatures } from "../Platform/Hosted/extractFeatures"; +import { parseResourceTokenConnectionString } from "../Platform/Hosted/Helpers/ResourceTokenUtils"; +import { + getDatabaseAccountKindFromExperience, + getDatabaseAccountPropertiesFromMetadata, +} from "../Platform/Hosted/HostedUtils"; +import { SelfServeType } from "../SelfServe/SelfServeUtils"; +import { CollectionCreation } from "../Shared/Constants"; +import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; +import { updateUserContext } from "../UserContext"; +import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts"; +import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; + +// This hook will create a new instance of Explorer.ts and bind it to the DOM +// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React +// Pleas tread carefully :) +let explorer: Explorer; + +export function useKnockoutExplorer(config: ConfigContext): Explorer { + explorer = explorer || new Explorer(); + useEffect(() => { + const effect = async () => { + if (config) { + if (config.platform === Platform.Hosted) { + await configureHosted(config); + } else if (config.platform === Platform.Emulator) { + configureEmulator(); + } else if (config.platform === Platform.Portal) { + configurePortal(); + } + applyExplorerBindings(explorer); + } + }; + effect(); + }, [config]); + return explorer; +} + +async function configureHosted(config: ConfigContext) { + const win = (window as unknown) as HostedExplorerChildFrame; + explorer.selfServeType(SelfServeType.none); + if (win.hostedConfig.authType === AuthType.EncryptedToken) { + configureHostedWithEncryptedToken(win.hostedConfig, config); + } else if (win.hostedConfig.authType === AuthType.ResourceToken) { + configureHostedWithResourceToken(win.hostedConfig); + } else if (win.hostedConfig.authType === AuthType.ConnectionString) { + configureHostedWithConnectionString(win.hostedConfig); + } else if (win.hostedConfig.authType === AuthType.AAD) { + await configureHostedWithAAD(win.hostedConfig); + } +} + +async function configureHostedWithAAD(config: AAD) { + window.authType = AuthType.AAD; + const account = config.databaseAccount; + const accountResourceId = account.id; + const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; + const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; + updateUserContext({ + authorizationToken: `Bearer ${config.authorizationToken}`, + databaseAccount: config.databaseAccount, + }); + const keys = await listKeys(subscriptionId, resourceGroup, account.name); + explorer.configure({ + databaseAccount: account, + subscriptionId, + resourceGroup, + masterKey: keys.primaryMasterKey, + hasWriteAccess: true, + authorizationToken: `Bearer ${config.authorizationToken}`, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + }); + explorer.isAccountReady(true); +} + +function configureHostedWithConnectionString(config: ConnectionString) { + // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login + window.authType = AuthType.EncryptedToken; + // Impossible to tell if this is a try cosmos sub using an encrypted token + explorer.isTryCosmosDBSubscription(false); + updateUserContext({ + accessToken: encodeURIComponent(config.encryptedToken), + }); + + const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( + config.encryptedTokenMetadata.apiKind + ); + explorer.configure({ + databaseAccount: { + id: "", + // id: Main._databaseAccountId, + name: config.encryptedTokenMetadata.accountName, + kind: getDatabaseAccountKindFromExperience(apiExperience), + properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata), + tags: [], + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: config.masterKey, + hasWriteAccess: true, + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + }); + explorer.isAccountReady(true); +} + +function configureHostedWithResourceToken(config: ResourceToken) { + window.authType = AuthType.ResourceToken; + // Resource tokens can only be used with SQL API + const apiExperience: string = DefaultAccountExperience.DocumentDB; + const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken); + updateUserContext({ + resourceToken: parsedResourceToken.resourceToken, + endpoint: parsedResourceToken.accountEndpoint, + }); + explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); + explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); + if (parsedResourceToken.partitionKey) { + explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); + } + explorer.configure({ + databaseAccount: { + id: "", + name: parsedResourceToken.accountEndpoint, + kind: AccountKind.GlobalDocumentDB, + properties: { documentEndpoint: parsedResourceToken.accountEndpoint }, + tags: { defaultExperience: apiExperience }, + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: undefined, + hasWriteAccess: true, + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + isAuthWithresourceToken: true, + }); + explorer.isAccountReady(true); + explorer.isRefreshingExplorer(false); +} + +function configureHostedWithEncryptedToken(config: EncryptedToken, configContext: ConfigContext) { + window.authType = AuthType.EncryptedToken; + // Impossible to tell if this is a try cosmos sub using an encrypted token + explorer.isTryCosmosDBSubscription(false); + updateUserContext({ + accessToken: encodeURIComponent(config.encryptedToken), + }); + + const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( + config.encryptedTokenMetadata.apiKind + ); + explorer.configure({ + databaseAccount: { + id: "", + name: config.encryptedTokenMetadata.accountName, + kind: getDatabaseAccountKindFromExperience(apiExperience), + properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata), + tags: [], + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: undefined, + hasWriteAccess: true, + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + }); + explorer.isAccountReady(true); +} + +function configureEmulator() { + window.authType = AuthType.MasterKey; + explorer.selfServeType(SelfServeType.none); + explorer.databaseAccount(emulatorAccount); + explorer.isAccountReady(true); +} + +function configurePortal() { + window.authType = AuthType.AAD; + // In development mode, try to load the iframe message from session storage. + // This allows webpack hot reload to function properly in the portal + if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) { + const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); + if (initMessage) { + const message = JSON.parse(initMessage); + console.warn( + "Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message" + ); + console.dir(message); + explorer.configure(message); + } + } + // In the Portal, configuration of Explorer happens via iframe message + window.addEventListener( + "message", + (event) => { + console.dir(event); + if (isInvalidParentFrameOrigin(event)) { + return; + } + + if (!shouldProcessMessage(event)) { + return; + } + + // Check for init message + const message: PortalMessage = event.data?.data; + const inputs = message?.inputs; + if (inputs) { + if ( + configContext.BACKEND_ENDPOINT && + configContext.platform === Platform.Portal && + process.env.NODE_ENV === "development" + ) { + inputs.extensionEndpoint = configContext.PROXY_PATH; + } + + explorer.configure(inputs); + } + }, + false + ); + + sendMessage("ready"); +} + +function shouldProcessMessage(event: MessageEvent): boolean { + if (typeof event.data !== "object") { + return false; + } + if (event.data["signature"] !== "pcIframe") { + return false; + } + if (!("data" in event.data)) { + return false; + } + if (typeof event.data["data"] !== "object") { + return false; + } + + return true; +} + +interface PortalMessage { + openAction?: DataExplorerAction; + actionType?: ActionType; + type?: MessageTypes; + inputs?: DataExplorerInputsFrame; +} diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index fec9c10b2..612ae2f64 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -1,7 +1,5 @@ -import { MessageTypes } from "../../src/Contracts/ExplorerContracts"; import "../../less/hostedexplorer.less"; import { TestExplorerParams } from "./TestExplorerParams"; -import { ClientSecretCredential } from "@azure/identity"; import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models"; import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import * as msRest from "@azure/ms-rest-js"; @@ -19,26 +17,6 @@ class CustomSigner implements msRest.ServiceClientCredentials { } } -const handleMessage = (event: MessageEvent): void => { - if (event.data.type === MessageTypes.InitTestExplorer) { - sendMessageToExplorerFrame(event.data); - } -}; - -const AADLogin = async ( - notebooksTestRunnerApplicationId: string, - notebooksTestRunnerClientId: string, - notebooksTestRunnerClientSecret: string -): Promise => { - const credentials = new ClientSecretCredential( - notebooksTestRunnerApplicationId, - notebooksTestRunnerClientId, - notebooksTestRunnerClientSecret - ); - const token = await credentials.getToken("https://management.core.windows.net/.default"); - return token.token; -}; - const getDatabaseAccount = async ( token: string, notebooksAccountSubscriptonId: string, @@ -49,34 +27,8 @@ const getDatabaseAccount = async ( return client.databaseAccounts.get(notebooksAccountResourceGroup, notebooksAccountName); }; -const sendMessageToExplorerFrame = (data: unknown): void => { - const explorerFrame = document.getElementById("explorerMenu") as HTMLIFrameElement; - - explorerFrame && - explorerFrame.contentDocument && - explorerFrame.contentDocument.referrer && - explorerFrame.contentWindow.postMessage( - { - signature: "pcIframe", - data: data, - }, - explorerFrame.contentDocument.referrer || window.location.href - ); -}; - const initTestExplorer = async (): Promise => { - window.addEventListener("message", handleMessage, false); - const urlSearchParams = new URLSearchParams(window.location.search); - const notebooksTestRunnerTenantId = decodeURIComponent( - urlSearchParams.get(TestExplorerParams.notebooksTestRunnerTenantId) - ); - const notebooksTestRunnerClientId = decodeURIComponent( - urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientId) - ); - const notebooksTestRunnerClientSecret = decodeURIComponent( - urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientSecret) - ); const portalRunnerDatabaseAccount = decodeURIComponent( urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount) ); @@ -89,11 +41,7 @@ const initTestExplorer = async (): Promise => { ); const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType); - const token = await AADLogin( - notebooksTestRunnerTenantId, - notebooksTestRunnerClientId, - notebooksTestRunnerClientSecret - ); + const token = decodeURIComponent(urlSearchParams.get(TestExplorerParams.token)); const databaseAccount = await getDatabaseAccount( token, portalRunnerSubscripton, @@ -102,7 +50,6 @@ const initTestExplorer = async (): Promise => { ); const initTestExplorerContent = { - type: MessageTypes.InitTestExplorer, inputs: { databaseAccount: databaseAccount, subscriptionId: portalRunnerSubscripton, @@ -130,11 +77,35 @@ const initTestExplorer = async (): Promise => { }, // add UI test only when feature is not dependent on flights anymore flights: [], - selfServeType: selfServeType, + selfServeType, } as ViewModels.DataExplorerInputsFrame, }; - window.postMessage(initTestExplorerContent, window.location.href); + const iframe = document.createElement("iframe"); + window.addEventListener( + "message", + (event) => { + // After we have received the "ready" message from the child iframe we can post configuration + // This simulates the same action that happens in the portal + console.dir(event.data); + if (event.data?.data === "ready") { + iframe.contentWindow.postMessage( + { + signature: "pcIframe", + data: initTestExplorerContent, + }, + iframe.contentDocument.referrer || window.location.href + ); + } + }, + false + ); + iframe.id = "explorerMenu"; + iframe.name = "explorer"; + iframe.classList.add("iframe"); + iframe.title = "explorer"; + iframe.src = "explorer.html?platform=Portal&disablePortalInitCache"; + document.body.appendChild(iframe); }; -window.addEventListener("load", initTestExplorer); +initTestExplorer(); diff --git a/test/testExplorer/TestExplorerParams.ts b/test/testExplorer/TestExplorerParams.ts index bcf65b458..43e7c28f1 100644 --- a/test/testExplorer/TestExplorerParams.ts +++ b/test/testExplorer/TestExplorerParams.ts @@ -1,10 +1,8 @@ export enum TestExplorerParams { - notebooksTestRunnerTenantId = "notebooksTestRunnerTenantId", - notebooksTestRunnerClientId = "notebooksTestRunnerClientId", - notebooksTestRunnerClientSecret = "notebooksTestRunnerClientSecret", portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount", portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey", portalRunnerSubscripton = "portalRunnerSubscripton", portalRunnerResourceGroup = "portalRunnerResourceGroup", selfServeType = "selfServeType", + token = "token", } diff --git a/test/testExplorer/TestExplorerUtils.ts b/test/testExplorer/TestExplorerUtils.ts index be3728e6e..f26bc2778 100644 --- a/test/testExplorer/TestExplorerUtils.ts +++ b/test/testExplorer/TestExplorerUtils.ts @@ -1,5 +1,6 @@ import { Frame } from "puppeteer"; import { TestExplorerParams } from "./TestExplorerParams"; +import { ClientSecretCredential } from "@azure/identity"; let testExplorerFrame: Frame; export const getTestExplorerFrame = async (params?: Map): Promise => { @@ -15,19 +16,15 @@ export const getTestExplorerFrame = async (params?: Map): Promis const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION; const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP; + const credentials = new ClientSecretCredential( + notebooksTestRunnerTenantId, + notebooksTestRunnerClientId, + notebooksTestRunnerClientSecret + ); + + const { token } = await credentials.getToken("https://management.core.windows.net/.default"); + const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234"); - testExplorerUrl.searchParams.append( - TestExplorerParams.notebooksTestRunnerTenantId, - encodeURI(notebooksTestRunnerTenantId) - ); - testExplorerUrl.searchParams.append( - TestExplorerParams.notebooksTestRunnerClientId, - encodeURI(notebooksTestRunnerClientId) - ); - testExplorerUrl.searchParams.append( - TestExplorerParams.notebooksTestRunnerClientSecret, - encodeURI(notebooksTestRunnerClientSecret) - ); testExplorerUrl.searchParams.append( TestExplorerParams.portalRunnerDatabaseAccount, encodeURI(portalRunnerDatabaseAccount) @@ -41,6 +38,7 @@ export const getTestExplorerFrame = async (params?: Map): Promis TestExplorerParams.portalRunnerResourceGroup, encodeURI(portalRunnerResourceGroup) ); + testExplorerUrl.searchParams.append(TestExplorerParams.token, encodeURI(token)); if (params) { for (const key of params.keys()) { diff --git a/test/testExplorer/testExplorer.html b/test/testExplorer/testExplorer.html index d9a0c8184..92632d41b 100644 --- a/test/testExplorer/testExplorer.html +++ b/test/testExplorer/testExplorer.html @@ -6,13 +6,5 @@ - - - +