Compare commits

...

3 Commits

Author SHA1 Message Date
Ashley Stanton-Nurse
48950613a7 some wordsmithing 2024-06-07 13:53:21 -07:00
Ashley Stanton-Nurse
2bd913acbb when opening hosted explorer from portal, jump to selected sub/account 2024-06-07 12:05:46 -07:00
Ashley Stanton-Nurse
4866d3c902 stuck in a redirect loop, need to work that out 2024-06-07 11:03:20 -07:00
10 changed files with 93 additions and 20 deletions

View File

@@ -238,6 +238,15 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
updateConfigContext({ platform }); updateConfigContext({ platform });
} }
} }
if (window.location.origin !== configContext.hostedExplorerURL) {
if (window.location.origin === "https://localhost:1234") {
// Special case for localhost, we need to send them to 'hostedExplorer.html'.
updateConfigContext({ hostedExplorerURL: "https://localhost:1234/hostedExplorer.html" });
} else {
const newOrigin = window.location.origin.endsWith("/") ? window.location.origin : `${window.location.origin}/`;
updateConfigContext({ hostedExplorerURL: newOrigin });
}
}
} catch (error) { } catch (error) {
console.error("No configuration file found using defaults"); console.error("No configuration file found using defaults");
} }
@@ -245,3 +254,4 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
} }
export { configContext }; export { configContext };

View File

