Files
cosmos-explorer/src/HostedExplorer.tsx
T
Asier Isayas 0f0e58740d Refer to the Table API as Table in comments
The comments added by this branch alternated between Table and Tables when naming the API alongside SQL and Gremlin. TablesDB is left alone since that is the literal database name, as is the plural noun where it refers to actual tables.
2026-08-19 16:05:28 -07:00

249 lines
9.7 KiB
TypeScript

import { initializeIcons } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { getErrorMessage } from "Common/ErrorHandlingUtils";
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
import * as React from "react";
import { render } from "react-dom";
import ChevronRight from "../images/chevron-right.svg";
import "../less/hostedexplorer.less";
import { AuthType } from "./AuthType";
import { logError } from "./Common/Logger";
import { fetchAccessData, fetchEncryptedToken } from "./Common/PortalBackendClient";
import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels";
import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton";
import { MeControl } from "./Platform/Hosted/Components/MeControl";
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import "./Platform/Hosted/ConnectScreen.less";
import { parseConnectionString } from "./Platform/Hosted/Helpers/ConnectionStringParser";
import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
import {
extractMasterKeyFromDirectLoginConnectionString,
isDirectConnectionStringLoginApi,
} from "./Platform/Hosted/HostedUtils";
import "./Shared/appInsights";
import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils";
import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig";
initializeIcons();
const App: React.FunctionComponent = () => {
// For handling encrypted portal tokens sent via query paramter
const params = new URLSearchParams(window.location.search);
const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key"));
// Encrypted token logins resolve the account metadata through the Portal Backend, while SQL/Table/Gremlin
// connection-string logins derive it client-side, so both paths write to the same value.
const [accountMetadata, setAccountMetadata] = React.useState<AccessInputMetadata>();
React.useEffect(() => {
if (encryptedToken) {
fetchAccessData(encryptedToken).then(setAccountMetadata);
}
}, [encryptedToken]);
// For showing/hiding panel
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const config = useConfig();
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
useAADAuth(config);
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
const [connectionString, setConnectionString] = React.useState<string>();
const ref = React.useRef<HTMLIFrameElement>();
const connectWithConnectionString = React.useCallback(
(connStr: string) => {
if (!connStr || authType) {
return;
}
setConnectionString(connStr);
if (isResourceTokenConnectionString(connStr)) {
setAuthType(AuthType.ResourceToken);
return;
}
const metadata = parseConnectionString(connStr);
if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) {
// SQL, Table, and Gremlin sign data-plane requests client-side with the account key, so we skip
// the Portal Backend proxy and use the metadata derived from the connection string directly.
setAccountMetadata(metadata);
setAuthType(AuthType.ConnectionString);
return;
}
fetchEncryptedToken(connStr)
.then((token) => {
setEncryptedToken(token);
setAuthType(AuthType.ConnectionString);
})
.catch((error) => {
logError(
`Failed to connect with connection string: ${getErrorMessage(error)}`,
"HostedExplorer/connectWithConnectionString",
);
});
},
[authType],
);
// Listen for connection string sent via postMessage (from TryCosmosDB)
React.useEffect(() => {
const MSG_READY = "tryCosmosDBReady";
const MSG_CONNECTION_STRING = "tryCosmosDBConnectionString";
// Signal to the opener that we are ready to receive a connection string
if (window.opener) {
try {
for (const origin of allowedHostedExplorerEndpoints) {
window.opener.postMessage({ type: MSG_READY }, origin);
}
} catch {
// opener may be cross-origin, ignore
}
}
const handler = (event: MessageEvent) => {
if (!allowedHostedExplorerEndpoints.includes(event.origin)) {
return;
}
if (event.data?.type === MSG_CONNECTION_STRING) {
const connStr: string = event.data.connectionString;
if (parseConnectionString(connStr)) {
connectWithConnectionString(connStr);
}
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [connectWithConnectionString]);
React.useEffect(() => {
// If ref.current is undefined no iframe has been rendered
if (ref.current) {
// In hosted mode, we can set global properties directly on the child iframe.
// This is not possible in the portal where the iframes have different origins
const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame;
if (authType === AuthType.EncryptedToken) {
frameWindow.hostedConfig = {
authType: AuthType.EncryptedToken,
encryptedToken,
encryptedTokenMetadata: accountMetadata,
};
} else if (authType === AuthType.ConnectionString) {
frameWindow.hostedConfig = {
authType: AuthType.ConnectionString,
encryptedToken,
encryptedTokenMetadata: accountMetadata,
masterKey: extractMasterKeyFromDirectLoginConnectionString(connectionString),
};
} else if (authType === AuthType.ResourceToken) {
frameWindow.hostedConfig = {
authType: AuthType.ResourceToken,
resourceToken: connectionString,
};
} else if (isLoggedIn && !connectionString) {
frameWindow.hostedConfig = {
authType: AuthType.AAD,
databaseAccount,
authorizationToken: armToken,
};
}
}
});
const showExplorer =
(config && isLoggedIn && databaseAccount && !connectionString) ||
accountMetadata ||
(authType === AuthType.ResourceToken && connectionString);
return (
<>
<header>
<div className="items" role="menubar">
<div className="cosmosDBTitle">
<span
className="title"
onClick={() => window.open("https://portal.azure.com", "_blank")}
tabIndex={0}
title="Go to Azure Portal"
>
Microsoft Azure
</span>
<span className="accontSplitter" /> <span className="serviceTitle">Cosmos DB</span>
{(isLoggedIn || accountMetadata?.accountName) && (
<img className="chevronRight" src={ChevronRight} alt="account separator" />
)}
{isLoggedIn && !connectionString && (
<span className="accountSwitchComponentContainer">
<AccountSwitcher armToken={armToken} setDatabaseAccount={setDatabaseAccount} />
</span>
)}
{(!isLoggedIn || connectionString) && accountMetadata?.accountName && (
<span className="accountSwitchComponentContainer">
<span className="accountNameHeader">{accountMetadata?.accountName}</span>
</span>
)}
</div>
<FeedbackCommandButton />
<div className="meControl">
{isLoggedIn ? (
<MeControl {...{ graphToken, openPanel, logout, account }} />
) : (
<SignInButton {...{ login }} />
)}
</div>
</div>
</header>
{showExplorer && (
// Ideally we would import and render data explorer like any other React component, however
// because it still has a significant amount of Knockout code, this would lead to memory leaks.
// Knockout does not have a way to tear down all of its binding and listeners with a single method.
// It's possible this can be changed once all knockout code has been removed.
<iframe
// Setting key is needed so React will re-render this element on any account change
key={authType ? `${authType}-${accountMetadata?.accountName || connectionString}` : databaseAccount?.id}
ref={ref}
data-test="DataExplorerFrame"
id="explorerMenu"
name="explorer"
className="iframe"
title="explorer"
src="explorer.html?v=1.0.1&platform=Hosted"
></iframe>
)}
{!isLoggedIn && !accountMetadata && (
<ConnectExplorer
{...{
login,
setEncryptedToken,
setAuthType,
connectionString,
setConnectionString,
setAccountMetadata,
}}
/>
)}
{isLoggedIn && authFailure && <AadAuthorizationFailure {...{ authFailure }} />}
{isLoggedIn && !authFailure && (
<DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />
)}
</>
);
};
export { App };
const appElement = document.getElementById("App");
if (appElement) {
render(<App />, appElement);
}