RSS Git Download  Clone
Raw Blame History 6kB 153 lines
import { Component, Inject, OnInit, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation, effect } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { BreakpointObserver } from '@angular/cdk/layout';

import { I18nService } from '../../services/i18n.service';
import { MainCommandService } from '../../services/main-command.service';
import { RedisStateService } from '../../services/redis-state.service';

require('./statistics.component.scss');

@Component({
    selector: 'p3xr-database-statistics',
    standalone: true,
    imports: [MatTabsModule],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    templateUrl: './statistics.component.html',
    encapsulation: ViewEncapsulation.None,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StatisticsComponent implements OnInit, OnDestroy {

    maxHeight: number | string = 'auto';
    hasDatabases = false;
    isCluster = false;

    // Parsed from state.info() (snapshot taken in ngOnInit)
    keyspaceDatabaseEntries: Array<{ key: string; value: any }> = [];
    keyspaceItems: Record<string, Array<{ key: string; value: any }>> = {};
    infoSections: Array<{ key: string; items: Array<{ key: string; value: any }> }> = [];

    private readonly unsubFns: Array<() => void> = [];

    private static readonly EXCLUDE = ['in', 'run', 'per'];
    private static readonly INCLUDE = ['sha1'];
    private static readonly REPLACE: Record<string, string> = { perc: 'percent', sec: 'seconds' };

    constructor(
        @Inject(BreakpointObserver) private readonly breakpointObserver: BreakpointObserver,
        @Inject(I18nService) readonly i18n: I18nService,
        @Inject(MainCommandService) private readonly cmd: MainCommandService,
        @Inject(ChangeDetectorRef) private readonly cdr: ChangeDetectorRef,
        @Inject(RedisStateService) private readonly state: RedisStateService,
    ) {
        effect(() => {
            this.i18n.currentLang();
            this.cdr.markForCheck();
        });
    }

    ngOnInit(): void {
        const info = this.state.info();

        // Check if tree needs refresh
        if (this.state.redisChanged()) {
            this.state.redisChanged.set(false);
            this.broadcastRefresh();
        }

        // Parse info data
        const connection = this.state.connection();
        this.isCluster = connection?.cluster === true;

        if (info) {
            const ksDbs = info.keyspaceDatabases ?? {};
            this.hasDatabases = Object.keys(ksDbs).length > 0;
            this.keyspaceDatabaseEntries = Object.keys(ksDbs).map(k => ({ key: k, value: ksDbs[k] }));

            // Snapshot keyspace items per DB so the template doesn't read live data
            for (const dbEntry of this.keyspaceDatabaseEntries) {
                const ks = info?.keyspace?.['db' + dbEntry.key];
                this.keyspaceItems[dbEntry.key] = ks
                    ? Object.keys(ks).map(k => ({ key: k, value: ks[k] }))
                    : [];
            }

            this.infoSections = Object.keys(info)
                .filter(k => k !== 'keyspace' && k !== 'keyspaceDatabases')
                .map(k => ({
                    key: k,
                    items: Object.keys(info[k]).map(ik => ({ key: ik, value: info[k][ik] })),
                }));

            // Replace or add Modules section with full MODULE LIST data
            const modules = Array.isArray(this.state.modules()) ? this.state.modules() : [];
            if (modules.length > 0) {
                const moduleItems = modules.map((m: any) => ({
                    key: m.name,
                    value: `v${m.ver}`,
                }));
                const existingIdx = this.infoSections.findIndex(s => s.key.toLowerCase() === 'modules');
                if (existingIdx >= 0) {
                    this.infoSections[existingIdx].items = moduleItems;
                } else {
                    this.infoSections.push({ key: 'modules', items: moduleItems });
                }
            }
        }

        // Responsive height
        const sub = this.breakpointObserver.observe('(max-width: 599px)').subscribe(r => {
            this.recalcHeight(r.matches);
            this.cdr.markForCheck();
        });
        this.unsubFns.push(() => sub.unsubscribe());
    }

    ngOnDestroy(): void {
        this.unsubFns.forEach(fn => fn());
    }

    getKeyspaceItems(dbKey: string): Array<{ key: string; value: any }> {
        return this.keyspaceItems[dbKey] ?? [];
    }

    formatValue(value: any): string {
        if (value === null || value === undefined) return '';
        if (typeof value === 'object') return JSON.stringify(value);
        return String(value);
    }

    generateKey(key: string): string {
        const strings = this.i18n.strings();
        if (strings?.title?.hasOwnProperty(key)) {
            return strings.title[key];
        }
        return key.split('_').map((instance, index) => {
            if (StatisticsComponent.REPLACE.hasOwnProperty(instance)) {
                instance = StatisticsComponent.REPLACE[instance];
            }
            if (StatisticsComponent.INCLUDE.includes(instance) ||
                (instance.length < 4 && !StatisticsComponent.EXCLUDE.includes(instance))) {
                return instance.toUpperCase();
            } else if (index === 0) {
                return instance[0].toUpperCase() + instance.substring(1);
            }
            return instance;
        }).join(' ');
    }

    private recalcHeight(isXSmall: boolean): void {
        if (isXSmall) {
            this.maxHeight = 'auto';
        } else {
            const container = document.getElementById('p3xr-database-content-container');
            this.maxHeight = container ? container.offsetHeight - 50 : 'auto';
        }
    }

    private broadcastRefresh(): void {
        this.cmd.treeRefresh$.next();
    }
}