From bd4d8da065bff92e9a6537673dd29b2880eeff2c Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Tue, 26 Jan 2021 15:32:37 -0800 Subject: [PATCH] Move notification console to react (#400) --- .../SettingsComponent.test.tsx.snap | 40 ++-- src/Explorer/Explorer.ts | 34 ++-- .../NotificationConsoleComponent.test.tsx | 128 ++++++++----- .../NotificationConsoleComponent.tsx | 106 ++++++----- .../NotificationConsoleComponentAdapter.tsx | 47 ----- ...NotificationConsoleComponent.test.tsx.snap | 177 +++++++++++++++++- src/Explorer/Panes/ContextualPaneBase.ts | 4 - .../DeleteCollectionConfirmationPane.test.ts | 2 - .../DeleteDatabaseConfirmationPane.test.ts | 2 - .../Panes/GenericRightPaneComponent.tsx | 7 - .../DataTable/DataTableBindingManager.ts | 5 +- src/Main.tsx | 25 ++- src/Utils/NotificationConsoleUtils.ts | 5 +- src/hooks/useKnockoutExplorer.ts | 6 +- 14 files changed, 370 insertions(+), 218 deletions(-) delete mode 100644 src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index dd65e9163..a9084dbc3 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -958,7 +958,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isNotificationConsoleExpanded": [Function], "isPreferredApiCassandra": [Function], "isPreferredApiDocumentDB": [Function], "isPreferredApiGraph": [Function], @@ -1018,12 +1017,6 @@ exports[`SettingsComponent renders 1`] = ` "nonSystemDatabases": [Function], "notebookBasePath": [Function], "notebookServerInfo": [Function], - "notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter { - "consoleData": [Function], - "container": [Circular], - "parameters": [Function], - }, - "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], @@ -1129,6 +1122,9 @@ exports[`SettingsComponent renders 1`] = ` }, "selfServeType": [Function], "serverId": [Function], + "setInProgressConsoleDataIdToBeDeleted": undefined, + "setIsNotificationConsoleExpanded": undefined, + "setNotificationConsoleData": undefined, "settingsPane": SettingsPane { "container": [Circular], "crossPartitionQueryEnabled": [Function], @@ -2241,7 +2237,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isNotificationConsoleExpanded": [Function], "isPreferredApiCassandra": [Function], "isPreferredApiDocumentDB": [Function], "isPreferredApiGraph": [Function], @@ -2301,12 +2296,6 @@ exports[`SettingsComponent renders 1`] = ` "nonSystemDatabases": [Function], "notebookBasePath": [Function], "notebookServerInfo": [Function], - "notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter { - "consoleData": [Function], - "container": [Circular], - "parameters": [Function], - }, - "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], @@ -2412,6 +2401,9 @@ exports[`SettingsComponent renders 1`] = ` }, "selfServeType": [Function], "serverId": [Function], + "setInProgressConsoleDataIdToBeDeleted": undefined, + "setIsNotificationConsoleExpanded": undefined, + "setNotificationConsoleData": undefined, "settingsPane": SettingsPane { "container": [Circular], "crossPartitionQueryEnabled": [Function], @@ -3537,7 +3529,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isNotificationConsoleExpanded": [Function], "isPreferredApiCassandra": [Function], "isPreferredApiDocumentDB": [Function], "isPreferredApiGraph": [Function], @@ -3597,12 +3588,6 @@ exports[`SettingsComponent renders 1`] = ` "nonSystemDatabases": [Function], "notebookBasePath": [Function], "notebookServerInfo": [Function], - "notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter { - "consoleData": [Function], - "container": [Circular], - "parameters": [Function], - }, - "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], @@ -3708,6 +3693,9 @@ exports[`SettingsComponent renders 1`] = ` }, "selfServeType": [Function], "serverId": [Function], + "setInProgressConsoleDataIdToBeDeleted": undefined, + "setIsNotificationConsoleExpanded": undefined, + "setNotificationConsoleData": undefined, "settingsPane": SettingsPane { "container": [Circular], "crossPartitionQueryEnabled": [Function], @@ -4820,7 +4808,6 @@ exports[`SettingsComponent renders 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isNotificationConsoleExpanded": [Function], "isPreferredApiCassandra": [Function], "isPreferredApiDocumentDB": [Function], "isPreferredApiGraph": [Function], @@ -4880,12 +4867,6 @@ exports[`SettingsComponent renders 1`] = ` "nonSystemDatabases": [Function], "notebookBasePath": [Function], "notebookServerInfo": [Function], - "notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter { - "consoleData": [Function], - "container": [Circular], - "parameters": [Function], - }, - "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], "onSwitchToConnectionString": [Function], @@ -4991,6 +4972,9 @@ exports[`SettingsComponent renders 1`] = ` }, "selfServeType": [Function], "serverId": [Function], + "setInProgressConsoleDataIdToBeDeleted": undefined, + "setIsNotificationConsoleExpanded": undefined, + "setNotificationConsoleData": undefined, "settingsPane": SettingsPane { "container": [Circular], "crossPartitionQueryEnabled": [Function], diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 33c557106..2a1f25286 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -55,7 +55,6 @@ import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; -import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { QueriesClient } from "../Common/QueriesClient"; import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; @@ -107,6 +106,12 @@ interface AdHocAccessData { readUrl: string; } +export interface ExplorerParams { + setIsNotificationConsoleExpanded: (isExpanded: boolean) => void; + setNotificationConsoleData: (consoleData: ConsoleData) => void; + setInProgressConsoleDataIdToBeDeleted: (id: string) => void; +} + export default class Explorer { public flight: ko.Observable = ko.observable( SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight @@ -146,8 +151,9 @@ export default class Explorer { public mostRecentActivity: MostRecentActivity.MostRecentActivity; // Notification Console - public notificationConsoleData: ko.ObservableArray; - public isNotificationConsoleExpanded: ko.Observable; + private setIsNotificationConsoleExpanded: (isExpanded: boolean) => void; + private setNotificationConsoleData: (consoleData: ConsoleData) => void; + private setInProgressConsoleDataIdToBeDeleted: (id: string) => void; // Panes public contextPanes: ContextualPaneBase[]; @@ -260,7 +266,6 @@ export default class Explorer { // React adapters private commandBarComponentAdapter: CommandBarComponentAdapter; private splashScreenAdapter: SplashScreenComponentAdapter; - private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter; private dialogComponentAdapter: DialogComponentAdapter; private _dialogProps: ko.Observable; private addSynapseLinkDialog: DialogComponentAdapter; @@ -269,7 +274,11 @@ export default class Explorer { private static readonly MaxNbDatabasesToAutoExpand = 5; - constructor() { + constructor(params?: ExplorerParams) { + this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded; + this.setNotificationConsoleData = params?.setNotificationConsoleData; + this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted; + const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); @@ -430,7 +439,6 @@ export default class Explorer { ); this.isSchemaEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSchema)); - this.isNotificationConsoleExpanded = ko.observable(false); this.isAutoscaleDefaultEnabled = ko.observable(false); @@ -478,7 +486,6 @@ export default class Explorer { bounds: splitterBounds, direction: SplitterDirection.Vertical, }); - this.notificationConsoleData = ko.observableArray([]); this.defaultExperience = ko.observable(); this.databaseAccount.subscribe((databaseAccount) => { const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount( @@ -892,7 +899,6 @@ export default class Explorer { this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter(); - this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this); this._initSettings(); @@ -1349,23 +1355,19 @@ export default class Explorer { } public logConsoleData(consoleData: ConsoleData): void { - this.notificationConsoleData.splice(0, 0, consoleData); + this.setNotificationConsoleData(consoleData); } public deleteInProgressConsoleDataWithId(id: string): void { - const updatedConsoleData = _.reject( - this.notificationConsoleData(), - (data: ConsoleData) => data.type === ConsoleDataType.InProgress && data.id === id - ); - this.notificationConsoleData(updatedConsoleData); + this.setInProgressConsoleDataIdToBeDeleted(id); } public expandConsole(): void { - this.isNotificationConsoleExpanded(true); + this.setIsNotificationConsoleExpanded(true); } public collapseConsole(): void { - this.isNotificationConsoleExpanded(false); + this.setIsNotificationConsoleExpanded(false); } public toggleLeftPaneExpanded() { diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx index cedf397bd..692793050 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx @@ -2,7 +2,6 @@ import React from "react"; import { shallow } from "enzyme"; import { NotificationConsoleComponentProps, - ConsoleData, NotificationConsoleComponent, ConsoleDataType, } from "./NotificationConsoleComponent"; @@ -10,38 +9,40 @@ import { describe("NotificationConsoleComponent", () => { const createBlankProps = (): NotificationConsoleComponentProps => { return { - consoleData: [], - isConsoleExpanded: true, - onConsoleDataChange: (consoleData: ConsoleData[]) => {}, - onConsoleExpandedChange: (isExpanded: boolean) => {}, + consoleData: undefined, + isConsoleExpanded: false, + inProgressConsoleDataIdToBeDeleted: "", + setIsConsoleExpanded: (isExpanded: boolean): void => {}, }; }; - it("renders the console (expanded)", () => { + it("renders the console", () => { const props = createBlankProps(); - props.consoleData.push({ + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + + props.consoleData = { type: ConsoleDataType.Info, date: "date", message: "message", - }); - - const wrapper = shallow(); + }; + wrapper.setProps(props); expect(wrapper).toMatchSnapshot(); }); it("shows proper progress count", () => { const count = 100; const props = createBlankProps(); + const wrapper = shallow(); for (let i = 0; i < count; i++) { - props.consoleData.push({ + props.consoleData = { type: ConsoleDataType.InProgress, - date: "date", + date: "date" + i, message: "message", - }); + }; + wrapper.setProps(props); } - - const wrapper = shallow(); expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual(count.toString()); expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0"); @@ -50,16 +51,17 @@ describe("NotificationConsoleComponent", () => { it("shows proper error count", () => { const count = 100; const props = createBlankProps(); + const wrapper = shallow(); for (let i = 0; i < count; i++) { - props.consoleData.push({ + props.consoleData = { type: ConsoleDataType.Error, - date: "date", + date: "date" + i, message: "message", - }); + }; + wrapper.setProps(props); } - const wrapper = shallow(); expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual(count.toString()); expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0"); @@ -68,31 +70,34 @@ describe("NotificationConsoleComponent", () => { it("shows proper info count", () => { const count = 100; const props = createBlankProps(); + const wrapper = shallow(); for (let i = 0; i < count; i++) { - props.consoleData.push({ + props.consoleData = { type: ConsoleDataType.Info, - date: "date", + date: "date" + i, message: "message", - }); + }; + wrapper.setProps(props); } - const wrapper = shallow(); expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual(count.toString()); }); - const testRenderNotification = (date: string, msg: string, type: ConsoleDataType, iconClassName: string) => { + const testRenderNotification = (date: string, message: string, type: ConsoleDataType, iconClassName: string) => { const props = createBlankProps(); - props.consoleData.push({ - date: date, - message: msg, - type: type, - }); const wrapper = shallow(); + + props.consoleData = { + type, + date, + message, + }; + wrapper.setProps(props); expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date); - expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(msg); + expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message); expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)); }; @@ -110,55 +115,78 @@ describe("NotificationConsoleComponent", () => { it("clears notifications", () => { const props = createBlankProps(); - props.consoleData.push({ + const wrapper = shallow(); + + props.consoleData = { type: ConsoleDataType.InProgress, date: "date", message: "message1", - }); - props.consoleData.push({ + }; + wrapper.setProps(props); + + props.consoleData = { type: ConsoleDataType.Error, date: "date", message: "message2", - }); - props.consoleData.push({ + }; + wrapper.setProps(props); + + props.consoleData = { type: ConsoleDataType.Info, date: "date", message: "message3", - }); + }; + wrapper.setProps(props); - const wrapper = shallow(); wrapper.find(".clearNotificationsButton").simulate("click"); - expect(!wrapper.exists(".notificationConsoleData")); }); it("collapses and hide content", () => { const props = createBlankProps(); - props.consoleData.push({ + const wrapper = shallow(); + + props.consoleData = { + type: ConsoleDataType.Info, date: "date", message: "message", - type: ConsoleDataType.Info, - }); + }; props.isConsoleExpanded = true; + wrapper.setProps(props); - const wrapper = shallow(); wrapper.find(".notificationConsoleHeader").simulate("click"); expect(!wrapper.exists(".notificationConsoleContent")); }); it("display latest data in header", () => { const latestData = "latest data"; - const props1 = createBlankProps(); - const props2 = createBlankProps(); - props2.consoleData.push({ + const props = createBlankProps(); + const wrapper = shallow(); + + props.consoleData = { + type: ConsoleDataType.Info, date: "date", message: latestData, - type: ConsoleDataType.Info, - }); - props2.isConsoleExpanded = true; + }; + props.isConsoleExpanded = true; + wrapper.setProps(props); - const wrapper = shallow(); - wrapper.setProps(props2); expect(wrapper.find(".headerStatusEllipsis").text()).toEqual(latestData); }); + + it("delete in progress message", () => { + const props = createBlankProps(); + props.consoleData = { + type: ConsoleDataType.InProgress, + date: "date", + message: "message", + id: "1", + }; + const wrapper = shallow(); + expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("1"); + + props.inProgressConsoleDataIdToBeDeleted = "1"; + wrapper.setProps(props); + expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0"); + }); }); diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index 48b2f332c..27b4add80 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -37,15 +37,15 @@ export interface ConsoleData { export interface NotificationConsoleComponentProps { isConsoleExpanded: boolean; - onConsoleExpandedChange: (isExpanded: boolean) => void; - consoleData: ConsoleData[]; - onConsoleDataChange: (consoleData: ConsoleData[]) => void; + consoleData: ConsoleData; + inProgressConsoleDataIdToBeDeleted: string; + setIsConsoleExpanded: (isExpanded: boolean) => void; } interface NotificationConsoleComponentState { headerStatus: string; selectedFilter: string; - isExpanded: boolean; + allConsoleData: ConsoleData[]; } export class NotificationConsoleComponent extends React.Component< @@ -60,28 +60,28 @@ export class NotificationConsoleComponent extends React.Component< { key: "Error", text: "Error" }, ]; private headerTimeoutId?: number; - private prevHeaderStatus: string | null; + private prevHeaderStatus: string; private consoleHeaderElement?: HTMLElement; constructor(props: NotificationConsoleComponentProps) { super(props); this.state = { - headerStatus: "", - selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "", - isExpanded: props.isConsoleExpanded, + headerStatus: undefined, + selectedFilter: NotificationConsoleComponent.FilterOptions[0].key, + allConsoleData: props.consoleData ? [props.consoleData] : [], }; - this.prevHeaderStatus = null; + this.prevHeaderStatus = undefined; } public componentDidUpdate( prevProps: NotificationConsoleComponentProps, prevState: NotificationConsoleComponentState ) { - const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props); + const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData); if ( this.prevHeaderStatus !== currentHeaderStatus && - currentHeaderStatus !== null && + currentHeaderStatus !== undefined && prevState.headerStatus !== currentHeaderStatus ) { this.setHeaderStatus(currentHeaderStatus); @@ -92,10 +92,8 @@ export class NotificationConsoleComponent extends React.Component< // updates: currentHeaderStatus -> "" -> currentHeaderStatus -> "" etc. this.prevHeaderStatus = currentHeaderStatus; - if (prevProps.isConsoleExpanded !== this.props.isConsoleExpanded) { - // Sync state and props - // TODO react anti-pattern: remove isExpanded from state which duplicates prop's isConsoleExpanded - this.setState({ isExpanded: this.props.isConsoleExpanded }); + if (this.props.consoleData || this.props.inProgressConsoleDataIdToBeDeleted) { + this.updateConsoleData(prevProps); } } @@ -104,12 +102,14 @@ export class NotificationConsoleComponent extends React.Component< }; public render(): JSX.Element { - const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress) + const numInProgress = this.state.allConsoleData.filter( + (data: ConsoleData) => data.type === ConsoleDataType.InProgress + ).length; + const numErroredItems = this.state.allConsoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Error) .length; - const numErroredItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Error) - .length; - const numInfoItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info) + const numInfoItems = this.state.allConsoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info) .length; + return (
{this.state.isExpanded
@@ -189,7 +189,7 @@ export class NotificationConsoleComponent extends React.Component< ); } private expandCollapseConsole() { - this.setState({ isExpanded: !this.state.isExpanded }); + this.props.setIsConsoleExpanded(!this.props.isConsoleExpanded); } private onExpandCollapseKeyPress = (event: React.KeyboardEvent): void => { @@ -209,7 +209,7 @@ export class NotificationConsoleComponent extends React.Component< }; private clearNotifications(): void { - this.props.onConsoleDataChange([]); + this.setState({ allConsoleData: [] }); } private renderAllFilteredConsoleData(rowData: ConsoleData[]): JSX.Element[] { @@ -229,12 +229,9 @@ export class NotificationConsoleComponent extends React.Component< }; private getFilteredConsoleData(): ConsoleData[] { - let filterType: ConsoleDataType | null = null; + let filterType: ConsoleDataType; switch (this.state.selectedFilter) { - case "All": - filterType = null; - break; case "In Progress": filterType = ConsoleDataType.InProgress; break; @@ -245,12 +242,12 @@ export class NotificationConsoleComponent extends React.Component< filterType = ConsoleDataType.Error; break; default: - filterType = null; + filterType = undefined; } - return filterType == null - ? this.props.consoleData - : this.props.consoleData.filter((data: ConsoleData) => data.type === filterType); + return filterType + ? this.state.allConsoleData.filter((data: ConsoleData) => data.type === filterType) + : this.state.allConsoleData; } private setHeaderStatus(statusMessage: string): void { @@ -266,18 +263,43 @@ export class NotificationConsoleComponent extends React.Component< ); } - private static extractHeaderStatus(props: NotificationConsoleComponentProps) { - if (props.consoleData && props.consoleData.length > 0) { - return props.consoleData[0].message.split(":\n")[0]; - } else { - return null; - } + private static extractHeaderStatus(consoleData: ConsoleData) { + return consoleData?.message.split(":\n")[0]; } private onConsoleWasExpanded = (): void => { - this.props.onConsoleExpandedChange(this.state.isExpanded); - if (this.state.isExpanded && this.consoleHeaderElement) { + if (this.props.isConsoleExpanded && this.consoleHeaderElement) { this.consoleHeaderElement.focus(); } }; + + private updateConsoleData = (prevProps: NotificationConsoleComponentProps): void => { + if (!this.areConsoleDataEqual(this.props.consoleData, prevProps.consoleData)) { + this.setState({ allConsoleData: [this.props.consoleData, ...this.state.allConsoleData] }); + } + + if ( + this.props.inProgressConsoleDataIdToBeDeleted && + prevProps.inProgressConsoleDataIdToBeDeleted !== this.props.inProgressConsoleDataIdToBeDeleted + ) { + const allConsoleData = this.state.allConsoleData.filter( + (data: ConsoleData) => + !(data.type === ConsoleDataType.InProgress && data.id === this.props.inProgressConsoleDataIdToBeDeleted) + ); + this.setState({ allConsoleData }); + } + }; + + private areConsoleDataEqual = (currentData: ConsoleData, prevData: ConsoleData): boolean => { + if (!currentData || !prevData) { + return !currentData && !prevData; + } + + return ( + currentData.date === prevData.date && + currentData.message === prevData.message && + currentData.type === prevData.type && + currentData.id === prevData.id + ); + }; } diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx deleted file mode 100644 index db1eae4b0..000000000 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { NotificationConsoleComponent } from "./NotificationConsoleComponent"; -import { ConsoleData } from "./NotificationConsoleComponent"; -import Explorer from "../../Explorer"; - -export class NotificationConsoleComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - public container: Explorer; - private consoleData: ko.ObservableArray; - - constructor(container: Explorer) { - this.container = container; - - this.consoleData = container.notificationConsoleData; - this.consoleData.subscribe((newValue: ConsoleData[]) => this.triggerRender()); - container.isNotificationConsoleExpanded.subscribe(() => this.triggerRender()); - this.parameters = ko.observable(Date.now()); - } - - private onConsoleExpandedChange(isExpanded: boolean): void { - isExpanded ? this.container.expandConsole() : this.container.collapseConsole(); - this.triggerRender(); - } - - private onConsoleDataChange(consoleData: ConsoleData[]): void { - this.consoleData(consoleData); - this.triggerRender(); - } - - public renderComponent(): JSX.Element { - return ( - - ); - } - - private triggerRender() { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index 08d3750a3..b50d21678 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -1,6 +1,169 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`NotificationConsoleComponent renders the console (expanded) 1`] = ` +exports[`NotificationConsoleComponent renders the console 1`] = ` +
+
+
+ + + in progress items + + 0 + + + + error items + + 0 + + + + info items + + 0 + + + + + + + +
+
+ ChevronUpIcon +
+
+ +
+
+ + + + clear notifications image + Clear Notifications + +
+
+
+ +
+`; + +exports[`NotificationConsoleComponent renders the console 2`] = `
@@ -64,18 +227,20 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = ` > + > + message +
ChevronDownIcon
@@ -100,7 +265,7 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = ` delay={0} duration={200} easing="ease" - height="auto" + height={0} onAnimationEnd={[Function]} style={Object {}} > diff --git a/src/Explorer/Panes/ContextualPaneBase.ts b/src/Explorer/Panes/ContextualPaneBase.ts index ac9ec7446..cc97b02b3 100644 --- a/src/Explorer/Panes/ContextualPaneBase.ts +++ b/src/Explorer/Panes/ContextualPaneBase.ts @@ -29,10 +29,6 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel { this.title = ko.observable(); this.formErrorsDetails = ko.observable(); this.isExecuting = ko.observable(false); - this.container.isNotificationConsoleExpanded.subscribe((isExpanded: boolean) => { - this.resizePane(); - }); - this.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 }); } public cancel() { diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts b/src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts index 6fffd9aec..67a8097a6 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts @@ -57,7 +57,6 @@ describe("Delete Collection Confirmation Pane", () => { describe("shouldRecordFeedback()", () => { it("should return true if last collection and database does not have shared throughput else false", () => { let fakeExplorer = new Explorer(); - fakeExplorer.isNotificationConsoleExpanded = ko.observable(false); fakeExplorer.refreshAllDatabases = () => Q.resolve(); let pane = new DeleteCollectionConfirmationPane({ @@ -101,7 +100,6 @@ describe("Delete Collection Confirmation Pane", () => { rid: "test", } as ViewModels.Collection; }; - fakeExplorer.isNotificationConsoleExpanded = ko.observable(false); fakeExplorer.selectedCollectionId = ko.computed(() => selectedCollectionId); fakeExplorer.isSelectedDatabaseShared = () => false; const SubscriptionId = "testId"; diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts b/src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts index a86c7848b..f5c0b65d1 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts @@ -55,7 +55,6 @@ describe("Delete Database Confirmation Pane", () => { describe("shouldRecordFeedback()", () => { it("should return true if last non empty database or is last database that has shared throughput, else false", () => { let fakeExplorer = {} as Explorer; - fakeExplorer.isNotificationConsoleExpanded = ko.observable(false); let pane = new DeleteDatabaseConfirmationPane({ id: "deletedatabaseconfirmationpane", @@ -92,7 +91,6 @@ describe("Delete Database Confirmation Pane", () => { } as ViewModels.Database; }; fakeExplorer.refreshAllDatabases = () => Q.resolve(); - fakeExplorer.isNotificationConsoleExpanded = ko.observable(false); fakeExplorer.selectedDatabaseId = ko.computed(() => selectedDatabaseId); fakeExplorer.isSelectedDatabaseShared = () => false; const SubscriptionId = "testId"; diff --git a/src/Explorer/Panes/GenericRightPaneComponent.tsx b/src/Explorer/Panes/GenericRightPaneComponent.tsx index a0c895149..f68bf0074 100644 --- a/src/Explorer/Panes/GenericRightPaneComponent.tsx +++ b/src/Explorer/Panes/GenericRightPaneComponent.tsx @@ -34,13 +34,6 @@ export class GenericRightPaneComponent extends React.Component { - this.setState({ panelHeight: this.getPanelHeight() }); - }); - this.props.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 }); - } - public componentWillUnmount(): void { this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose(); } diff --git a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts index ab7b2ee21..799bb1b70 100644 --- a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts +++ b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts @@ -240,10 +240,7 @@ function updateTableScrollableRegionHeight(): void { var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top; var dataTablesInfoElem = $(tabElement).find(".dataTables_info"); var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate"); - const explorer = window.dataExplorer; - const notificationConsoleHeight = explorer.isNotificationConsoleExpanded() - ? 252 /** 32px(header) + 220px(content height) **/ - : 32; /** Header height **/ + const notificationConsoleHeight = 32; /** Header height **/ var scrollHeight = bodyHeight - diff --git a/src/Main.tsx b/src/Main.tsx index ad4a9caaa..f562554f7 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -54,7 +54,8 @@ import "./Libs/is-integer-polyfill"; import "url-polyfill/url-polyfill.min"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; -import React from "react"; +import { ExplorerParams } from "./Explorer/Explorer"; +import React, { useState } from "react"; import ReactDOM from "react-dom"; import copyImage from "../images/Copy.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; @@ -63,12 +64,22 @@ import arrowLeftImg from "../images/imgarrowlefticon.svg"; import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import { useConfig } from "./hooks/useConfig"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; +import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; initializeIcons(); const App: React.FunctionComponent = () => { + const [isNotificationConsoleExpanded, setIsNotificationConsoleExpanded] = useState(false); + const [notificationConsoleData, setNotificationConsoleData] = useState(undefined); + //TODO: Refactor so we don't need to pass the id to remove a console data + const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState(""); + const explorerParams: ExplorerParams = { + setIsNotificationConsoleExpanded, + setNotificationConsoleData, + setInProgressConsoleDataIdToBeDeleted, + }; const config = useConfig(); - useKnockoutExplorer(config); + useKnockoutExplorer(config, explorerParams); return (
@@ -270,8 +281,14 @@ const App: React.FunctionComponent = () => { role="contentinfo" aria-label="Notification console" id="explorerNotificationConsole" - data-bind="react: notificationConsoleComponentAdapter" - /> + > + +
{/* Global loader - Start */} diff --git a/src/Utils/NotificationConsoleUtils.ts b/src/Utils/NotificationConsoleUtils.ts index 7446b957b..c003f7aa4 100644 --- a/src/Utils/NotificationConsoleUtils.ts +++ b/src/Utils/NotificationConsoleUtils.ts @@ -19,14 +19,13 @@ export function logConsoleMessage(type: ConsoleDataType, message: string, id?: s if (!id) { id = _.uniqueId(); } - dataExplorer.logConsoleData({ type: type, date: formattedDate, message: message, id: id }); + dataExplorer.logConsoleData({ type, date: formattedDate, message, id }); } return id || ""; } export function clearInProgressMessageWithId(id: string): void { - const dataExplorer = _global.dataExplorer; - dataExplorer && dataExplorer.deleteInProgressConsoleDataWithId(id); + _global.dataExplorer?.deleteInProgressConsoleDataWithId(id); } export function logConsoleProgress(message: string): () => void { diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 3dce2a113..5c46c3a19 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -7,7 +7,7 @@ import { configContext, ConfigContext, Platform } from "../ConfigContext"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; -import Explorer from "../Explorer/Explorer"; +import Explorer, { ExplorerParams } from "../Explorer/Explorer"; import { AAD, ConnectionString, @@ -34,8 +34,8 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; // Pleas tread carefully :) let explorer: Explorer; -export function useKnockoutExplorer(config: ConfigContext): Explorer { - explorer = explorer || new Explorer(); +export function useKnockoutExplorer(config: ConfigContext, explorerParams: ExplorerParams): Explorer { + explorer = explorer || new Explorer(explorerParams); useEffect(() => { const effect = async () => { if (config) {