mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 09:20:16 +00:00
Refactor explorer config into useKnockoutExplorer hook (#397)
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
This commit is contained in:
14
src/hooks/useConfig.ts
Normal file
14
src/hooks/useConfig.ts
Normal 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;
|
||||
}
|
||||
298
src/hooks/useKnockoutExplorer.ts
Normal file
298
src/hooks/useKnockoutExplorer.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user