Загрузка данных
import { BehaviorSubject, Observable } from 'rxjs';
import { ChartSeriesType, Intervals, SymbolInfo, TimeFormat, Timeframes } from '@src/types';
import { DateFormat } from '@src/utils';
export type UndoKey = keyof UndoConfig;
export interface UndoRedoState {
canUndo: boolean;
canRedo: boolean;
}
interface UndoConfig {
timeframe: (value: Timeframes) => void;
seriesSelected: (value: ChartSeriesType) => void;
symbolInfo: (value: SymbolInfo) => void;
timeFormat: (value: TimeFormat) => void;
dateFormat: (value: DateFormat) => void;
interval: (value: Intervals | null) => void;
}
interface HistoryItem {
kind: 'item';
key: UndoKey;
prev: unknown;
next: unknown;
}
interface HistoryCommand {
kind: 'command';
undo: () => void;
redo: () => void;
}
interface HistoryGroup {
kind: 'group';
entries: HistoryEntry[];
}
type HistoryEntry = HistoryItem | HistoryCommand | HistoryGroup;
export class UndoRedo {
private undoStack: HistoryEntry[] = [];
private redoStack: HistoryEntry[] = [];
private groupStack: HistoryEntry[][] = [];
private state$ = new BehaviorSubject<UndoRedoState>({
canUndo: false,
canRedo: false,
});
constructor(private readonly config: UndoConfig) {}
private beginGroup(): void {
this.groupStack.push([]);
}
private endGroup(): void {
const entries = this.groupStack.pop();
if (!entries || entries.length === 0) {
return;
}
this.addEntry({
kind: 'group',
entries,
});
}
public group<T>(fn: () => T): T {
this.beginGroup();
try {
return fn();
} finally {
this.endGroup();
}
}
public push(key: UndoKey, prev: unknown, next: unknown): void {
if (Object.is(prev, next)) {
return;
}
this.addEntry({
kind: 'item',
key,
prev,
next,
});
}
public pushCommand({
undo,
redo,
}: {
undo: () => void;
redo: () => void;
}): void {
this.addEntry({
kind: 'command',
undo,
redo,
});
}
public undo(): void {
const entry = this.undoStack.pop();
if (!entry) {
return;
}
this.applyEntry(entry, 'undo');
this.redoStack.push(entry);
this.updateState();
}
public redo(): void {
const entry = this.redoStack.pop();
if (!entry) {
return;
}
this.applyEntry(entry, 'redo');
this.undoStack.push(entry);
this.updateState();
}
public canUndo(): boolean {
return this.undoStack.length > 0;
}
public canRedo(): boolean {
return this.redoStack.length > 0;
}
public getState(): Observable<UndoRedoState> {
return this.state$.asObservable();
}
public clear(): void {
this.undoStack = [];
this.redoStack = [];
this.groupStack = [];
this.updateState();
}
private addEntry(entry: HistoryEntry): void {
if (this.redoStack.length > 0) {
this.redoStack = [];
}
const currentGroup = this.groupStack[this.groupStack.length - 1];
if (currentGroup) {
currentGroup.push(entry);
this.updateState();
return;
}
this.undoStack.push(entry);
this.updateState();
}
private applyEntry(entry: HistoryEntry, direction: 'undo' | 'redo'): void {
if (entry.kind === 'group') {
const entries = entry.entries;
if (direction === 'undo') {
for (let index = entries.length - 1; index >= 0; index -= 1) {
this.applyEntry(entries[index], direction);
}
} else {
for (let index = 0; index < entries.length; index += 1) {
this.applyEntry(entries[index], direction);
}
}
return;
}
if (entry.kind === 'command') {
if (direction === 'undo') {
entry.undo();
} else {
entry.redo();
}
return;
}
const apply = this.config[entry.key] as (value: unknown) => void;
apply(direction === 'undo' ? entry.prev : entry.next);
}
private updateState(): void {
const nextState: UndoRedoState = {
canUndo: this.canUndo(),
canRedo: this.canRedo(),
};
const currentState = this.state$.value;
if (
currentState.canUndo === nextState.canUndo &&
currentState.canRedo === nextState.canRedo
) {
return;
}
this.state$.next(nextState);
}
}
import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
import { cloneDeep, isEqual } from 'lodash-es';
import { BehaviorSubject, distinctUntilChanged, map, Observable, Subscription } from 'rxjs';
import { EventManager } from '@core';
import { DOMModel } from '@core/DOMModel';
import { Drawing } from '@core/Drawings';
import { Hotkeys, Keys } from '@core/Hotkeys';
import { EntitySettingsModal } from '@src/components/EntitySettingsModal';
import { drawingLabelById, drawingsMap, DrawingsNames } from '@src/constants';
import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool } from '@src/types';
import type { DrawingInteraction } from '@src/core/Drawings/common';
interface DrawingsManagerParams {
eventManager: EventManager;
mainSeries$: Observable<SeriesStrategies | null>;
lwcChart: IChartApi;
DOM: DOMModel;
container: HTMLElement;
modalRenderer: ModalRenderer;
paneId: number;
hotkeys: Hotkeys;
}
export interface DrawingSnapshotItem {
id: string;
drawingName: DrawingsNames;
state: unknown;
isLocked?: boolean;
}
interface CreateDrawingOptions {
id?: string;
state?: unknown;
isLocked?: boolean;
shouldUpdateDrawingsList?: boolean;
historyStartSnapshot?: DrawingsManagerSnapshot;
}
export type DrawingsManagerSnapshot = DrawingSnapshotItem[];
export class DrawingsManager {
private eventManager: EventManager;
private lwcChart: IChartApi;
private DOM: DOMModel;
private container: HTMLElement;
private modalRenderer: ModalRenderer;
private mainSeries: SeriesStrategies | null = null;
private subscriptions = new Subscription();
private drawings$ = new BehaviorSubject<Drawing[]>([]);
private selectedDrawing$ = new BehaviorSubject<Drawing | null>(null);
private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
private endlessMode$ = new BehaviorSubject(false);
private recreateScheduled = false;
private pendingSnapshot: DrawingsManagerSnapshot | null = null;
private pointerDownSnapshot: DrawingsManagerSnapshot | null = null;
private paneId: number;
private hotkeys: Hotkeys;
private copyPasteBuffer: DrawingSnapshotItem | null = null;
private escapeUnregisterHash: string | null = null;
constructor({
eventManager,
mainSeries$,
lwcChart,
DOM,
container,
modalRenderer,
paneId,
hotkeys,
}: DrawingsManagerParams) {
this.DOM = DOM;
this.eventManager = eventManager;
this.paneId = paneId;
this.lwcChart = lwcChart;
this.container = container;
this.modalRenderer = modalRenderer;
this.hotkeys = hotkeys;
this.subscriptions.add(
mainSeries$.subscribe((series) => {
if (!series) {
return;
}
this.mainSeries = series;
this.drawings$.value.forEach((drawing) => drawing.rebind(series));
if (this.pendingSnapshot) {
const snapshot = this.pendingSnapshot;
this.pendingSnapshot = null;
this.setSnapshot(snapshot);
}
}),
);
window.addEventListener('pointerup', this.handlePointerUp);
window.addEventListener('pointercancel', this.handlePointerUp);
this.container.addEventListener('click', this.handleClick);
this.container.addEventListener('pointerdown', this.handlePointerDown);
// todo: implement ctrl+v
// hotkeys.register({
// keys: [Keys.control, Keys.v],
// callback: () => {
// const bufferWithAppliedPosition = {
// ...this.copyPasteBuffer,
// state: {
// ...this.copyPasteBuffer?.state,
// startAnchor: {
// price: 73.36210252637723,
// time: 1783679170
// }
// }
// }
//
// this.setSnapshot([
// ...this.getSnapshot(),
// bufferWithAppliedPosition
// ])
// // hotkeys.unregister({ // todo: unregister all else ctrl+c's
// // keys: [Keys.control, Keys.c]
// // })
// }
// })
}
private handlePointerDown = (): void => {
const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());
this.pointerDownSnapshot = hasPendingDrawing ? null : this.createHistorySnapshot();
this.DOM.refreshEntities();
};
private handlePointerUp = (): void => {
const historyStartSnapshot = this.pointerDownSnapshot;
this.pointerDownSnapshot = null;
queueMicrotask(() => {
if (!historyStartSnapshot) {
return;
}
this.recordHistory(historyStartSnapshot, this.createHistorySnapshot());
});
this.DOM.refreshEntities();
this.updateActiveTool();
};
private handleClick = (): void => {
this.DOM.refreshEntities();
this.updateActiveTool();
};
private createHistorySnapshot(): DrawingsManagerSnapshot {
return cloneDeep(this.getSnapshot());
}
private recordHistory(
previousSnapshot: DrawingsManagerSnapshot,
nextSnapshot: DrawingsManagerSnapshot,
): void {
if (isEqual(previousSnapshot, nextSnapshot)) {
return;
}
this.eventManager.getUndoRedo().pushCommand({
undo: () => {
this.applyHistorySnapshot(previousSnapshot);
},
redo: () => {
this.applyHistorySnapshot(nextSnapshot);
},
});
}
private applyHistorySnapshot(snapshot: DrawingsManagerSnapshot): void {
this.pointerDownSnapshot = null;
this.setSnapshot(cloneDeep(snapshot));
}
private runWithHistory(callback: () => void): void {
const previousSnapshot = this.createHistorySnapshot();
callback();
const nextSnapshot = this.createHistorySnapshot();
this.recordHistory(previousSnapshot, nextSnapshot);
}
private updateActiveTool = (): void => {
const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());
if (hasPendingDrawing) {
return;
}
const activeTool = this.activeTool$.value;
const isSingleInstanceTool = activeTool !== 'crosshair' && drawingsMap[activeTool]?.singleInstance;
if (activeTool !== 'crosshair' && this.endlessMode$.value && !isSingleInstanceTool) {
if (this.recreateScheduled) {
return;
}
this.recreateScheduled = true;
queueMicrotask(() => {
this.recreateScheduled = false;
const currentTool = this.activeTool$.value;
const hasPendingAfterTick = this.drawings$.value.some((drawing) => drawing.isCreationPending());
if (currentTool === 'crosshair') {
return;
}
if (!this.endlessMode$.value) {
return;
}
if (drawingsMap[currentTool]?.singleInstance) {
return;
}
if (hasPendingAfterTick) {
return;
}
this.createDrawing(currentTool);
});
return;
}
this.activeTool$.next('crosshair');
};
private removeDrawing = (id: string): void => {
const drawing = this.drawings$.value.find((item) => item.id === id);
if (!drawing) {
return;
}
this.runWithHistory(() => {
this.removeDrawings([drawing]);
});
};
private removeDrawingsByName(name: DrawingsNames, shouldUpdateTool = true): void {
const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.getDrawingName() === name);
this.removeDrawings(drawingsToRemove, shouldUpdateTool);
}
private removePendingDrawings(shouldUpdateTool = true): void {
const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.isCreationPending());
this.removeDrawings(drawingsToRemove, shouldUpdateTool);
}
private removeDrawings(drawingsToRemove: Drawing[], shouldUpdateTool = true): void {
if (!drawingsToRemove.length) {
return;
}
const selectedDrawing = this.selectedDrawing$.value;
if (selectedDrawing && drawingsToRemove.includes(selectedDrawing)) {
this.selectedDrawing$.next(null);
}
drawingsToRemove.forEach((drawing) => {
drawing.destroy();
this.DOM.removeEntity(drawing);
});
this.drawings$.next(this.drawings$.value.filter((drawing) => !drawingsToRemove.includes(drawing)));
if (shouldUpdateTool) {
this.updateActiveTool();
}
this.DOM.refreshEntities();
}
public addDrawingForce = (name: DrawingsNames): Promise<void> => {
const historyStartSnapshot = this.createHistorySnapshot();
this.removePendingDrawings(false);
if (drawingsMap[name].singleInstance) {
this.removeDrawingsByName(name, false);
}
this.activeTool$.next(name);
const drawing = this.createDrawing(name, {
historyStartSnapshot,
});
this.DOM.refreshEntities();
return drawing.waitForCreation();
};
private createDrawing(name: DrawingsNames, options: CreateDrawingOptions = {}): Drawing {
if (!this.mainSeries) {
throw new Error('[Drawings] main series is not defined');
}
const {
id,
state,
isLocked = false,
shouldUpdateDrawingsList = true,
historyStartSnapshot,
} = options;
const shouldSelectAfterCreation = state === undefined;
const creationHistoryStartSnapshot = shouldSelectAfterCreation
? historyStartSnapshot ?? this.createHistorySnapshot()
: null;
if (shouldSelectAfterCreation && this.selectedDrawing$.value) {
this.selectedDrawing$.next(null);
}
const config = drawingsMap[name];
const drawingId = id ?? crypto.randomUUID();
let createdDrawing: Drawing | null = null;
const selected$ = this.selectedDrawing$.pipe(
map((drawing) => drawing?.id === drawingId),
distinctUntilChanged(),
);
const construct = (
chart: IChartApi,
series: ISeriesApi<SeriesType>,
interaction: DrawingInteraction,
) => {
const paneElement = series.getPane().getHTMLElement();
if (!paneElement) {
throw new Error('[Drawing Manager]: cannot place drawing, there is no pane');
}
const cells = paneElement.querySelectorAll<HTMLTableCellElement>(':scope > td');
const canvasElement = cells.item(1);
return config.construct({
chart,
series,
eventManager: this.eventManager,
container: canvasElement,
interaction,
removeSelf: () => this.removeDrawing(drawingId),
openSettings: () => {
if (createdDrawing) {
this.openSettings(createdDrawing);
}
},
});
};
const drawingFactory = (
zIndex: number,
moveUp: (id: string) => void,
moveDown: (id: string) => void,
) =>
new Drawing({
lwcChart: this.lwcChart,
mainSeries: this.mainSeries as SeriesStrategies,
id: drawingId,
drawingName: name,
name: drawingLabelById()[name],
onDelete: this.removeDrawing,
zIndex,
moveDown,
moveUp,
construct,
selected$,
isSelected: () => this.selectedDrawing$.value?.id === drawingId,
select: () => {
if (
!createdDrawing ||
createdDrawing.isCreationPending() ||
this.selectedDrawing$.value === createdDrawing
) {
return;
}
this.selectedDrawing$.next(createdDrawing);
},
deselect: () => {
if (!createdDrawing || this.selectedDrawing$.value !== createdDrawing) {
return;
}
this.selectedDrawing$.next(null);
},
isLocked,
paneId: this.paneId,
hotkeys: this.hotkeys,
setCopyPasteBuffer: (copyPasteBuffer) => {
this.copyPasteBuffer = copyPasteBuffer;
},
resetActiveTool: () => {
this.activeTool$.next('crosshair');
},
});
const entity = this.DOM.setEntity<Drawing>(drawingFactory);
createdDrawing = entity;
if (state !== undefined) {
entity.setState(state);
}
if (shouldUpdateDrawingsList) {
this.drawings$.next([...this.drawings$.value, entity]);
}
if (shouldSelectAfterCreation) {
entity.waitForCreation().then(() => {
if (creationHistoryStartSnapshot) {
this.recordHistory(
creationHistoryStartSnapshot,
this.createHistorySnapshot(),
);
}
if (!this.drawings$.value.includes(entity)) {
return;
}
this.selectedDrawing$.next(entity);
this.updateActiveTool();
this.DOM.refreshEntities();
});
}
return entity;
}
public getSnapshot(): DrawingsManagerSnapshot {
return this.drawings$.value
.filter((drawing) => !drawing.isCreationPending())
.map((drawing) => ({
id: drawing.id,
drawingName: drawing.getDrawingName(),
state: drawing.getState(),
isLocked: drawing.isLocked(),
}));
}
public setSnapshot(snapshot: DrawingsManagerSnapshot): void {
if (!Array.isArray(snapshot)) {
return;
}
if (!this.mainSeries) {
this.pendingSnapshot = snapshot;
return;
}
this.removeDrawings(this.drawings$.value, false);
const restoredDrawings = snapshot.reduce<Drawing[]>((drawings, item) => {
if (!drawingsMap[item.drawingName]) {
return drawings;
}
drawings.push(
this.createDrawing(item.drawingName, {
id: item.id,
state: item.state,
isLocked: item.isLocked,
shouldUpdateDrawingsList: false,
}),
);
return drawings;
}, []);
this.drawings$.next(restoredDrawings);
this.activeTool$.next('crosshair');
this.DOM.refreshEntities();
}
public setEndlessDrawingMode = (value: boolean): void => {
if (value) {
this.escapeUnregisterHash = this.hotkeys.register({
keys: [Keys.escape],
callback: () => {
this.setEndlessDrawingMode(false);
},
});
} else {
this.hotkeys.unregister({
keys: [Keys.escape],
hash: this.escapeUnregisterHash,
});
this.escapeUnregisterHash = null;
}
this.endlessMode$.next(value);
};
public isEndlessDrawingsMode(): Observable<boolean> {
return this.endlessMode$.asObservable();
}
public getActiveTool(): Observable<ActiveDrawingTool> {
return this.activeTool$.asObservable();
}
public activateCrosshair(): void {
this.removePendingDrawings(false);
this.activeTool$.next('crosshair');
this.DOM.refreshEntities();
}
public entities(): Observable<Drawing[]> {
return this.drawings$.asObservable();
}
public selectedDrawing(): Observable<Drawing | null> {
return this.selectedDrawing$.asObservable();
}
public openSelectedDrawingSettings(): void {
const drawing = this.selectedDrawing$.value;
if (!drawing) {
return;
}
this.openSettings(drawing);
}
public deleteSelectedDrawing(): void {
const drawing = this.selectedDrawing$.value;
if (!drawing) {
return;
}
this.removeDrawing(drawing.id);
}
public toggleSelectedDrawingLock(): void {
const drawing = this.selectedDrawing$.value;
if (!drawing) {
return;
}
this.runWithHistory(() => {
drawing.toggleLock();
});
}
private openSettings = (drawing: Drawing): void => {
const tabs = drawing.getSettingsTabs();
if (!tabs.length || tabs.every((tab) => tab.fields.length === 0)) {
return;
}
let settings = drawing.getSettings();
this.modalRenderer.renderComponent(
<EntitySettingsModal
tabs={tabs}
values={settings}
onChange={(nextSettings) => {
settings = nextSettings;
}}
initialTabKey={tabs[0]?.key}
/>,
{
size: 'sm',
title: drawing.name,
onSave: () => {
this.runWithHistory(() => {
drawing.updateSettings(settings);
});
},
},
);
};
public getDrawings(): Drawing[] {
return this.drawings$.value;
}
public hideAll(): void {
if (this.selectedDrawing$.value) {
this.selectedDrawing$.next(null);
}
this.drawings$.value.forEach((drawing) => drawing.hide());
this.DOM.refreshEntities();
}
public destroy(): void {
this.hotkeys.unregister({
keys: [Keys.escape],
hash: this.escapeUnregisterHash,
});
window.removeEventListener('pointerup', this.handlePointerUp);
window.removeEventListener('pointercancel', this.handlePointerUp);
this.container.removeEventListener('click', this.handleClick);
this.container.removeEventListener('pointerdown', this.handlePointerDown);
this.drawings$.value.forEach((drawing) => drawing.destroy());
this.pointerDownSnapshot = null;
this.subscriptions.unsubscribe();
this.drawings$.complete();
this.selectedDrawing$.complete();
this.activeTool$.complete();
this.endlessMode$.complete();
}
}
import { Button, Divider } from 'exchange-elements/v2';
import { useEffect, useState } from 'react';
import { Observable } from 'rxjs';
import { IndicatorsSelect } from '@components/IndicatorsSelect';
import { IndicatorsIds } from '@src/constants';
import { FullscreenController } from '@src/core/Fullscreen';
import { UndoRedo, type UndoRedoState } from '@src/core/UndoRedo';
import { t } from '@src/translations';
import { ChartSeriesType } from '@src/types';
import { Timeframes } from '@src/types/timeframes';
import { useObservable } from '@src/utils';
import { Dropdown } from '../Dropdown';
import {
BarIcon,
BurgerMenuIcon,
CandleStickIcon,
FullscreenIcon,
LineIcon,
PlusCircleIcon,
RedoIcon,
SearchIcon,
UndoIcon,
} from '../Icon';
import { SeriesMenu, TimeframesMenu } from '../Menu';
import styles from './index.module.scss';
interface HeaderProps {
timeframes: Timeframes[];
selectedTimeframeObs: Observable<Timeframes>;
setTimeframe: (value: Timeframes) => void;
seriesTypes: ChartSeriesType[];
setSelectedSeries: (next: ChartSeriesType) => void;
selectedSeriesObs: Observable<ChartSeriesType>;
showSettingsModal: (() => void) | undefined;
addIndicatorToChart: (id: IndicatorsIds) => void;
toggleToolbarVisible: () => boolean;
showCompareButton: boolean;
showSymbolSearchButton: boolean;
undoRedo: UndoRedo | undefined;
showMenuButton?: boolean;
showFullscreenButton?: boolean;
fullscreen: FullscreenController;
openCompareModal?: () => void;
openSymbolSearchModal?: () => void;
isMXT: boolean;
}
export function Header({
setTimeframe,
selectedTimeframeObs,
setSelectedSeries,
selectedSeriesObs,
timeframes,
seriesTypes,
showSettingsModal,
addIndicatorToChart,
toggleToolbarVisible,
showCompareButton,
showSymbolSearchButton,
fullscreen,
undoRedo,
showFullscreenButton,
showMenuButton,
openCompareModal,
openSymbolSearchModal,
isMXT,
}: HeaderProps) {
const [isToolbarOpen, setIsToolbarOpen] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(fullscreen.isFullscreen);
const [undoRedoState, setUndoRedoState] = useState<UndoRedoState>(() => ({
canUndo: undoRedo?.canUndo() ?? false,
canRedo: undoRedo?.canRedo() ?? false,
}));
useEffect(() => {
const unsubscribe = fullscreen.onChange(() => setIsFullscreen(fullscreen.isFullscreen));
return unsubscribe;
}, [fullscreen]);
useEffect(() => {
const subscription = undoRedo?.getState().subscribe(setUndoRedoState);
if (!undoRedo) {
setUndoRedoState({
canUndo: false,
canRedo: false,
});
}
return () => {
subscription?.unsubscribe();
};
}, [undoRedo]);
const selectedTimeframe = useObservable(selectedTimeframeObs);
const selectedSeries = useObservable(selectedSeriesObs);
const seriesDropdownValue =
selectedSeries === 'Line' ? <LineIcon /> : selectedSeries === 'Bar' ? <BarIcon /> : <CandleStickIcon />;
const handleOpenToolbar = () => setIsToolbarOpen(() => toggleToolbarVisible());
return (
<header className={styles.header}>
<div className={styles.group}>
{showMenuButton && (
<Button
size="sm"
className={`${styles.button} ${isToolbarOpen ? styles.pressed : ''}`}
onClick={handleOpenToolbar}
label={<BurgerMenuIcon />}
/>
)}
{showSymbolSearchButton && (
<Button
size="sm"
className={styles.button}
onClick={openSymbolSearchModal}
label={<SearchIcon />}
/>
)}
{showCompareButton && (
<Button
size="sm"
className={styles.button}
onClick={openCompareModal}
label={<PlusCircleIcon />}
/>
)}
</div>
{(showMenuButton || showCompareButton) && (
<Divider
direction="vertical"
pt={{ divider: { className: styles.divider } }}
/>
)}
<Dropdown
menuClassName={styles.menu}
selectedValue={selectedTimeframe ? t(selectedTimeframe) : selectedTimeframe}
>
<TimeframesMenu
onClick={setTimeframe}
{...{ selectedTimeframe, timeframes }}
/>
</Dropdown>
<Divider
direction="vertical"
pt={{ divider: { className: styles.divider } }}
/>
<div className={styles.group}>
<IndicatorsSelect
addIndicatorToChart={addIndicatorToChart}
isMXT={isMXT}
/>
{showSettingsModal && (
<Button
size="sm"
className={styles.button}
onClick={() => showSettingsModal()}
label={t('Settings')}
/>
)}
<Dropdown
menuClassName={styles.menu}
selectedValue={seriesDropdownValue}
>
<SeriesMenu
selectedType={selectedSeries}
seriesTypes={seriesTypes}
onClick={setSelectedSeries}
/>
</Dropdown>
</div>
{(!!undoRedo || showFullscreenButton) && (
<Divider
direction="vertical"
pt={{ divider: { className: styles.divider } }}
/>
)}
{!!undoRedo && (
<div className={styles.group}>
<Button
size="sm"
className={styles.button}
onClick={() => undoRedo.undo()}
disabled={!undoRedoState.canUndo}
label={<UndoIcon />}
/>
<Button
size="sm"
className={styles.button}
onClick={() => undoRedo.redo()}
disabled={!undoRedoState.canRedo}
label={<RedoIcon />}
/>
</div>
)}
{showFullscreenButton && (
<Button
size="sm"
className={`${styles.button} ${isFullscreen ? styles.pressed : ''}`}
onClick={() => fullscreen.toggle()}
label={<FullscreenIcon />}
/>
)}
</header>
);
}