Move notification console to react (#400)

This commit is contained in:
victor-meng 2021-01-26 15:32:37 -08:00 committed by GitHub
parent 59ec18cd9b
commit bd4d8da065
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 370 additions and 218 deletions

View File

@ -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],

View File

@ -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<string> = ko.observable<string>(
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
@ -146,8 +151,9 @@ export default class Explorer {
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
// Notification Console
public notificationConsoleData: ko.ObservableArray<ConsoleData>;
public isNotificationConsoleExpanded: ko.Observable<boolean>;
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<DialogProps>;
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<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
@ -478,7 +486,6 @@ export default class Explorer {
bounds: splitterBounds,
direction: SplitterDirection.Vertical,
});
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
this.defaultExperience = ko.observable<string>();
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() {

View File

@ -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(<NotificationConsoleComponent {...props} />);
expect(wrapper).toMatchSnapshot();
props.consoleData = {
type: ConsoleDataType.Info,
date: "date",
message: "message",
});
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
};
wrapper.setProps(props);
expect(wrapper).toMatchSnapshot();
});
it("shows proper progress count", () => {
const count = 100;
const props = createBlankProps();
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
wrapper.find(".clearNotificationsButton").simulate("click");
expect(!wrapper.exists(".notificationConsoleData"));
});
it("collapses and hide content", () => {
const props = createBlankProps();
props.consoleData.push({
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
props.consoleData = {
type: ConsoleDataType.Info,
date: "date",
message: "message",
type: ConsoleDataType.Info,
});
};
props.isConsoleExpanded = true;
wrapper.setProps(props);
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
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(<NotificationConsoleComponent {...props} />);
props.consoleData = {
type: ConsoleDataType.Info,
date: "date",
message: latestData,
type: ConsoleDataType.Info,
});
props2.isConsoleExpanded = true;
};
props.isConsoleExpanded = true;
wrapper.setProps(props);
const wrapper = shallow(<NotificationConsoleComponent {...props1} />);
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(<NotificationConsoleComponent {...props} />);
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("1");
props.inProgressConsoleDataIdToBeDeleted = "1";
wrapper.setProps(props);
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
});
});

View File

@ -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 (
<div className="notificationConsoleContainer">
<div
@ -143,18 +143,18 @@ export class NotificationConsoleComponent extends React.Component<
className="expandCollapseButton"
role="button"
tabIndex={0}
aria-label={"console button" + (this.state.isExpanded ? " collapsed" : " expanded")}
aria-expanded={!this.state.isExpanded}
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
aria-expanded={!this.props.isConsoleExpanded}
>
<img
src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.state.isExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
/>
</div>
</div>
<AnimateHeight
duration={NotificationConsoleComponent.transitionDurationMs}
height={this.state.isExpanded ? "auto" : 0}
height={this.props.isConsoleExpanded ? "auto" : 0}
onAnimationEnd={this.onConsoleWasExpanded}
>
<div className="notificationConsoleContents">
@ -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<HTMLDivElement>): 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
);
};
}

View File

