400 lines
11 KiB
TypeScript
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;
|