|
|
@@ -1,53 +1,29 @@
|
|
|
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
+import { Button, ConfigProvider, Layout, Modal, Result, Spin, message } from 'antd';
|
|
|
import {
|
|
|
- Button,
|
|
|
- Card,
|
|
|
- Col,
|
|
|
- ConfigProvider,
|
|
|
- Layout,
|
|
|
- message,
|
|
|
- Modal,
|
|
|
- Result,
|
|
|
- Row,
|
|
|
- Space,
|
|
|
- Spin,
|
|
|
- Statistic,
|
|
|
- Tag,
|
|
|
- Tooltip,
|
|
|
-} from 'antd';
|
|
|
-import {
|
|
|
- BarsOutlined,
|
|
|
- ControlOutlined,
|
|
|
- CloudServerOutlined,
|
|
|
+ CopyOutlined,
|
|
|
CloudDownloadOutlined,
|
|
|
- CloudUploadOutlined,
|
|
|
- ArrowUpOutlined,
|
|
|
- ArrowDownOutlined,
|
|
|
- AreaChartOutlined,
|
|
|
- GlobalOutlined,
|
|
|
- SwapOutlined,
|
|
|
- EyeOutlined,
|
|
|
- EyeInvisibleOutlined,
|
|
|
- ThunderboltOutlined,
|
|
|
- DesktopOutlined,
|
|
|
+ DashboardOutlined,
|
|
|
DatabaseOutlined,
|
|
|
- ForkOutlined,
|
|
|
- CopyOutlined,
|
|
|
- TelegramFilled,
|
|
|
+ HddOutlined,
|
|
|
+ SwapOutlined,
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
|
-import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
|
|
|
-import { formatPanelVersion } from '@/lib/panel-version';
|
|
|
-import { activateOnKey } from '@/utils/a11y';
|
|
|
+import { HttpUtil, CPUFormatter, SizeFormatter, ClipboardManager, FileManager } from '@/utils';
|
|
|
+import { USAGE_CRIT_COLOR, USAGE_CRIT_PERCENT, USAGE_WARN_COLOR, USAGE_WARN_PERCENT } from '@/models/status';
|
|
|
import { useTheme } from '@/hooks/useTheme';
|
|
|
import { useStatusQuery } from '@/api/queries/useStatusQuery';
|
|
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
|
import AppSidebar from '@/layouts/AppSidebar';
|
|
|
import { LazyMount } from '@/components/utility';
|
|
|
import { setMessageInstance } from '@/utils/messageBus';
|
|
|
-import StatusCard from './StatusCard';
|
|
|
-import XrayStatusCard from './XrayStatusCard';
|
|
|
+import OverviewActionBar from './OverviewActionBar';
|
|
|
+import VitalTile from './VitalTile';
|
|
|
+import ThroughputCard from './ThroughputCard';
|
|
|
+import ConnectionsCard from './ConnectionsCard';
|
|
|
+import SystemStrip from './SystemStrip';
|
|
|
+import { mean, peak, useOverviewHistory } from './useOverviewHistory';
|
|
|
import type { PanelUpdateInfo } from './PanelUpdateModal';
|
|
|
const JsonEditor = lazy(() => import('@/components/form/JsonEditor'));
|
|
|
const PanelUpdateModal = lazy(() => import('./PanelUpdateModal'));
|
|
|
@@ -90,6 +66,8 @@ export default function IndexPage() {
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
const [loadingTip, setLoadingTip] = useState(t('loading'));
|
|
|
|
|
|
+ const history = useOverviewHistory(status, fetched && !fetchError);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
HttpUtil.post<{ accessLogEnable?: boolean; devChannelEnable?: boolean }>(
|
|
|
'/panel/api/setting/defaultSettings',
|
|
|
@@ -127,10 +105,6 @@ export default function IndexPage() {
|
|
|
await refresh();
|
|
|
}, [refresh]);
|
|
|
|
|
|
- function openPanelVersion() {
|
|
|
- setPanelUpdateOpen(true);
|
|
|
- }
|
|
|
-
|
|
|
async function handleChannelChange(dev: boolean) {
|
|
|
const res = await HttpUtil.post('/panel/api/server/setUpdateChannel', { dev });
|
|
|
if (!res?.success) return;
|
|
|
@@ -139,10 +113,6 @@ export default function IndexPage() {
|
|
|
if (msg?.success && msg.obj) setPanelUpdateInfo(msg.obj);
|
|
|
}
|
|
|
|
|
|
- function openTelegram() {
|
|
|
- window.open('https://t.me/XrayUI', '_blank', 'noopener,noreferrer');
|
|
|
- }
|
|
|
-
|
|
|
async function openConfig() {
|
|
|
setLoading(true);
|
|
|
try {
|
|
|
@@ -165,6 +135,23 @@ export default function IndexPage() {
|
|
|
}
|
|
|
|
|
|
const pageClass = `index-page ${isDark ? 'is-dark' : ''} ${isUltra ? 'is-ultra' : ''}`.trim();
|
|
|
+ const totalDisk = status.disk.total;
|
|
|
+ const freeDisk = Math.max(0, totalDisk - status.disk.current);
|
|
|
+
|
|
|
+ const health = useMemo(() => {
|
|
|
+ const items = [
|
|
|
+ { name: t('pages.index.cpu'), value: status.cpu.percent },
|
|
|
+ { name: t('pages.index.memory'), value: status.mem.percent },
|
|
|
+ { name: t('pages.index.swap'), value: status.swap.percent },
|
|
|
+ { name: t('pages.index.storage'), value: status.disk.percent },
|
|
|
+ ];
|
|
|
+ const list = (xs: typeof items) => xs.map((i) => `${i.name} ${i.value.toFixed(0)}%`).join(', ');
|
|
|
+ const crit = items.filter((i) => i.value >= USAGE_CRIT_PERCENT);
|
|
|
+ if (crit.length) return { text: t('pages.index.healthCritical', { list: list(crit) }), color: USAGE_CRIT_COLOR };
|
|
|
+ const warm = items.filter((i) => i.value >= USAGE_WARN_PERCENT);
|
|
|
+ if (warm.length) return { text: t('pages.index.healthWarm', { list: list(warm) }), color: USAGE_WARN_COLOR };
|
|
|
+ return null;
|
|
|
+ }, [status, t]);
|
|
|
|
|
|
return (
|
|
|
<ConfigProvider theme={antdThemeConfig}>
|
|
|
@@ -190,277 +177,105 @@ export default function IndexPage() {
|
|
|
extra={<Button type="primary" onClick={refresh}>{t('refresh')}</Button>}
|
|
|
/>
|
|
|
) : (
|
|
|
- <Row gutter={[isMobile ? 8 : 16, 12]}>
|
|
|
- <Col span={24}>
|
|
|
- <StatusCard status={status} isMobile={isMobile} />
|
|
|
- </Col>
|
|
|
+ <div className="ov-page">
|
|
|
+ <OverviewActionBar
|
|
|
+ status={status}
|
|
|
+ isMobile={isMobile}
|
|
|
+ accessLogEnable={accessLogEnable}
|
|
|
+ panelVersion={displayVersion}
|
|
|
+ latestVersion={panelUpdateInfo.latestVersion}
|
|
|
+ updateAvailable={panelUpdateInfo.updateAvailable}
|
|
|
+ onStopXray={stopXray}
|
|
|
+ onRestartXray={restartXray}
|
|
|
+ onOpenLogs={() => setLogsOpen(true)}
|
|
|
+ onOpenXrayLogs={() => setXrayLogsOpen(true)}
|
|
|
+ onOpenConfig={openConfig}
|
|
|
+ onOpenBackup={() => setBackupOpen(true)}
|
|
|
+ onOpenSystemHistory={() => setSysHistoryOpen(true)}
|
|
|
+ onOpenXrayMetrics={() => setXrayMetricsOpen(true)}
|
|
|
+ onOpenPanelUpdate={() => setPanelUpdateOpen(true)}
|
|
|
+ onOpenVersionSwitch={() => setVersionOpen(true)}
|
|
|
+ />
|
|
|
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <XrayStatusCard
|
|
|
- status={status}
|
|
|
+ {health && (
|
|
|
+ <div className="ov-health" style={{ color: health.color }}>
|
|
|
+ <span className="ov-health-mark" />
|
|
|
+ {health.text}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <hr className="ov-rule" />
|
|
|
+
|
|
|
+ <div className="ov-vitals">
|
|
|
+ <VitalTile
|
|
|
+ icon={<DashboardOutlined />}
|
|
|
+ label={t('pages.index.cpu')}
|
|
|
+ percent={status.cpu.percent}
|
|
|
+ statusColor={status.cpu.color}
|
|
|
+ detail={`${CPUFormatter.cpuCoreFormat(status.cpuCores)} / ${status.logicalPro}T · ${CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz)}`}
|
|
|
+ footLeft={`${t('pages.index.avg')} ${mean(history.series.cpu).toFixed(0)}%`}
|
|
|
+ footRight={`${t('pages.index.peak')} ${peak(history.series.cpu).toFixed(0)}%`}
|
|
|
+ data={history.series.cpu}
|
|
|
isMobile={isMobile}
|
|
|
- accessLogEnable={accessLogEnable}
|
|
|
- onStopXray={stopXray}
|
|
|
- onRestartXray={restartXray}
|
|
|
- onOpenXrayLogs={() => setXrayLogsOpen(true)}
|
|
|
- onOpenLogs={() => setLogsOpen(true)}
|
|
|
- onOpenVersionSwitch={() => setVersionOpen(true)}
|
|
|
/>
|
|
|
- </Col>
|
|
|
-
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card
|
|
|
- title={t('menu.link')}
|
|
|
- hoverable
|
|
|
- actions={[
|
|
|
- <Space className="action" key="logs" role="button" tabIndex={0} aria-label={t('pages.index.logs')} onClick={() => setLogsOpen(true)} onKeyDown={activateOnKey(() => setLogsOpen(true))}>
|
|
|
- <BarsOutlined />
|
|
|
- {!isMobile && <span>{t('pages.index.logs')}</span>}
|
|
|
- </Space>,
|
|
|
- <Space className="action" key="config" role="button" tabIndex={0} aria-label={t('pages.index.config')} onClick={openConfig} onKeyDown={activateOnKey(openConfig)}>
|
|
|
- <ControlOutlined />
|
|
|
- {!isMobile && <span>{t('pages.index.config')}</span>}
|
|
|
- </Space>,
|
|
|
- <Space className="action" key="backup" role="button" tabIndex={0} aria-label={t('pages.index.backupTitle')} onClick={() => setBackupOpen(true)} onKeyDown={activateOnKey(() => setBackupOpen(true))}>
|
|
|
- <CloudServerOutlined />
|
|
|
- {!isMobile && <span>{t('pages.index.backupTitle')}</span>}
|
|
|
- </Space>,
|
|
|
- ]}
|
|
|
+ <VitalTile
|
|
|
+ icon={<DatabaseOutlined />}
|
|
|
+ label={t('pages.index.memory')}
|
|
|
+ percent={status.mem.percent}
|
|
|
+ statusColor={status.mem.color}
|
|
|
+ detail={`${SizeFormatter.sizeFormat(status.mem.current)} / ${SizeFormatter.sizeFormat(status.mem.total)}`}
|
|
|
+ footLeft={`${t('pages.index.avg')} ${mean(history.series.mem).toFixed(0)}%`}
|
|
|
+ footRight={`${t('pages.index.peak')} ${peak(history.series.mem).toFixed(0)}%`}
|
|
|
+ data={history.series.mem}
|
|
|
+ isMobile={isMobile}
|
|
|
/>
|
|
|
- </Col>
|
|
|
-
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card
|
|
|
- title={
|
|
|
- <Space>
|
|
|
- <span>3X-UI</span>
|
|
|
- {isMobile && displayVersion && (
|
|
|
- <Tag color={panelUpdateInfo.updateAvailable ? 'orange' : 'green'}>
|
|
|
- {panelUpdateInfo.updateAvailable
|
|
|
- ? formatPanelVersion(panelUpdateInfo.latestVersion)
|
|
|
- : formatPanelVersion(displayVersion)}
|
|
|
- </Tag>
|
|
|
- )}
|
|
|
- </Space>
|
|
|
- }
|
|
|
- hoverable
|
|
|
- actions={[
|
|
|
- <Space className="action" key="tg" role="button" tabIndex={0} aria-label="@XrayUI" onClick={openTelegram} onKeyDown={activateOnKey(openTelegram)}>
|
|
|
- <TelegramFilled aria-hidden="true" />
|
|
|
- {!isMobile && <span>@XrayUI</span>}
|
|
|
- </Space>,
|
|
|
- <Space
|
|
|
- key="panel-version"
|
|
|
- className={`action ${panelUpdateInfo.updateAvailable ? 'action-update' : ''}`}
|
|
|
- role="button"
|
|
|
- tabIndex={0}
|
|
|
- aria-label={t('pages.index.updatePanel')}
|
|
|
- onClick={openPanelVersion}
|
|
|
- onKeyDown={activateOnKey(openPanelVersion)}
|
|
|
- >
|
|
|
- <CloudDownloadOutlined />
|
|
|
- {!isMobile && (
|
|
|
- <span>
|
|
|
- {panelUpdateInfo.updateAvailable
|
|
|
- ? `${t('update')} ${formatPanelVersion(panelUpdateInfo.latestVersion)}`
|
|
|
- : formatPanelVersion(displayVersion)}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </Space>,
|
|
|
- ]}
|
|
|
+ <VitalTile
|
|
|
+ icon={<SwapOutlined />}
|
|
|
+ label={t('pages.index.swap')}
|
|
|
+ percent={status.swap.percent}
|
|
|
+ statusColor={status.swap.color}
|
|
|
+ detail={`${SizeFormatter.sizeFormat(status.swap.current)} / ${SizeFormatter.sizeFormat(status.swap.total)}`}
|
|
|
+ footLeft={`${t('pages.index.avg')} ${mean(history.series.swap).toFixed(1)}%`}
|
|
|
+ footRight={`${t('pages.index.peak')} ${peak(history.series.swap).toFixed(0)}%`}
|
|
|
+ data={history.series.swap}
|
|
|
+ isMobile={isMobile}
|
|
|
/>
|
|
|
- </Col>
|
|
|
-
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card
|
|
|
- title={t('pages.index.charts')}
|
|
|
- hoverable
|
|
|
- actions={[
|
|
|
- <Space
|
|
|
- className="action"
|
|
|
- key="sys-history"
|
|
|
- role="button"
|
|
|
- tabIndex={0}
|
|
|
- aria-label={t('pages.index.systemHistoryTitle')}
|
|
|
- onClick={() => setSysHistoryOpen(true)}
|
|
|
- onKeyDown={activateOnKey(() => setSysHistoryOpen(true))}
|
|
|
- >
|
|
|
- <AreaChartOutlined />
|
|
|
- {!isMobile && <span>{t('pages.index.systemHistoryTitle')}</span>}
|
|
|
- </Space>,
|
|
|
- <Space
|
|
|
- className="action"
|
|
|
- key="xray-metrics"
|
|
|
- role="button"
|
|
|
- tabIndex={0}
|
|
|
- aria-label={t('pages.index.xrayMetricsTitle')}
|
|
|
- onClick={() => setXrayMetricsOpen(true)}
|
|
|
- onKeyDown={activateOnKey(() => setXrayMetricsOpen(true))}
|
|
|
- >
|
|
|
- <AreaChartOutlined />
|
|
|
- {!isMobile && <span>{t('pages.index.xrayMetricsTitle')}</span>}
|
|
|
- </Space>,
|
|
|
- ]}
|
|
|
+ <VitalTile
|
|
|
+ icon={<HddOutlined />}
|
|
|
+ label={t('pages.index.storage')}
|
|
|
+ percent={status.disk.percent}
|
|
|
+ statusColor={status.disk.color}
|
|
|
+ detail={`${SizeFormatter.sizeFormat(status.disk.current)} / ${SizeFormatter.sizeFormat(totalDisk)}`}
|
|
|
+ footLeft={`${t('pages.index.free')} ${SizeFormatter.sizeFormat(freeDisk)}`}
|
|
|
+ footRight={`${t('pages.index.avg')} ${mean(history.series.diskUsage).toFixed(1)}%`}
|
|
|
+ data={history.series.diskUsage}
|
|
|
+ isMobile={isMobile}
|
|
|
/>
|
|
|
- </Col>
|
|
|
-
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card title={t('pages.index.operationHours')} hoverable>
|
|
|
- <Row gutter={isMobile ? [8, 8] : 0}>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title="Xray"
|
|
|
- value={TimeFormatter.formatSecond(status.appStats.uptime)}
|
|
|
- prefix={<ThunderboltOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title="OS"
|
|
|
- value={TimeFormatter.formatSecond(status.uptime)}
|
|
|
- prefix={<DesktopOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
- </Card>
|
|
|
- </Col>
|
|
|
-
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card title={t('usage')} hoverable>
|
|
|
- <Row gutter={isMobile ? [8, 8] : 0}>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title={t('pages.index.memory')}
|
|
|
- value={SizeFormatter.sizeFormat(status.appStats.mem)}
|
|
|
- prefix={<DatabaseOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title={t('pages.index.threads')}
|
|
|
- value={status.appStats.threads}
|
|
|
- prefix={<ForkOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
- </Card>
|
|
|
- </Col>
|
|
|
-
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card title={t('pages.index.overallSpeed')} hoverable>
|
|
|
- <Row gutter={isMobile ? [8, 8] : 0}>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title={t('pages.index.upload')}
|
|
|
- value={SizeFormatter.sizeFormat(status.netIO.up)}
|
|
|
- prefix={<ArrowUpOutlined />}
|
|
|
- suffix="/s"
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title={t('pages.index.download')}
|
|
|
- value={SizeFormatter.sizeFormat(status.netIO.down)}
|
|
|
- prefix={<ArrowDownOutlined />}
|
|
|
- suffix="/s"
|
|
|
- />
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
- </Card>
|
|
|
- </Col>
|
|
|
+ </div>
|
|
|
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card title={t('pages.index.totalData')} hoverable>
|
|
|
- <Row gutter={isMobile ? [8, 8] : 0}>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title={t('pages.index.sent')}
|
|
|
- value={SizeFormatter.sizeFormat(status.netTraffic.sent)}
|
|
|
- prefix={<CloudUploadOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title={t('pages.index.received')}
|
|
|
- value={SizeFormatter.sizeFormat(status.netTraffic.recv)}
|
|
|
- prefix={<CloudDownloadOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
- </Card>
|
|
|
- </Col>
|
|
|
-
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card
|
|
|
- title={t('pages.index.ipAddresses')}
|
|
|
- hoverable
|
|
|
- extra={
|
|
|
- <Tooltip
|
|
|
- title={t('pages.index.toggleIpVisibility')}
|
|
|
- placement={isMobile ? 'topRight' : 'top'}
|
|
|
- >
|
|
|
- {showIp ? (
|
|
|
- <EyeOutlined
|
|
|
- className="ip-toggle-icon"
|
|
|
- role="button"
|
|
|
- tabIndex={0}
|
|
|
- aria-label={t('pages.index.toggleIpVisibility')}
|
|
|
- onClick={() => setShowIp(false)}
|
|
|
- onKeyDown={activateOnKey(() => setShowIp(false))}
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <EyeInvisibleOutlined
|
|
|
- className="ip-toggle-icon"
|
|
|
- role="button"
|
|
|
- tabIndex={0}
|
|
|
- aria-label={t('pages.index.toggleIpVisibility')}
|
|
|
- onClick={() => setShowIp(true)}
|
|
|
- onKeyDown={activateOnKey(() => setShowIp(true))}
|
|
|
- />
|
|
|
- )}
|
|
|
- </Tooltip>
|
|
|
- }
|
|
|
- >
|
|
|
- <Row className={showIp ? 'ip-visible' : 'ip-hidden'} gutter={isMobile ? [8, 8] : 0}>
|
|
|
- <Col span={isMobile ? 24 : 12}>
|
|
|
- <Statistic
|
|
|
- title="IPv4"
|
|
|
- value={status.publicIP.ipv4}
|
|
|
- prefix={<GlobalOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={isMobile ? 24 : 12}>
|
|
|
- <Statistic
|
|
|
- title="IPv6"
|
|
|
- value={status.publicIP.ipv6}
|
|
|
- prefix={<GlobalOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
- </Card>
|
|
|
- </Col>
|
|
|
+ <div className="ov-mid">
|
|
|
+ <ThroughputCard
|
|
|
+ status={status}
|
|
|
+ up={history.series.netUp}
|
|
|
+ down={history.series.netDown}
|
|
|
+ labels={history.labels}
|
|
|
+ isMobile={isMobile}
|
|
|
+ />
|
|
|
+ <ConnectionsCard
|
|
|
+ status={status}
|
|
|
+ tcp={history.series.tcpCount}
|
|
|
+ udp={history.series.udpCount}
|
|
|
+ labels={history.labels}
|
|
|
+ isMobile={isMobile}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
|
|
|
- <Col xs={24} lg={12}>
|
|
|
- <Card title={t('pages.index.connectionCount')} hoverable>
|
|
|
- <Row gutter={isMobile ? [8, 8] : 0}>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title="TCP"
|
|
|
- value={status.tcpCount}
|
|
|
- prefix={<SwapOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={12}>
|
|
|
- <Statistic
|
|
|
- title="UDP"
|
|
|
- value={status.udpCount}
|
|
|
- prefix={<SwapOutlined />}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
- </Card>
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
+ <SystemStrip
|
|
|
+ status={status}
|
|
|
+ showIp={showIp}
|
|
|
+ onToggleIp={() => setShowIp((v) => !v)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
)}
|
|
|
</Spin>
|
|
|
</Layout.Content>
|