@ -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<number>;
public container: Explorer;
private consoleData: ko.ObservableArray<ConsoleData>;
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 (
<NotificationConsoleComponent
isConsoleExpanded={this.container.isNotificationConsoleExpanded()}
onConsoleExpandedChange={this.onConsoleExpandedChange.bind(this)}
consoleData={this.consoleData()}
onConsoleDataChange={this.onConsoleDataChange.bind(this)}
/>
);
}
private triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@ -1,6 +1,169 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
exports[`NotificationConsoleComponent renders the console 1`] = `
<div
className="notificationConsoleContainer"
>
<div
className="notificationConsoleHeader"
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}
>
<div
className="statusBar"
>
<span
className="dataTypeIcons"
>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="in progress items"
src=""
/>
<span
className="numInProgress"
>
0
</span>
</span>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="error items"
src=""
/>
<span
className="numErroredItems"
>
0
</span>
</span>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="info items"
src=""
/>
<span
className="numInfoItems"
>
0
</span>
</span>
</span>
<span
className="consoleSplitter"
/>
<span
className="headerStatus"
>
<span
className="headerStatusEllipsis"
/>
</span>
</div>
<div
aria-expanded={true}
aria-label="console button expanded"
className="expandCollapseButton"
role="button"
tabIndex={0}
>
<img
alt="ChevronUpIcon"
src=""
/>
</div>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
Object {
"animating": "rah-animating",
"animatingDown": "rah-animating--down",
"animatingToHeightAuto": "rah-animating--to-height-auto",
"animatingToHeightSpecific": "rah-animating--to-height-specific",
"animatingToHeightZero": "rah-animating--to-height-zero",
"animatingUp": "rah-animating--up",
"static": "rah-static",
"staticHeightAuto": "rah-static--height-auto",
"staticHeightSpecific": "rah-static--height-specific",
"staticHeightZero": "rah-static--height-zero",
}
}
applyInlineTransitions={true}
delay={0}
duration={200}
easing="ease"
height={0}
onAnimationEnd={[Function]}
style={Object {}}
>
<div
className="notificationConsoleContents"
>
<div
className="notificationConsoleControls"
>
<StyledWithResponsiveMode
aria-label="All"
aria-labelledby="consoleFilterLabel"
label="Filter:"
onChange={[Function]}
options={
Array [
Object {
"key": "All",
"text": "All",
},
Object {
"key": "In Progress",
"text": "In progress",
},
Object {
"key": "Info",
"text": "Info",
},
Object {
"key": "Error",
"text": "Error",
},
]
}
role="combobox"
selectedKey="All"
/>
<span
className="consoleSplitter"
/>
<span
className="clearNotificationsButton"
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
<img
alt="clear notifications image"
src=""
/>
Clear Notifications
</span>
</div>
<div
className="notificationConsoleData"
/>
</div>
</AnimateHeight>
</div>
`;
exports[`NotificationConsoleComponent renders the console 2`] = `
<div
className="notificationConsoleContainer"
>
@ -64,18 +227,20 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
>
<span
className="headerStatusEllipsis"
/>
>
message
</span>
</span>
</div>
<div
aria-expanded={false}
aria-label="console button collapsed"
aria-expanded={true}
aria-label="console button expanded"
className="expandCollapseButton"
role="button"
tabIndex={0}
>
<img
alt="ChevronDownIcon"
alt="ChevronUpIcon"
src=""
/>
</div>
@ -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 {}}
>

View File

@ -29,10 +29,6 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
this.title = ko.observable<string>();
this.formErrorsDetails = ko.observable<string>();
this.isExecuting = ko.observable<boolean>(false);
this.container.isNotificationConsoleExpanded.subscribe((isExpanded: boolean) => {
this.resizePane();
});
this.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
}
public cancel() {

View File

@ -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<boolean>(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<boolean>(false);
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
fakeExplorer.isSelectedDatabaseShared = () => false;
const SubscriptionId = "testId";

View File

@ -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<boolean>(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<boolean>(false);
fakeExplorer.selectedDatabaseId = ko.computed<string>(() => selectedDatabaseId);
fakeExplorer.isSelectedDatabaseShared = () => false;
const SubscriptionId = "testId";

View File

@ -34,13 +34,6 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
};
}
public componentDidMount(): void {
this.notificationConsoleSubscription = this.props.container.isNotificationConsoleExpanded.subscribe(() => {
this.setState({ panelHeight: this.getPanelHeight() });
});
this.props.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
}
public componentWillUnmount(): void {
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
}

View File

@ -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 -

View File

@ -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 (
<div className="flexContainer">
@ -270,9 +281,15 @@ const App: React.FunctionComponent = () => {
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
data-bind="react: notificationConsoleComponentAdapter"
>
<NotificationConsoleComponent
isConsoleExpanded={isNotificationConsoleExpanded}
consoleData={notificationConsoleData}
inProgressConsoleDataIdToBeDeleted={inProgressConsoleDataIdToBeDeleted}
setIsConsoleExpanded={setIsNotificationConsoleExpanded}
/>
</div>
</div>
{/* Global loader - Start */}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">

View File

@ -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 {

View File

@ -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) {