/** * React component for control bar */ import * as React from "react"; import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import AnimateHeight from "react-animate-height"; import LoadingIcon from "../../../../images/loading.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import InfoIcon from "../../../../images/info_color.svg"; import ErrorRedIcon from "../../../../images/error_red.svg"; import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; import ClearIcon from "../../../../images/Clear.svg"; import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png"; import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; /** * Log levels */ export enum ConsoleDataType { Info = 0, Error = 1, InProgress = 2 } /** * Interface for the data/content that will be recorded */ export interface ConsoleData { type: ConsoleDataType; date: string; message: string; id?: string; } export interface NotificationConsoleComponentProps { isConsoleExpanded: boolean; onConsoleExpandedChange: (isExpanded: boolean) => void; consoleData: ConsoleData[]; onConsoleDataChange: (consoleData: ConsoleData[]) => void; } interface NotificationConsoleComponentState { headerStatus: string; selectedFilter: string; isExpanded: boolean; } export class NotificationConsoleComponent extends React.Component< NotificationConsoleComponentProps, NotificationConsoleComponentState > { private static readonly transitionDurationMs = 200; private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"]; private headerTimeoutId: number; private prevHeaderStatus: string; private consoleHeaderElement: HTMLElement; constructor(props: NotificationConsoleComponentProps) { super(props); this.state = { headerStatus: "", selectedFilter: NotificationConsoleComponent.FilterOptions[0], isExpanded: props.isConsoleExpanded }; this.prevHeaderStatus = null; } public componentDidUpdate( prevProps: NotificationConsoleComponentProps, prevState: NotificationConsoleComponentState ) { const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props); if ( this.prevHeaderStatus !== currentHeaderStatus && currentHeaderStatus !== null && prevState.headerStatus !== currentHeaderStatus ) { this.setHeaderStatus(currentHeaderStatus); } // Call setHeaderStatus() only to clear HeaderStatus or update status to a different value. // Cache previous headerStatus externally. Otherwise, simply comparing with previous state/props will cause circular // 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 }); } } public render(): JSX.Element { const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress) .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) .length; return (
(this.consoleHeaderElement = element)} onClick={(event: React.MouseEvent) => this.expandCollapseConsole()} onKeyDown={(event: React.KeyboardEvent) => this.onExpandCollapseKeyPress(event)} tabIndex={0} >
in progress items {numInProgress} error items {numErroredItems} info items {numInfoItems} {this.state.headerStatus}
{this.state.isExpanded
this.clearNotifications()} role="button" onKeyDown={(event: React.KeyboardEvent) => this.onClearNotificationsKeyPress(event)} tabIndex={0} > clear notifications image Clear Notifications
{this.renderAllFilteredConsoleData(this.getFilteredConsoleData())}
); } private expandCollapseConsole() { this.setState({ isExpanded: !this.state.isExpanded }); } private onExpandCollapseKeyPress = (event: React.KeyboardEvent): void => { if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) { this.expandCollapseConsole(); event.stopPropagation(); event.preventDefault(); } }; private onClearNotificationsKeyPress = (event: React.KeyboardEvent): void => { if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) { this.clearNotifications(); event.stopPropagation(); event.preventDefault(); } }; private clearNotifications(): void { this.props.onConsoleDataChange([]); } private renderAllFilteredConsoleData(rowData: ConsoleData[]): JSX.Element[] { return rowData.map((item: ConsoleData, index: number) => (
{item.type === ConsoleDataType.Info && info} {item.type === ConsoleDataType.Error && error} {item.type === ConsoleDataType.InProgress && in progress} {item.date} {item.message}
)); } private onFilterSelected(event: React.ChangeEvent): void { this.setState({ selectedFilter: event.target.value }); } private getFilteredConsoleData(): ConsoleData[] { let filterType: ConsoleDataType = null; switch (this.state.selectedFilter) { case "All": filterType = null; break; case "In Progress": filterType = ConsoleDataType.InProgress; break; case "Info": filterType = ConsoleDataType.Info; break; case "Error": filterType = ConsoleDataType.Error; break; default: filterType = null; } return filterType == null ? this.props.consoleData : this.props.consoleData.filter((data: ConsoleData) => data.type === filterType); } private setHeaderStatus(statusMessage: string): void { if (this.state.headerStatus === statusMessage) { return; } this.headerTimeoutId && clearTimeout(this.headerTimeoutId); this.setState({ headerStatus: statusMessage }); this.headerTimeoutId = window.setTimeout( () => this.setState({ headerStatus: "" }), ClientDefaults.errorNotificationTimeoutMs ); } private static extractHeaderStatus(props: NotificationConsoleComponentProps) { if (props.consoleData && props.consoleData.length > 0) { return props.consoleData[0].message.split(":\n")[0]; } else { return null; } } private onConsoleWasExpanded = (): void => { this.props.onConsoleExpandedChange(this.state.isExpanded); if (this.state.isExpanded) { this.consoleHeaderElement.focus(); } }; }