RSS Git Download  Clone
Raw Blame History 4kB 112 lines
import { Injectable, Inject, signal, computed } from '@angular/core';
import { SettingsService } from './settings.service';

declare const P3XR_API_PORT: number;

/**
 * Runtime state service using Angular signals.
 *
 * Single source of truth for all Redis UI runtime state. No global object dependency.
 */
@Injectable({ providedIn: 'root' })
export class RedisStateService {

    // --- Writable signals for runtime state ---

    readonly theme = signal<string | undefined>(undefined);
    readonly connection = signal<any>(undefined);
    readonly currentDatabase = signal<number | undefined>(undefined);
    readonly databaseIndexes = signal<number[]>([0]);
    readonly connections = signal<any>({ list: [] });
    readonly redisConnections = signal<Record<string, any>>({});
    readonly keysRaw = signal<string[]>([]);
    readonly keysInfo = signal<any>(undefined);
    readonly search = signal<string>(this.getStoredSearch());
    readonly page = signal<number>(1);
    readonly info = signal<any>(undefined);
    readonly dbsize = signal<number | undefined>(undefined);
    readonly redisChanged = signal<boolean>(false);
    readonly failed = signal<boolean>(false);
    readonly monitor = signal<boolean>(false);
    readonly monitorPattern = signal<string>('*');
    readonly commands = signal<string[]>([]);
    readonly commandsMeta = signal<Record<string, { syntax: string; group: string }>>({});
    readonly cfg = signal<any>(undefined);
    readonly version = signal<string | undefined>(undefined);
    readonly modules = signal<any[]>([]);
    readonly hasRediSearch = signal<boolean>(false);
    readonly hasReJSON = signal<boolean>(false);
    readonly hasTimeSeries = signal<boolean>(false);
    readonly reducedFunctions = signal<boolean>(false);
    readonly keysInfoFetchedAt = signal<number>(Date.now());

    // --- Computed values ---

    readonly themeLayout = computed(() => {
        const t = this.theme();
        return t ? t + 'Layout' : undefined;
    });

    readonly themeCommon = computed(() => {
        const t = this.theme();
        return t ? t + 'Common' : undefined;
    });

    readonly filteredKeys = computed(() => {
        let keys = this.keysRaw().slice();
        const search = this.search();
        const settings = this.settings;

        // Apply client-side search filter
        if (settings.searchClientSide() && typeof search === 'string' && search.length > 0) {
            if (settings.searchStartsWith()) {
                keys = keys.filter((key) => key.startsWith(search));
            } else {
                keys = keys.filter((key) => key.includes(search));
            }
        }
        return keys;
    });

    readonly paginatedKeys = computed(() => {
        const keys = this.filteredKeys();
        const pageSize = this.settings.pageCount();
        if (keys.length <= pageSize) {
            return keys;
        }
        const start = (this.page() - 1) * pageSize;
        return keys.slice(start, start + pageSize);
    });

    readonly pages = computed(() => {
        return Math.ceil(this.filteredKeys().length / this.settings.pageCount());
    });

    // --- API host (computed once at startup) ---
    readonly apiHost: string = (() => {
        const apiUrl = new URL(location.toString());
        if ((globalThis as any).p3xrDevMode === true) {
            const apiPort = typeof P3XR_API_PORT !== 'undefined' ? P3XR_API_PORT : 7843;
            return `http://${apiUrl.hostname}:${apiPort}`;
        }
        return `${apiUrl.protocol}//${apiUrl.host}`;
    })();

    constructor(@Inject(SettingsService) private settings: SettingsService) {}

    /**
     * Resets connections to default state.
     */
    resetConnections(): void {
        this.connections.set({ list: [] });
    }

    private getStoredSearch(): string {
        try {
            return localStorage.getItem('p3xr-state-search') ?? '';
        } catch {
            return '';
        }
    }
}