@@ -1,20 +1,43 @@
import { PrimaryButton, Stack, Text } from "@fluentui/react"; import { PrimaryButton, Stack, Text } from "@fluentui/react";
import { AuthType } from "AuthType";
import { configContext } from "ConfigContext";
import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
export const OpenFullScreen: React.FunctionComponent = () => { export const OpenFullScreen: React.FunctionComponent = () => {
const searchParams = new URLSearchParams();
searchParams.append("openFrom", "portal");
let hasAccountContext = false;
let requiresConnectionString = false;
if (userContext.authType === AuthType.AAD) {
if (userContext.subscriptionId && userContext.databaseAccount) {
searchParams.append("subscription", userContext.subscriptionId);
searchParams.append("account", userContext.databaseAccount.name);
searchParams.append("authType", "entra");
hasAccountContext = true;
}
} else if (userContext.authType === AuthType.MasterKey || userContext.authType === AuthType.ResourceToken) {
requiresConnectionString = true;
}
return ( return (
<> <>
<div style={{ padding: "34px" }}> <div style={{ padding: "34px" }}>
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
<Text> <Text>
Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your Open this database account in a new browser tab with Cosmos DB Explorer.
Microsoft account or a connection string. {requiresConnectionString && " You'll need to provide a connection string."}
{hasAccountContext && " You may be prompted to sign in with Entra ID again."}
</Text>
<Text>
Open tabs and queries will not be carried over, but you can still access them in this tab.
</Text> </Text>
<Stack horizontal tokens={{ childrenGap: 10 }}> <Stack horizontal tokens={{ childrenGap: 10 }}>
<PrimaryButton <PrimaryButton
onClick={() => { href={`${configContext.hostedExplorerURL}?${searchParams.toString()}`}
window.open("https://cosmos.azure.com/", "_blank"); target="_blank"
}}
text="Open" text="Open"
iconProps={{ iconName: "OpenInNewWindow" }} iconProps={{ iconName: "OpenInNewWindow" }}
/> />

View File

@@ -33,7 +33,7 @@ const App: React.FunctionComponent = () => {
// For showing/hiding panel // For showing/hiding panel
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const config = useConfig(); const config = useConfig();
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } = const { isLoggedIn, armToken, graphToken, account, tenantId, logout, forgetUser, login, switchTenant, authFailure } =
useAADAuth(); useAADAuth();
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>(); const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined); const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);

View File

@@ -9,7 +9,6 @@ import "../less/index.less";
const Index = (): JSX.Element => { const Index = (): JSX.Element => {
const [navigationSelection, setNavigationSelection] = useState("quickstart"); const [navigationSelection, setNavigationSelection] = useState("quickstart");
const quickstart_click = () => { const quickstart_click = () => {
setNavigationSelection("quickstart"); setNavigationSelection("quickstart");
}; };

View File

@@ -2,6 +2,7 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { DefaultButton, IButtonStyles, IContextualMenuItem } from "@fluentui/react"; import { DefaultButton, IButtonStyles, IContextualMenuItem } from "@fluentui/react";
import { urlContext } from "Utils/UrlContext";
import * as React from "react"; import * as React from "react";
import { FunctionComponent, useEffect, useState } from "react"; import { FunctionComponent, useEffect, useState } from "react";
import { StyleConstants } from "../../../Common/StyleConstants"; import { StyleConstants } from "../../../Common/StyleConstants";
@@ -50,12 +51,12 @@ interface Props {
export const AccountSwitcher: FunctionComponent<Props> = ({ armToken, setDatabaseAccount }: Props) => { export const AccountSwitcher: FunctionComponent<Props> = ({ armToken, setDatabaseAccount }: Props) => {
const subscriptions = useSubscriptions(armToken); const subscriptions = useSubscriptions(armToken);
const [selectedSubscriptionId, setSelectedSubscriptionId] = useState<string>(() => const [selectedSubscriptionId, setSelectedSubscriptionId] = useState<string>(() =>
localStorage.getItem("cachedSubscriptionId"), urlContext.subscription || localStorage.getItem("cachedSubscriptionId"),
); );
const selectedSubscription = subscriptions?.find((sub) => sub.subscriptionId === selectedSubscriptionId); const selectedSubscription = subscriptions?.find((sub) => sub.subscriptionId === selectedSubscriptionId);
const accounts = useDatabaseAccounts(selectedSubscription?.subscriptionId, armToken); const accounts = useDatabaseAccounts(selectedSubscription?.subscriptionId, armToken);
const [selectedAccountName, setSelectedAccountName] = useState<string>(() => const [selectedAccountName, setSelectedAccountName] = useState<string>(() =>
localStorage.getItem("cachedDatabaseAccountName"), urlContext.account || localStorage.getItem("cachedDatabaseAccountName"),
); );
const selectedAccount = accounts?.find((account) => account.name === selectedAccountName); const selectedAccount = accounts?.find((account) => account.name === selectedAccountName);

View File

@@ -1,6 +1,7 @@
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { OpenedFrom, RequestedAuthType, urlContext } from "Utils/UrlContext";
import * as React from "react"; import * as React from "react";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import ErrorImage from "../../../../images/error.svg"; import ErrorImage from "../../../../images/error.svg";
@@ -69,7 +70,9 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
}: Props) => { }: Props) => {
const [isFormVisible, { setTrue: showForm }] = useBoolean(false); const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
const [errorMessage, setErrorMessage] = React.useState(""); const [errorMessage, setErrorMessage] = React.useState("");
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin; const enableConnectionStringLogin =
urlContext.authType !== RequestedAuthType.Entra &&
!userContext.features.disableConnectionStringLogin;
return ( return (
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}> <div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
@@ -137,6 +140,11 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
Connect to your account with connection string Connect to your account with connection string
</p> </p>
)} )}
{urlContext.openFrom === OpenedFrom.Portal && (
<p className="switchConnectTypeText">
Click sign in to continue your session from the Azure Portal.
</p>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -192,3 +192,4 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
} }
export { updateUserContext, userContext }; export { updateUserContext, userContext };

View File

@@ -128,7 +128,7 @@ export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.micr
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"]; export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"]; export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/", "https://localhost:1234/"];
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [ export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
"https://cosmos-explorer-preview.azurewebsites.net/", "https://cosmos-explorer-preview.azurewebsites.net/",

17
src/Utils/UrlContext.ts Normal file
View File

@@ -0,0 +1,17 @@
const urlParams = new URLSearchParams(window.location.search);
export enum OpenedFrom {
Portal = "portal",
}
export enum RequestedAuthType {
Entra = "entra",
ConnectionString = "connectionstring",
}
export const urlContext = {
openFrom: urlParams.get("openFrom") as OpenedFrom,
authType: urlParams.get("authType") as RequestedAuthType,
subscription: urlParams.get("subscription"),
account: urlParams.get("account"),
};

View File

@@ -38,6 +38,7 @@ export function useAADAuth(): ReturnType {
const [authFailure, setAuthFailure] = React.useState<AadAuthFailure>(undefined); const [authFailure, setAuthFailure] = React.useState<AadAuthFailure>(undefined);
msalInstance.setActiveAccount(account); msalInstance.setActiveAccount(account);
const login = React.useCallback(async () => { const login = React.useCallback(async () => {
const response = await msalInstance.loginPopup({ const response = await msalInstance.loginPopup({
redirectUri: configContext.msalRedirectURI, redirectUri: configContext.msalRedirectURI,
@@ -47,13 +48,13 @@ export function useAADAuth(): ReturnType {
setAccount(response.account); setAccount(response.account);
setTenantId(response.tenantId); setTenantId(response.tenantId);
localStorage.setItem("cachedTenantId", response.tenantId); localStorage.setItem("cachedTenantId", response.tenantId);
}, []); }, [setLoggedIn, setAccount, setTenantId]);
const logout = React.useCallback(() => { const logout = React.useCallback(() => {
setLoggedOut(); setLoggedOut();
localStorage.removeItem("cachedTenantId"); localStorage.removeItem("cachedTenantId");
msalInstance.logoutRedirect(); msalInstance.logoutRedirect();
}, []); }, [setLoggedOut]);
const switchTenant = React.useCallback( const switchTenant = React.useCallback(
async (id) => { async (id) => {
@@ -66,7 +67,7 @@ export function useAADAuth(): ReturnType {
setAccount(response.account); setAccount(response.account);
localStorage.setItem("cachedTenantId", response.tenantId); localStorage.setItem("cachedTenantId", response.tenantId);
}, },
[account, tenantId], [setTenantId, setAccount],
); );
const acquireTokens = React.useCallback(async () => { const acquireTokens = React.useCallback(async () => {
@@ -123,6 +124,19 @@ export function useAADAuth(): ReturnType {
} }
}, [account, tenantId, acquireTokens, authFailure]); }, [account, tenantId, acquireTokens, authFailure]);
React.useEffect(() => {
(async () => {
// If we're on a redirect, handle it
const response = await msalInstance.handleRedirectPromise();
if (response) {
setLoggedIn();
setAccount(response.account);
setTenantId(response.tenantId);
localStorage.setItem("cachedTenantId", response.tenantId);
}
})()
}, [setLoggedIn])
return { return {
account, account,
tenantId, tenantId,