RSS Git Download  Clone
Raw Blame History 13kB 297 lines
import { useState, useEffect, useRef } from 'react'
import { Box, TextField, List, ListItem, Divider } from '@mui/material'
import { Add, Search, Delete, Refresh, BarChart, Undo, CheckBox, CheckBoxOutlineBlank } from '@mui/icons-material'
import { useI18nStore } from '../../../stores/i18n.store'
import { useRedisStateStore } from '../../../stores/redis-state.store'
import { useCommonStore } from '../../../stores/common.store'
import { request } from '../../../stores/socket.service'
import { KeyTypeProps } from './key-type-base'
import P3xrAccordion from '../../../components/P3xrAccordion'
import P3xrButton from '../../../components/P3xrButton'

export default function KeyProbabilistic({ response, value, keyName, onRefresh }: KeyTypeProps) {
    const strings = useI18nStore(s => s.strings)
    const connection = useRedisStateStore(s => s.connection)
    const { toast, confirm } = useCommonStore()
    const readonly = connection?.readonly === true

    const type = response?.type || ''

    const [itemInput, setItemInput] = useState('')
    const [incrementInput, setIncrementInput] = useState(1)
    const [quantileInput, setQuantileInput] = useState(0.5)
    const [topkItems, setTopkItems] = useState<Array<{ item: string; count: number }>>([])
    const [autoRefresh, setAutoRefresh] = useState(false)
    const autoRefreshRef = useRef<any>(null)

    useEffect(() => {
        if (autoRefresh) {
            autoRefreshRef.current = setInterval(() => { onRefresh(); if (type === 'topk') loadTopkList() }, 10000)
        } else {
            clearInterval(autoRefreshRef.current)
        }
        return () => clearInterval(autoRefreshRef.current)
    }, [autoRefresh])

    // Parse info directly from value — no effect needed, computed on render
    const infoItems = (() => {
        try {
            let info: any
            if (typeof value === 'object' && value !== null && !ArrayBuffer.isView(value)) {
                info = value
            } else if (typeof value === 'string') {
                info = JSON.parse(value)
            } else if (ArrayBuffer.isView(value)) {
                info = JSON.parse(new TextDecoder().decode(value))
            }
            if (info && typeof info === 'object') {
                return Object.entries(info).map(([key, value]) => ({ key, value }))
            }
        } catch {}
        return []
    })()

    useEffect(() => {
        if (type === 'topk') loadTopkList()
    }, [type])

    async function addItem() {
        if (!itemInput.trim()) return
        try {
            await request({
                action: 'probabilistic/add',
                payload: { key: keyName, type, item: itemInput.trim(), increment: incrementInput },
            })
            toast(strings?.page?.key?.probabilistic?.addedSuccessfully)
            setItemInput('')
            onRefresh()
            if (type === 'topk') loadTopkList()
        } catch (e: any) {
            toast(e.message || 'Error')
        }
    }

    async function checkItem() {
        if (!itemInput.trim()) return
        try {
            const resp: any = await request({
                action: 'probabilistic/check',
                payload: { key: keyName, type, item: itemInput.trim() },
            })
            const exists = resp.result === 1
            toast(`"${itemInput}" — ${exists
                ? (strings?.page?.key?.probabilistic?.exists)
                : (strings?.page?.key?.probabilistic?.doesNotExist)}`)
        } catch (e: any) {
            toast(e.message || 'Error')
        }
    }

    async function deleteItem() {
        if (!itemInput.trim()) return
        try {
            await confirm({ message: strings?.confirm?.delete })
            await request({
                action: 'probabilistic/delete',
                payload: { key: keyName, type, item: itemInput.trim() },
            })
            toast(strings?.page?.key?.probabilistic?.deletedSuccessfully)
            setItemInput('')
            onRefresh()
        } catch (e: any) {
            if (e?.message) toast(e.message)
        }
    }

    async function queryItem() {
        if (!itemInput.trim()) return
        try {
            const resp: any = await request({
                action: 'probabilistic/check',
                payload: { key: keyName, type, item: itemInput.trim() },
            })
            const count = Array.isArray(resp.result) ? resp.result[0] : resp.result
            toast(`"${itemInput}" — ${strings?.page?.key?.probabilistic?.topkCount}: ${count}`)
        } catch (e: any) {
            toast(e.message || 'Error')
        }
    }

    async function queryQuantile() {
        try {
            const resp: any = await request({
                action: 'probabilistic/check',
                payload: { key: keyName, type, quantile: quantileInput },
            })
            const result = Array.isArray(resp.result) ? resp.result[0] : resp.result
            toast(`${strings?.page?.key?.probabilistic?.quantile} ${quantileInput} = ${result}`)
        } catch (e: any) {
            toast(e.message || 'Error')
        }
    }

    async function resetTdigest() {
        try {
            await confirm({ message: strings?.page?.key?.probabilistic?.resetConfirm })
            await request({
                action: 'probabilistic/delete',
                payload: { key: keyName, type: 'tdigest' },
            })
            toast('Reset')
            onRefresh()
        } catch (e: any) {
            if (e?.message) toast(e.message)
        }
    }

    async function loadTopkList() {
        try {
            const resp: any = await request({
                action: 'probabilistic/check',
                payload: { key: keyName, type: 'topk' },
            })
            setTopkItems(resp.result || [])
        } catch {
            setTopkItems([])
        }
    }

    return (
        <Box className="p3xr-key-type-content">
            {/* INFO */}
            <br />
            <P3xrAccordion
                title={strings?.page?.key?.probabilistic?.info}
                accordionKey="prob-info"
                actions={<>
                    <P3xrButton
                        icon={autoRefresh ? <CheckBox sx={{ fontSize: 18 }} /> : <CheckBoxOutlineBlank sx={{ fontSize: 18 }} />}
                        label={strings?.label?.autoRefresh} breakpoint={1280}
                        onClick={(e) => { e.stopPropagation(); setAutoRefresh(v => !v) }} />
                    {!autoRefresh && (
                        <P3xrButton icon={<Refresh sx={{ fontSize: 18 }} />} label={strings?.intention?.refresh}
                            breakpoint={1280}
                            onClick={(e) => { e.stopPropagation(); onRefresh(); if (type === 'topk') loadTopkList() }} />
                    )}
                </>}
            >
                <List disablePadding>
                    {infoItems.map((item, i) => (
                        <Box key={item.key}>
                            <ListItem>
                                <Box sx={{ display: 'flex', width: '100%' }}>
                                    <span className="p3xr-settings-label" style={{ flex: 1 }}>{item.key}</span>
                                    <span className="p3xr-settings-value">{String(item.value)}</span>
                                </Box>
                            </ListItem>
                            {i < infoItems.length - 1 && <Divider />}
                        </Box>
                    ))}
                </List>
            </P3xrAccordion>

            {/* ACTIONS */}
            <br />
            <P3xrAccordion
                title={type === 'topk'
                    ? (strings?.page?.key?.probabilistic?.topkList)
                    : (strings?.page?.key?.probabilistic?.addItem)}
                accordionKey="prob-actions"
            >
                <Box>
                    <Box sx={{ p: 2 }}>
                    <Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-start', gap: 1.5, py: 1 }}>
                        {type === 'tdigest' ? (
                            <TextField size="small" type="number" margin="none"
                                className="p3xr-timeseries-field"
                                label={strings?.form?.key?.field?.value}
                                value={itemInput}
                                onChange={e => setItemInput(e.target.value)}
                                onKeyUp={e => e.key === 'Enter' && addItem()} />
                        ) : (
                            <TextField size="small" margin="none"
                                className="p3xr-timeseries-field"
                                sx={{ flex: 1, minWidth: 200 }}
                                label={strings?.page?.key?.probabilistic?.item}
                                value={itemInput}
                                onChange={e => setItemInput(e.target.value)}
                                onKeyUp={e => e.key === 'Enter' && addItem()} />
                        )}

                        {type === 'cms' && (
                            <TextField size="small" type="number" margin="none"
                                className="p3xr-timeseries-field"
                                sx={{ maxWidth: 120 }}
                                label={strings?.form?.key?.field?.increment}
                                value={incrementInput}
                                onChange={e => setIncrementInput(parseInt(e.target.value) || 1)} />
                        )}

                        {type === 'tdigest' && (
                            <TextField size="small" type="number" margin="none"
                                slotProps={{ htmlInput: { step: 0.1 } }}
                                className="p3xr-timeseries-field"
                                sx={{ maxWidth: 120 }}
                                label={strings?.page?.key?.probabilistic?.quantile}
                                value={quantileInput}
                                onChange={e => setQuantileInput(parseFloat(e.target.value) || 0.5)}
                                onKeyUp={e => e.key === 'Enter' && queryQuantile()} />
                        )}

                        {!readonly && (
                            <P3xrButton icon={<Add fontSize="small" />} label={strings?.intention?.add}
                                raised color="primary" onClick={addItem} />
                        )}

                        {(type === 'bloom' || type === 'cuckoo') && (
                            <P3xrButton icon={<Search fontSize="small" />} label={strings?.page?.key?.probabilistic?.checkItem}
                                raised color="primary" onClick={checkItem} />
                        )}

                        {type === 'cuckoo' && !readonly && (
                            <P3xrButton icon={<Delete fontSize="small" />} label={strings?.intention?.delete}
                                raised color="primary" onClick={deleteItem} />
                        )}

                        {type === 'cms' && (
                            <P3xrButton icon={<Search fontSize="small" />} label={strings?.page?.key?.probabilistic?.queryCount}
                                raised color="primary" onClick={queryItem} />
                        )}

                        {type === 'tdigest' && (
                            <P3xrButton icon={<BarChart fontSize="small" />} label={strings?.page?.key?.probabilistic?.quantile}
                                raised color="primary" onClick={queryQuantile} />
                        )}

                        {type === 'tdigest' && !readonly && (
                            <P3xrButton icon={<Undo fontSize="small" />} label="Reset"
                                raised color="warning" onClick={resetTdigest} />
                        )}
                    </Box>

                    </Box>
                    {/* TOPK ITEMS LIST */}
                    {type === 'topk' && topkItems.length > 0 && (
                        <>
                        <Divider />
                        <List disablePadding>
                            {topkItems.map((entry, i) => (
                                <Box key={i}>
                                    <ListItem>
                                        <Box sx={{ display: 'flex', width: '100%' }}>
                                            <span className="p3xr-settings-label" style={{ flex: 1 }}>{entry.item}</span>
                                            <span className="p3xr-settings-value">{entry.count}</span>
                                        </Box>
                                    </ListItem>
                                    {i < topkItems.length - 1 && <Divider />}
                                </Box>
                            ))}
                        </List>
                        </>
                    )}
                </Box>
            </P3xrAccordion>
        </Box>
    )
}