Refactor explorer config into useKnockoutExplorer hook (#397)

Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
This commit is contained in:
Steve Faulkner 2021-01-25 13:56:15 -06:00 committed by GitHub
parent 3529e80f0d
commit b0b973b21a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 368 additions and 343 deletions

View File

@ -4,7 +4,7 @@ export enum Platform {
Emulator = "Emulator", Emulator = "Emulator",
} }
interface ConfigContext { export interface ConfigContext {
platform: Platform; platform: Platform;
allowedParentFrameOrigins: string[]; allowedParentFrameOrigins: string[];
gitSha?: string; gitSha?: string;

View File

@ -33,7 +33,6 @@ export enum MessageTypes {
CreateWorkspace, CreateWorkspace,
CreateSparkPool, CreateSparkPool,
RefreshDatabaseAccount, RefreshDatabaseAccount,
InitTestExplorer,
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@ -1718,58 +1718,7 @@ export default class Explorer {
this._addSynapseLinkDialogProps.valueHasMutated(); this._addSynapseLinkDialogProps.valueHasMutated();
}; };
private _shouldProcessMessage(event: MessageEvent): boolean { public handleMessage(message: any) {
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);
const openAction: ActionContracts.DataExplorerAction = message.openAction; const openAction: ActionContracts.DataExplorerAction = message.openAction;
if (!!openAction) { if (!!openAction) {
if (this.isRefreshingExplorer()) { 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) { if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage. // In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly // This allows webpack hot reload to funciton properly

View File

@ -1,17 +1,18 @@
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels"; import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels";
type HostedConfig = AAD | ConnectionString | EncryptedToken | ResourceToken;
export interface HostedExplorerChildFrame extends Window { export interface HostedExplorerChildFrame extends Window {
hostedConfig: AAD | ConnectionString | EncryptedToken | ResourceToken; hostedConfig: HostedConfig;
} }
interface AAD { export interface AAD {
authType: AuthType.AAD; authType: AuthType.AAD;
databaseAccount: DatabaseAccount; databaseAccount: DatabaseAccount;
authorizationToken: string; authorizationToken: string;
} }
interface ConnectionString { export interface ConnectionString {
authType: AuthType.ConnectionString; authType: AuthType.ConnectionString;
// Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy // Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy
encryptedToken: string; encryptedToken: string;
@ -20,13 +21,13 @@ interface ConnectionString {
masterKey?: string; masterKey?: string;
} }
interface EncryptedToken { export interface EncryptedToken {
authType: AuthType.EncryptedToken; authType: AuthType.EncryptedToken;
encryptedToken: string; encryptedToken: string;
encryptedTokenMetadata: AccessInputMetadata; encryptedTokenMetadata: AccessInputMetadata;
} }
interface ResourceToken { export interface ResourceToken {
authType: AuthType.ResourceToken; authType: AuthType.ResourceToken;
resourceToken: string; resourceToken: string;
} }

View File

@ -53,215 +53,22 @@ import "object.entries/auto";
import "./Libs/is-integer-polyfill"; import "./Libs/is-integer-polyfill";
import "url-polyfill/url-polyfill.min"; import "url-polyfill/url-polyfill.min";
initializeIcons();
import { AuthType } from "./AuthType";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { applyExplorerBindings } from "./applyExplorerBindings"; import React from "react";
import { configContext, initializeConfiguration, Platform } from "./ConfigContext";
import Explorer from "./Explorer/Explorer";
import React, { useEffect } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import copyImage from "../images/Copy.svg"; import copyImage from "../images/Copy.svg";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import refreshImg from "../images/refresh-cosmos.svg"; import refreshImg from "../images/refresh-cosmos.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import { updateUserContext } from "./UserContext"; import { useConfig } from "./hooks/useConfig";
import { CollectionCreation } from "./Shared/Constants"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { extractFeatures } from "./Platform/Hosted/extractFeatures";
import { emulatorAccount } from "./Platform/Emulator/emulatorAccount"; initializeIcons();
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";
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
useEffect(() => { const config = useConfig();
initializeConfiguration().then(async (config) => { useKnockoutExplorer(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);
});
}, []);
return ( return (
<div className="flexContainer"> <div className="flexContainer">

View File

@ -1,5 +1,4 @@
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
import { sendMessage } from "./Common/MessageHandler";
import * as ko from "knockout"; import * as ko from "knockout";
import Explorer from "./Explorer/Explorer"; import Explorer from "./Explorer/Explorer";
@ -10,7 +9,6 @@ export const applyExplorerBindings = (explorer: Explorer) => {
ko.applyBindings(explorer); ko.applyBindings(explorer);
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times. // 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 // TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
sendMessage("ready");
$("#divExplorer").show(); $("#divExplorer").show();
} }
}; };

14
src/hooks/useConfig.ts Normal file
View File

@ -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<ConfigContext> {
const [state, setState] = useState<ConfigContext>();
useEffect(() => {
initializeConfiguration().then((response) => setState(response));
}, []);
return state;
}

View File

@ -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;
}

View File

@ -1,7 +1,5 @@
import { MessageTypes } from "../../src/Contracts/ExplorerContracts";
import "../../less/hostedexplorer.less"; import "../../less/hostedexplorer.less";
import { TestExplorerParams } from "./TestExplorerParams"; import { TestExplorerParams } from "./TestExplorerParams";
import { ClientSecretCredential } from "@azure/identity";
import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models"; import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import * as msRest from "@azure/ms-rest-js"; 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<string> => {
const credentials = new ClientSecretCredential(
notebooksTestRunnerApplicationId,
notebooksTestRunnerClientId,
notebooksTestRunnerClientSecret
);
const token = await credentials.getToken("https://management.core.windows.net/.default");
return token.token;
};
const getDatabaseAccount = async ( const getDatabaseAccount = async (
token: string, token: string,
notebooksAccountSubscriptonId: string, notebooksAccountSubscriptonId: string,
@ -49,34 +27,8 @@ const getDatabaseAccount = async (
return client.databaseAccounts.get(notebooksAccountResourceGroup, notebooksAccountName); 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<void> => { const initTestExplorer = async (): Promise<void> => {
window.addEventListener("message", handleMessage, false);
const urlSearchParams = new URLSearchParams(window.location.search); 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( const portalRunnerDatabaseAccount = decodeURIComponent(
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount) urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount)
); );
@ -89,11 +41,7 @@ const initTestExplorer = async (): Promise<void> => {
); );
const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType); const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType);
const token = await AADLogin( const token = decodeURIComponent(urlSearchParams.get(TestExplorerParams.token));
notebooksTestRunnerTenantId,
notebooksTestRunnerClientId,
notebooksTestRunnerClientSecret
);
const databaseAccount = await getDatabaseAccount( const databaseAccount = await getDatabaseAccount(
token, token,
portalRunnerSubscripton, portalRunnerSubscripton,
@ -102,7 +50,6 @@ const initTestExplorer = async (): Promise<void> => {
); );
const initTestExplorerContent = { const initTestExplorerContent = {
type: MessageTypes.InitTestExplorer,
inputs: { inputs: {
databaseAccount: databaseAccount, databaseAccount: databaseAccount,
subscriptionId: portalRunnerSubscripton, subscriptionId: portalRunnerSubscripton,
@ -130,11 +77,35 @@ const initTestExplorer = async (): Promise<void> => {
}, },
// add UI test only when feature is not dependent on flights anymore // add UI test only when feature is not dependent on flights anymore
flights: [], flights: [],
selfServeType: selfServeType, selfServeType,
} as ViewModels.DataExplorerInputsFrame, } 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();

View File

@ -1,10 +1,8 @@
export enum TestExplorerParams { export enum TestExplorerParams {
notebooksTestRunnerTenantId = "notebooksTestRunnerTenantId",
notebooksTestRunnerClientId = "notebooksTestRunnerClientId",
notebooksTestRunnerClientSecret = "notebooksTestRunnerClientSecret",
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount", portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey", portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
portalRunnerSubscripton = "portalRunnerSubscripton", portalRunnerSubscripton = "portalRunnerSubscripton",
portalRunnerResourceGroup = "portalRunnerResourceGroup", portalRunnerResourceGroup = "portalRunnerResourceGroup",
selfServeType = "selfServeType", selfServeType = "selfServeType",
token = "token",
} }

View File

@ -1,5 +1,6 @@
import { Frame } from "puppeteer"; import { Frame } from "puppeteer";
import { TestExplorerParams } from "./TestExplorerParams"; import { TestExplorerParams } from "./TestExplorerParams";
import { ClientSecretCredential } from "@azure/identity";
let testExplorerFrame: Frame; let testExplorerFrame: Frame;
export const getTestExplorerFrame = async (params?: Map<string, string>): Promise<Frame> => { export const getTestExplorerFrame = async (params?: Map<string, string>): Promise<Frame> => {
@ -15,19 +16,15 @@ export const getTestExplorerFrame = async (params?: Map<string, string>): Promis
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION; const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP; 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"); 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( testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccount, TestExplorerParams.portalRunnerDatabaseAccount,
encodeURI(portalRunnerDatabaseAccount) encodeURI(portalRunnerDatabaseAccount)
@ -41,6 +38,7 @@ export const getTestExplorerFrame = async (params?: Map<string, string>): Promis
TestExplorerParams.portalRunnerResourceGroup, TestExplorerParams.portalRunnerResourceGroup,
encodeURI(portalRunnerResourceGroup) encodeURI(portalRunnerResourceGroup)
); );
testExplorerUrl.searchParams.append(TestExplorerParams.token, encodeURI(token));
if (params) { if (params) {
for (const key of params.keys()) { for (const key of params.keys()) {

View File

@ -6,13 +6,5 @@
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" /> <link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
</head> </head>
<body> <body></body>
<iframe
id="explorerMenu"
name="explorer"
class="iframe"
title="explorer"
src="explorer.html?v=1.0.1&platform=Portal"
></iframe>
</body>
</html> </html>