Migrated Hosted Explorer to React (#360)

Co-authored-by: Victor Meng <vimeng@microsoft.com>
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
This commit is contained in:
Steve Faulkner
2021-01-19 16:31:55 -06:00
committed by GitHub
parent 8c40df0fa1
commit 2b2de7c645
79 changed files with 2250 additions and 6025 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,177 +0,0 @@
import { AuthType } from "../../../AuthType";
import { StyleConstants } from "../../../Common/Constants";
import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels";
import * as React from "react";
import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
export interface AccountSwitchComponentProps {
authType: AuthType;
selectedAccountName: string;
accounts: DatabaseAccount[];
isLoadingAccounts: boolean;
onAccountChange: (newAccount: DatabaseAccount) => void;
selectedSubscriptionId: string;
subscriptions: Subscription[];
isLoadingSubscriptions: boolean;
onSubscriptionChange: (newSubscription: Subscription) => void;
displayText?: string;
}
export class AccountSwitchComponent extends React.Component<AccountSwitchComponentProps> {
public render(): JSX.Element {
return this.props.authType === AuthType.AAD ? this._renderSwitchDropDown() : this._renderAccountName();
}
private _renderSwitchDropDown(): JSX.Element {
const { displayText, selectedAccountName } = this.props;
const menuProps: IContextualMenuProps = {
directionalHintFixed: true,
className: "accountSwitchContextualMenu",
items: [
{
key: "switchSubscription",
onRender: this._renderSubscriptionDropdown.bind(this)
},
{
key: "switchAccount",
onRender: this._renderAccountDropDown.bind(this)
}
]
};
const buttonStyles: IButtonStyles = {
root: {
fontSize: StyleConstants.DefaultFontSize,
height: 40,
padding: 0,
paddingLeft: 10,
marginRight: 5,
backgroundColor: StyleConstants.BaseDark,
color: StyleConstants.BaseLight
},
rootHovered: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootFocused: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootPressed: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
rootExpanded: {
backgroundColor: StyleConstants.BaseHigh,
color: StyleConstants.BaseLight
},
textContainer: {
flexGrow: "initial"
}
};
const buttonProps: IButtonProps = {
text: displayText || selectedAccountName,
menuProps: menuProps,
styles: buttonStyles,
className: "accountSwitchButton",
id: "accountSwitchButton"
};
return <DefaultButton {...buttonProps} />;
}
private _renderSubscriptionDropdown(): JSX.Element {
const { subscriptions, selectedSubscriptionId, isLoadingSubscriptions } = this.props;
const options: IDropdownOption[] = subscriptions.map(sub => {
return {
key: sub.subscriptionId,
text: sub.displayName,
data: sub
};
});
const placeHolderText = isLoadingSubscriptions
? "Loading subscriptions"
: !options || !options.length
? "No subscriptions found in current directory"
: "Select subscription from list";
const dropdownProps: IDropdownProps = {
label: "Subscription",
className: "accountSwitchSubscriptionDropdown",
options: options,
onChange: this._onSubscriptionDropdownChange,
defaultSelectedKey: selectedSubscriptionId,
placeholder: placeHolderText,
styles: {
callout: "accountSwitchSubscriptionDropdownMenu"
}
};
return <Dropdown {...dropdownProps} />;
}
private _onSubscriptionDropdownChange = (e: React.FormEvent<HTMLDivElement>, option: IDropdownOption): void => {
if (!option) {
return;
}
this.props.onSubscriptionChange(option.data);
};
private _renderAccountDropDown(): JSX.Element {
const { accounts, selectedAccountName, isLoadingAccounts } = this.props;
const options: IDropdownOption[] = accounts.map(account => {
return {
key: account.name,
text: account.name,
data: account
};
});
// Fabric UI will also try to select the first non-disabled option from dropdown.
// Add a option to prevent pop the message when user click on dropdown on first time.
options.unshift({
key: "select from list",
text: "Select Cosmos DB account from list",
data: undefined
});
const placeHolderText = isLoadingAccounts
? "Loading Cosmos DB accounts"
: !options || !options.length
? "No Cosmos DB accounts found"
: "Select Cosmos DB account from list";
const dropdownProps: IDropdownProps = {
label: "Cosmos DB Account Name",
className: "accountSwitchAccountDropdown",
options: options,
onChange: this._onAccountDropdownChange,
defaultSelectedKey: selectedAccountName,
placeholder: placeHolderText,
styles: {
callout: "accountSwitchAccountDropdownMenu"
}
};
return <Dropdown {...dropdownProps} />;
}
private _onAccountDropdownChange = (e: React.FormEvent<HTMLDivElement>, option: IDropdownOption): void => {
if (!option) {
return;
}
this.props.onAccountChange(option.data);
};
private _renderAccountName(): JSX.Element {
const { displayText, selectedAccountName } = this.props;
return <span className="accountNameHeader">{displayText || selectedAccountName}</span>;
}
}

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

@@ -38,7 +38,7 @@ export interface CommandButtonComponentProps {
/**
* Label for the button
*/
commandButtonLabel: string;
commandButtonLabel?: string;
/**
* True if this button opens a tab or pane, false otherwise.

View File

@@ -5,7 +5,6 @@ import {
getSanitizedInputValue,
hasDatabaseSharedThroughput,
isDirty,
isDirtyTypes,
MongoIndexTypes,
MongoNotificationType,
parseConflictResolutionMode,
@@ -71,17 +70,18 @@ describe("SettingsUtils", () => {
excludedPaths: []
} as DataModels.IndexingPolicy;
const cases = [
["baseline", "current"],
[0, 1],
[true, false],
[undefined, indexingPolicy],
[indexingPolicy, { ...indexingPolicy, automatic: false }]
];
it("works on all types", () => {
expect(isDirty("baseline", "baseline")).toEqual(false);
expect(isDirty(0, 0)).toEqual(false);
expect(isDirty(true, true)).toEqual(false);
expect(isDirty(undefined, undefined)).toEqual(false);
expect(isDirty(indexingPolicy, indexingPolicy)).toEqual(false);
test.each(cases)("", (baseline: isDirtyTypes, current: isDirtyTypes) => {
expect(isDirty(baseline, baseline)).toEqual(false);
expect(isDirty(baseline, current)).toEqual(true);
expect(isDirty("baseline", "current")).toEqual(true);
expect(isDirty(0, 1)).toEqual(true);
expect(isDirty(true, false)).toEqual(true);
expect(isDirty(undefined, indexingPolicy)).toEqual(true);
expect(isDirty(indexingPolicy, { ...indexingPolicy, automatic: false })).toEqual(true);
});
});

View File

@@ -1026,7 +1026,6 @@ exports[`SettingsComponent renders 1`] = `
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
@@ -1072,8 +1071,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
@@ -1166,7 +1163,6 @@ exports[`SettingsComponent renders 1`] = `
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function],
"sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
@@ -1233,7 +1229,6 @@ exports[`SettingsComponent renders 1`] = `
"toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane {
"container": [Circular],
"extensions": [Function],
@@ -2301,7 +2296,6 @@ exports[`SettingsComponent renders 1`] = `
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
@@ -2347,8 +2341,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
@@ -2441,7 +2433,6 @@ exports[`SettingsComponent renders 1`] = `
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function],
"sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
@@ -2508,7 +2499,6 @@ exports[`SettingsComponent renders 1`] = `
"toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane {
"container": [Circular],
"extensions": [Function],
@@ -3589,7 +3579,6 @@ exports[`SettingsComponent renders 1`] = `
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
@@ -3635,8 +3624,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
@@ -3729,7 +3716,6 @@ exports[`SettingsComponent renders 1`] = `
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function],
"sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
@@ -3796,7 +3782,6 @@ exports[`SettingsComponent renders 1`] = `
"toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane {
"container": [Circular],
"extensions": [Function],
@@ -4864,7 +4849,6 @@ exports[`SettingsComponent renders 1`] = `
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
@@ -4910,8 +4894,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
@@ -5004,7 +4986,6 @@ exports[`SettingsComponent renders 1`] = `
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function],
"sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
@@ -5071,7 +5052,6 @@ exports[`SettingsComponent renders 1`] = `
"toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane {
"container": [Circular],
"extensions": [Function],

View File

@@ -1,6 +1,6 @@
import React from "react";
import { shallow } from "enzyme";
import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent";
import React from "react";
import { Descriptor, SmartUiComponent } from "./SmartUiComponent";
describe("SmartUiComponent", () => {
const exampleData: Descriptor = {
@@ -77,9 +77,7 @@ describe("SmartUiComponent", () => {
}
};
const exampleCallbacks = (newValues: Map<string, InputType>): void => {
console.log("New values:", newValues);
};
const exampleCallbacks = (): void => undefined;
it("should render", () => {
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} onChange={exampleCallbacks} />);

View File

@@ -214,8 +214,6 @@ export default class Explorer {
public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<AdHocAccessData>;
public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise<void>;
public renewTokenError: ko.Observable<string>;
public tokenForRenewal: ko.Observable<string>;
public shareAccessToggleState: ko.Observable<ShareAccessToggleState>;
public shareAccessUrl: ko.Observable<string>;
public shareUrlCopyHelperText: ko.Observable<string>;
@@ -314,7 +312,7 @@ export default class Explorer {
this.isSynapseLinkUpdating = ko.observable<boolean>(false);
this.isAccountReady.subscribe(async (isAccountReady: boolean) => {
if (isAccountReady) {
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
this.isAuthWithResourceToken() ? await this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
RouteHandler.getInstance().initHandler();
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
this.arcadiaWorkspaces = ko.observableArray();
@@ -381,8 +379,6 @@ export default class Explorer {
readWriteUrl: undefined,
readUrl: undefined
});
this.tokenForRenewal = ko.observable<string>("");
this.renewTokenError = ko.observable<string>("");
this.shareAccessUrl = ko.observable<string>();
this.shareUrlCopyHelperText = ko.observable<string>("Click to copy");
this.shareTokenCopyHelperText = ko.observable<string>("Click to copy");
@@ -414,7 +410,6 @@ export default class Explorer {
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
this.databases = ko.observableArray<ViewModels.Database>();
@@ -1095,25 +1090,6 @@ export default class Explorer {
return true;
}
public renewToken = (): void => {
TelemetryProcessor.trace(Action.ConnectEncryptionToken);
this.renewTokenError("");
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Initiating connection to account"
);
this.renewExplorerShareAccess(this, this.tokenForRenewal())
.fail((error: any) => {
const stringifiedError: string = getErrorMessage(error);
this.renewTokenError("Invalid connection string specified");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to initiate connection to account: ${stringifiedError}`
);
})
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
};
public generateSharedAccessData(): void {
const id: string = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Generating share url");
AuthHeadersUtil.generateEncryptedToken().then(
@@ -1265,16 +1241,6 @@ export default class Explorer {
$("#contextSwitchPrompt").dialog("open");
}
public displayConnectExplorerForm(): void {
$("#divExplorer").hide();
$("#connectExplorer").css("display", "flex");
}
public hideConnectExplorerForm(): void {
$("#connectExplorer").hide();
$("#divExplorer").show();
}
public isReadWriteToggled: () => boolean = (): boolean => {
return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite;
};
@@ -1364,21 +1330,17 @@ export default class Explorer {
}
}
public refreshDatabaseForResourceToken(): Q.Promise<any> {
public async refreshDatabaseForResourceToken(): Promise<any> {
const databaseId = this.resourceTokenDatabaseId();
const collectionId = this.resourceTokenCollectionId();
if (!databaseId || !collectionId) {
return Q.reject();
throw new Error("No collection ID or database ID for resource token");
}
const deferred: Q.Deferred<void> = Q.defer();
readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
this.selectedNode(this.resourceTokenCollection());
deferred.resolve();
});
return deferred.promise;
}
public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> {
@@ -1848,7 +1810,7 @@ export default class Explorer {
if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
if (process.env.NODE_ENV === "development" && configContext.platform === Platform.Portal) {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
}
@@ -1883,16 +1845,6 @@ export default class Explorer {
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId
});
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
{
resourceId: this.databaseAccount && this.databaseAccount().id,
dataExplorerArea: Constants.Areas.ResourceTree,
databaseAccount: this.databaseAccount && this.databaseAccount()
},
inputs.loadDatabaseAccountTimestamp
);
this.isAccountReady(true);
}
}
@@ -1974,18 +1926,6 @@ export default class Explorer {
this.commandBarComponentAdapter.onUpdateTabsButtons(buttons);
}
public signInAad = () => {
TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" });
sendMessage({
type: MessageTypes.AadSignIn
});
};
public onSwitchToConnectionString = () => {
$("#connectWithAad").hide();
$("#connectWithConnectionString").show();
};
public clickHostedAccountSwitch = () => {
sendMessage({
type: MessageTypes.UpdateAccountSwitch,

View File

@@ -1,108 +0,0 @@
import React from "react";
import { shallow, mount } from "enzyme";
import { MeControlComponent, MeControlComponentProps } from "./MeControlComponent";
const createNotSignedInProps = (): MeControlComponentProps => {
return {
isUserSignedIn: false,
user: null,
onSignInClick: jest.fn(),
onSignOutClick: jest.fn(),
onSwitchDirectoryClick: jest.fn()
};
};
const createSignedInProps = (): MeControlComponentProps => {
return {
isUserSignedIn: true,
user: {
name: "Test User",
email: "testuser@contoso.com",
tenantName: "Contoso",
imageUrl: "../../../../images/dotnet.png"
},
onSignInClick: jest.fn(),
onSignOutClick: jest.fn(),
onSwitchDirectoryClick: jest.fn()
};
};
describe("test render", () => {
it("renders not signed in", () => {
const props = createNotSignedInProps();
const wrapper = shallow(<MeControlComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders signed in with full info", () => {
const props = createSignedInProps();
const wrapper = shallow(<MeControlComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("change not signed in to signed in", () => {
const notSignInProps = createNotSignedInProps();
const wrapper = mount(<MeControlComponent {...notSignInProps} />);
expect(wrapper.exists(".mecontrolSigninButton")).toBe(true);
expect(wrapper.exists(".mecontrolHeaderButton")).toBe(false);
const signInProps = createSignedInProps();
wrapper.setProps(signInProps);
expect(wrapper.exists(".mecontrolSigninButton")).toBe(false);
expect(wrapper.exists(".mecontrolHeaderButton")).toBe(true);
wrapper.unmount;
});
it("render contextual menu", () => {
const signInProps = createSignedInProps();
const wrapper = mount(<MeControlComponent {...signInProps} />);
wrapper.find("button.mecontrolHeaderButton").simulate("click");
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true);
wrapper.find("button.mecontrolHeaderButton").simulate("click");
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(false);
wrapper.unmount;
});
});
describe("test function got called", () => {
it("sign in click", () => {
const notSignInProps = createNotSignedInProps();
const wrapper = mount(<MeControlComponent {...notSignInProps} />);
wrapper.find("button.mecontrolSigninButton").simulate("click");
expect(notSignInProps.onSignInClick).toBeCalled();
expect(notSignInProps.onSignInClick).toHaveBeenCalled();
});
it("sign out click", () => {
const signInProps = createSignedInProps();
const wrapper = mount(<MeControlComponent {...signInProps} />);
wrapper.find("button.mecontrolHeaderButton").simulate("click");
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true);
wrapper.find("div.signOutLink").simulate("click");
expect(signInProps.onSignOutClick).toBeCalled();
expect(signInProps.onSignOutClick).toHaveBeenCalled();
});
it("switch directory", () => {
const signInProps = createSignedInProps();
const wrapper = mount(<MeControlComponent {...signInProps} />);
wrapper.find("button.mecontrolHeaderButton").simulate("click");
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true);
wrapper.find("div.switchDirectoryLink").simulate("click");
expect(signInProps.onSwitchDirectoryClick).toBeCalled();
expect(signInProps.onSwitchDirectoryClick).toHaveBeenCalled();
});
});

View File

@@ -1,167 +0,0 @@
import * as React from "react";
import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
import { FocusZone } from "office-ui-fabric-react/lib/FocusZone";
import { IPersonaSharedProps, Persona, PersonaInitialsColor, PersonaSize } from "office-ui-fabric-react/lib/Persona";
export interface MeControlComponentProps {
/**
* Wheather user is signed in or not
*/
isUserSignedIn: boolean;
/**
* User info
*/
user: MeControlUser;
/**
* Click handler for sign in click
*/
onSignInClick: (e: React.MouseEvent<BaseButton>) => void;
/**
* Click handler for sign out click
*/
onSignOutClick: (e: React.SyntheticEvent) => void;
/**
* Click handler for switch directory click
*/
onSwitchDirectoryClick: (e: React.SyntheticEvent) => void;
}
export interface MeControlUser {
/**
* Display name for user
*/
name: string;
/**
* Display email for user
*/
email: string;
/**
* Display tenant for user
*/
tenantName: string;
/**
* image source for the profic photo
*/
imageUrl: string;
}
export class MeControlComponent extends React.Component<MeControlComponentProps> {
public render(): JSX.Element {
return this.props.isUserSignedIn ? this._renderProfileComponent() : this._renderSignInComponent();
}
private _renderProfileComponent(): JSX.Element {
const { user } = this.props;
const menuProps: IContextualMenuProps = {
className: "mecontrolContextualMenu",
isBeakVisible: false,
directionalHintFixed: true,
directionalHint: DirectionalHint.bottomRightEdge,
calloutProps: {
minPagePadding: 0
},
items: [
{
key: "Persona",
onRender: this._renderPersonaComponent
},
{
key: "SwitchDirectory",
onRender: this._renderSwitchDirectory
},
{
key: "SignOut",
onRender: this._renderSignOut
}
]
};
const personaProps: IPersonaSharedProps = {
imageUrl: user.imageUrl,
text: user.email,
secondaryText: user.tenantName,
showSecondaryText: true,
showInitialsUntilImageLoads: true,
initialsColor: PersonaInitialsColor.teal,
size: PersonaSize.size28,
className: "mecontrolHeaderPersona"
};
const buttonProps: IButtonProps = {
id: "mecontrolHeader",
className: "mecontrolHeaderButton",
menuProps: menuProps,
onRenderMenuIcon: () => <span />,
styles: {
rootHovered: { backgroundColor: "#393939" },
rootFocused: { backgroundColor: "#393939" },
rootPressed: { backgroundColor: "#393939" },
rootExpanded: { backgroundColor: "#393939" }
}
};
return (
<FocusZone>
<DefaultButton {...buttonProps}>
<Persona {...personaProps} />
</DefaultButton>
</FocusZone>
);
}
private _renderPersonaComponent = (): JSX.Element => {
const { user } = this.props;
const personaProps: IPersonaSharedProps = {
imageUrl: user.imageUrl,
text: user.name,
secondaryText: user.email,
showSecondaryText: true,
showInitialsUntilImageLoads: true,
initialsColor: PersonaInitialsColor.teal,
size: PersonaSize.size72,
className: "mecontrolContextualMenuPersona"
};
return <Persona {...personaProps} />;
};
private _renderSwitchDirectory = (): JSX.Element => {
return (
<div
className="switchDirectoryLink"
onClick={(e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) =>
this.props.onSwitchDirectoryClick(e)
}
>
Switch Directory
</div>
);
};
private _renderSignOut = (): JSX.Element => {
return (
<div
className="signOutLink"
onClick={(e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => this.props.onSignOutClick(e)}
>
Sign out
</div>
);
};
private _renderSignInComponent = (): JSX.Element => {
const buttonProps: IButtonProps = {
className: "mecontrolSigninButton",
text: "Sign In",
onClick: (e: React.MouseEvent<BaseButton>) => this.props.onSignInClick(e),
styles: {
rootHovered: { backgroundColor: "#393939", color: "#fff" },
rootFocused: { backgroundColor: "#393939", color: "#fff" },
rootPressed: { backgroundColor: "#393939", color: "#fff" }
}
};
return <DefaultButton {...buttonProps} />;
};
}

View File

@@ -1,16 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { MeControlComponent, MeControlComponentProps } from "./MeControlComponent";
export class MeControlComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<MeControlComponentProps>;
public renderComponent(): JSX.Element {
return <MeControlComponent {...this.parameters()} />;
}
}

View File

@@ -1,91 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test render renders not signed in 1`] = `
<CustomizedDefaultButton
className="mecontrolSigninButton"
onClick={[Function]}
styles={
Object {
"rootFocused": Object {
"backgroundColor": "#393939",
"color": "#fff",
},
"rootHovered": Object {
"backgroundColor": "#393939",
"color": "#fff",
},
"rootPressed": Object {
"backgroundColor": "#393939",
"color": "#fff",
},
}
}
text="Sign In"
/>
`;
exports[`test render renders signed in with full info 1`] = `
<FocusZone
direction={2}
isCircularNavigation={false}
shouldRaiseClicks={true}
>
<CustomizedDefaultButton
className="mecontrolHeaderButton"
id="mecontrolHeader"
menuProps={
Object {
"calloutProps": Object {
"minPagePadding": 0,
},
"className": "mecontrolContextualMenu",
"directionalHint": 6,
"directionalHintFixed": true,
"isBeakVisible": false,
"items": Array [
Object {
"key": "Persona",
"onRender": [Function],
},
Object {
"key": "SwitchDirectory",
"onRender": [Function],
},
Object {
"key": "SignOut",
"onRender": [Function],
},
],
}
}
onRenderMenuIcon={[Function]}
styles={
Object {
"rootExpanded": Object {
"backgroundColor": "#393939",
},
"rootFocused": Object {
"backgroundColor": "#393939",
},
"rootHovered": Object {
"backgroundColor": "#393939",
},
"rootPressed": Object {
"backgroundColor": "#393939",
},
}
}
>
<StyledPersonaBase
className="mecontrolHeaderPersona"
imageUrl="../../../../images/dotnet.png"
initialsColor={3}
secondaryText="Contoso"
showInitialsUntilImageLoads={true}
showSecondaryText={true}
size={7}
text="testuser@contoso.com"
/>
</CustomizedDefaultButton>
</FocusZone>
`;

View File

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