web/frontend/src/app/pages/streams/components/stream-view-reverse/stream-view-reverse.component.ts (476 lines of code) (raw):

import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HdDate } from '@assets/hd-date/hd-date'; import { select, Store } from '@ngrx/store'; import { AgGridModule } from 'ag-grid-angular'; import { CellClickedEvent, CellDoubleClickedEvent, Column, ColumnApi, ColumnMovedEvent, ColumnPinnedEvent, ColumnResizedEvent, ColumnVisibleEvent, GridOptions, GridReadyEvent, ICellRendererParams, NavigateToNextCellParams, RowNode, } from 'ag-grid-community'; import { CellContextMenuEvent, CellKeyDownEvent } from 'ag-grid-community/dist/lib/events'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject, timer } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, take, takeUntil, tap, } from 'rxjs/operators'; import { StreamsService } from 'src/app/shared/services/streams.service'; import { AppState } from '../../../../core/store'; import { GridTotalService } from '../../../../shared/components/grid-total/grid-total.service'; import { GridContextMenuService } from '../../../../shared/grid-components/grid-context-menu.service'; import { SchemaAllTypeModel, SchemaTypeModel, } from '../../../../shared/models/schema.type.model'; import { HasRightPanel } from '../../../../shared/right-pane/has-right-panel'; import { RightPaneService } from '../../../../shared/right-pane/right-pane.service'; import { GlobalFiltersService } from '../../../../shared/services/global-filters.service'; import { GridEventsService } from '../../../../shared/services/grid-events.service'; import { GridService } from '../../../../shared/services/grid.service'; import { PermissionsService } from '../../../../shared/services/permissions.service'; import { SchemaService } from '../../../../shared/services/schema.service'; import { StreamModelsService } from '../../../../shared/services/stream-models.service'; import { TabStorageService } from '../../../../shared/services/tab-storage.service'; import { autosizeAllColumns, columnIsMoved, columnIsPinned, columnIsVisible, columnsVisibleColumn, defaultGridOptions, gridStateLSInit, } from '../../../../shared/utils/grid/config.defaults'; import { ChartTypes } from '../../models/chart.model'; import { FilterModel } from '../../models/filter.model'; import { GridStateModel } from '../../models/grid.state.model'; import { StreamDetailsModel } from '../../models/stream.details.model'; import { TabModel } from '../../models/tab.model'; import { StreamDataService } from '../../services/stream-data.service'; import * as StreamDetailsActions from '../../store/stream-details/stream-details.actions'; import { StreamDetailsEffects } from '../../store/stream-details/stream-details.effects'; import * as fromStreamDetails from '../../store/stream-details/stream-details.reducer'; import * as fromStreams from '../../store/streams-list/streams.reducer'; import { getActiveOrFirstTab, getActiveTab, getActiveTabFilters, } from '../../store/streams-tabs/streams-tabs.selectors'; import { editedMessageProps, ModalSendMessageComponent } from '../modals/modal-send-message/modal-send-message.component'; const now = new HdDate(); export const toUtc = (date: any) => { const newDate = new HdDate(date); newDate.setMilliseconds(newDate.getMilliseconds() + new Date().getTimezoneOffset() * 60 * 1000); return newDate; }; export const fromUtc = (date: any) => { const newDate = new HdDate(date); newDate.setMilliseconds(newDate.getMilliseconds() - new Date().getTimezoneOffset() * 60 * 1000); return newDate; }; @Component({ selector: 'app-stream-view-reverse', templateUrl: './stream-view-reverse.component.html', styleUrls: ['./stream-view-reverse.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, providers: [GridService, GridContextMenuService, StreamDataService, GridTotalService], }) export class StreamViewReverseComponent implements OnInit, OnDestroy { @ViewChild('streamDetailsGrid', {static: true}) agGrid: AgGridModule; bsModalRef: BsModalRef; streamName: string; tabName: string; streamDetails: Observable<fromStreamDetails.State>; activeTab: Observable<TabModel>; gridOptions: GridOptions; hideGrid$ = new BehaviorSubject(true); error$ = new BehaviorSubject<HttpErrorResponse>(null); private schema = {}; private periodicity: number; private editingMessageNanoTime: string; private reverseStreamOrder: boolean; private messageInfo: editedMessageProps; private streamFilters = {symbols: [], types: []}; private tabSymbolFilter: string[]; private tabFilter; private columnsIdVisible: { [index: string]: boolean } = {}; private gridStateLS: GridStateModel = {visibleArray: [], pinnedArray: [], resizedArray: [], autoSized: []}; private rowData; private destroy$ = new ReplaySubject(1); private readyApi: GridOptions; private streamId: string; private gridDefaults: GridOptions = { ...defaultGridOptions, rowBuffer: 10, enableFilter: true, enableSorting: true, suppressRowClickSelection: false, rowSelection: 'single', defaultColDef: { filter: false, sortable: false, lockPinned: true, headerComponent: 'GridHeaderComponent', }, enableRangeSelection: true, rowModelType: 'infinite', infiniteInitialRowCount: 1, maxConcurrentDatasourceRequests: 1, enableServerSideSorting: true, enableServerSideFilter: true, gridAutoHeight: false, stopEditingWhenGridLosesFocus: true, suppressCellSelection: true, onCellDoubleClicked: (event: CellDoubleClickedEvent) => { this.messageInfoService.doubleClicked(event.data); }, onCellClicked: (event: CellClickedEvent) => { this.messageInfoService.cellClicked(event); }, onPinnedRowDataChanged: () => { this.messageInfoService.onPinnedRowDataChanged(); }, onGridReady: (readyEvent: GridReadyEvent) => this.gridIsReady(readyEvent), onColumnResized: (resizedEvent: ColumnResizedEvent) => this.gridEventsService.columnIsResized(resizedEvent, this.tabName, this.gridStateLS), onColumnVisible: (visibleEvent: ColumnVisibleEvent) => columnIsVisible(visibleEvent, this.tabName, this.gridStateLS), onColumnMoved: (movedEvent: ColumnMovedEvent) => columnIsMoved(movedEvent, this.tabName, this.gridStateLS), onColumnPinned: (pinnedEvent: ColumnPinnedEvent) => columnIsPinned(pinnedEvent, this.tabName, this.gridStateLS), onModelUpdated: (params) => { this.gridService.onCellFormatting().pipe(take(1)).subscribe(() => { autosizeAllColumns(params.columnApi, true, this.gridStateLS.autoSized || []); if (this.gridStateLS.resizedArray.length) { for (const item of this.gridStateLS.resizedArray) { params.columnApi.setColumnWidth(item.colId, item.actualWidth, true); } } }); }, navigateToNextCell: (params: NavigateToNextCellParams) => this.gridService.upDownKeysNavigation(this.gridOptions.api, params), onCellKeyDown: (event: CellKeyDownEvent) => this.onKeyDown(event), onCellContextMenu: (event: CellContextMenuEvent) => this.onContextMenu(event), }; constructor( private appStore: Store<AppState>, private route: ActivatedRoute, private streamsStore: Store<fromStreams.FeatureState>, private streamDetailsStore: Store<fromStreamDetails.FeatureState>, private streamDetailsEffects: StreamDetailsEffects, private dataSource: StreamDataService, private modalService: BsModalService, private gridEventsService: GridEventsService, private globalFiltersService: GlobalFiltersService, private gridService: GridService, private gridContextMenuService: GridContextMenuService, private permissionsService: PermissionsService, private streamModelsService: StreamModelsService, private tabStorageService: TabStorageService<HasRightPanel>, private messageInfoService: RightPaneService, private schemaService: SchemaService, private streamDataService: StreamDataService, private streamsService: StreamsService ) {} ngOnInit() { combineLatest([ this.permissionsService.isWriter().pipe(take(1)), this.appStore.pipe(select(getActiveTab), filter(Boolean)), ]).subscribe(([isWriter, tab]: [boolean, TabModel]) => { this.reverseStreamOrder = !!tab.reverse; if (tab.symbol) { this.tabSymbolFilter = [tab.symbol]; } this.streamId = tab.stream; this.streamName = tab.name; this.tabName = tab.stream + tab.id; const sendMessageMenu = { data: (event) => ({ disabled: !event.node || !this.streamId, name: 'Send Message', action: () => { this.bsModalRef = this.modalService.show(ModalSendMessageComponent, { initialState: { stream: { id: this.streamId, name: this.streamName, }, formData: (event.node.data as StreamDetailsModel)?.original, // (params.node.data as StreamDetailsModel).$type, // editMessageMode: false, }, ignoreBackdropClick: true, class: 'modal-message scroll-content-modal', }); }, }), alias: 'sendMessage', }; const editMessageMenu = { data: (event) => ({ disabled: !event.node || !this.streamId, name: 'Edit Message', action: () => { this.bsModalRef = this.modalService.show(ModalSendMessageComponent, { initialState: { stream: { id: this.streamId, name: this.streamName, }, formData: (event.node.data as StreamDetailsModel)?.original, editMessageMode: true, messageInfo: this.messageInfo, editingMessageNanoTime: this.editingMessageNanoTime, }, ignoreBackdropClick: true, class: 'modal-message scroll-content-modal', }); }, }), alias: 'editMessage', } const orderBookMenu = { data: (event: { node: RowNode }) => ({ disabled: true, name: 'View Oder Book', action: () => this.messageInfoService.viewOrderBook(event.node.data, event.node.rowIndex), }), alias: 'openOrderBook', }; const items = []; if (isWriter && !tab.isView) { typeof tab.space === 'string' ? items.push(sendMessageMenu) : items.push(sendMessageMenu, editMessageMenu); } if (tab.chartType?.includes(ChartTypes.PRICES_L2)) { items.push(orderBookMenu); } this.gridContextMenuService.addCellMenuItems(items); this.gridContextMenuService.addColumnSizeMenuItems( () => this.gridStateLS, () => this.rowData, this.tabName, ).pipe(takeUntil(this.destroy$)).subscribe(event => { this.columnsIdVisible = {}; this.columnsVisibleData(event.columnApi, this.rowData); }); }); this.modalService.onHide .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.streamDataService.getRows(this.streamDataService.getRowsParams); }) this.activeTab = this.appStore.pipe(select(getActiveOrFirstTab)); this.globalFiltersService .getFilters() .pipe(takeUntil(this.destroy$)) .subscribe(() => setTimeout(() => this.readyApi?.api.redrawRows(), 0)); this.gridOptions = this.gridDefaults; } columnsVisibleData(columnApi: ColumnApi, data: any) { const cols: Column[] = columnApi.getAllColumns(); // TODO: sometimes if > 1000 columns don't work getAllColumns() - is NULL , need another method if (cols && cols.length) { for (let i = 0; i < cols.length; i++) { const colIdArr = cols[i]['colId'].split('.'); if (colIdArr.length === 2) { if (this.columnsIdVisible[cols[i]['colId']]) { return; } if ( data.find((item) => { if (item.hasOwnProperty(colIdArr[0])) { return item[colIdArr[0]][colIdArr[1]]; } }) ) { columnApi.setColumnVisible(cols[i]['colId'], true); this.columnsIdVisible[cols[i]['colId']] = true; } } } } this.gridStateLS = gridStateLSInit(columnApi, this.tabName, this.gridStateLS); this.appStore .pipe( select(getActiveTabFilters), filter((filter) => !!filter), takeUntil(this.destroy$), ) .subscribe((filter: FilterModel) => { this.streamFilters = { symbols: filter.filter_symbols ?? this.tabSymbolFilter, types: filter.filter_types, } }); } ngOnDestroy(): void { this.destroy$.next(true); this.destroy$.complete(); this.streamsStore.dispatch(new StreamDetailsActions.StopSubscriptions()); } onContextMenu(event: CellContextMenuEvent) { let sameTimeStampId = 0; let counter = 0; event.api.forEachNode(node => { if (counter === event.rowIndex) { return; } if (node.data.timestamp === event.data.timestamp) { sameTimeStampId += 1; } counter += 1; }) this.messageInfo = { symbols: this.streamFilters.symbols, types: this.streamFilters.types, timestamp: event.data.timestamp, offset: sameTimeStampId, reverse: this.reverseStreamOrder, }; this.editingMessageNanoTime = event.data.original['deltix-securitymaster-messages-EventMessage']?.nanoTime; } private gridIsReady(readyEvent: GridReadyEvent) { this.readyApi = {...readyEvent}; this.gridService.setTooltipDelay(readyEvent); const tabToUnique = (tab) => JSON.stringify({id: tab.id, filter: {...tab.filter, silent: null, manuallyChanged: null}}); const getProps = (schema) => { return [ columnsVisibleColumn(), { headerName: 'Symbol', field: 'symbol', tooltipField: 'symbol', pinned: 'left', filter: false, sortable: false, headerTooltip: 'Symbol', }, { headerName: 'Timestamp', field: 'timestamp', pinned: 'left', filter: false, sortable: false, headerTooltip: 'Timestamp', width: 180, cellRenderer: (params: ICellRendererParams) => this.gridService.dateFormat(params, params.data?.nanoTime, true), tooltipValueGetter: (params: ICellRendererParams) => this.gridService.dateFormat(params, params.data?.nanoTime, true), }, { headerName: 'Time', field: 'time', pinned: 'left', filter: false, sortable: false, headerTooltip: 'Time', hide: !this.periodicity, cellRenderer: (params: ICellRendererParams) => this.gridService.dateFormat(params, params.data?.nanoTime, false, this.periodicity), tooltipValueGetter: (params: ICellRendererParams) => this.gridService.dateFormat(params, params.data?.nanoTime, false, this.periodicity), }, { headerName: 'Type', field: '$type', tooltipField: '$type', pinned: 'left', filter: false, sortable: false, headerTooltip: 'Type', hide: true, }, ...this.gridService.columnFromSchema( this.streamModelsService.getSchemaForColumns(schema.types, schema.all), true, ), ]; }; this.appStore .pipe( select(getActiveTab), filter(Boolean), distinctUntilChanged((prev, current) => tabToUnique(prev) === tabToUnique(current)), filter(tab => !!tab['filter'].from), ) .pipe( debounceTime(0), switchMap((activeTab: TabModel) => { this.hideGrid$.next(true); this.error$.next(null); return this.schemaService.getSchema(activeTab.stream, null, true).pipe( map((schema) => [activeTab, schema]), take(1), catchError(e => { this.error$.next(e); return of(null); }), filter(Boolean), ); }), tap( ([activeTab, schema]: [ TabModel, { types: SchemaTypeModel[]; all: SchemaAllTypeModel[] }, ]) => { this.schema = schema; this.messageInfoService.tabChanged(); this.tabFilter = {...activeTab.filter}; this.columnsIdVisible = {}; readyEvent.api.setDatasource(this.dataSource.withTab(activeTab, schema.all)); }, ), switchMap(([activeTab, schema]) => this.streamsService.getProps(activeTab.stream)), tap(streamInfo => { if (streamInfo.props.periodicity.type === 'REGULAR') { this.periodicity = streamInfo.props.periodicity.milliseconds; } }), switchMap(() => this.dataSource.onLoadedData().pipe( take(1), map((data) => [getProps(this.schema), data]), ), ), takeUntil(this.destroy$), ) .subscribe(([props, data]) => { this.rowData = data; readyEvent.api.setColumnDefs(null); readyEvent.api.setColumnDefs(props); this.columnsIdVisible = {}; this.columnsVisibleData(readyEvent.columnApi, data); timer().subscribe(() => this.hideGrid$.next(false)); }); this.columnsIdVisible = {}; this.messageInfoService.setGridApi(this.readyApi); } private onKeyDown(e) { if (e.event.code === 'Enter') { this.messageInfoService.doubleClicked(e.data); } else if (e.event.code === 'Tab') { this.gridService.tabKeyNavigation(e); } } }