Refactor explorer config into useKnockoutExplorer hook (#397)
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
This commit is contained in:
parent
3529e80f0d
commit
b0b973b21a
|
@ -4,7 +4,7 @@ export enum Platform {
|
|||
Emulator = "Emulator",
|
||||
}
|
||||
|
||||
interface ConfigContext {
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedParentFrameOrigins: string[];
|
||||
gitSha?: string;
|
||||
|
|
|
@ -33,7 +33,6 @@ export enum MessageTypes {
|
|||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
InitTestExplorer,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
207
src/Main.tsx
207
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 (
|
||||
<div className="flexContainer">
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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<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 (
|
||||
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<void> => {
|
||||
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<void> => {
|
|||
);
|
||||
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<void> => {
|
|||
);
|
||||
|
||||
const initTestExplorerContent = {
|
||||
type: MessageTypes.InitTestExplorer,
|
||||
inputs: {
|
||||
databaseAccount: databaseAccount,
|
||||
subscriptionId: portalRunnerSubscripton,
|
||||
|
@ -130,11 +77,35 @@ const initTestExplorer = async (): Promise<void> => {
|
|||
},
|
||||
// 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();
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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<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 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<string, string>): Promis
|
|||
TestExplorerParams.portalRunnerResourceGroup,
|
||||
encodeURI(portalRunnerResourceGroup)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(TestExplorerParams.token, encodeURI(token));
|
||||
|
||||
if (params) {
|
||||
for (const key of params.keys()) {
|
||||
|
|
|
@ -6,13 +6,5 @@
|
|||
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<iframe
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
class="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Portal"
|
||||
></iframe>
|
||||
</body>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue