RSS Git Download  Clone
Raw Blame History 9kB 261 lines
import { Injectable, Inject } from '@angular/core';
import { Subject } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import type { ConfirmDialogData } from '../components/confirm-dialog.component';
import { createDialogPopupSettings } from '../dialogs/dialog-popup';
import { I18nService } from './i18n.service';
import { RedisParserService } from './redis-parser.service';
import { RedisStateService } from './redis-state.service';
import { SettingsService } from './settings.service';
import { TreeBuilderService } from './tree-builder.service';

/**
 * Common service — Angular replacement for AngularJS p3xrCommon factory.
 *
 * Provides:
 * - toast(): notification via MatSnackBar (replaces $mdToast)
 * - confirm(): confirmation dialog via MatDialog (replaces $mdDialog.confirm())
 * - alert(): alert dialog via MatDialog (replaces $mdDialog.alert())
 * - generalHandleError(): centralized error handling with i18n code lookup
 * - loadRedisInfoResponse(): parses Redis info and populates state
 *
 * During hybrid mode, both this service and the AngularJS p3xrCommon factory coexist.
 * New Angular components use this service; existing AngularJS components keep using the factory.
 */
@Injectable({ providedIn: 'root' })
export class CommonService {

    readonly treeExpandAll$ = new Subject<void>();
    readonly treeCollapseAll$ = new Subject<void>();
    readonly treeExpandToLevel$ = new Subject<number>();

    private lastResponse: any;

    constructor(
        @Inject(MatSnackBar) private snackBar: MatSnackBar,
        @Inject(MatDialog) private dialog: MatDialog,
        @Inject(I18nService) private i18n: I18nService,
        @Inject(RedisParserService) private redisParser: RedisParserService,
        @Inject(RedisStateService) private state: RedisStateService,
        @Inject(SettingsService) private settings: SettingsService,
        @Inject(TreeBuilderService) private treeBuilder: TreeBuilderService,
    ) {}

    /**
     * Show a toast notification.
     * Replaces AngularJS $mdToast.
     */
    toast(options: string | { message: string; hideDelay?: number }): void {
        if (typeof options === 'string') {
            options = { message: options };
        }
        const ref = this.snackBar.open(options.message, 'x', {
            duration: options.hideDelay || 5000,
            horizontalPosition: 'right',
            verticalPosition: 'bottom',
        });
        ref.onAction().subscribe(() => ref.dismiss());
    }

    /**
     * Show a toast with an "Undo" action button.
     * Returns a Promise that resolves to true if Undo was clicked, false if dismissed.
     */
    toastWithUndo(message: string): Promise<boolean> {
        return new Promise(resolve => {
            const ref = this.snackBar.open(message, 'Undo', {
                duration: 5000,
                horizontalPosition: 'right',
                verticalPosition: 'bottom',
            });
            let acted = false;
            ref.onAction().subscribe(() => { acted = true; resolve(true); });
            ref.afterDismissed().subscribe(() => { if (!acted) resolve(false); });
        });
    }

    /**
     * Show a confirmation dialog with OK and Cancel buttons.
     * Returns a Promise that resolves on OK and rejects on Cancel.
     * Replaces AngularJS $mdDialog.confirm().
     */
    async confirm(options: {
        message: string;
        title?: string;
        event?: any;
        disableCancel?: boolean;
        panelClass?: string | string[];
        autoFocus?: boolean;
    }): Promise<void> {
        const strings = this.i18n.strings();
        const isAlert = options.hasOwnProperty('disableCancel') && options.disableCancel;

        const data: ConfirmDialogData = {
            title: options.title || (isAlert ? (strings.confirm?.info) : (strings.confirm?.title)),
            message: options.message,
            disableCancel: isAlert,
            okButton: isAlert ? (strings.intention?.ok) : (strings.intention?.sure),
            cancelButton: strings.intention?.cancel,
        };

        const { ConfirmDialogComponent } = await import(
            /* webpackChunkName: "dialog-confirm" */
            '../components/confirm-dialog.component'
        );
        const dialogRef = this.dialog.open(ConfirmDialogComponent, createDialogPopupSettings({
            data,
            autoFocus: options.autoFocus ?? true,
            panelClass: options.panelClass,
        }));

        return new Promise<void>((resolve, reject) => {
            dialogRef.afterClosed().subscribe((result) => {
                if (result) {
                    resolve();
                } else {
                    reject();
                }
            });
        });
    }

