新增功能:用量可视化、操作日志页面优化、CSV导出
- RechargeCard 增加额度使用进度条与实时 RPM/TPM 展示(5列网格布局) - 统一操作日志页面布局风格,与其他控制台页面一致 - 充值账单与使用日志均支持 CSV 导出 - 新增 helpers/export.js 导出工具函数
This commit is contained in:
parent
6a210b9667
commit
006e723ac5
|
|
@ -0,0 +1,76 @@
|
|||
import paramiko
|
||||
import os
|
||||
import sys
|
||||
|
||||
host = "26.0.12.13"
|
||||
user = "root"
|
||||
password = "root@1234"
|
||||
local_dist = r"C:\Users\wangxj\tokenFactory\web\dist"
|
||||
remote_dist = "/root/tokenFactory/web/dist"
|
||||
|
||||
print("Connecting...")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(host, username=user, password=password)
|
||||
|
||||
# Clean old dist files AND rebuild Go
|
||||
print("Cleaning old dist and rebuilding...")
|
||||
stdin, stdout, stderr = ssh.exec_command(
|
||||
"rm -rf /root/tokenFactory/web/dist && mkdir -p /root/tokenFactory/web/dist"
|
||||
)
|
||||
stdout.read()
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
uploaded = 0
|
||||
for root, dirs, files in os.walk(local_dist):
|
||||
rel_path = os.path.relpath(root, local_dist)
|
||||
if rel_path == ".":
|
||||
remote_dir = remote_dist
|
||||
else:
|
||||
remote_dir = remote_dist + "/" + rel_path.replace("\\", "/")
|
||||
stdin, stdout, stderr = ssh.exec_command(f"mkdir -p {remote_dir}")
|
||||
stdout.read()
|
||||
|
||||
for f in files:
|
||||
local_file = os.path.join(root, f)
|
||||
remote_file = (remote_dir + "/" + f).replace("\\", "/")
|
||||
try:
|
||||
sftp.put(local_file, remote_file)
|
||||
uploaded += 1
|
||||
if uploaded % 30 == 0:
|
||||
print(f" Uploaded {uploaded} files...")
|
||||
except Exception as e:
|
||||
print(f" FAILED: {f}: {e}")
|
||||
|
||||
sftp.close()
|
||||
print(f"Uploaded {uploaded} files.")
|
||||
|
||||
# Rebuild Go
|
||||
print("Building Go...")
|
||||
stdin, stdout, stderr = ssh.exec_command(
|
||||
"export PATH=$PATH:/usr/local/go/bin && cd /root/tokenFactory && go build -o tf . 2>&1"
|
||||
)
|
||||
out = stdout.read().decode()
|
||||
err = stderr.read().decode()
|
||||
if out.strip():
|
||||
print("stdout:", out[-300:].strip())
|
||||
if err.strip():
|
||||
print("stderr:", err[-300:].strip())
|
||||
|
||||
# Replace binary file (not in a subdirectory — Docker mounts ./token-factory as a file)
|
||||
print("Replacing binary...")
|
||||
stdin, stdout, stderr = ssh.exec_command(
|
||||
"rm -rf /root/tokenFactory/token-factory && cp /root/tokenFactory/tf /root/tokenFactory/token-factory && ls -la /root/tokenFactory/token-factory"
|
||||
)
|
||||
print(stdout.read().decode().strip())
|
||||
|
||||
# Docker restart
|
||||
print("Restarting Docker...")
|
||||
stdin, stdout, stderr = ssh.exec_command(
|
||||
"cd /root/tokenFactory && docker compose down 2>&1 && docker compose up -d 2>&1"
|
||||
)
|
||||
print(stdout.read().decode()[-500:].strip())
|
||||
|
||||
ssh.close()
|
||||
print("Done!")
|
||||
|
|
@ -18,10 +18,11 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Tag, Space, Skeleton } from '@douyinfe/semi-ui';
|
||||
import { Tag, Space, Skeleton, Button } from '@douyinfe/semi-ui';
|
||||
import { renderQuota } from '../../../helpers';
|
||||
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
||||
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
|
||||
import { Download } from 'lucide-react';
|
||||
|
||||
const LogsActions = ({
|
||||
stat,
|
||||
|
|
@ -29,6 +30,8 @@ const LogsActions = ({
|
|||
showStat,
|
||||
compactMode,
|
||||
setCompactMode,
|
||||
exporting,
|
||||
handleExportLogs,
|
||||
t,
|
||||
}) => {
|
||||
const showSkeleton = useMinimumLoadingTime(loadingStat);
|
||||
|
|
@ -83,11 +86,23 @@ const LogsActions = ({
|
|||
</Space>
|
||||
</Skeleton>
|
||||
|
||||
<CompactModeToggle
|
||||
compactMode={compactMode}
|
||||
setCompactMode={setCompactMode}
|
||||
t={t}
|
||||
/>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<Download size={14} />}
|
||||
size='small'
|
||||
theme='outline'
|
||||
type='tertiary'
|
||||
loading={exporting}
|
||||
onClick={handleExportLogs}
|
||||
>
|
||||
{t('导出 CSV')}
|
||||
</Button>
|
||||
<CompactModeToggle
|
||||
compactMode={compactMode}
|
||||
setCompactMode={setCompactMode}
|
||||
t={t}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Tag,
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
Tooltip,
|
||||
Input,
|
||||
InputNumber,
|
||||
Progress,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
|
||||
import {
|
||||
|
|
@ -21,7 +22,10 @@ import {
|
|||
DollarSign,
|
||||
Ticket,
|
||||
ChevronRight,
|
||||
Gauge,
|
||||
TrendingUp,
|
||||
} from 'lucide-react';
|
||||
import { API } from '../../helpers';
|
||||
import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime';
|
||||
import { getCurrencyConfig } from '../../helpers/render';
|
||||
import SubscriptionPlansCard from './SubscriptionPlansCard';
|
||||
|
|
@ -80,6 +84,7 @@ const RechargeCard = ({
|
|||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('topup');
|
||||
const [customAmount, setCustomAmount] = useState(10);
|
||||
const [rpmTpmData, setRpmTpmData] = useState(null);
|
||||
const showAmountSkeleton = useMinimumLoadingTime(amountLoading);
|
||||
const shouldShowSubscription =
|
||||
!subscriptionLoading && subscriptionPlans.length > 0;
|
||||
|
|
@ -133,6 +138,26 @@ const RechargeCard = ({
|
|||
}
|
||||
}, [shouldShowSubscription, subscriptionLoading]);
|
||||
|
||||
const fetchRpmTpm = useCallback(async () => {
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const res = await API.get(
|
||||
`/api/log/self/stat?start_timestamp=${now - 3600}&end_timestamp=${now}`,
|
||||
);
|
||||
if (res?.data?.success) {
|
||||
setRpmTpmData(res.data.data);
|
||||
}
|
||||
} catch {
|
||||
// 静默失败
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRpmTpm();
|
||||
const interval = setInterval(fetchRpmTpm, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchRpmTpm]);
|
||||
|
||||
const isAnyPayEnabled =
|
||||
enableOnlineTopUp ||
|
||||
enableStripeTopUp ||
|
||||
|
|
@ -146,6 +171,9 @@ const RechargeCard = ({
|
|||
{ key: 'subscription', label: t('订阅'), desc: t('享更低单价') },
|
||||
];
|
||||
|
||||
const totalQuota = (userState?.user?.used_quota || 0) + (userState?.user?.quota || 0);
|
||||
const usagePercent = totalQuota > 0 ? Math.round(((userState?.user?.used_quota || 0) / totalQuota) * 100) : 0;
|
||||
|
||||
// 统计卡片配置
|
||||
const statCards = [
|
||||
{
|
||||
|
|
@ -153,7 +181,7 @@ const RechargeCard = ({
|
|||
icon: WalletCards,
|
||||
label: t('当前余额'),
|
||||
value: userState?.user?.quota,
|
||||
sublabel: t('剩余配额'),
|
||||
sublabel: `${t('已用')} ${usagePercent}%`,
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -171,6 +199,24 @@ const RechargeCard = ({
|
|||
sublabel: t('总请求数'),
|
||||
raw: true,
|
||||
},
|
||||
{
|
||||
key: 'rpm',
|
||||
icon: Gauge,
|
||||
label: t('RPM'),
|
||||
value: rpmTpmData?.rpm,
|
||||
sublabel: t('最近1分钟'),
|
||||
raw: true,
|
||||
live: true,
|
||||
},
|
||||
{
|
||||
key: 'tpm',
|
||||
icon: TrendingUp,
|
||||
label: t('TPM'),
|
||||
value: rpmTpmData?.tpm,
|
||||
sublabel: t('最近1分钟'),
|
||||
raw: true,
|
||||
live: true,
|
||||
},
|
||||
];
|
||||
|
||||
const topupContent = (
|
||||
|
|
@ -456,12 +502,12 @@ const RechargeCard = ({
|
|||
return (
|
||||
<div className='flex w-full flex-col gap-5'>
|
||||
{/* 统计卡片 */}
|
||||
<div className='grid grid-cols-3 gap-0 overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-white/10 dark:bg-white/[0.02]'>
|
||||
<div className='grid grid-cols-3 md:grid-cols-5 gap-0 overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-white/10 dark:bg-white/[0.02]'>
|
||||
{statCards.map((card, i) => (
|
||||
<div
|
||||
key={card.key}
|
||||
className={`relative flex flex-col px-5 py-4 ${
|
||||
i < 2 ? 'border-r border-slate-100 dark:border-white/5' : ''
|
||||
className={`relative flex flex-col px-4 py-4 ${
|
||||
i < 4 ? 'border-r border-slate-100 dark:border-white/5' : ''
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
|
|
@ -469,24 +515,31 @@ const RechargeCard = ({
|
|||
className={`h-3.5 w-3.5 ${
|
||||
card.highlight
|
||||
? 'text-emerald-500 dark:text-emerald-400'
|
||||
: 'text-slate-400 dark:text-white/40'
|
||||
: card.live
|
||||
? 'text-blue-500 dark:text-blue-400'
|
||||
: 'text-slate-400 dark:text-white/40'
|
||||
}`}
|
||||
/>
|
||||
<span className='text-xs font-medium text-slate-400 dark:text-white/40'>
|
||||
{card.label}
|
||||
{card.live && (
|
||||
<span className='ml-1 inline-block h-1.5 w-1.5 rounded-full bg-blue-500 animate-pulse' />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-2 font-mono text-2xl font-bold tabular-nums ${
|
||||
card.highlight
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: 'text-slate-900 dark:text-white'
|
||||
: card.live
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-slate-900 dark:text-white'
|
||||
}`}
|
||||
>
|
||||
{statusLoading ? (
|
||||
{statusLoading && !card.live ? (
|
||||
<Skeleton.Title style={{ width: 60, height: 28, margin: 0 }} />
|
||||
) : card.raw ? (
|
||||
card.value
|
||||
card.value != null ? card.value : '—'
|
||||
) : (
|
||||
renderQuota(card.value)
|
||||
)}
|
||||
|
|
@ -494,6 +547,17 @@ const RechargeCard = ({
|
|||
<div className='mt-1 text-xs text-slate-400 dark:text-white/40'>
|
||||
{card.sublabel}
|
||||
</div>
|
||||
{card.key === 'balance' && (
|
||||
<div className='mt-3'>
|
||||
<Progress
|
||||
percent={usagePercent}
|
||||
strokeWidth={6}
|
||||
stroke='var(--semi-color-success)'
|
||||
aria-label='usage percent'
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,15 +28,17 @@ import {
|
|||
Input,
|
||||
Tag,
|
||||
Select,
|
||||
Space,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { Coins } from 'lucide-react';
|
||||
import { Coins, Download } from 'lucide-react';
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { API, timestamp2string } from '../../../helpers';
|
||||
import { isAdmin } from '../../../helpers/utils';
|
||||
import { exportCSV } from '../../../helpers/export';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -202,6 +204,45 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const adminNow = isAdmin();
|
||||
const base = adminNow ? '/api/user/topup' : '/api/user/topup/self';
|
||||
const tn = tradeNoFilter.trim();
|
||||
const un = usernameFilter.trim();
|
||||
const params = { p: 1, page_size: 10000 };
|
||||
if (tn) params.trade_no = tn;
|
||||
if (adminNow && un) params.username = un;
|
||||
if (statusFilter && statusFilter !== TOPUP_STATUS_ALL) {
|
||||
params.status = statusFilter;
|
||||
}
|
||||
const res = await API.get(base, { params, disableDuplicate: true });
|
||||
const { success, data } = res.data;
|
||||
if (success && data?.items?.length) {
|
||||
const columns = [
|
||||
{ title: t('订单号'), dataIndex: 'trade_no' },
|
||||
{ title: t('用户名'), dataIndex: 'username' },
|
||||
{ title: t('支付方式'), dataIndex: 'payment_method' },
|
||||
{ title: t('充值额度'), dataIndex: 'amount' },
|
||||
{ title: t('支付金额'), dataIndex: 'money' },
|
||||
{ title: t('状态'), dataIndex: 'status' },
|
||||
{ title: t('创建时间'), dataIndex: 'create_time', render: (v) => v ? timestamp2string(v) : '' },
|
||||
];
|
||||
exportCSV(data.items, columns, `topup-records-${new Date().toISOString().slice(0, 10)}`);
|
||||
Toast.success({ content: t('导出成功') });
|
||||
} else {
|
||||
Toast.info({ content: t('无数据可导出') });
|
||||
}
|
||||
} catch {
|
||||
Toast.error({ content: t('导出失败') });
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAdminComplete = (tradeNo) => {
|
||||
Modal.confirm({
|
||||
title: t('确认补单'),
|
||||
|
|
@ -332,6 +373,18 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
|||
>
|
||||
{/* 筛选条件单行排列;宽度不足时横向滚动,避免折成两行 */}
|
||||
<div className='mb-3 flex w-full flex-row flex-nowrap items-center gap-2 overflow-x-auto'>
|
||||
<div className='shrink-0'>
|
||||
<Button
|
||||
icon={<Download size={14} />}
|
||||
size='small'
|
||||
theme='outline'
|
||||
type='tertiary'
|
||||
loading={exporting}
|
||||
onClick={handleExport}
|
||||
>
|
||||
{t('导出 CSV')}
|
||||
</Button>
|
||||
</div>
|
||||
{userIsAdmin ? (
|
||||
<Input
|
||||
className='min-w-[104px] flex-1'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* 导出数据为 CSV 文件并触发浏览器下载。
|
||||
* @param {Array<object>} rows 数据行
|
||||
* @param {Array<{title: string, dataIndex: string, render?: function}>} columns 列定义
|
||||
* @param {string} filename 文件名(不含扩展名)
|
||||
*/
|
||||
export function exportCSV(rows, columns, filename = 'export') {
|
||||
if (!rows || !rows.length) return;
|
||||
|
||||
const BOM = '';
|
||||
const header = columns.map((col) => `"${(col.title || col.dataIndex).replace(/"/g, '""')}"`).join(',');
|
||||
const body = rows
|
||||
.map((row) =>
|
||||
columns
|
||||
.map((col) => {
|
||||
let val = row[col.dataIndex];
|
||||
if (typeof col.render === 'function') {
|
||||
// render 函数返回 React 元素时提取文本
|
||||
const rendered = col.render(val, row);
|
||||
if (rendered == null || rendered === '') return '""';
|
||||
if (typeof rendered === 'string' || typeof rendered === 'number') {
|
||||
val = String(rendered).replace(/"/g, '""');
|
||||
} else if (rendered && typeof rendered === 'object' && rendered.props) {
|
||||
// 简单提取 React 元素文本
|
||||
val = extractText(rendered).replace(/"/g, '""');
|
||||
} else {
|
||||
val = String(rendered).replace(/"/g, '""');
|
||||
}
|
||||
} else {
|
||||
val = val != null ? String(val).replace(/"/g, '""') : '';
|
||||
}
|
||||
return `"${val}"`;
|
||||
})
|
||||
.join(','),
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const csv = BOM + header + '\n' + body;
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${filename}.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function extractText(element) {
|
||||
if (element == null) return '';
|
||||
if (typeof element === 'string' || typeof element === 'number') return String(element);
|
||||
if (Array.isArray(element)) return element.map(extractText).join('');
|
||||
if (element.props && element.props.children) return extractText(element.props.children);
|
||||
return '';
|
||||
}
|
||||
|
|
@ -17,9 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal, Tag } from '@douyinfe/semi-ui';
|
||||
import { Modal, Tag, Toast } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
API,
|
||||
getTodayStartTimestamp,
|
||||
|
|
@ -38,6 +38,7 @@ import {
|
|||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
import { exportCSV } from '../../helpers/export';
|
||||
import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry';
|
||||
|
||||
export const useLogsData = () => {
|
||||
|
|
@ -550,6 +551,7 @@ export const useLogsData = () => {
|
|||
visible: false,
|
||||
record: null,
|
||||
});
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
// Initialize default column visibility
|
||||
const initDefaultColumns = () => {
|
||||
|
|
@ -1307,6 +1309,60 @@ export const useLogsData = () => {
|
|||
await loadLogs(1, pageSize);
|
||||
};
|
||||
|
||||
const handleExportLogs = useCallback(async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const {
|
||||
username,
|
||||
token_name,
|
||||
model_name,
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
channel,
|
||||
group,
|
||||
request_id,
|
||||
logType: formLogType,
|
||||
} = getFormValues();
|
||||
const currentLogType =
|
||||
formLogType !== undefined ? formLogType : logType;
|
||||
const localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
const localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let url;
|
||||
if (isAdminUser) {
|
||||
url = `/api/log/?p=1&page_size=10000&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}&request_id=${request_id}`;
|
||||
} else {
|
||||
url = `/api/log/self/?p=1&page_size=10000&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}&request_id=${request_id}`;
|
||||
}
|
||||
url = encodeURI(url);
|
||||
const res = await API.get(url);
|
||||
const { success, data } = res.data;
|
||||
if (success && data?.items?.length) {
|
||||
const typeMap = { 0: t('未知'), 1: t('充值'), 2: t('消费'), 3: t('管理'), 4: t('系统'), 5: t('错误'), 6: t('退款') };
|
||||
const columns = [
|
||||
{ title: t('时间'), dataIndex: 'created_at', render: (v) => v ? timestamp2string(v) : '' },
|
||||
{ title: t('渠道'), dataIndex: 'channel' },
|
||||
{ title: t('用户名'), dataIndex: 'username' },
|
||||
{ title: t('令牌'), dataIndex: 'token_name' },
|
||||
{ title: t('分组'), dataIndex: 'group' },
|
||||
{ title: t('类型'), dataIndex: 'type', render: (v) => typeMap[v] || t('未知') },
|
||||
{ title: t('模型'), dataIndex: 'model_name' },
|
||||
{ title: t('输入Tokens'), dataIndex: 'prompt_tokens' },
|
||||
{ title: t('输出Tokens'), dataIndex: 'completion_tokens' },
|
||||
{ title: t('花费'), dataIndex: 'quota', render: (v) => renderQuota(v) },
|
||||
{ title: t('IP'), dataIndex: 'ip' },
|
||||
];
|
||||
exportCSV(data.items, columns, `usage-logs-${new Date().toISOString().slice(0, 10)}`);
|
||||
Toast.success({ content: t('导出成功') });
|
||||
} else {
|
||||
Toast.info({ content: t('无数据可导出') });
|
||||
}
|
||||
} catch {
|
||||
Toast.error({ content: t('导出失败') });
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}, [getFormValues, logType, isAdminUser, t]);
|
||||
|
||||
// Copy text function
|
||||
const copyText = async (e, text) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -1408,6 +1464,10 @@ export const useLogsData = () => {
|
|||
setLogType,
|
||||
openParamOverrideModal,
|
||||
|
||||
// Export
|
||||
exporting,
|
||||
handleExportLogs,
|
||||
|
||||
// Translation
|
||||
t,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -239,7 +239,8 @@ const OperationLog = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||
<Card>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -401,6 +402,7 @@ const OperationLog = () => {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue