/** * React component for control bar */ import { Dropdown, IDropdownOption } from "@fluentui/react"; import * as React from "react"; import AnimateHeight from "react-animate-height"; import ClearIcon from "../../../../images/Clear-1.svg"; import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png"; import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorRedIcon from "../../../../images/error_red.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import InfoIcon from "../../../../images/info_color.svg"; import LoadingIcon from "../../../../images/loading.svg"; import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import { userContext } from "../../../UserContext"; import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; import { ConsoleData, ConsoleDataType } from "./ConsoleData"; export interface NotificationConsoleComponentProps { isConsoleExpanded: boolean; consoleData: ConsoleData; inProgressConsoleDataIdToBeDeleted: string; setIsConsoleExpanded: (isExpanded: boolean) => void; } interface NotificationConsoleComponentState { headerStatus: string; selectedFilter: string; allConsoleData: ConsoleData[]; } export class NotificationConsoleComponent extends React.Component< NotificationConsoleComponentProps, NotificationConsoleComponentState > { private static readonly transitionDurationMs = 200; private static readonly FilterOptions = [ { key: "All", text: "All" }, { key: "In Progress", text: "In progress" }, { key: "Info", text: "Info" }, { key: "Error", text: "Error" }, ]; private headerTimeoutId?: number; private prevHeaderStatus: string; private consoleHeaderElement?: HTMLElement; constructor(props: NotificationConsoleComponentProps) { super(props); this.state = { headerStatus: undefined, selectedFilter: NotificationConsoleComponent.FilterOptions[0].key, allConsoleData: props.consoleData ? [props.consoleData] : [], }; this.prevHeaderStatus = undefined; } public componentDidUpdate( prevProps: NotificationConsoleComponentProps, prevState: NotificationConsoleComponentState ): void { const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData); if ( this.prevHeaderStatus !== currentHeaderStatus && currentHeaderStatus !== undefined && 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 (this.props.consoleData || this.props.inProgressConsoleDataIdToBeDeleted) { this.updateConsoleData(prevProps); } } public setElememntRef = (element: HTMLElement): void => { this.consoleHeaderElement = element; }; public render(): JSX.Element { 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 numInfoItems = this.state.allConsoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info) .length; return (
this.expandCollapseConsole()} onKeyDown={(event: React.KeyboardEvent) => this.onExpandCollapseKeyPress(event)} tabIndex={0} >
in progress items {numInProgress} error items {numErroredItems} info items {numInfoItems} {userContext.features.pr && } {this.state.headerStatus}
{this.props.isConsoleExpanded
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.props.setIsConsoleExpanded(!this.props.isConsoleExpanded); } 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.setState({ allConsoleData: [] }); } 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, option: IDropdownOption): void => { this.setState({ selectedFilter: String(option.key) }); }; private getFilteredConsoleData(): ConsoleData[] { let filterType: ConsoleDataType; switch (this.state.selectedFilter) { case "In Progress": filterType = ConsoleDataType.InProgress; break; case "Info": filterType = ConsoleDataType.Info; break; case "Error": filterType = ConsoleDataType.Error; break; default: filterType = undefined; } return filterType ? this.state.allConsoleData.filter((data: ConsoleData) => data.type === filterType) : this.state.allConsoleData; } 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(consoleData: ConsoleData) { return consoleData?.message.split(":\n")[0]; } private onConsoleWasExpanded = (): void => { if (this.props.isConsoleExpanded && this.consoleHeaderElement) { this.consoleHeaderElement.focus(); } useNotificationConsole.getState().setConsoleAnimationFinished(true); }; 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 ); }; } const PrPreview = (props: { pr: string }) => { const url = new URL(props.pr); const [, ref] = url.hash.split("#"); url.hash = ""; return ( <> {ref} ); }; export const NotificationConsole: React.FC = () => { const setIsExpanded = useNotificationConsole((state) => state.setIsExpanded); const isExpanded = useNotificationConsole((state) => state.isExpanded); const consoleData = useNotificationConsole((state) => state.consoleData); const inProgressConsoleDataIdToBeDeleted = useNotificationConsole( (state) => state.inProgressConsoleDataIdToBeDeleted ); // TODO Refactor NotificationConsoleComponent into a functional component and remove this wrapper // This component only exists so we can use hooks and pass them down to a non-functional component return ( ); };