From ddee5f927c170884ca16412e19da3201898509a0 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 8 Feb 2024 17:44:32 +0000 Subject: [PATCH] Make dashboard filters persistent, add disjunctive filter mode --- .../block-filters.component.html | 11 ++++- .../block-filters.component.scss | 43 +++++++++++++++++ .../block-filters/block-filters.component.ts | 26 +++++++---- .../block-overview-graph.component.ts | 19 +++++--- .../mempool-block-overview.component.ts | 3 +- .../app/dashboard/dashboard.component.html | 6 +-- .../src/app/dashboard/dashboard.component.ts | 46 ++++++++++++++++--- frontend/src/app/services/state.service.ts | 3 +- frontend/src/app/shared/filters.utils.ts | 15 ++++++ 9 files changed, 145 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index f60b04cdd..8c79cd438 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -1,4 +1,4 @@ -
+
+
Match
+
+ + +
{{ group.label }}
diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss index 6406a1d93..4830f9540 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.scss +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -77,6 +77,49 @@ } } + &.any-mode { + .filter-tag { + border: solid 1px #1a9436; + &.selected { + background-color: #1a9436; + } + } + } + + .btn-group { + font-size: 0.9em; + margin-right: 0.25em; + } + + .mode-toggle { + padding: 0.2em 0.5em; + pointer-events: all; + line-height: 1.5; + background: #181b2daf; + + &:first-child { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; + } + &:last-child { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; + } + + &.blue { + border: solid 1px #105fb0; + &.active { + background: #105fb0; + } + } + &.green { + border: solid 1px #1a9436; + &.active { + background: #1a9436; + } + } + } + :host-context(.block-overview-graph:hover) &, &:hover, &:active { .menu-toggle { opacity: 0.5; diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts index 9951984df..a16475c23 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.ts +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; -import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; +import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils'; import { StateService } from '../../services/state.service'; import { Subscription } from 'rxjs'; @@ -12,7 +12,7 @@ import { Subscription } from 'rxjs'; export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { @Input() cssWidth: number = 800; @Input() excludeFilters: string[] = []; - @Output() onFilterChanged: EventEmitter = new EventEmitter(); + @Output() onFilterChanged: EventEmitter = new EventEmitter(); filterSubscription: Subscription; @@ -21,6 +21,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { disabledFilters: { [key: string]: boolean } = {}; activeFilters: string[] = []; filterFlags: { [key: string]: boolean } = {}; + filterMode: FilterMode = 'and'; menuOpen: boolean = false; constructor( @@ -29,15 +30,16 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { ) {} ngOnInit(): void { - this.filterSubscription = this.stateService.activeGoggles$.subscribe((activeFilters: string[]) => { + this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => { + this.filterMode = active.mode; for (const key of Object.keys(this.filterFlags)) { this.filterFlags[key] = false; } - for (const key of activeFilters) { + for (const key of active.filters) { this.filterFlags[key] = !this.disabledFilters[key]; } - this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])]; - this.onFilterChanged.emit(this.getBooleanFlags()); + this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])]; + this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters }); }); } @@ -53,6 +55,12 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { } } + setFilterMode(mode): void { + this.filterMode = mode; + this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); + this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); + } + toggleFilter(key): void { const filter = this.filters[key]; this.filterFlags[key] = !this.filterFlags[key]; @@ -73,8 +81,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { this.activeFilters = this.activeFilters.filter(f => f != key); } const booleanFlags = this.getBooleanFlags(); - this.onFilterChanged.emit(booleanFlags); - this.stateService.activeGoggles$.next([...this.activeFilters]); + this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); + this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); } getBooleanFlags(): bigint | null { @@ -90,7 +98,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { @HostListener('document:click', ['$event']) onClick(event): boolean { // click away from menu - if (!event.target.closest('button')) { + if (!event.target.closest('button') && !event.target.closest('label')) { this.menuOpen = false; } return true; diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index ac1df2bf5..f250f3744 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -9,6 +9,7 @@ import { Price } from '../../services/price.service'; import { StateService } from '../../services/state.service'; import { Subscription } from 'rxjs'; import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; +import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; const unmatchedOpacity = 0.2; const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); @@ -42,7 +43,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() showFilters: boolean = false; @Input() excludeFilters: string[] = []; @Input() filterFlags: bigint | null = null; - @Input() filterMode: 'and' | 'or' = 'and'; + @Input() filterMode: FilterMode = 'and'; @Input() blockConversion: Price; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @@ -119,10 +120,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } } - setFilterFlags(flags?: bigint | null): void { - this.activeFilterFlags = this.filterFlags || flags || null; + setFilterFlags(goggle?: ActiveFilter): void { + this.filterMode = goggle?.mode || this.filterMode; + this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags; if (this.scene) { - if (this.activeFilterFlags != null) { + if (this.activeFilterFlags != null && this.filtersAvailable) { this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags)); } else { this.scene.setColorFunction(this.overrideColors); @@ -157,7 +159,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On // initialize the scene without any entry transition setup(transactions: TransactionStripped[]): void { - this.filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); + const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); + if (filtersAvailable !== this.filtersAvailable) { + this.setFilterFlags(); + } + this.filtersAvailable = filtersAvailable; if (this.scene) { this.scene.setup(transactions); this.readyNextFrame = true; @@ -523,8 +529,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { + console.log('getting filter color function: ', flags, this.filterMode); return (tx: TxView) => { - if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (tx.bigintFlags & flags) > 0n)) { + if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) { return defaultColorFunction(tx); } else { return defaultColorFunction( diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 7fb036718..ba804912b 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -10,6 +10,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi import { Router } from '@angular/router'; import { Color } from '../block-overview-graph/sprite-types'; import TxView from '../block-overview-graph/tx-view'; +import { FilterMode } from '../../shared/filters.utils'; @Component({ selector: 'app-mempool-block-overview', @@ -22,7 +23,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang @Input() showFilters: boolean = false; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Input() filterFlags: bigint | undefined = undefined; - @Input() filterMode: 'and' | 'or' = 'and'; + @Input() filterMode: FilterMode = 'and'; @Output() txPreviewEvent = new EventEmitter(); @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index a2c46a198..6425e3b9a 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -26,7 +26,7 @@
@@ -34,8 +34,8 @@
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 5dfd68419..ce0db77c1 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -7,6 +7,7 @@ import { ApiService } from '../services/api.service'; import { StateService } from '../services/state.service'; import { WebsocketService } from '../services/websocket.service'; import { SeoService } from '../services/seo.service'; +import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils'; interface MempoolBlocksData { blocks: number; @@ -55,6 +56,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { currentReserves$: Observable; fullHistory$: Observable; isLoad: boolean = true; + filterSubscription: Subscription; mempoolInfoSubscription: Subscription; currencySubscription: Subscription; currency: string; @@ -65,13 +67,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { private lastReservesBlockUpdate: number = 0; goggleResolution = 82; - goggleCycle = [ - { index: 0, name: 'All' }, - { index: 1, name: 'Consolidations', flag: 0b00000010_00000000_00000000_00000000_00000000n }, - { index: 2, name: 'Coinjoin', flag: 0b00000001_00000000_00000000_00000000_00000000n }, - { index: 3, name: '💩', flag: 0b00000100_00000000_00000000_00000000n | 0b00000010_00000000_00000000_00000000n | 0b00000001_00000000_00000000_00000000n }, + goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [ + { index: 0, name: 'All', mode: 'and', filters: [] }, + { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] }, + { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] }, + { index: 3, name: '💩', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'] }, ]; - goggleIndex = 0; // Math.floor(Math.random() * this.goggleCycle.length); + goggleFlags = 0n; + goggleMode: FilterMode = 'and'; + goggleIndex = 0; private destroy$ = new Subject(); @@ -87,6 +91,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { } ngOnDestroy(): void { + this.filterSubscription.unsubscribe(); this.mempoolInfoSubscription.unsubscribe(); this.currencySubscription.unsubscribe(); this.websocketService.stopTrackRbfSummary(); @@ -107,6 +112,30 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100) ); + this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => { + const activeFilters = active.filters.sort().join(','); + for (const goggle of this.goggleCycle) { + if (goggle.mode === active.mode) { + const goggleFilters = goggle.filters.sort().join(','); + if (goggleFilters === activeFilters) { + this.goggleIndex = goggle.index; + this.goggleFlags = toFlags(goggle.filters); + this.goggleMode = goggle.mode; + return; + } + } + } + this.goggleCycle.push({ + index: this.goggleCycle.length, + name: 'Custom', + mode: active.mode, + filters: active.filters, + }); + this.goggleIndex = this.goggleCycle.length - 1; + this.goggleFlags = toFlags(active.filters); + this.goggleMode = active.mode; + }); + this.mempoolInfoData$ = combineLatest([ this.stateService.mempoolInfo$, this.stateService.vbytesPerSecond$ @@ -375,6 +404,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { return Array.from({ length: num }, (_, i) => i + 1); } + setFilter(index): void { + const selected = this.goggleCycle[index]; + this.stateService.activeGoggles$.next(selected); + } + @HostListener('window:resize', ['$event']) onResize(): void { if (window.innerWidth >= 992) { diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index f87a3dc31..8b69546ba 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -9,6 +9,7 @@ import { filter, map, scan, shareReplay } from 'rxjs/operators'; import { StorageService } from './storage.service'; import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; import { ApiService } from './api.service'; +import { ActiveFilter } from '../shared/filters.utils'; export interface MarkBlockState { blockHeight?: number; @@ -150,7 +151,7 @@ export class StateService { searchFocus$: Subject = new Subject(); menuOpen$: BehaviorSubject = new BehaviorSubject(false); - activeGoggles$: BehaviorSubject = new BehaviorSubject([]); + activeGoggles$: BehaviorSubject = new BehaviorSubject({ mode: 'and', filters: [] }); constructor( @Inject(PLATFORM_ID) private platformId: any, diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts index 0b652a192..3930dc8ca 100644 --- a/frontend/src/app/shared/filters.utils.ts +++ b/frontend/src/app/shared/filters.utils.ts @@ -7,6 +7,13 @@ export interface Filter { important?: boolean, } +export type FilterMode = 'and' | 'or'; + +export interface ActiveFilter { + mode: FilterMode, + filters: string[], +} + // binary flags for transaction classification export const TransactionFlags = { // features @@ -43,6 +50,14 @@ export const TransactionFlags = { sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, }; +export function toFlags(filters: string[]): bigint { + let flag = 0n; + for (const filter of filters) { + flag |= TransactionFlags[filter]; + } + return flag; +} + export const TransactionFilters: { [key: string]: Filter } = { /* features */ rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true },