RSS Git Download  Clone
Raw Blame History 9kB 227 lines
import { useMemo, useState, useEffect } from 'react'
import { Box, Tabs, Tab, Typography, useTheme } from '@mui/material'
import { useI18nStore } from '../../stores/i18n.store'
import { useRedisStateStore } from '../../stores/redis-state.store'
import { useThemeStore } from '../../stores/theme.store'
import { useMainCommandStore, onCommandEvent } from '../../stores/main-command.store'

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

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

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

interface TabSection {
    key: string
    label: string
    items: Array<{ key: string; value: any }>
}

export default function StatisticsPage() {
    const strings = useI18nStore(s => s.strings)
    const info = useRedisStateStore(s => s.info)
    const modules = useRedisStateStore(s => s.modules)
    const connection = useRedisStateStore(s => s.connection)
    const redisChanged = useRedisStateStore(s => s.redisChanged)
    const muiTheme = useTheme()
    const themeKey = useThemeStore(s => s.themeKey)
    const [topTab, setTopTab] = useState(0)
    const [dbTab, setDbTab] = useState(0)
    const itemBorderColor = muiTheme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'

    // Broadcast tree refresh if state changed
    useEffect(() => {
        if (redisChanged) {
            useRedisStateStore.setState({ redisChanged: false })
        }
    }, [redisChanged])

    const isCluster = connection?.cluster === true

    // Parse keyspace databases
    const keyspaceDatabaseEntries = useMemo(() => {
        const ksDbs = info?.keyspaceDatabases ?? {}
        return Object.keys(ksDbs).map(k => ({ key: k, value: ksDbs[k] }))
    }, [info])

    const keyspaceItems = useMemo(() => {
        const result: Record<string, Array<{ key: string; value: any }>> = {}
        for (const dbEntry of keyspaceDatabaseEntries) {
            const ks = info?.keyspace?.['db' + dbEntry.key]
            result[dbEntry.key] = ks
                ? Object.keys(ks).map(k => ({ key: k, value: ks[k] }))
                : []
        }
        return result
    }, [info, keyspaceDatabaseEntries])

    const hasDatabases = keyspaceDatabaseEntries.length > 0

    // Parse info sections + modules
    const infoSections: TabSection[] = useMemo(() => {
        if (!info) return []
        const sections = Object.keys(info)
            .filter(k => k !== 'keyspace' && k !== 'keyspaceDatabases')
            .map(k => ({
                key: k,
                label: generateKey(k, strings),
                items: Object.keys(info[k]).map(ik => ({ key: ik, value: info[k][ik] })),
            }))

        // Replace or add modules section
        const mods = Array.isArray(modules) ? modules : []
        if (mods.length > 0) {
            const moduleItems = mods.map((m: any) => ({ key: m.name, value: `v${m.ver}` }))
            const existingIdx = sections.findIndex(s => s.key.toLowerCase() === 'modules')
            if (existingIdx >= 0) {
                sections[existingIdx].items = moduleItems
            } else {
                sections.push({ key: 'modules', label: generateKey('modules', strings), items: moduleItems })
            }
        }

        return sections
    }, [info, modules, strings])

    // All top-level tabs: DB (if applicable) + info sections
    const allTabs = useMemo(() => {
        const tabs: TabSection[] = []
        if (hasDatabases && !isCluster) {
            tabs.push({
                key: '__db__',
                label: strings?.title?.db ?? 'DB',
                items: [],
            })
        }
        tabs.push(...infoSections)
        return tabs
    }, [hasDatabases, isCluster, infoSections, strings])

    if (!connection) {
        return <Box sx={{ p: 2 }}><Typography>{strings?.title?.main}</Typography></Box>
    }

    const currentTab = allTabs[topTab]
    const isDbTab = currentTab?.key === '__db__'

    return (
        <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
            {/* Top-level tabs */}
            <Tabs
                value={Math.min(topTab, allTabs.length - 1)}
                onChange={(_, v) => setTopTab(v)}
                variant="scrollable"
                scrollButtons="auto"
                textColor="inherit"
                sx={{
                    position: 'sticky',
                    top: 0,
                    zIndex: 2,
                    bgcolor: muiTheme.palette.background.paper,
                    backgroundImage: 'none',
                    flexShrink: 0,
                    '& .MuiTab-root': { color: muiTheme.p3xr.treecontrolIconColor, textTransform: 'none' },
                    '& .MuiTab-root.Mui-selected': { color: muiTheme.palette.text.primary },
                    '& .MuiTabs-indicator': { bgcolor: muiTheme.p3xr.matSysPrimary },
                }}
            >
                {allTabs.map(tab => (
                    <Tab key={tab.key} label={tab.label} />
                ))}
            </Tabs>

            {/* Tab content */}
            <Box sx={{ flex: 1, overflow: 'auto' }}>
                {isDbTab ? (
                    /* Nested DB keyspace tabs */
                    <Box>
                        <Tabs
                            value={Math.min(dbTab, keyspaceDatabaseEntries.length - 1)}
                            onChange={(_, v) => setDbTab(v)}
                            variant="scrollable"
                            scrollButtons="auto"
                            textColor="inherit"
                            sx={() => {
                                const isMatrix = themeKey === 'matrix'
                                const activeColor = isMatrix ? 'rgba(0,0,0,0.87)' : 'white'
                                const inactiveColor = isMatrix ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.7)'
                                return {
                                    bgcolor: 'primary.main',
                                    '& .MuiTab-root': { color: inactiveColor },
                                    '& .MuiTab-root.Mui-selected': { color: activeColor },
                                    '& .MuiTabs-indicator': { bgcolor: activeColor },
                                    '& .MuiTabScrollButton-root': { color: inactiveColor },
                                }
                            }}
                        >
                            {keyspaceDatabaseEntries.map(dbEntry => (
                                <Tab key={dbEntry.key} label={dbEntry.key} />
                            ))}
                        </Tabs>
                        {keyspaceDatabaseEntries.map((dbEntry, i) => (
                            dbTab === i && (
                                <Box key={dbEntry.key} sx={{ px: 2, py: 1 }}>
                                    {(keyspaceItems[dbEntry.key] ?? []).map(item => (
                                        <Box key={item.key} sx={{
                                            display: 'flex',
                                            alignItems: 'baseline',
                                            justifyContent: 'space-between',
                                            py: '8px',
                                            borderBottom: '1px solid',
                                            borderColor: itemBorderColor,
                                        }}>
                                            <strong>{generateKey(item.key, strings)}</strong>
                                            <span style={{ textAlign: 'right' }}>{item.value}</span>
                                        </Box>
                                    ))}
                                </Box>
                            )
                        ))}
                    </Box>
                ) : currentTab ? (
                    /* Info section content */
                    <Box sx={{ px: 2, py: 1 }}>
                        {currentTab.items.map(item => (
                            <Box key={item.key} sx={{
                                display: 'flex',
                                alignItems: 'baseline',
                                justifyContent: 'space-between',
                                py: '8px',
                                borderBottom: '1px solid',
                                borderColor: itemBorderColor,
                            }}>
                                <strong>{generateKey(item.key, strings)}</strong>
                                <Box sx={{ textAlign: 'right', wordBreak: 'break-all', maxWidth: '60%' }}>
                                    {formatValue(item.value)}
                                </Box>
                            </Box>
                        ))}
                    </Box>
                ) : null}
            </Box>
        </Box>
    )
}