✨ feat(ui): System Tool
This commit is contained in:
parent
4c67387eab
commit
1836980691
179
apps/admin/app/dashboard/tool/page.tsx
Normal file
179
apps/admin/app/dashboard/tool/page.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getSystemLog, restartSystem } from '@/services/admin/tool';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@shadcn/ui/accordion';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@shadcn/ui/alert-dialog';
|
||||||
|
import { Badge } from '@shadcn/ui/badge';
|
||||||
|
import { Button } from '@shadcn/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@shadcn/ui/card';
|
||||||
|
import { ScrollArea } from '@shadcn/ui/scroll-area';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
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';
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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-6'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='text-lg font-semibold'>
|
||||||
|
{t('currentVersion')} <span>V1.0.0</span>
|
||||||
|
</div>
|
||||||
|
<div className='text-muted-foreground text-sm'>
|
||||||
|
{t('lastUpdated')} <span>2024-12-16 12:00:00</span>
|
||||||
|
</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, index) => (
|
||||||
|
<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'>
|
||||||
|
<Badge
|
||||||
|
variant='outline'
|
||||||
|
className={`${getLogLevelColor(log.level)} mb-2 sm:mb-0`}
|
||||||
|
>
|
||||||
|
{log.level}
|
||||||
|
</Badge>
|
||||||
|
<span className='text-xs font-medium sm:text-sm'>{log.time}</span>
|
||||||
|
<span className='text-muted-foreground flex-1 truncate text-xs sm:text-sm'>
|
||||||
|
{log.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className='px-2'>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -74,6 +74,11 @@ export const navs = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'System Tool',
|
||||||
|
url: '/dashboard/tool',
|
||||||
|
icon: 'flat-color-icons:info',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function findNavByUrl(url: string) {
|
export function findNavByUrl(url: string) {
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
"60002": "Unable to use the subscription at the moment, please try again later.",
|
"60002": "Unable to use the subscription at the moment, please try again later.",
|
||||||
"70001": "Incorrect verification code, please re-enter.",
|
"70001": "Incorrect verification code, please re-enter.",
|
||||||
"80001": "Task was not successfully queued, please try again later.",
|
"80001": "Task was not successfully queued, please try again later.",
|
||||||
|
"90001": "Please disable DEBUG mode and try again.",
|
||||||
"undefined": "An error occurred in the system, please try again later."
|
"undefined": "An error occurred in the system, please try again later."
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Subscribe Management": "Subscribe Management",
|
"Subscribe Management": "Subscribe Management",
|
||||||
"System Config": "System Config",
|
"System Config": "System Config",
|
||||||
|
"System Tool": "System Tool",
|
||||||
"Ticket Management": "Ticket Management",
|
"Ticket Management": "Ticket Management",
|
||||||
"User": "User",
|
"User": "User",
|
||||||
"User Management": "User Management"
|
"User Management": "User Management"
|
||||||
|
|||||||
@ -106,7 +106,7 @@
|
|||||||
"subscriptionDomainDescription": "Used for subscription; leave blank to use site domain",
|
"subscriptionDomainDescription": "Used for subscription; leave blank to use site domain",
|
||||||
"subscriptionDomainPlaceholder": "Enter subscription domain, one per line",
|
"subscriptionDomainPlaceholder": "Enter subscription domain, one per line",
|
||||||
"subscriptionPath": "Subscription Path",
|
"subscriptionPath": "Subscription Path",
|
||||||
"subscriptionPathDescription": "Used for subscription. The system will automatically restart after modification, please wait for 5 seconds.",
|
"subscriptionPathDescription": "Used for subscription; be sure to restart the system after modification for optimal performance",
|
||||||
"subscriptionPathPlaceholder": "Enter",
|
"subscriptionPathPlaceholder": "Enter",
|
||||||
"subscriptionProtocol": "Subscription Protocol",
|
"subscriptionProtocol": "Subscription Protocol",
|
||||||
"wildcardResolution": "Wildcard Resolution",
|
"wildcardResolution": "Wildcard Resolution",
|
||||||
|
|||||||
26
apps/admin/locales/en-US/tool.json
Normal file
26
apps/admin/locales/en-US/tool.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"caller": "Caller",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirmReboot": "Confirm Reboot",
|
||||||
|
"confirmSystemReboot": "Confirm System Reboot",
|
||||||
|
"confirmSystemUpgrade": "Confirm System Upgrade",
|
||||||
|
"confirmUpgrade": "Confirm Upgrade",
|
||||||
|
"currentVersion": "Current System Version:",
|
||||||
|
"errors": "Errors",
|
||||||
|
"ip": "IP",
|
||||||
|
"lastUpdated": "Last Updated:",
|
||||||
|
"none": "None",
|
||||||
|
"query": "Query",
|
||||||
|
"rebootDescription": "Are you sure you want to reboot the system? This operation will cause a brief service interruption.",
|
||||||
|
"rebooting": "Rebooting system...",
|
||||||
|
"refreshLogs": "Refresh Logs",
|
||||||
|
"request": "Request",
|
||||||
|
"status": "Status",
|
||||||
|
"systemLogs": "System Logs",
|
||||||
|
"systemReboot": "System Reboot",
|
||||||
|
"systemServices": "System Services",
|
||||||
|
"systemUpgrade": "System Upgrade",
|
||||||
|
"upgradeDescription": "Are you sure you want to perform a system upgrade? This operation may take a few minutes, during which the system may be unresponsive.",
|
||||||
|
"userAgent": "User Agent",
|
||||||
|
"viewLogsAndManage": "View system logs, perform system upgrade and reboot operations"
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ export default getRequestConfig(async () => {
|
|||||||
announcement: (await import(`./${locale}/announcement.json`)).default,
|
announcement: (await import(`./${locale}/announcement.json`)).default,
|
||||||
ticket: (await import(`./${locale}/ticket.json`)).default,
|
ticket: (await import(`./${locale}/ticket.json`)).default,
|
||||||
document: (await import(`./${locale}/document.json`)).default,
|
document: (await import(`./${locale}/document.json`)).default,
|
||||||
|
tool: (await import(`./${locale}/tool.json`)).default,
|
||||||
index: (await import(`./${locale}/index.json`)).default,
|
index: (await import(`./${locale}/index.json`)).default,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
"60002": "暂时无法使用该订阅,请稍后再试。",
|
"60002": "暂时无法使用该订阅,请稍后再试。",
|
||||||
"70001": "验证码有误,请重新输入。",
|
"70001": "验证码有误,请重新输入。",
|
||||||
"80001": "任务未成功加入队列,请稍后重试。",
|
"80001": "任务未成功加入队列,请稍后重试。",
|
||||||
|
"90001": "请关闭 DEBUG 模式后再试。",
|
||||||
"undefined": "系统发生错误,请稍后重试"
|
"undefined": "系统发生错误,请稍后重试"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"Settings": "设置",
|
"Settings": "设置",
|
||||||
"Subscribe Management": "订阅管理",
|
"Subscribe Management": "订阅管理",
|
||||||
"System Config": "系统配置",
|
"System Config": "系统配置",
|
||||||
|
"System Tool": "系统工具",
|
||||||
"Ticket Management": "工单管理",
|
"Ticket Management": "工单管理",
|
||||||
"User": "用户",
|
"User": "用户",
|
||||||
"User Management": "用户管理"
|
"User Management": "用户管理"
|
||||||
|
|||||||
@ -106,7 +106,7 @@
|
|||||||
"subscriptionDomainDescription": "用于订阅,留空则使用站点域名",
|
"subscriptionDomainDescription": "用于订阅,留空则使用站点域名",
|
||||||
"subscriptionDomainPlaceholder": "请输入订阅域名,多个域名请每行一个",
|
"subscriptionDomainPlaceholder": "请输入订阅域名,多个域名请每行一个",
|
||||||
"subscriptionPath": "订阅路径",
|
"subscriptionPath": "订阅路径",
|
||||||
"subscriptionPathDescription": "用于订阅, 修改后系统会自动重启,请等待5s",
|
"subscriptionPathDescription": "用于订阅, 修改后请务必重启系统,以确保最佳性能体验",
|
||||||
"subscriptionPathPlaceholder": "请输入",
|
"subscriptionPathPlaceholder": "请输入",
|
||||||
"subscriptionProtocol": "订阅协议",
|
"subscriptionProtocol": "订阅协议",
|
||||||
"wildcardResolution": "通配符解析",
|
"wildcardResolution": "通配符解析",
|
||||||
|
|||||||
26
apps/admin/locales/zh-CN/tool.json
Normal file
26
apps/admin/locales/zh-CN/tool.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"caller": "Caller",
|
||||||
|
"cancel": "取消",
|
||||||
|
"confirmReboot": "确认重启",
|
||||||
|
"confirmSystemReboot": "确认系统重启",
|
||||||
|
"confirmSystemUpgrade": "确认系统升级",
|
||||||
|
"confirmUpgrade": "确认升级",
|
||||||
|
"currentVersion": "当前系统版本:",
|
||||||
|
"errors": "Errors",
|
||||||
|
"ip": "IP",
|
||||||
|
"lastUpdated": "最后更新:",
|
||||||
|
"none": "None",
|
||||||
|
"query": "Query",
|
||||||
|
"rebootDescription": "您确定要重启系统吗?此操作将导致短暂的服务中断。",
|
||||||
|
"rebooting": "正在重启系统...",
|
||||||
|
"refreshLogs": "刷新日志",
|
||||||
|
"request": "Request",
|
||||||
|
"status": "Status",
|
||||||
|
"systemLogs": "系统日志",
|
||||||
|
"systemReboot": "系统重启",
|
||||||
|
"systemServices": "系统服务",
|
||||||
|
"systemUpgrade": "系统升级",
|
||||||
|
"upgradeDescription": "您确定要执行系统升级吗?此操作可能需要几分钟时间,期间系统可能无法响应。",
|
||||||
|
"userAgent": "User Agent",
|
||||||
|
"viewLogsAndManage": "查看系统日志,执行系统升级和重启操作"
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
/* eslint-disable */
|
|
||||||
// API 更新时间:
|
// API 更新时间:
|
||||||
// API 唯一标识:
|
// API 唯一标识:
|
||||||
import * as announcement from './announcement';
|
import * as announcement from './announcement';
|
||||||
@ -11,6 +11,7 @@ import * as server from './server';
|
|||||||
import * as subscribe from './subscribe';
|
import * as subscribe from './subscribe';
|
||||||
import * as system from './system';
|
import * as system from './system';
|
||||||
import * as ticket from './ticket';
|
import * as ticket from './ticket';
|
||||||
|
import * as tool from './tool';
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
export default {
|
export default {
|
||||||
announcement,
|
announcement,
|
||||||
@ -22,5 +23,6 @@ export default {
|
|||||||
subscribe,
|
subscribe,
|
||||||
system,
|
system,
|
||||||
ticket,
|
ticket,
|
||||||
|
tool,
|
||||||
user,
|
user,
|
||||||
};
|
};
|
||||||
|
|||||||
19
apps/admin/services/admin/tool.ts
Normal file
19
apps/admin/services/admin/tool.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
/* eslint-disable */
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
/** Get System Log GET /v1/admin/tool/log */
|
||||||
|
export async function getSystemLog(options?: { [key: string]: any }) {
|
||||||
|
return request<API.Response & { data?: API.LogResponse }>('/v1/admin/tool/log', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restart System GET /v1/admin/tool/restart */
|
||||||
|
export async function restartSystem(options?: { [key: string]: any }) {
|
||||||
|
return request<API.Response & { data?: any }>('/v1/admin/tool/restart', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
4
apps/admin/services/admin/typings.d.ts
vendored
4
apps/admin/services/admin/typings.d.ts
vendored
@ -494,6 +494,10 @@ declare namespace API {
|
|||||||
only_first_purchase: boolean;
|
only_first_purchase: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LogResponse = {
|
||||||
|
list: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
type NodeConfig = {
|
type NodeConfig = {
|
||||||
node_secret: string;
|
node_secret: string;
|
||||||
node_pull_interval: number;
|
node_pull_interval: number;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user