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(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(); 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(); const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); const [connectionString, setConnectionString] = React.useState(); const ref = React.useRef(); 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 ( <>
window.open("https://portal.azure.com", "_blank")} tabIndex={0} title="Go to Azure Portal" > Microsoft Azure Cosmos DB {(isLoggedIn || accountMetadata?.accountName) && ( account separator )} {isLoggedIn && !connectionString && ( )} {(!isLoggedIn || connectionString) && accountMetadata?.accountName && ( {accountMetadata?.accountName} )}
{isLoggedIn ? ( ) : ( )}
{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. )} {!isLoggedIn && !accountMetadata && ( )} {isLoggedIn && authFailure && } {isLoggedIn && !authFailure && ( )} ); }; export { App }; const appElement = document.getElementById("App"); if (appElement) { render(, appElement); }