pms-front-react/src/pages/monitor/CacheListPage.tsx

400 lines
11 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { Card, Col, Row, message, Table, Button, Input, Form, Space } from 'antd';
import type { TableColumnsType } from 'antd';
import { ReloadOutlined, DeleteOutlined, KeyOutlined, FileTextOutlined, AppstoreOutlined } from '@ant-design/icons';
import {
listCacheName,
listCacheKey,
getCacheValue,
clearCacheName,
clearCacheKey,
clearCacheAll,
} from '../../api/monitor/cache';
import type {
CacheKeyPayload,
CacheKeyRecord,
CacheNamePayload,
CacheNameRecord,
} from '@/types/api';
import Permission from '@/components/Permission';
import { usePermission } from '@/contexts/PermissionContext';
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
import './cache-list.css';
interface CacheForm {
cacheName: string;
cacheKey: string;
cacheValue: string;
}
const defaultForm: CacheForm = {
cacheName: '',
cacheKey: '',
cacheValue: '',
};
const stringifyValue = (value: unknown): string => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'string') {
return value;
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
};
const toCacheNameRecord = (item: string | CacheNameRecord): CacheNameRecord => {
if (typeof item === 'string') {
return { cacheName: item, remark: `缓存 ${item}` };
}
return {
cacheName: item.cacheName,
remark: item.remark ?? `缓存 ${item.cacheName}`,
};
};
const toCacheKeyString = (item: string | CacheKeyRecord): string => {
return typeof item === 'string' ? item : item.cacheKey;
};
const extractCacheNames = (payload: CacheNamePayload): CacheNameRecord[] => {
if (!payload) {
return [];
}
if (Array.isArray(payload)) {
return payload.map((item) => toCacheNameRecord(item));
}
if ('cacheNames' in payload && Array.isArray(payload.cacheNames)) {
return payload.cacheNames.map((item) => toCacheNameRecord(item));
}
return [];
};
const extractCacheKeys = (payload: CacheKeyPayload): string[] => {
if (!payload) {
return [];
}
if (Array.isArray(payload)) {
return payload.map((item) => toCacheKeyString(item));
}
if ('cacheKeys' in payload && Array.isArray(payload.cacheKeys)) {
return payload.cacheKeys.map((item) => toCacheKeyString(item));
}
return [];
};
const CacheListPage: React.FC = () => {
const { hasPermi } = usePermission();
const [cacheNames, setCacheNames] = useState<CacheNameRecord[]>([]);
const [cacheKeys, setCacheKeys] = useState<string[]>([]);
const [cacheForm, setCacheForm] = useState<CacheForm>(defaultForm);
const [loading, setLoading] = useState(false);
const [subLoading, setSubLoading] = useState(false);
const [nowCacheName, setNowCacheName] = useState('');
const [tableHeight, setTableHeight] = useState(window.innerHeight - 200);
const canClearCache = hasPermi('monitor:cache:list');
const getCacheKeys = useCallback(
async (cacheName?: string) => {
const targetCacheName = cacheName ?? nowCacheName;
if (!targetCacheName) {
setCacheKeys([]);
setCacheForm(defaultForm);
return;
}
setSubLoading(true);
try {
const response = await listCacheKey(targetCacheName);
const keys = extractCacheKeys(response);
setCacheKeys(keys);
setNowCacheName(targetCacheName);
setCacheForm({
cacheName: targetCacheName,
cacheKey: '',
cacheValue: '',
});
} catch (error: unknown) {
console.error('Failed to fetch cache keys:', error);
message.error('获取缓存键名列表失败');
} finally {
setSubLoading(false);
}
},
[nowCacheName],
);
const getCacheNames = useCallback(async () => {
setLoading(true);
try {
const response = await listCacheName();
const names = extractCacheNames(response);
setCacheNames(names);
if (names.length === 0) {
setNowCacheName('');
setCacheKeys([]);
setCacheForm(defaultForm);
return;
}
const targetCacheName =
nowCacheName && names.some((item) => item.cacheName === nowCacheName)
? nowCacheName
: names[0].cacheName;
await getCacheKeys(targetCacheName);
} catch (error: unknown) {
console.error('Failed to fetch cache names:', error);
message.error('获取缓存名称列表失败');
} finally {
setLoading(false);
}
}, [getCacheKeys, nowCacheName]);
const handleCacheValue = useCallback(
async (fullCacheKey: string) => {
if (!nowCacheName || !fullCacheKey) {
return;
}
try {
const response = await getCacheValue(nowCacheName, fullCacheKey);
setCacheForm({
cacheName: nowCacheName,
cacheKey: fullCacheKey,
cacheValue: stringifyValue(response),
});
} catch (error: unknown) {
console.error('Failed to fetch cache value:', error);
message.error('获取缓存内容失败');
}
},
[nowCacheName],
);
useEffect(() => {
void getCacheNames();
const handleResize = () => {
setTableHeight(window.innerHeight - 200);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [getCacheNames]);
const refreshCacheNames = async () => {
await getCacheNames();
message.success('刷新缓存列表成功');
};
const handleClearCacheName = async (row: CacheNameRecord) => {
if (!canClearCache) {
return;
}
try {
await clearCacheName(row.cacheName);
message.success(`清理缓存名称[${row.cacheName}]成功`);
await getCacheNames();
} catch {
message.error('清理缓存名称失败');
}
};
const refreshCacheKeys = async () => {
await getCacheKeys();
message.success('刷新键名列表成功');
};
const handleClearCacheKey = async (fullCacheKey: string) => {
if (!canClearCache) {
return;
}
if (!nowCacheName) {
return;
}
try {
await clearCacheKey(fullCacheKey);
message.success(`清理缓存键名[${fullCacheKey}]成功`);
await getCacheKeys(nowCacheName);
setCacheForm({ cacheName: nowCacheName, cacheKey: '', cacheValue: '' });
} catch {
message.error('清理缓存键名失败');
}
};
const handleClearCacheAll = async () => {
if (!canClearCache) {
return;
}
try {
await clearCacheAll();
message.success('清理全部缓存成功');
await getCacheNames();
} catch {
message.error('清理全部缓存失败');
}
};
const nameFormatter = (cacheName: string) => {
return cacheName.endsWith(':') ? cacheName.slice(0, -1) : cacheName;
};
const keyFormatter = (fullCacheKey: string) => {
const prefix = nowCacheName.endsWith(':') ? nowCacheName : `${nowCacheName}:`;
return fullCacheKey.replace(prefix, '');
};
const cacheNamesColumns: TableColumnsType<CacheNameRecord> = [
{
title: '序号',
key: 'index',
width: 60,
render: (_text, _record, index) => index + 1,
},
{
title: '缓存名称',
dataIndex: 'cacheName',
ellipsis: true,
render: (text) => nameFormatter(String(text ?? '')),
},
{ title: '备注', dataIndex: 'remark', ellipsis: true },
{
title: '操作',
key: 'operation',
width: 60,
render: (_text, record) => (
<Permission
permissions="monitor:cache:list"
fallback={<ReadonlyAction icon={<DeleteOutlined />} danger></ReadonlyAction>}
>
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => void handleClearCacheName(record)} />
</Permission>
),
},
];
const cacheKeysColumns: TableColumnsType<{ key: string; cacheKey: string }> = [
{
title: '序号',
key: 'index',
width: 60,
render: (_text, _record, index) => index + 1,
},
{
title: '缓存键名',
dataIndex: 'cacheKey',
ellipsis: true,
render: (text) => keyFormatter(String(text ?? '')),
},
{
title: '操作',
key: 'operation',
width: 60,
render: (_text, record) => (
<Permission
permissions="monitor:cache:list"
fallback={<ReadonlyAction icon={<DeleteOutlined />} danger></ReadonlyAction>}
>
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => void handleClearCacheKey(record.cacheKey)}
/>
</Permission>
),
},
];
return (
<div className="app-container cache-list-container">
<Row gutter={[16, 16]}>
<Col span={8}>
<Card
title={<Space><AppstoreOutlined /> </Space>}
extra={<Button type="text" icon={<ReloadOutlined />} onClick={() => void refreshCacheNames()} />}
style={{ height: tableHeight + 50, overflow: 'auto' }}
>
<Table
columns={cacheNamesColumns}
dataSource={cacheNames}
rowKey="cacheName"
loading={loading}
pagination={false}
size="small"
onRow={(record) => ({
onClick: () => void getCacheKeys(record.cacheName),
})}
scroll={{ y: tableHeight - 60 }}
/>
</Card>
</Col>
<Col span={8}>
<Card
title={<Space><KeyOutlined /> </Space>}
extra={
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => void refreshCacheKeys()}
disabled={!nowCacheName}
/>
}
style={{ height: tableHeight + 50, overflow: 'auto' }}
>
<Table
columns={cacheKeysColumns}
dataSource={cacheKeys.map((key) => ({ key, cacheKey: key }))}
rowKey="key"
loading={subLoading}
pagination={false}
size="small"
onRow={(record) => ({
onClick: () => void handleCacheValue(record.cacheKey),
})}
scroll={{ y: tableHeight - 60 }}
/>
</Card>
</Col>
<Col span={8}>
<Card
title={<Space><FileTextOutlined /> </Space>}
extra={
<Permission permissions="monitor:cache:list">
<Button type="text" danger onClick={() => void handleClearCacheAll()}></Button>
</Permission>
}
style={{ height: tableHeight + 50, overflow: 'auto' }}
>
<Form layout="vertical" initialValues={cacheForm}>
<Form.Item label="缓存名称:">
<Input value={nameFormatter(cacheForm.cacheName)} readOnly />
</Form.Item>
<Form.Item label="缓存键名:">
<Input value={cacheForm.cacheKey} readOnly />
</Form.Item>
<Form.Item label="缓存内容:">
<Input.TextArea value={cacheForm.cacheValue} rows={8} readOnly />
</Form.Item>
</Form>
</Card>
</Col>
</Row>
</div>
);
};
export default CacheListPage;