Files
cosmos-explorer/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx
sakshigupta12feb 2c31ec2a8d Dark theme for Explorer (#2185)
* First dark theme commits for command bar

* Updated theme on sidebar

* Updated tabs, sidebar, splash screen

* settings theme changes

* Dark theme applied to Monaco editor

* Dark theme to stored procedures

* Fixed sidebar scroll

* Updated scroll issue in sidebar

* Command bar items fixed

* Fixed lint errors

* fixed lint errors

* settings side panel fixed

* Second last iteration for css

* Fixed all the issues of css

* Updated the theme icon for now on DE to change the theme from portal/DE itself

* Formatting issue resolved

* Remove CloudShellTerminalComponent changes - revert to master version

* Fixed test issue

* Fixed formatting issue

* Fix tests: update snapshots and revert xterm imports for compatibility

* Fix xterm imports in CloudShellTerminalComponent to use @xterm packages

* Fix Cloud Shell component imports for compatibility

* Update test snapshots

* Fix xterm package consistency across all CloudShell components

* Fix TypeScript compilation errors in CloudShell components and query Documents

- Standardized xterm package imports across CloudShell components to use legacy 'xterm' package
- Fixed Terminal type compatibility issues in CommonUtils.tsx
- Added type casting for enableQueryControl property in queryDocuments.ts to handle Azure Cosmos SDK interface limitations
- Applied code formatting to ensure consistency

* Update failing snapshot tests

- Updated TreeNodeComponent snapshot tests for loading states
- Updated ThroughputInputAutoPilotV3Component snapshots for number formatting changes (10,00,000 -> 1,000,000)
- All snapshot tests now pass

* Fixed test issue

* Fixed test issue

* Updated the buttons for theme

* Updated the Theme changes based on portal theme changes

* Updated review comments

* Updated the duplicate code and fixed the fabric react error

* Few places styling added and resolving few comments

* Fixed errors

* Fixed comments

* Fixed comments

* Fixed comments

* Fixed full text policy issue for mongoru accounts

* Resolved comments for class Name and few others

* Added css for homepage in ru accounts

* Final commit with all the feedback issues resolved

* Lint error resolved

* Updated the review comments and few Ui issues

* Resolved review comments and changed header bg and active state color

* Moved svg code to different file and imported

* css fixed for the hpome page boxed for ru account

* Lint errors

* Fixed boxes issue in ru accounts

* Handled the initial theme from the portal

* Updated snap

* Update snapshots for TreeNodeComponent and CreateCopyJobScreensProvider tests

* Fix duplicate DataExplorerRoot test id causing Playwright strict mode violation

* Fix locale-dependent number formatting in ThroughputInputAutoPilotV3Component

---------

Co-authored-by: Sakshi Gupta <sakshig+microsoft@microsoft.com>
Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2025-12-16 12:21:58 +05:30

388 lines
15 KiB
TypeScript

/**
* 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 WarningIcon from "../../../../images/warning.svg";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
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 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;
const numWarningItems = this.state.allConsoleData.filter(
(data: ConsoleData) => data.type === ConsoleDataType.Warning,
).length;
return (
<div className="notificationConsoleContainer">
<div
className="notificationConsoleHeader"
id="notificationConsoleHeader"
role="button"
aria-label="Console"
aria-expanded={this.props.isConsoleExpanded}
onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0}
>
<div className="statusBar">
<span className="dataTypeIcons">
<span className="notificationConsoleHeaderIconWithData">
<img src={LoadingIcon} alt="In progress items" />
<span className="numInProgress">{numInProgress}</span>
</span>
<span className="notificationConsoleHeaderIconWithData">
<img src={ErrorBlackIcon} alt="Error items" />
<span className="numErroredItems">{numErroredItems}</span>
</span>
<span className="notificationConsoleHeaderIconWithData">
<img src={infoBubbleIcon} alt="Info items" />
<span className="numInfoItems">{numInfoItems}</span>
</span>
<span className="notificationConsoleHeaderIconWithData">
<img src={WarningIcon} alt="Warning items" />
<span className="numWarningItems">{numWarningItems}</span>
</span>
</span>
<span className="consoleSplitter" />
<span className="headerStatus" data-test="notification-console/header-status">
<span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
{this.state.headerStatus}
</span>
</span>
</div>
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
<img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
/>
</div>
</div>
<AnimateHeight
duration={NotificationConsoleComponent.transitionDurationMs}
height={this.props.isConsoleExpanded ? "auto" : 0}
onAnimationEnd={this.onConsoleWasExpanded}
>
<div data-test="NotificationConsole/Contents" className="notificationConsoleContents">
<div className="notificationConsoleControls">
<Dropdown
label="Filter:"
selectedKey={this.state.selectedFilter}
options={NotificationConsoleComponent.FilterOptions}
onChange={this.onFilterSelected.bind(this)}
styles={{
root: {
color: "var(--colorNeutralForeground1)",
},
label: {
color: "var(--colorNeutralForeground1)",
},
dropdown: {
backgroundColor: "var(--colorNeutralBackground2)",
borderColor: "var(--colorNeutralStroke1)",
color: "var(--colorNeutralForeground1)",
},
title: {
backgroundColor: "var(--colorNeutralBackground2)",
color: "var(--colorNeutralForeground1)",
borderColor: "var(--colorNeutralStroke1)",
fontSize: "14px",
selectors: {
"&:hover": {
backgroundColor: "var(--colorNeutralBackground3)",
color: "var(--colorNeutralForeground1)",
borderColor: "var(--colorNeutralStroke1)",
},
"&:focus": {
backgroundColor: "var(--colorNeutralBackground2)",
color: "var(--colorNeutralForeground1)",
borderColor: "var(--colorBrandStroke1)",
},
"&:after": {
borderColor: "var(--colorNeutralStroke1)",
},
span: {
color: "var(--colorNeutralForeground1)",
},
},
},
caretDown: {
color: "var(--colorNeutralForeground1)",
},
callout: {
backgroundColor: "var(--colorNeutralBackground2)",
border: "1px solid var(--colorNeutralStroke1)",
},
dropdownItems: {
backgroundColor: "var(--colorNeutralBackground2)",
},
dropdownItem: {
backgroundColor: "transparent",
color: "var(--colorNeutralForeground1)",
selectors: {
"&:hover": {
backgroundColor: "var(--colorNeutralBackground4)",
color: "var(--colorNeutralForeground1)",
},
"&:focus": {
backgroundColor: "var(--colorNeutralBackground4)",
color: "var(--colorNeutralForeground1)",
},
".ms-Dropdown-optionText": {
color: "var(--colorNeutralForeground1)",
},
},
},
dropdownItemSelected: {
backgroundColor: "var(--colorBrandBackground)",
color: "var(--colorNeutralForegroundOnBrand)",
selectors: {
".ms-Dropdown-optionText": {
color: "var(--colorNeutralForegroundOnBrand)",
},
},
},
dropdownOptionText: {
color: "var(--colorNeutralForeground1)",
},
}}
/>
<span className="consoleSplitter" />
<span
className="clearNotificationsButton"
onClick={() => this.clearNotifications()}
role="button"
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
tabIndex={0}
style={{ border: "1px solid black", borderRadius: "2px" }}
>
<img src={ClearIcon} alt="clear notifications image" />
Clear Notifications
</span>
</div>
<div className="notificationConsoleData">
{this.renderAllFilteredConsoleData(this.getFilteredConsoleData())}
</div>
</div>
</AnimateHeight>
</div>
);
}
private expandCollapseConsole() {
this.props.setIsConsoleExpanded(!this.props.isConsoleExpanded);
}
private onExpandCollapseKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
this.expandCollapseConsole();
event.stopPropagation();
event.preventDefault();
}
};
private onClearNotificationsKeyPress = (event: React.KeyboardEvent<HTMLSpanElement>): 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) => (
<div className="rowData" key={index}>
{item.type === ConsoleDataType.Info && <img className="infoIcon" src={InfoIcon} alt="info" />}
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
{item.type === ConsoleDataType.Warning && <img className="warningIcon" src={WarningIcon} alt="warning" />}
<span className="date">{item.date}</span>
<span className="message" role="alert" aria-live="assertive">
{item.message}
</span>
</div>
));
}
private onFilterSelected = (event: React.ChangeEvent<HTMLSelectElement>, 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 => {
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
);
};
}
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 (
<NotificationConsoleComponent
consoleData={consoleData}
inProgressConsoleDataIdToBeDeleted={inProgressConsoleDataIdToBeDeleted}
isConsoleExpanded={isExpanded}
setIsConsoleExpanded={setIsExpanded}
/>
);
};