Checkpoint

This commit is contained in:
Steve Faulkner
2021-01-01 00:09:38 -06:00
parent bf30c3190a
commit 15cb4a8fc4
16 changed files with 155 additions and 619 deletions

View File

@@ -1,159 +0,0 @@
import React from "react";
import { shallow, mount } from "enzyme";
import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels";
import { AccountKind } from "../../../Common/Constants";
const createBlankProps = (): AccountSwitchComponentProps => {
return {
authType: null,
displayText: "",
accounts: [],
selectedAccountName: null,
isLoadingAccounts: false,
onAccountChange: jest.fn(),
subscriptions: [],
selectedSubscriptionId: null,
isLoadingSubscriptions: false,
onSubscriptionChange: jest.fn()
};
};
const createBlankAccount = (): DatabaseAccount => {
return {
id: "",
kind: AccountKind.Default,
name: "",
properties: null,
location: "",
tags: null,
type: ""
};
};
const createBlankSubscription = (): Subscription => {
return {
subscriptionId: "",
displayName: "",
authorizationSource: "",
state: "",
subscriptionPolicies: null,
tenantId: "",
uniqueDisplayName: ""
};
};
const createFullProps = (): AccountSwitchComponentProps => {
const props = createBlankProps();
props.authType = AuthType.AAD;
const account1 = createBlankAccount();
account1.name = "account1";
const account2 = createBlankAccount();
account2.name = "account2";
const account3 = createBlankAccount();
account3.name = "superlongaccountnamestringtest";
props.accounts = [account1, account2, account3];
props.selectedAccountName = "account2";
const sub1 = createBlankSubscription();
sub1.displayName = "sub1";
sub1.subscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297";
const sub2 = createBlankSubscription();
sub2.displayName = "subsubsubsubsubsubsub2";
sub2.subscriptionId = "b20b3e93-0185-4326-8a9c-d44bac276b6b";
props.subscriptions = [sub1, sub2];
props.selectedSubscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297";
return props;
};
describe("test render", () => {
it("renders no auth type -> handle error in code", () => {
const props = createBlankProps();
const wrapper = shallow(<AccountSwitchComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
// Encrypted Token
it("renders auth security token, with selected account name", () => {
const props = createBlankProps();
props.authType = AuthType.EncryptedToken;
props.selectedAccountName = "testaccount";
const wrapper = shallow(<AccountSwitchComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
// AAD
it("renders auth aad, with all information", () => {
const props = createFullProps();
const wrapper = shallow(<AccountSwitchComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders auth aad all dropdown menus", () => {
const props = createFullProps();
const wrapper = mount(<AccountSwitchComponent {...props} />);
expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false);
wrapper.find("button.accountSwitchButton").simulate("click");
expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(true);
expect(wrapper.exists("div.accountSwitchSubscriptionDropdown")).toBe(true);
wrapper.find("DropdownBase.accountSwitchSubscriptionDropdown").simulate("click");
// Click will dismiss the first contextual menu in enzyme. Need to dig deeper to achieve below test
// expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(true);
// expect(wrapper.find("button.ms-Dropdown-item").length).toBe(2);
// wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click");
// expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(false);
// expect(wrapper.exists("div.accountSwitchAccountDropdown")).toBe(true);
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
// expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(true);
// expect(wrapper.find("button.ms-Dropdown-item").length).toBe(3);
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
// expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(false);
// wrapper.find("button.accountSwitchButton").simulate("click");
// expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false);
wrapper.unmount();
});
});
// describe("test function", () => {
// it("switch subscription function", () => {
// const props = createFullProps();
// const wrapper = mount(<AccountSwitchComponent {...props} />);
// wrapper.find("button.accountSwitchButton").simulate("click");
// wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click");
// wrapper
// .find("button.ms-Dropdown-item")
// .at(1)
// .simulate("click");
// expect(props.onSubscriptionChange).toBeCalled();
// expect(props.onSubscriptionChange).toHaveBeenCalled();
// wrapper.unmount();
// });
// it("switch account", () => {
// const props = createFullProps();
// const wrapper = mount(<AccountSwitchComponent {...props} />);
// wrapper.find("button.accountSwitchButton").simulate("click");
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
// wrapper
// .find("button.ms-Dropdown-item")
// .at(0)
// .simulate("click");
// expect(props.onAccountChange).toBeCalled();
// expect(props.onAccountChange).toHaveBeenCalled();
// wrapper.unmount();
// });
// });

View File

@@ -1,5 +1,4 @@
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import * as React from "react"; import * as React from "react";
import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button"; import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"; import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
@@ -38,10 +37,10 @@ const buttonStyles: IButtonStyles = {
} }
}; };
export const AccountSwitchComponent: React.FunctionComponent = () => { export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }> = ({ armToken }) => {
const subscriptions = useSubscriptions(); const subscriptions = useSubscriptions(armToken);
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>(); const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>();
const accounts = useDatabaseAccounts(selectedSubscriptionId); const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>(); const [selectedAccountName, setSelectedAccoutName] = React.useState<string>();
const menuProps: IContextualMenuProps = { const menuProps: IContextualMenuProps = {

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent";
export class AccountSwitchComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<AccountSwitchComponentProps>;
public renderComponent(): JSX.Element {
return <AccountSwitchComponent {...this.parameters()} />;
}
}

View File

@@ -1,71 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test render renders auth aad, with all information 1`] = `
<CustomizedDefaultButton
className="accountSwitchButton"
id="accountSwitchButton"
menuProps={
Object {
"className": "accountSwitchContextualMenu",
"directionalHintFixed": true,
"items": Array [
Object {
"key": "switchSubscription",
"onRender": [Function],
},
Object {
"key": "switchAccount",
"onRender": [Function],
},
],
}
}
styles={
Object {
"root": Object {
"backgroundColor": undefined,
"color": undefined,
"fontSize": undefined,
"height": 40,
"marginRight": 5,
"padding": 0,
"paddingLeft": 10,
},
"rootExpanded": Object {
"backgroundColor": undefined,
"color": undefined,
},
"rootFocused": Object {
"backgroundColor": undefined,
"color": undefined,
},
"rootHovered": Object {
"backgroundColor": undefined,
"color": undefined,
},
"rootPressed": Object {
"backgroundColor": undefined,
"color": undefined,
},
"textContainer": Object {
"flexGrow": "initial",
},
}
}
text="account2"
/>
`;
exports[`test render renders auth security token, with selected account name 1`] = `
<span
className="accountNameHeader"
>
testaccount
</span>
`;
exports[`test render renders no auth type -> handle error in code 1`] = `
<span
className="accountNameHeader"
/>
`;

View File

@@ -2,7 +2,7 @@ import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { ConnectionStringParser } from "../../Platform/Hosted/Helpers/ConnectionStringParser"; import { parseConnectionString } from "../../Platform/Hosted/Helpers/ConnectionStringParser";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
@@ -48,9 +48,7 @@ export class RenewAdHocAccessPane extends ContextualPaneBase {
}; };
private _shouldShowContextSwitchPrompt(): boolean { private _shouldShowContextSwitchPrompt(): boolean {
const inputMetadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( const inputMetadata: DataModels.AccessInputMetadata = parseConnectionString(this.accessKey());
this.accessKey()
);
const apiKind: DataModels.ApiKind = const apiKind: DataModels.ApiKind =
this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()); this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience());
const hasOpenedTabs: boolean = const hasOpenedTabs: boolean =

View File

@@ -2,8 +2,6 @@ import "./Shared/appInsights";
import * as _ from "underscore"; import * as _ from "underscore";
import * as ko from "knockout"; import * as ko from "knockout";
import hasher from "hasher"; import hasher from "hasher";
import { AccountSwitchComponentProps } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponent";
import { AccountSwitchComponentAdapter } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter";
import { Action } from "./Shared/Telemetry/TelemetryConstants"; import { Action } from "./Shared/Telemetry/TelemetryConstants";
import { ArmResourceUtils } from "./Platform/Hosted/ArmResourceUtils"; import { ArmResourceUtils } from "./Platform/Hosted/ArmResourceUtils";
import AuthHeadersUtil from "./Platform/Hosted/Authorization"; import AuthHeadersUtil from "./Platform/Hosted/Authorization";
@@ -50,13 +48,11 @@ class HostedExplorer {
public firewallWarningComponentAdapter: DialogComponentAdapter; public firewallWarningComponentAdapter: DialogComponentAdapter;
public dialogComponentAdapter: DialogComponentAdapter; public dialogComponentAdapter: DialogComponentAdapter;
public meControlComponentAdapter: MeControlComponentAdapter; public meControlComponentAdapter: MeControlComponentAdapter;
public accountSwitchComponentAdapter: AccountSwitchComponentAdapter;
public switchDirectoryPane: SwitchDirectoryPane; public switchDirectoryPane: SwitchDirectoryPane;
private _firewallWarningDialogProps: ko.Observable<DialogProps>; private _firewallWarningDialogProps: ko.Observable<DialogProps>;
private _dialogProps: ko.Observable<DialogProps>; private _dialogProps: ko.Observable<DialogProps>;
private _meControlProps: ko.Observable<MeControlComponentProps>; private _meControlProps: ko.Observable<MeControlComponentProps>;
private _accountSwitchProps: ko.Observable<AccountSwitchComponentProps>;
private _controlbarCommands: ko.ObservableArray<CommandButtonComponentProps>; private _controlbarCommands: ko.ObservableArray<CommandButtonComponentProps>;
private _directoryDropdownProps: ko.Observable<DefaultDirectoryDropdownProps>; private _directoryDropdownProps: ko.Observable<DefaultDirectoryDropdownProps>;
private _directoryListProps: ko.Observable<DirectoryListProps>; private _directoryListProps: ko.Observable<DirectoryListProps>;
@@ -161,35 +157,10 @@ class HostedExplorer {
this.meControlComponentAdapter = new MeControlComponentAdapter(); this.meControlComponentAdapter = new MeControlComponentAdapter();
this.meControlComponentAdapter.parameters = this._meControlProps; this.meControlComponentAdapter.parameters = this._meControlProps;
this._accountSwitchProps = ko.observable<AccountSwitchComponentProps>({
authType: AuthType.EncryptedToken,
selectedAccountName: "",
accounts: [],
isLoadingAccounts: false,
onAccountChange: this._onAccountChange,
selectedSubscriptionId: undefined,
subscriptions: [],
isLoadingSubscriptions: false,
onSubscriptionChange: this._onSubscriptionChange
});
this.accountSwitchComponentAdapter = new AccountSwitchComponentAdapter();
this.accountSwitchComponentAdapter.parameters = this._accountSwitchProps;
this.isAccountActive = ko.computed<boolean>(() => {
if (
this._accountSwitchProps() &&
(this._accountSwitchProps().displayText || this._accountSwitchProps().selectedAccountName)
) {
return true;
}
return false;
});
hasher.initialized.add(updateExplorerHash); hasher.initialized.add(updateExplorerHash);
hasher.changed.add(updateExplorerHash); hasher.changed.add(updateExplorerHash);
hasher.init(); hasher.init();
window.addEventListener("message", this._handleMessage.bind(this), false); window.addEventListener("message", this._handleMessage.bind(this), false);
this._handleAadLogin();
} }
public explorer_click() { public explorer_click() {
@@ -348,43 +319,10 @@ class HostedExplorer {
this._meControlProps(propsToUpdate); this._meControlProps(propsToUpdate);
} }
private _updateAccountSwitchProps(props: Partial<AccountSwitchComponentProps>) { private _updateAccountSwitchProps(props: any) {
if (!props) { if (!props) {
return; return;
} }
const propsToUpdate = this._accountSwitchProps();
if (props.authType) {
if (props.selectedAccountName != null) {
propsToUpdate.selectedAccountName = props.selectedAccountName;
}
if (props.authType === AuthType.EncryptedToken) {
propsToUpdate.authType = AuthType.EncryptedToken;
} else if (props.authType === AuthType.AAD) {
propsToUpdate.authType = AuthType.AAD;
if (props.displayText != null) {
propsToUpdate.displayText = props.displayText;
}
if (props.isLoadingAccounts != null) {
propsToUpdate.isLoadingAccounts = props.isLoadingAccounts;
}
if (props.accounts) {
propsToUpdate.accounts = props.accounts.sort((a, b) => (a.name < b.name ? -1 : 1));
}
if (props.isLoadingSubscriptions != null) {
propsToUpdate.isLoadingSubscriptions = props.isLoadingSubscriptions;
}
if (props.subscriptions) {
propsToUpdate.subscriptions = props.subscriptions.sort((a, b) => (a.displayName < b.displayName ? -1 : 1));
}
if (props.selectedSubscriptionId != null) {
propsToUpdate.selectedSubscriptionId = props.selectedSubscriptionId;
}
}
}
this._accountSwitchProps(propsToUpdate);
} }
private _onAccountChange = (newAccount: DatabaseAccount) => { private _onAccountChange = (newAccount: DatabaseAccount) => {
@@ -522,36 +460,6 @@ class HostedExplorer {
} }
} }
private _handleAadLogin() {
AuthHeadersUtil.processTokenResponse();
if (AuthHeadersUtil.isUserSignedIn()) {
window.authType = AuthType.AAD;
const user = AuthHeadersUtil.getCachedUser();
this._updateMeControlProps({
isUserSignedIn: true,
user: {
name: user.profile.name,
email: user.userName,
tenantName: undefined,
imageUrl: undefined
}
});
AuthHeadersUtil.getPhotoFromGraphAPI().then(blob => {
const imageUrl = URL.createObjectURL(blob);
this._updateMeControlProps({
isUserSignedIn: true,
user: {
name: undefined,
email: undefined,
tenantName: undefined,
imageUrl: imageUrl
}
});
});
}
}
private _handleGetAccessAadRequest() { private _handleGetAccessAadRequest() {
this._getAccessAad().then( this._getAccessAad().then(
response => { response => {

View File

@@ -2,23 +2,19 @@ import "./Platform/Hosted/ConnectScreen.less";
import { useBoolean } from "@uifabric/react-hooks"; import { useBoolean } from "@uifabric/react-hooks";
import { import {
DefaultButton, DefaultButton,
DetailsList,
DirectionalHint, DirectionalHint,
FocusZone, FocusZone,
IContextualMenuProps,
initializeIcons, initializeIcons,
Panel, Panel,
PanelType, PanelType,
Persona, Persona,
PersonaInitialsColor, PersonaInitialsColor,
PersonaSize, PersonaSize,
SelectionMode, ChoiceGroup
Selection
} from "office-ui-fabric-react"; } 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 FeedbackIcon from "../images/Feedback.svg";
import ConnectIcon from "../images/HostedConnectwhite.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 { CommandButtonComponent } from "./Explorer/Controls/CommandButton/CommandButtonComponent";
@@ -26,111 +22,74 @@ import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { useGraphPhoto } from "./hooks/useGraphPhoto"; import { useGraphPhoto } from "./hooks/useGraphPhoto";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { AccountSwitchComponent } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponent"; import { AccountSwitchComponent } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponent";
import { AuthContext, AuthProvider } from "./contexts/authContext";
import { usePortalAccessToken } from "./hooks/usePortalAccessToken"; import { usePortalAccessToken } from "./hooks/usePortalAccessToken";
import { useDirectories } from "./hooks/useDirectories"; import { useDirectories } from "./hooks/useDirectories";
import * as Msal from "msal";
import { configContext } from "./ConfigContext";
import { HttpHeaders } from "./Common/Constants";
import { GenerateTokenResponse } from "./Contracts/DataModels";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
initializeIcons(); initializeIcons();
const App: React.FunctionComponent = () => { const msal = new Msal.UserAgentApplication({
const params = new URLSearchParams(window.location.search); auth: {
const encryptedToken = params && params.get("key"); authority: "https://login.microsoft.com/common",
const encryptedTokenMetadata = usePortalAccessToken(encryptedToken); clientId: "203f1145-856a-4232-83d4-a43568fba23d",
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set in development
const { isLoggedIn, aadlogin: login, account, aadlogout: logout, tenantId } = React.useContext(AuthContext);
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
const photo = useGraphPhoto();
const directories = useDirectories();
// const [selectedItem, setSelectedItem] = React.useState<any>(undefined);
const selection = new Selection({
getKey: item => item.tenantId,
items: directories,
onSelectionChanged: () => {
const selected = selection.getSelection()[0];
if (selected.tenantId !== tenantId) {
console.log("new Tenant", selected.tenantId);
} }
},
selectionMode: SelectionMode.single
}); });
selection.setKeySelected(tenantId, true, false);
// private _renderPersonaComponent = (): JSX.Element => { const App: React.FunctionComponent = () => {
// const { user } = this.props; // Hooks for handling encrypted portal tokens
// const personaProps: IPersonaSharedProps = { const params = new URLSearchParams(window.location.search);
// imageUrl: user.imageUrl, const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key"));
// text: user.name, const encryptedTokenMetadata = usePortalAccessToken(encryptedToken);
// secondaryText: user.email,
// showSecondaryText: true,
// showInitialsUntilImageLoads: true,
// initialsColor: PersonaInitialsColor.teal,
// size: PersonaSize.size72,
// className: "mecontrolContextualMenuPersona"
// };
// return <Persona {...personaProps} />; // Hooks for showing/hiding UI
// }; const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
const menuProps: IContextualMenuProps = { // Hooks for AAD authentication
className: "mecontrolContextualMenu", const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(false);
isBeakVisible: false, const [account, setAccount] = React.useState<Msal.Account>();
directionalHintFixed: true, const [graphToken, setGraphToken] = React.useState<string>();
directionalHint: DirectionalHint.bottomRightEdge, const [armToken, setArmToken] = React.useState<string>();
calloutProps: { const [tenantId, setTenantId] = React.useState<string>();
minPagePadding: 0 const [connectionString, setConnectionString] = React.useState<string>("");
},
items: [ const login = React.useCallback(async () => {
{ const response = await msal.loginPopup();
key: "SwitchDirectory", setLoggedIn();
text: "Switch Directory", setAccount(response.account);
onClick: openPanel setTenantId(response.tenantId);
}, }, []);
{
key: "SignOut", const logout = React.useCallback(() => {
text: "Sign Out", msal.logout();
onClick: logout setLoggedOut();
}, []);
React.useEffect(() => {
if (account && tenantId) {
console.log(msal.authority);
console.log("Getting tokens for", tenantId);
Promise.all([
msal.acquireTokenSilent({
scopes: ["https://graph.windows.net//.default"]
}),
msal.acquireTokenSilent({
scopes: ["https://management.azure.com//.default"]
})
]).then(([graphTokenResponse, armTokenResponse]) => {
setGraphToken(graphTokenResponse.accessToken);
setArmToken(armTokenResponse.accessToken);
});
} }
] }, [account, tenantId]);
};
// { const photo = useGraphPhoto(graphToken);
// id: "commandbutton-settings", const directories = useDirectories(armToken);
// iconSrc: SettingsIcon,
// iconAlt: "setting button",
// onCommandClick: () => {},
// commandButtonLabel: undefined,
// ariaLabel: "setting button",
// tooltipText: "Global settings",
// hasPopup: true,
// disabled: false
// },
// {
// id: "commandbutton-feedback",
// iconSrc: FeedbackIcon,
// iconAlt: "feeback button",
// onCommandClick: () =>
// window.open(
// "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"
// ),
// commandButtonLabel: undefined,
// ariaLabel: "feeback button",
// tooltipText: "Send feedback",
// hasPopup: true,
// disabled: false
// }
const buttonProps = {
id: "mecontrolHeader",
className: "mecontrolHeaderButton",
menuProps,
styles: {
rootHovered: { backgroundColor: "#393939" },
rootFocused: { backgroundColor: "#393939" },
rootPressed: { backgroundColor: "#393939" },
rootExpanded: { backgroundColor: "#393939" }
}
};
return ( return (
<div> <div>
@@ -151,7 +110,7 @@ const App: React.FunctionComponent = () => {
)} )}
{isLoggedIn && ( {isLoggedIn && (
<span className="accountSwitchComponentContainer"> <span className="accountSwitchComponentContainer">
<AccountSwitchComponent /> <AccountSwitchComponent armToken={armToken} />
</span> </span>
)} )}
{!isLoggedIn && encryptedTokenMetadata?.accountName && ( {!isLoggedIn && encryptedTokenMetadata?.accountName && (
@@ -161,18 +120,6 @@ const App: React.FunctionComponent = () => {
)} )}
</div> </div>
<div className="feedbackConnectSettingIcons"> <div className="feedbackConnectSettingIcons">
{isLoggedIn && (
<CommandButtonComponent
id="commandbutton-connect"
iconSrc={ConnectIcon}
iconAlt="connect button"
onCommandClick={() => {}}
ariaLabel="connect button"
tooltipText="Connect to a Cosmos DB account"
hasPopup={true}
disabled={false}
/>
)}
<CommandButtonComponent <CommandButtonComponent
id="commandbutton-feedback" id="commandbutton-feedback"
iconSrc={FeedbackIcon} iconSrc={FeedbackIcon}
@@ -189,7 +136,37 @@ const App: React.FunctionComponent = () => {
<div className="meControl"> <div className="meControl">
{isLoggedIn ? ( {isLoggedIn ? (
<FocusZone> <FocusZone>
<DefaultButton {...buttonProps}> <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 <Persona
imageUrl={photo} imageUrl={photo}
text={account?.name} text={account?.name}
@@ -228,7 +205,7 @@ const App: React.FunctionComponent = () => {
)}&metadata=${JSON.stringify(encryptedTokenMetadata)}`} )}&metadata=${JSON.stringify(encryptedTokenMetadata)}`}
></iframe> ></iframe>
)} )}
{!encryptedTokenMetadata && isLoggedIn && ( {/* {!encryptedTokenMetadata && isLoggedIn && (
<iframe <iframe
id="explorerMenu" id="explorerMenu"
name="explorer" name="explorer"
@@ -236,7 +213,7 @@ const App: React.FunctionComponent = () => {
title="explorer" title="explorer"
src={`explorer.html?v=1.0.1&platform=Hosted&authType=${AuthType.AAD}`} src={`explorer.html?v=1.0.1&platform=Hosted&authType=${AuthType.AAD}`}
></iframe> ></iframe>
)} )} */}
{!isLoggedIn && !encryptedTokenMetadata && ( {!isLoggedIn && !encryptedTokenMetadata && (
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}> <div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
<div className="connectExplorerFormContainer"> <div className="connectExplorerFormContainer">
@@ -246,15 +223,42 @@ const App: React.FunctionComponent = () => {
</p> </p>
<p className="welcomeText">Welcome to Azure Cosmos DB</p> <p className="welcomeText">Welcome to Azure Cosmos DB</p>
{isConnectionStringVisible ? ( {isConnectionStringVisible ? (
<form id="connectWithConnectionString"> <form
id="connectWithConnectionString"
onSubmit={async event => {
event.preventDefault();
// const foo = parseConnectionString(connectionString);
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));
event.preventDefault();
}}
>
<p className="connectExplorerContent connectStringText"> <p className="connectExplorerContent connectStringText">
Connect to your account with connection string Connect to your account with connection string
</p> </p>
<p className="connectExplorerContent"> <p className="connectExplorerContent">
<input className="inputToken" type="text" required placeholder="Please enter a connection string" /> <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" }}> <span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
<img className="errorImg" src="images/error.svg" alt="Error notification" /> <img className="errorImg" src="images/error.svg" alt="Error notification" />
<span className="errorDetails" /> <span className="errorDetails"></span>
</span> </span>
</p> </p>
<p className="connectExplorerContent"> <p className="connectExplorerContent">
@@ -285,35 +289,30 @@ const App: React.FunctionComponent = () => {
onDismiss={dismissPanel} onDismiss={dismissPanel}
closeButtonAriaLabel="Close" closeButtonAriaLabel="Close"
> >
<DetailsList <ChoiceGroup
items={selection.getItems()} options={directories.map(dir => ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))}
columns={[ selectedKey={tenantId}
{ onChange={async () => {
key: "name", dismissPanel();
name: "Name", // TODO!!! This does not work. Still not sure why. Tried lots of stuff.
minWidth: 200, // const response = await msal.loginPopup({
maxWidth: 200, // authority: `https://login.microsoftonline.com/${option.key}`
fieldName: "displayName" // });
}, // // msal = new Msal.UserAgentApplication({
{ // // auth: {
key: "id", // // authority: `https://login.microsoftonline.com/${option.key}`,
name: "ID", // // clientId: "203f1145-856a-4232-83d4-a43568fba23d",
minWidth: 200, // // redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set in development
maxWidth: 200, // // }
fieldName: "tenantId" // // });
} // setTenantId(option.key);
]} // setAccount(response.account);
selectionMode={SelectionMode.single} // console.log(account);
selection={selection} }}
/> />
</Panel> </Panel>
</div> </div>
); );
}; };
render( render(<App />, document.body);
<AuthProvider>
<App />
</AuthProvider>,
document.body
);

