Checkpoint

This commit is contained in:
Steve Faulkner
2021-01-02 19:00:49 -06:00
parent 2e10b96678
commit aba583abd8
6 changed files with 259 additions and 198 deletions

78
src/ConnectExplorer.tsx Normal file
View File

@@ -0,0 +1,78 @@
import * as React from "react";
import { useBoolean } from "@uifabric/react-hooks";
import { HttpHeaders } from "./Common/Constants";
import { GenerateTokenResponse } from "./Contracts/DataModels";
import { configContext } from "./ConfigContext";
interface Props {
login: () => void;
setEncryptedToken: (token: string) => void;
}
export const ConnectExplorer: React.FunctionComponent<Props> = ({ setEncryptedToken, login }: Props) => {
const [connectionString, setConnectionString] = React.useState<string>("");
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
return (
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
<div className="connectExplorerFormContainer">
<div className="connectExplorer">
<p className="connectExplorerContent">
<img src="images/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" />
</p>
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
{isConnectionStringVisible ? (
<form
id="connectWithConnectionString"
onSubmit={async event => {
event.preventDefault();
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
// This API has a quirk where it must be parsed twice
const result: GenerateTokenResponse = JSON.parse(await response.json());
console.log(result.readWrite || result.read);
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
}}
>
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
<p className="connectExplorerContent">
<input
className="inputToken"
type="text"
required
placeholder="Please enter a connection string"
value={connectionString}
onChange={event => {
setConnectionString(event.target.value);
}}
/>
<span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
<img className="errorImg" src="images/error.svg" alt="Error notification" />
<span className="errorDetails"></span>
</span>
</p>
<p className="connectExplorerContent">
<input className="filterbtnstyle" type="submit" value="Connect" />
</p>
<p className="switchConnectTypeText" onClick={login}>
Sign In with Azure Account
</p>
</form>
) : (
<div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
<p className="switchConnectTypeText" onClick={showConnectionString}>
Connect to your account with connection string
</p>
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,50 @@
import { Panel, PanelType, ChoiceGroup } from "office-ui-fabric-react";
import * as React from "react";
import { useDirectories } from "./hooks/useDirectories";
interface Props {
isOpen: boolean;
dismissPanel: () => void;
tenantId: string;
armToken: string;
}
export const DirectoryPickerPanel: React.FunctionComponent<Props> = ({
isOpen,
dismissPanel,
armToken,
tenantId
}: Props) => {
const directories = useDirectories(armToken);
return (
<Panel
type={PanelType.medium}
headerText="Select Directory"
isOpen={isOpen}
onDismiss={dismissPanel}
closeButtonAriaLabel="Close"
>
<ChoiceGroup
options={directories.map(dir => ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))}
selectedKey={tenantId}
onChange={async () => {
dismissPanel();
// TODO!!! This does not work. Still not sure why. Tried lots of stuff.
// const response = await msal.loginPopup({
// authority: `https://login.microsoftonline.com/${option.key}`
// });
// // msal = new Msal.UserAgentApplication({
// // auth: {
// // authority: `https://login.microsoftonline.com/${option.key}`,
// // clientId: "203f1145-856a-4232-83d4-a43568fba23d",
// // redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set in development
// // }
// // });
// setTenantId(option.key);
// setAccount(response.account);
// console.log(account);
}}
/>
</Panel>
);
};

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import { CommandButtonComponent } from "./Explorer/Controls/CommandButton/CommandButtonComponent";
import FeedbackIcon from "../images/Feedback.svg";
export const FeedbackCommandButton: React.FunctionComponent = () => {
return (
<div className="feedbackConnectSettingIcons">
<CommandButtonComponent
id="commandbutton-feedback"
iconSrc={FeedbackIcon}
iconAlt="feeback button"
onCommandClick={() =>
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback")
}
ariaLabel="feeback button"
tooltipText="Send feedback"
hasPopup={true}
disabled={false}
/>
</div>
);
};

View File

@@ -1,34 +1,22 @@
import "./Platform/Hosted/ConnectScreen.less";
import { useBoolean } from "@uifabric/react-hooks"; import { useBoolean } from "@uifabric/react-hooks";
import { import * as Msal from "msal";
DefaultButton, import { initializeIcons } from "office-ui-fabric-react";
DirectionalHint,
FocusZone,
initializeIcons,
Panel,
PanelType,
Persona,
PersonaInitialsColor,
PersonaSize,
ChoiceGroup
} from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { render } from "react-dom"; import { render } from "react-dom";
import FeedbackIcon from "../images/Feedback.svg";
import ChevronRight from "../images/chevron-right.svg"; import ChevronRight from "../images/chevron-right.svg";
import "../less/hostedexplorer.less"; import "../less/hostedexplorer.less";
import { CommandButtonComponent } from "./Explorer/Controls/CommandButton/CommandButtonComponent";
import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { useGraphPhoto } from "./hooks/useGraphPhoto";
import "./Shared/appInsights";
import { AccountSwitchComponent } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponent";
import { usePortalAccessToken } from "./hooks/usePortalAccessToken";
import { useDirectories } from "./hooks/useDirectories";
import * as Msal from "msal";
import { configContext } from "./ConfigContext";
import { HttpHeaders } from "./Common/Constants";
import { GenerateTokenResponse, DatabaseAccount } from "./Contracts/DataModels";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { ConnectExplorer } from "./ConnectExplorer";
import { DatabaseAccount } from "./Contracts/DataModels";
import { DirectoryPickerPanel } from "./DirectoryPickerPanel";
import { AccountSwitchComponent } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponent";
import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { FeedbackCommandButton } from "./FeedbackCommandButton";
import { usePortalAccessToken } from "./hooks/usePortalAccessToken";
import { MeControl } from "./MeControl";
import "./Platform/Hosted/ConnectScreen.less";
import "./Shared/appInsights";
import { SignInButton } from "./SignInButton";
initializeIcons(); initializeIcons();
@@ -58,9 +46,8 @@ const App: React.FunctionComponent = () => {
const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key")); const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key"));
const encryptedTokenMetadata = usePortalAccessToken(encryptedToken); const encryptedTokenMetadata = usePortalAccessToken(encryptedToken);
// Hooks for showing/hiding UI // Hooks for showing/hiding panel
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
// Hooks for AAD authentication // Hooks for AAD authentication
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean( const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(
@@ -70,11 +57,8 @@ const App: React.FunctionComponent = () => {
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId); const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
const [graphToken, setGraphToken] = React.useState<string>(); const [graphToken, setGraphToken] = React.useState<string>();
const [armToken, setArmToken] = React.useState<string>(); const [armToken, setArmToken] = React.useState<string>();
const [connectionString, setConnectionString] = React.useState<string>("");
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>(); const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
const ref = React.useRef<HTMLIFrameElement>();
const login = React.useCallback(async () => { const login = React.useCallback(async () => {
const response = await msal.loginPopup(); const response = await msal.loginPopup();
setLoggedIn(); setLoggedIn();
@@ -89,6 +73,8 @@ const App: React.FunctionComponent = () => {
msal.logout(); msal.logout();
}, []); }, []);
const ref = React.useRef<HTMLIFrameElement>();
React.useEffect(() => { React.useEffect(() => {
if (account && tenantId) { if (account && tenantId) {
Promise.all([ Promise.all([
@@ -120,11 +106,8 @@ const App: React.FunctionComponent = () => {
} }
}, [ref, encryptedToken, encryptedTokenMetadata, isLoggedIn, databaseAccount]); }, [ref, encryptedToken, encryptedTokenMetadata, isLoggedIn, databaseAccount]);
const photo = useGraphPhoto(graphToken);
const directories = useDirectories(armToken);
return ( return (
<div> <>
<header> <header>
<div className="items" role="menubar"> <div className="items" role="menubar">
<div className="cosmosDBTitle"> <div className="cosmosDBTitle">
@@ -151,77 +134,12 @@ const App: React.FunctionComponent = () => {
</span> </span>
)} )}
</div> </div>
<div className="feedbackConnectSettingIcons"> <FeedbackCommandButton />
<CommandButtonComponent
id="commandbutton-feedback"
iconSrc={FeedbackIcon}
iconAlt="feeback button"
onCommandClick={() =>
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback")
}
ariaLabel="feeback button"
tooltipText="Send feedback"
hasPopup={true}
disabled={false}
/>
</div>
<div className="meControl"> <div className="meControl">
{isLoggedIn ? ( {isLoggedIn ? (
<FocusZone> <MeControl {...{ graphToken, openPanel, logout, account }} />
<DefaultButton
id="mecontrolHeader"
className="mecontrolHeaderButton"
menuProps={{
className: "mecontrolContextualMenu",
isBeakVisible: false,
directionalHintFixed: true,
directionalHint: DirectionalHint.bottomRightEdge,
calloutProps: {
minPagePadding: 0
},
items: [
{
key: "SwitchDirectory",
text: "Switch Directory",
onClick: openPanel
},
{
key: "SignOut",
text: "Sign Out",
onClick: logout
}
]
}}
styles={{
rootHovered: { backgroundColor: "#393939" },
rootFocused: { backgroundColor: "#393939" },
rootPressed: { backgroundColor: "#393939" },
rootExpanded: { backgroundColor: "#393939" }
}}
>
<Persona
imageUrl={photo}
text={account?.name}
secondaryText={account?.userName}
showSecondaryText={true}
showInitialsUntilImageLoads={true}
initialsColor={PersonaInitialsColor.teal}
size={PersonaSize.size28}
className="mecontrolHeaderPersona"
/>
</DefaultButton>
</FocusZone>
) : ( ) : (
<DefaultButton <SignInButton {...{ login }} />
className="mecontrolSigninButton"
text="Sign In"
onClick={login}
styles={{
rootHovered: { backgroundColor: "#393939", color: "#fff" },
rootFocused: { backgroundColor: "#393939", color: "#fff" },
rootPressed: { backgroundColor: "#393939", color: "#fff" }
}}
/>
)} )}
</div> </div>
</div> </div>
@@ -242,102 +160,9 @@ const App: React.FunctionComponent = () => {
src="explorer.html?v=1.0.1&platform=Hosted" src="explorer.html?v=1.0.1&platform=Hosted"
></iframe> ></iframe>
)} )}
{!isLoggedIn && !encryptedTokenMetadata && ( {!isLoggedIn && !encryptedTokenMetadata && <ConnectExplorer {...{ login, setEncryptedToken }} />}
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}> <DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId }} />
<div className="connectExplorerFormContainer"> </>
<div className="connectExplorer">
<p className="connectExplorerContent">
<img src="images/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" />
</p>
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
{isConnectionStringVisible ? (
<form
id="connectWithConnectionString"
onSubmit={async event => {
event.preventDefault();
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
// This API has a quirk where it must be parsed twice
const result: GenerateTokenResponse = JSON.parse(await response.json());
console.log(result.readWrite || result.read);
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
}}
>
<p className="connectExplorerContent connectStringText">
Connect to your account with connection string
</p>
<p className="connectExplorerContent">
<input
className="inputToken"
type="text"
required
placeholder="Please enter a connection string"
value={connectionString}
onChange={event => {
setConnectionString(event.target.value);
}}
/>
<span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
<img className="errorImg" src="images/error.svg" alt="Error notification" />
<span className="errorDetails"></span>
</span>
</p>
<p className="connectExplorerContent">
<input className="filterbtnstyle" type="submit" value="Connect" />
</p>
<p className="switchConnectTypeText" onClick={login}>
Sign In with Azure Account
</p>
</form>
) : (
<div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
<p className="switchConnectTypeText" onClick={showConnectionString}>
Connect to your account with connection string
</p>
</div>
)}
</div>
</div>
</div>
)}
<div data-bind="react: firewallWarningComponentAdapter" />
<div data-bind="react: dialogComponentAdapter" />
<Panel
type={PanelType.medium}
headerText="Select Directory"
isOpen={isOpen}
onDismiss={dismissPanel}
closeButtonAriaLabel="Close"
>
<ChoiceGroup
options={directories.map(dir => ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))}
selectedKey={tenantId}
onChange={async () => {
dismissPanel();
// TODO!!! This does not work. Still not sure why. Tried lots of stuff.
// const response = await msal.loginPopup({
// authority: `https://login.microsoftonline.com/${option.key}`
// });
// // msal = new Msal.UserAgentApplication({
// // auth: {
// // authority: `https://login.microsoftonline.com/${option.key}`,
// // clientId: "203f1145-856a-4232-83d4-a43568fba23d",
// // redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set in development
// // }
// // });
// setTenantId(option.key);
// setAccount(response.account);
// console.log(account);
}}
/>
</Panel>
</div>
); );
}; };

