🐛 fix: Added system version card and system log dialog components; updated statistics page to include total server and user statistics

This commit is contained in:
web 2025-09-13 04:07:31 -07:00
parent 3e5402f2fc
commit fe699809a4
5 changed files with 344 additions and 363 deletions

View File

@ -1,351 +0,0 @@
'use client';
import { getSystemLog, getVersion, restartSystem } from '@/services/admin/tool';
import { formatDate } from '@/utils/common';
import { useQuery } from '@tanstack/react-query';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@workspace/ui/components/accordion';
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@workspace/ui/components/alert-dialog';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@workspace/ui/components/card';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import packageJson from '../../../../../package.json';
const getLogLevelColor = (level: string) => {
const colorMap: { [key: string]: string } = {
INFO: 'bg-blue-100 text-blue-800 hover:bg-blue-200',
WARN: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200',
ERROR: 'bg-red-100 text-red-800 hover:bg-red-200',
};
return colorMap[level] || 'bg-gray-100 text-gray-800 hover:bg-gray-200';
};
export default function Page() {
const t = useTranslations('tool');
const {
data: logs,
refetch,
isLoading,
} = useQuery({
queryKey: ['getSystemLog'],
queryFn: async () => {
const { data } = await getSystemLog();
return data.data?.list || [];
},
});
const [openRestart, setOpenRestart] = useState(false);
const [isRestarting, setIsRestarting] = useState(false);
const { data: latestReleases } = useQuery({
queryKey: ['getLatestReleases'],
queryFn: async () => {
try {
const [webResponse, serverResponse] = await Promise.all([
fetch('https://api.github.com/repos/perfect-panel/ppanel-web/releases/latest'),
fetch('https://api.github.com/repos/perfect-panel/server/releases/latest'),
]);
const webData = webResponse.ok ? await webResponse.json() : null;
const serverData = serverResponse.ok ? await serverResponse.json() : null;
return {
web: webData
? {
version: webData.tag_name,
url: webData.html_url,
publishedAt: webData.published_at,
}
: null,
server: serverData
? {
version: serverData.tag_name,
url: serverData.html_url,
publishedAt: serverData.published_at,
}
: null,
};
} catch (error) {
console.error('Failed to fetch latest releases:', error);
return { web: null, server: null };
}
},
staleTime: 60 * 60 * 1000,
retry: 1,
retryDelay: 10000,
});
const hasNewVersion =
latestReleases?.web && packageJson.version !== latestReleases.web.version.replace(/^v/, '');
const { data: systemInfo } = useQuery({
queryKey: ['getVersion'],
queryFn: async () => {
const { data } = await getVersion();
const versionString = data.data?.version || '';
const releaseVersionRegex = /^[Vv]?\d+\.\d+\.\d+(-[a-zA-Z]+(\.\d+)?)?$/;
const timeMatch = versionString.match(/\(([^)]+)\)/);
const timeInBrackets = timeMatch ? timeMatch[1] : '';
const versionWithoutTime = versionString.replace(/\([^)]*\)/, '').trim();
const isDevelopment =
versionWithoutTime.includes('-dev') ||
versionWithoutTime.includes('-debug') ||
versionWithoutTime.includes('-nightly') ||
versionWithoutTime.includes('dev') ||
!releaseVersionRegex.test(versionWithoutTime);
let baseVersion = versionWithoutTime;
let lastUpdated = '';
if (isDevelopment && versionWithoutTime.includes('-')) {
const parts = versionWithoutTime.split('-');
baseVersion = parts[0] || versionWithoutTime;
}
lastUpdated = formatDate(new Date(timeInBrackets || Date.now())) || '';
const displayVersion =
baseVersion.startsWith('V') || baseVersion.startsWith('v')
? baseVersion
: `V${baseVersion}`;
return {
isRelease: !isDevelopment,
version: displayVersion,
lastUpdated,
};
},
});
const hasServerNewVersion =
latestReleases?.server &&
systemInfo &&
systemInfo.version.replace(/^V/, '') !== latestReleases.server.version.replace(/^v/, '');
return (
<Card className='border-none'>
<CardHeader className='flex flex-col items-start justify-between sm:flex-row sm:items-center'>
<div>
<CardTitle>{t('systemServices')}</CardTitle>
<CardDescription>{t('viewLogsAndManage')}</CardDescription>
</div>
<div className='mt-4 flex flex-col space-y-2 sm:mt-0 sm:flex-row sm:space-x-2 sm:space-y-0'>
{/* <AlertDialog>
<AlertDialogTrigger asChild>
<Button>{t('systemUpgrade')}</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('confirmSystemUpgrade')}</AlertDialogTitle>
<AlertDialogDescription>{t('upgradeDescription')}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction>{t('confirmUpgrade')}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog> */}
<AlertDialog open={openRestart} onOpenChange={setOpenRestart}>
<AlertDialogTrigger asChild>
<Button variant='destructive'>{t('systemReboot')}</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('confirmSystemReboot')}</AlertDialogTitle>
<AlertDialogDescription>{t('rebootDescription')}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<Button
onClick={async () => {
setIsRestarting(true);
await restartSystem();
await new Promise((resolve) => setTimeout(resolve, 5000));
setIsRestarting(false);
setOpenRestart(false);
}}
disabled={isRestarting}
>
{isRestarting && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
{isRestarting ? t('rebooting') : t('confirmReboot')}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardHeader>
<CardContent>
<div className='space-y-4'>
<div className='flex flex-col space-y-2 sm:flex-row sm:space-x-3 sm:space-y-0'>
<div className='bg-muted/30 flex flex-1 items-center justify-between rounded-md p-2'>
<div className='flex items-center'>
<Icon icon='mdi:web' className='mr-2 h-4 w-4 text-green-600' />
<div className='flex items-center space-x-2'>
<span className='text-xs font-medium'>{t('webVersion')}</span>
<Badge variant='default' className='px-1.5 py-0.5 text-xs'>
V{packageJson.version}
</Badge>
{hasNewVersion && (
<Badge variant='destructive' className='animate-pulse px-1.5 py-0.5 text-xs'>
{t('newVersionAvailable')}
</Badge>
)}
</div>
</div>
{hasNewVersion && (
<Button
variant='outline'
size='sm'
className='h-6 px-2 text-xs'
onClick={() =>
window.open(
latestReleases?.web?.url ||
'https://github.com/perfect-panel/ppanel-web/releases',
'_blank',
)
}
>
<Icon icon='mdi:open-in-new' className='mr-1 h-3 w-3' />
{t('viewNewVersion')}
</Button>
)}
</div>
<div className='bg-muted/30 flex flex-1 items-center justify-between rounded-md p-2'>
<div className='flex items-center'>
<Icon icon='mdi:server' className='mr-2 h-4 w-4 text-blue-600' />
<div className='flex items-center space-x-2'>
<span className='text-xs font-medium'>{t('serverVersion')}</span>
<Badge
variant={!systemInfo?.isRelease ? 'destructive' : 'default'}
className='px-1.5 py-0.5 text-xs'
>
{systemInfo?.version || 'V1.0.0'}
{!systemInfo?.isRelease && (
<span className='ml-1'>{t('developmentVersion')}</span>
)}
</Badge>
{hasServerNewVersion && (
<Badge variant='destructive' className='animate-pulse px-1.5 py-0.5 text-xs'>
{t('newVersionAvailable')}
</Badge>
)}
</div>
</div>
<div className='flex items-center space-x-2'>
{hasServerNewVersion && (
<Button
variant='outline'
size='sm'
className='h-6 px-2 text-xs'
onClick={() =>
window.open(
latestReleases?.server?.url ||
'https://github.com/perfect-panel/server/releases',
'_blank',
)
}
>
<Icon icon='mdi:open-in-new' className='mr-1 h-3 w-3' />
{t('viewNewVersion')}
</Button>
)}
<div className='text-muted-foreground hidden text-right text-xs sm:block'>
<div className='font-mono'>{systemInfo?.lastUpdated || '--'}</div>
</div>
</div>
</div>
</div>
<Card className='overflow-hidden'>
<CardHeader className='bg-secondary py-1'>
<div className='flex items-center justify-between'>
<CardTitle>{t('systemLogs')}</CardTitle>
<Button variant='ghost' size='icon' disabled={isLoading} onClick={() => refetch()}>
<Icon
icon='uil:refresh'
className={`h-5 w-5 ${isLoading ? 'animate-spin' : ''}`}
/>
<span className='sr-only'>{t('refreshLogs')}</span>
</Button>
</div>
</CardHeader>
<CardContent className='p-0'>
<ScrollArea className='h-[calc(100dvh-300px)] w-full rounded-md'>
{isLoading ? (
<div className='flex h-full items-center justify-center'>
<Icon icon='uil:loading' className='text-primary h-8 w-8 animate-spin' />
</div>
) : (
<Accordion type='single' collapsible className='w-full'>
{logs?.map((log: any, index: number) => (
<AccordionItem key={index} value={`item-${index}`} className='px-4'>
<AccordionTrigger className='hover:no-underline'>
<div className='flex w-full flex-col items-start space-y-2 sm:flex-row sm:items-center sm:space-x-4 sm:space-y-0'>
<span className='text-xs font-medium sm:text-sm'>{log.timestamp}</span>
</div>
</AccordionTrigger>
<AccordionContent className='px-2'>
{Object.entries(log).map(([key, value]) => (
<div
key={key}
className='grid grid-cols-1 gap-2 text-xs sm:grid-cols-2 sm:text-sm'
>
<span className='font-medium'>{key}:</span>
<span className='break-all'>{value as string}</span>
</div>
))}
{/* <div className='grid grid-cols-1 gap-2 text-xs sm:grid-cols-2 sm:text-sm'>
<div className='font-medium'>{t('ip')}:</div>
<div>{log.ip}</div>
<div className='font-medium'>{t('request')}:</div>
<div className='break-all'>{log.request}</div>
<div className='font-medium'>{t('status')}:</div>
<div>{log.status}</div>
<div className='font-medium'>{t('caller')}:</div>
<div className='break-all'>{log.caller}</div>
<div className='font-medium'>{t('errors')}:</div>
<div className='break-all'>{log.errors || t('none')}</div>
<div className='font-medium'>{t('query')}:</div>
<div className='break-all'>{log.query || t('none')}</div>
<div className='font-medium'>{t('userAgent')}:</div>
<div className='break-all'>{log['user-agent']}</div>
</div> */}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</ScrollArea>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
);
}

View File

@ -22,6 +22,7 @@ import { useState } from 'react';
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from 'recharts';
import { Empty } from '../empty';
import { RevenueStatisticsCard } from './revenue-statistics-card';
import SystemVersionCard from './system-version-card';
import { UserStatisticsCard } from './user-statistics-card';
export default function Statistics() {
@ -78,26 +79,18 @@ export default function Statistics() {
return (
<>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-5'>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3'>
{[
{
title: t('onlineUsersCount'),
value: ServerTotal?.online_users || 0,
subtitle: t('currentlyOnline'),
icon: 'uil:users-alt',
href: '/dashboard/servers',
href: '/dashboard/users',
color: 'text-blue-600 dark:text-blue-400',
iconBg: 'bg-blue-100 dark:bg-blue-900/30',
},
{
title: t('totalServers'),
value: (ServerTotal?.online_servers || 0) + (ServerTotal?.offline_servers || 0),
subtitle: `${t('online')} ${ServerTotal?.online_servers || 0} ${t('offline')} ${ServerTotal?.offline_servers || 0}`,
icon: 'uil:server-network',
href: '/dashboard/servers',
color: 'text-green-600 dark:text-green-400',
iconBg: 'bg-green-100 dark:bg-green-900/30',
},
{
title: t('todayTraffic'),
value: formatBytes(
@ -118,6 +111,15 @@ export default function Statistics() {
color: 'text-orange-600 dark:text-orange-400',
iconBg: 'bg-orange-100 dark:bg-orange-900/30',
},
{
title: t('totalServers'),
value: (ServerTotal?.online_servers || 0) + (ServerTotal?.offline_servers || 0),
subtitle: `${t('online')} ${ServerTotal?.online_servers || 0} ${t('offline')} ${ServerTotal?.offline_servers || 0}`,
icon: 'uil:server-network',
href: '/dashboard/servers',
color: 'text-green-600 dark:text-green-400',
iconBg: 'bg-green-100 dark:bg-green-900/30',
},
{
title: t('pendingTickets'),
value: TicketTotal || 0,
@ -151,6 +153,7 @@ export default function Statistics() {
</Card>
</Link>
))}
<SystemVersionCard />
</div>
<div className='grid gap-3 md:grid-cols-2 lg:grid-cols-3'>
<RevenueStatisticsCard />

View File

@ -0,0 +1,118 @@
'use client';
import { getSystemLog } from '@/services/admin/tool';
import { useQuery } from '@tanstack/react-query';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@workspace/ui/components/accordion';
import { Button } from '@workspace/ui/components/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@workspace/ui/components/dialog';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
const getLogLevelColor = (level: string) => {
const colorMap: { [key: string]: string } = {
INFO: 'bg-blue-100 text-blue-800 hover:bg-blue-200',
WARN: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200',
ERROR: 'bg-red-100 text-red-800 hover:bg-red-200',
};
return colorMap[level] || 'bg-gray-100 text-gray-800 hover:bg-gray-200';
};
interface SystemLogsDialogProps {
trigger?: React.ReactNode;
variant?: 'default' | 'outline' | 'ghost' | 'secondary';
size?: 'sm' | 'default' | 'lg';
}
export default function SystemLogsDialog({
trigger,
variant = 'outline',
size = 'sm',
}: SystemLogsDialogProps) {
const t = useTranslations('tool');
const [open, setOpen] = useState(false);
const {
data: logs,
refetch,
isLoading,
} = useQuery({
queryKey: ['getSystemLog'],
queryFn: async () => {
const { data } = await getSystemLog();
return data.data?.list || [];
},
enabled: open,
});
const defaultTrigger = (
<Button variant={variant} size={size}>
{t('systemLogs')}
</Button>
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger || defaultTrigger}</DialogTrigger>
<DialogContent className='max-w-4xl'>
<DialogHeader>
<DialogTitle>{t('systemLogs')}</DialogTitle>
</DialogHeader>
<ScrollArea className='bg-muted/30 h-[60vh] max-h-[80vh] min-h-[400px] w-full rounded-lg border p-1'>
{isLoading ? (
<div className='flex h-full items-center justify-center'>
<Icon icon='uil:loading' className='text-primary h-8 w-8 animate-spin' />
</div>
) : (
<Accordion type='single' collapsible className='w-full'>
{logs?.map((log: any, index: number) => (
<AccordionItem key={index} value={`item-${index}`} className='px-4'>
<AccordionTrigger className='hover:no-underline'>
<div className='flex w-full flex-col items-start space-y-2 sm:flex-row sm:items-center sm:space-x-4 sm:space-y-0'>
<span className='text-xs font-medium sm:text-sm'>{log.timestamp}</span>
</div>
</AccordionTrigger>
<AccordionContent className='px-2'>
{Object.entries(log).map(([key, value]) => (
<div
key={key}
className='grid grid-cols-1 gap-2 text-xs sm:grid-cols-2 sm:text-sm'
>
<span className='font-medium'>{key}:</span>
<span className='break-all'>{value as string}</span>
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</ScrollArea>
<DialogFooter>
<Button
variant='outline'
onClick={() => {
refetch();
}}
>
<Icon icon='uil:refresh' className={`h-5 w-5 ${isLoading ? 'animate-spin' : ''}`} />
<span>{t('refreshLogs')}</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,212 @@
'use client';
import { getVersion, restartSystem } from '@/services/admin/tool';
import { formatDate } from '@/utils/common';
import { useQuery } from '@tanstack/react-query';
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@workspace/ui/components/alert-dialog';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useState } from 'react';
import packageJson from '../../../../package.json';
import SystemLogsDialog from './system-logs-dialog';
export default function SystemVersionCard() {
const t = useTranslations('tool');
const [openRestart, setOpenRestart] = useState(false);
const [isRestarting, setIsRestarting] = useState(false);
const { data: latestReleases } = useQuery({
queryKey: ['getLatestReleases'],
queryFn: async () => {
try {
const [webResponse, serverResponse] = await Promise.all([
fetch('https://api.github.com/repos/perfect-panel/ppanel-web/releases/latest'),
fetch('https://api.github.com/repos/perfect-panel/server/releases/latest'),
]);
const webData = webResponse.ok ? await webResponse.json() : null;
const serverData = serverResponse.ok ? await serverResponse.json() : null;
return {
web: webData
? {
version: webData.tag_name,
url: webData.html_url,
publishedAt: webData.published_at,
}
: null,
server: serverData
? {
version: serverData.tag_name,
url: serverData.html_url,
publishedAt: serverData.published_at,
}
: null,
};
} catch (error) {
console.error('Failed to fetch latest releases:', error);
return { web: null, server: null };
}
},
staleTime: 60 * 60 * 1000,
retry: 1,
retryDelay: 10000,
});
const hasNewVersion =
latestReleases?.web && packageJson.version !== latestReleases.web.version.replace(/^v/, '');
const { data: systemInfo } = useQuery({
queryKey: ['getVersion'],
queryFn: async () => {
const { data } = await getVersion();
const versionString = (data.data?.version || '').replace(' Develop', '').trim();
const releaseVersionRegex = /^[Vv]?\d+\.\d+\.\d+(-[a-zA-Z]+(\.\d+)?)?$/;
const timeMatch = versionString.match(/\(([^)]+)\)/);
const timeInBrackets = timeMatch ? timeMatch[1] : '';
const versionWithoutTime = versionString.replace(/\([^)]*\)/, '').trim();
const isDevelopment =
versionWithoutTime.includes('-dev') ||
versionWithoutTime.includes('-debug') ||
versionWithoutTime.includes('-nightly') ||
versionWithoutTime.includes('dev') ||
!releaseVersionRegex.test(versionWithoutTime);
let baseVersion = versionWithoutTime;
let lastUpdated = '';
if (isDevelopment && versionWithoutTime.includes('-')) {
const parts = versionWithoutTime.split('-');
baseVersion = parts[0] || versionWithoutTime;
}
lastUpdated = formatDate(new Date(timeInBrackets || Date.now())) || '';
const displayVersion =
baseVersion.startsWith('V') || baseVersion.startsWith('v')
? baseVersion
: `V${baseVersion}`;
return {
isRelease: !isDevelopment,
version: displayVersion,
lastUpdated,
};
},
});
const hasServerNewVersion =
latestReleases?.server &&
systemInfo &&
systemInfo.version.replace(/^V/, '') !== latestReleases.server.version.replace(/^v/, '');
return (
<Card className='p-3'>
<CardHeader className='mb-2 p-0'>
<CardTitle className='flex items-center justify-between'>
{t('systemServices')}
<div className='flex items-center space-x-2'>
<SystemLogsDialog variant='outline' size='sm' />
<AlertDialog open={openRestart} onOpenChange={setOpenRestart}>
<AlertDialogTrigger asChild>
<Button variant='destructive' size='sm'>
{t('systemReboot')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('confirmSystemReboot')}</AlertDialogTitle>
<AlertDialogDescription>{t('rebootDescription')}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<Button
onClick={async () => {
setIsRestarting(true);
await restartSystem();
await new Promise((resolve) => setTimeout(resolve, 5000));
setIsRestarting(false);
setOpenRestart(false);
}}
disabled={isRestarting}
>
{isRestarting && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{isRestarting ? t('rebooting') : t('confirmReboot')}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardTitle>
</CardHeader>
<CardContent className='space-y-3 p-0'>
<div className='flex flex-1 items-center justify-between'>
<div className='flex items-center'>
<Icon icon='mdi:web' className='mr-2 h-4 w-4 text-green-600' />
<span className='text-sm font-medium'>{t('webVersion')}</span>
</div>
<div className='flex items-center space-x-2'>
<Badge>V{packageJson.version}</Badge>
{hasNewVersion && (
<Link
href={
latestReleases?.web?.url || 'https://github.com/perfect-panel/ppanel-web/releases'
}
target='_blank'
rel='noopener noreferrer'
className='flex items-center space-x-1'
>
<Badge variant='destructive' className='animate-pulse px-2 py-0.5 text-xs'>
{t('newVersionAvailable')}
</Badge>
<Icon icon='mdi:open-in-new' className='h-3 w-3' />
</Link>
)}
</div>
</div>
<div className='flex flex-1 items-center justify-between'>
<div className='flex items-center'>
<Icon icon='mdi:server' className='mr-2 h-4 w-4 text-blue-600' />
<span className='text-sm font-medium'>{t('serverVersion')}</span>
</div>
<div className='flex items-center space-x-2'>
<Badge variant={!systemInfo?.isRelease ? 'destructive' : 'default'}>
{systemInfo?.version || 'V1.0.0'}
</Badge>
{hasServerNewVersion && (
<Link
href={
latestReleases?.server?.url || 'https://github.com/perfect-panel/server/releases'
}
target='_blank'
rel='noopener noreferrer'
className='flex items-center space-x-1'
>
<Badge variant='destructive' className='animate-pulse px-2 py-0.5 text-xs'>
{t('newVersionAvailable')}
</Badge>
<Icon icon='mdi:open-in-new' className='h-3 w-3' />
</Link>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -77,7 +77,6 @@ export const navs = [
icon: 'flat-color-icons:currency-exchange',
},
{ title: 'ADS Config', url: '/dashboard/ads', icon: 'flat-color-icons:electrical-sensor' },
{ title: 'System Tool', url: '/dashboard/tool', icon: 'flat-color-icons:info' },
],
},