View File

@@ -55,11 +55,6 @@ import "url-polyfill/url-polyfill.min";
initializeIcons(); initializeIcons();
import * as ko from "knockout";
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
import * as Emulator from "./Platform/Emulator/Main"; import * as Emulator from "./Platform/Emulator/Main";
import Hosted from "./Platform/Hosted/Main"; import Hosted from "./Platform/Hosted/Main";
import * as Portal from "./Platform/Portal/Main"; import * as Portal from "./Platform/Portal/Main";

View File

@@ -107,14 +107,6 @@ export default class AuthHeadersUtil {
const user = AuthHeadersUtil._authContext.getCachedUser(); const user = AuthHeadersUtil._authContext.getCachedUser();
return !!user; return !!user;
} }
public static getCachedUser(): AuthenticationContext.UserInfo {
if (this.isUserSignedIn()) {
return AuthHeadersUtil._authContext.getCachedUser();
}
return undefined;
}
public static signIn() { public static signIn() {
if (!AuthHeadersUtil.isUserSignedIn()) { if (!AuthHeadersUtil.isUserSignedIn()) {
AuthHeadersUtil._authContext.login(); AuthHeadersUtil._authContext.login();

View File

@@ -1,24 +0,0 @@
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import * as UserUtils from "./UserUtils";
describe("UserUtils", () => {
it("getFullName works in regular data explorer (inside portal)", () => {
const user: AuthenticationContext.UserInfo = {
userName: "userName",
profile: {
name: "name"
}
};
AuthHeadersUtil.getCachedUser = jest.fn().mockReturnValue(user);
expect(UserUtils.getFullName()).toBe("name");
});
it("getFullName works in fullscreen data explorer (outside portal)", () => {
jest.mock("./AuthorizationUtils", () => {
(): { name: string } => ({ name: "name" });
});
expect(UserUtils.getFullName()).toBe("name");
});
});

View File

@@ -1,17 +1,8 @@
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import { decryptJWTToken } from "./AuthorizationUtils"; import { decryptJWTToken } from "./AuthorizationUtils";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export function getFullName(): string { export function getFullName(): string {
let fullName: string;
const user = AuthHeadersUtil.getCachedUser();
if (user) {
fullName = user.profile.name;
} else {
const authToken = userContext.authorizationToken; const authToken = userContext.authorizationToken;
const props = decryptJWTToken(authToken); const props = decryptJWTToken(authToken);
fullName = props.name; return props.name;
}
return fullName;
} }

View File

@@ -1,73 +0,0 @@
import * as React from "react";
import * as Msal from "msal";
import { createContext, useState, useCallback } from "react";
import { useBoolean } from "@uifabric/react-hooks";
const defaultError = "Auth context method was called witout a AuthProvider component in the component tree";
const msal = new Msal.UserAgentApplication({
auth: {
authority: "https://login.microsoft.com/common",
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set development
}
});
interface AuthContext {
isLoggedIn: boolean;
account?: Msal.Account;
graphToken?: string;
armToken?: string;
tenantId?: string;
aadlogout: () => unknown;
aadlogin: () => unknown;
}
export const AuthContext = createContext<AuthContext>({
isLoggedIn: false,
aadlogin: () => {
throw Error(defaultError);
},
aadlogout: () => {
throw Error(defaultError);
}
});
export const AuthProvider: React.FunctionComponent = ({ children }) => {
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(false);
const [account, setAccount] = useState<Msal.Account>();
const [graphToken, setGraphToken] = useState<string>();
const [armToken, setArmToken] = useState<string>();
const [tenantId, setTenantId] = useState<string>();
const aadlogin = useCallback(async () => {
const response = await msal.loginPopup();
setLoggedIn();
setAccount(response.account);
setTenantId(response.tenantId);
msal.authority = "https://login.microsoftonline.com/481f23b0-3fb3-4e76-812d-15513d11dbfc";
const [graphTokenResponse, armTokenResponse] = await Promise.all([
msal.acquireTokenSilent({
scopes: ["https://graph.windows.net//.default"]
}),
msal.acquireTokenSilent({
scopes: ["https://management.azure.com//.default"]
})
]);
setGraphToken(graphTokenResponse.accessToken);
setArmToken(armTokenResponse.accessToken);
}, []);
const aadlogout = useCallback(() => {
msal.logout();
setLoggedOut();
}, []);
return (
<AuthContext.Provider value={{ isLoggedIn, account, aadlogin, aadlogout, graphToken, armToken, tenantId }}>
{children}
</AuthContext.Provider>
);
};

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthContext } from "../contexts/authContext";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
interface AccountListResult { interface AccountListResult {
@@ -39,8 +38,7 @@ export async function fetchDatabaseAccounts(
return accounts; return accounts;
} }
export function useDatabaseAccounts(subscriptionId: string): DatabaseAccount[] { export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] {
const { armToken } = useContext(AuthContext);
const [state, setState] = useState<DatabaseAccount[]>(); const [state, setState] = useState<DatabaseAccount[]>();
useEffect(() => { useEffect(() => {

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthContext } from "../contexts/authContext";
import { Tenant } from "../Contracts/DataModels"; import { Tenant } from "../Contracts/DataModels";
interface TenantListResult { interface TenantListResult {
@@ -29,8 +28,7 @@ export async function fetchDirectories(accessToken: string): Promise<Tenant[]> {
return tenents; return tenents;
} }
export function useDirectories(): Tenant[] { export function useDirectories(armToken: string): Tenant[] {
const { armToken } = useContext(AuthContext);
const [state, setState] = useState<Tenant[]>(); const [state, setState] = useState<Tenant[]>();
useEffect(() => { useEffect(() => {

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthContext } from "../contexts/authContext";
export async function fetchPhoto(accessToken: string): Promise<Blob | void> { export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
const headers = new Headers(); const headers = new Headers();
@@ -18,9 +17,8 @@ export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
.catch(error => console.log(error)); .catch(error => console.log(error));
} }
export function useGraphPhoto(): string { export function useGraphPhoto(graphToken: string): string {
const [photo, setPhoto] = useState<string>(); const [photo, setPhoto] = useState<string>();
const { graphToken } = useContext(AuthContext);
useEffect(() => { useEffect(() => {
if (graphToken) { if (graphToken) {

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthContext } from "../contexts/authContext";
import { Subscription } from "../Contracts/DataModels"; import { Subscription } from "../Contracts/DataModels";
interface SubscriptionListResult { interface SubscriptionListResult {
@@ -32,8 +31,7 @@ export async function fetchSubscriptions(accessToken: string): Promise<Subscript
return subscriptions; return subscriptions;
} }
export function useSubscriptions(): Subscription[] { export function useSubscriptions(armToken: string): Subscription[] {
const { armToken } = useContext(AuthContext);
const [state, setState] = useState<Subscription[]>(); const [state, setState] = useState<Subscription[]>();
useEffect(() => { useEffect(() => {