68
src/MeControl.tsx Normal file
View File

@@ -0,0 +1,68 @@
import {
FocusZone,
DefaultButton,
DirectionalHint,
Persona,
PersonaInitialsColor,
PersonaSize
} from "office-ui-fabric-react";
import * as React from "react";
import { Account } from "msal";
import { useGraphPhoto } from "./hooks/useGraphPhoto";
interface Props {
graphToken: string;
account: Account;
openPanel: () => void;
logout: () => void;
}
export const MeControl: React.FunctionComponent<Props> = ({ openPanel, logout, account, graphToken }: Props) => {
const photo = useGraphPhoto(graphToken);
return (
<FocusZone>
<DefaultButton
id="mecontrolHeader"
className="mecontrolHeaderButton"
menuProps={{
className: "mecontrolContextualMenu",
isBeakVisible: false,
directionalHintFixed: true,
directionalHint: DirectionalHint.bottomRightEdge,
calloutProps: {
minPagePadding: 0
},
items: [
{
key: "SwitchDirectory",
text: "Switch Directory",
onClick: openPanel
},
{
key: "SignOut",
text: "Sign Out",
onClick: logout
}
]
}}
styles={{
rootHovered: { backgroundColor: "#393939" },
rootFocused: { backgroundColor: "#393939" },
rootPressed: { backgroundColor: "#393939" },
rootExpanded: { backgroundColor: "#393939" }
}}
>
<Persona
imageUrl={photo}
text={account?.name}
secondaryText={account?.userName}
showSecondaryText={true}
showInitialsUntilImageLoads={true}
initialsColor={PersonaInitialsColor.teal}
size={PersonaSize.size28}
className="mecontrolHeaderPersona"
/>
</DefaultButton>
</FocusZone>
);
};

18
src/SignInButton.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { DefaultButton } from "office-ui-fabric-react";
import * as React from "react";
interface Props {
login: () => void;
}
export const SignInButton: React.FunctionComponent<Props> = ({ login }: Props) => {
return <DefaultButton
className="mecontrolSigninButton"
text="Sign In"
onClick={login}
styles={{
rootHovered: { backgroundColor: "#393939", color: "#fff" },
rootFocused: { backgroundColor: "#393939", color: "#fff" },
rootPressed: { backgroundColor: "#393939", color: "#fff" }
}} />;
};