    /**
     * Show an alert dialog with only OK button.
     * Replaces AngularJS $mdDialog.alert().
     */
    async alert(options: string | {
        title?: string;
        message: string;
        panelClass?: string | string[];
        autoFocus?: boolean;
    }): Promise<void> {
        if (typeof options === 'string') {
            options = { message: options };
        }
        try {
            await this.confirm({
                title: options.title,
                message: options.message,
                disableCancel: true,
                panelClass: options.panelClass,
                autoFocus: options.autoFocus,
            });
        } catch {
            // Alert always resolves — user dismissed the dialog
        }
    }

    /**
     * Show a prompt dialog with text input.
     * Replaces AngularJS $mdDialog.prompt().
     * Returns the entered value, or throws if cancelled.
     */
    async prompt(options: {
        title: string;
        placeholder: string;
        initialValue?: string;
        ok: string;
        cancel: string;
    }): Promise<string> {
        const { PromptDialogComponent } = await import(
            /* webpackChunkName: "dialog-prompt" */
            '../dialogs/prompt-dialog.component'
        );
        const { createDialogPopupSettings } = await import('../dialogs/dialog-popup');
        const dialogRef = this.dialog.open(PromptDialogComponent, createDialogPopupSettings({
            data: {
                title: options.title,
                placeholder: options.placeholder,
                initialValue: options.initialValue ?? '',
                okButton: options.ok,
                cancelButton: options.cancel,
            },
        }));
        return new Promise<string>((resolve, reject) => {
            dialogRef.afterClosed().subscribe(result => {
                if (result !== undefined && result !== null) {
                    resolve(result);
                } else {
                    reject();
                }
            });
        });
    }

    /**
     * Centralized error handling with i18n code lookup.
     * Returns true if data is OK, false if it was an error.
     * Replaces AngularJS p3xrCommon.generalHandleError().
     */
    generalHandleError(dataOrError: any): boolean {
        if (dataOrError === undefined) {
            return true;
        }
        if (!(dataOrError instanceof Error || dataOrError instanceof Object)) {
            dataOrError = new Error(String(dataOrError));
        }
        if (dataOrError instanceof Error || dataOrError.status === 'error') {
            let error: any;
            if (dataOrError instanceof Error) {
                error = dataOrError;
            } else {
                error = dataOrError.error;
            }
            console.warn('generalHandleError');
            console.error(error);

            // i18n code lookup
            const strings = this.i18n.strings();
            const codes = strings.code || {};
            if (typeof error === 'string' && codes.hasOwnProperty(error)) {
                error = new Error(codes[error]);
            } else if (error?.code && codes.hasOwnProperty(error.code)) {
                error.message = codes[error.code];
            } else if (error?.message && codes.hasOwnProperty(error.message)) {
                error.message = codes[error.message];
            }

            // Handle connection closed
            if (error?.message === 'Connection is closed.') {
                this.state.connection.set(undefined);
            }

            this.alert({
                title: strings.title?.error,
                message: '<pre>' + (error?.message || error) + '</pre>',
            });
            return false;
        }
        return true;
    }

    /**
     * Parse Redis INFO response and populate state.
     * Replaces AngularJS p3xrCommon.loadRedisInfoResponse().
     */
    async loadRedisInfoResponse(options: { response?: any } = {}): Promise<void> {
        let response = options.response || this.lastResponse;
        this.lastResponse = response;
        if (!response) return;

        console.time('loadRedisInfoResponse');

        const info = this.redisParser.info(response.info);
        const shouldSort = this.settings.keysSort() && response.keys.length <= this.settings.maxLightKeysCount;

        // Sort in Web Worker if needed
        const keys = shouldSort
            ? await this.treeBuilder.sortKeys(response.keys)
            : response.keys;

        // Update signals
        this.state.info.set(info);
        this.state.keysRaw.set(keys);
        this.state.keysInfo.set(response.keysInfo);
        this.state.keysInfoFetchedAt.set(response.keysInfoFetchedAt || Date.now());

        console.timeEnd('loadRedisInfoResponse');
    }
}