| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
- import { useTranslation } from 'react-i18next';
- import { Button, Checkbox, Empty, Form, Input, Modal, Select, Tag } from 'antd';
- import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
- import { HttpUtil, FileManager, IntlUtil, PromiseUtil, SizeFormatter } from '@/utils';
- import { activateOnKey } from '@/utils/a11y';
- import { useDatepicker } from '@/hooks/useDatepicker';
- import { useMediaQuery } from '@/hooks/useMediaQuery';
- import type { AmneziaWGLogs } from '@/generated/types';
- import './XrayLogModal.css';
- import './AmneziaWGLogModal.css';
- interface AmneziaWGLogModalProps {
- open: boolean;
- onClose: () => void;
- }
- const AUTO_UPDATE_INTERVAL = 5000;
- function shortTime(value?: number): string {
- if (!value) return '';
- const d = new Date(value);
- if (isNaN(d.getTime())) return '';
- const hh = String(d.getHours()).padStart(2, '0');
- const mm = String(d.getMinutes()).padStart(2, '0');
- const ss = String(d.getSeconds()).padStart(2, '0');
- return `${hh}:${mm}:${ss}`;
- }
- export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalProps) {
- const { t } = useTranslation();
- const { datepicker } = useDatepicker();
- const { isMobile } = useMediaQuery();
- const [rows, setRows] = useState('50');
- const [filter, setFilter] = useState('');
- const [autoUpdate, setAutoUpdate] = useState(false);
- const [loading, setLoading] = useState(false);
- const [logs, setLogs] = useState<Partial<AmneziaWGLogs>>({});
- const peers = useMemo(() => logs.peers ?? [], [logs.peers]);
- const events = useMemo(() => logs.events ?? [], [logs.events]);
- const runRefresh = useCallback(async () => {
- try {
- const msg = await HttpUtil.post<AmneziaWGLogs>(`/panel/api/server/amneziawglogs/${rows}`, {
- filter,
- });
- if (msg?.success) setLogs(msg.obj || {});
- await PromiseUtil.sleep(300);
- } finally {
- setLoading(false);
- }
- }, [rows, filter]);
- const refresh = useCallback(() => {
- setLoading(true);
- void runRefresh();
- }, [runRefresh]);
- const refreshRef = useRef(refresh);
- useEffect(() => {
- refreshRef.current = refresh;
- });
- // The spinner is raised during render so the fetch effect stays side-effect
- // free until its response lands.
- const refreshKey = open ? `${rows}|${filter}` : null;
- const [loadingKey, setLoadingKey] = useState<string | null>(null);
- if (refreshKey !== loadingKey) {
- setLoadingKey(refreshKey);
- if (refreshKey) setLoading(true);
- }
- useEffect(() => {
- if (open) void runRefresh();
- }, [open, rows, filter, runRefresh]);
- useEffect(() => {
- if (!open || !autoUpdate) return;
- const id = setInterval(() => refreshRef.current(), AUTO_UPDATE_INTERVAL);
- return () => clearInterval(id);
- }, [open, autoUpdate]);
- function fullDate(value?: number): string {
- return value ? IntlUtil.formatDate(value, datepicker) : '';
- }
- function download() {
- const peerLines = peers.map((p) => {
- const at = p.handshake ? new Date(p.handshake).toISOString() : 'never';
- return `${at} IFACE=${p.interface || ''} INBOUND=${p.tag || ''} EMAIL=${p.email || ''} ENDPOINT=${p.endpoint || '-'} ALLOWEDIPS=${p.allowedIPs || ''} UP=${p.up ?? 0} DOWN=${p.down ?? 0} ONLINE=${p.online ? 'yes' : 'no'}`;
- });
- FileManager.downloadTextFile([...peerLines, '', ...events].join('\n'), 'amneziawg.log');
- }
- return (
- <Modal
- open={open}
- footer={null}
- width={isMobile ? '100vw' : '80vw'}
- style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
- className={isMobile ? 'xraylog-modal-mobile' : undefined}
- onCancel={onClose}
- title={
- <>
- {t('pages.index.amneziawgLogs')}
- <SyncOutlined
- spin={loading}
- className="reload-icon"
- role="button"
- tabIndex={0}
- aria-label={t('refresh')}
- onClick={refresh}
- onKeyDown={activateOnKey(refresh)}
- />
- </>
- }
- >
- <Form layout="inline" className="log-toolbar">
- <Form.Item>
- <Select
- value={rows}
- size="small"
- style={{ width: 70 }}
- onChange={setRows}
- options={[
- { value: '20', label: '20' },
- { value: '50', label: '50' },
- { value: '100', label: '100' },
- { value: '500', label: '500' },
- ]}
- />
- </Form.Item>
- <Form.Item label={t('filter')} className="filter-item">
- <Input
- value={filter}
- size="small"
- onChange={(e) => setFilter(e.target.value)}
- onKeyUp={(e) => {
- if (e.key === 'Enter') refresh();
- }}
- />
- </Form.Item>
- <Form.Item>
- <Checkbox checked={autoUpdate} onChange={(e) => setAutoUpdate(e.target.checked)}>
- {t('pages.index.autoUpdate')}
- </Checkbox>
- </Form.Item>
- <Form.Item className="download-item">
- <Button
- type="primary"
- onClick={download}
- icon={<DownloadOutlined />}
- aria-label={t('download')}
- />
- </Form.Item>
- </Form>
- <div className={`log-container ${isMobile ? 'log-container-mobile' : ''}`}>
- {peers.length === 0 ? (
- <div className="log-empty">
- <Empty
- image={Empty.PRESENTED_IMAGE_SIMPLE}
- description={t('pages.index.amneziawgNoPeers')}
- />
- </div>
- ) : isMobile ? (
- peers.map((peer, idx) => (
- <div key={idx} className="log-card">
- <div className="log-card-head">
- <span className="log-time" title={fullDate(peer.handshake)}>
- {shortTime(peer.handshake) || '—'}
- </span>
- <Tag color={peer.online ? 'green' : 'default'} className="log-event-tag">
- {peer.online ? t('online') : t('pages.index.amneziawgIdle')}
- </Tag>
- </div>
- <div className="log-route">
- <span className="log-addr">{peer.endpoint || '—'}</span>
- <span className="log-arrow">→</span>
- <span className="log-addr">{peer.allowedIPs}</span>
- </div>
- <div className="log-meta">
- <span className="log-meta-pair">
- <span className="log-meta-key">iface</span>
- <span className="log-meta-val">{peer.interface}</span>
- </span>
- <span className="log-meta-pair">
- <span className="log-meta-key">inbound</span>
- <span className="log-meta-val">{peer.tag}</span>
- </span>
- {peer.email && (
- <span className="log-meta-pair">
- <span className="log-meta-key">email</span>
- <span className="log-meta-val">{peer.email}</span>
- </span>
- )}
- <span className="log-meta-pair">
- <span className="log-meta-key">↑↓</span>
- <span className="log-meta-val">
- {`${SizeFormatter.sizeFormat(peer.up ?? 0)} / ${SizeFormatter.sizeFormat(peer.down ?? 0)}`}
- </span>
- </span>
- </div>
- </div>
- ))
- ) : (
- <table className="xraylog-table">
- <thead>
- <tr>
- <th>{t('pages.index.amneziawgHandshake')}</th>
- <th>{t('pages.index.amneziawgInterface')}</th>
- <th>{t('pages.index.amneziawgInbound')}</th>
- <th>Email</th>
- <th>{t('pages.index.amneziawgEndpoint')}</th>
- <th>{t('pages.clients.amneziaWgAllowedIPs')}</th>
- <th>↑ / ↓</th>
- </tr>
- </thead>
- <tbody>
- {peers.map((peer, idx) => (
- <tr key={idx} className={peer.online ? undefined : 'log-row-offline'}>
- <td>
- <b>{fullDate(peer.handshake) || '—'}</b>
- </td>
- <td>{peer.interface}</td>
- <td>{peer.tag}</td>
- <td>{peer.email}</td>
- <td>{peer.endpoint || '—'}</td>
- <td>{peer.allowedIPs}</td>
- <td>{`${SizeFormatter.sizeFormat(peer.up ?? 0)} / ${SizeFormatter.sizeFormat(peer.down ?? 0)}`}</td>
- </tr>
- ))}
- </tbody>
- </table>
- )}
- </div>
- <div className="awglog-events-title">{t('pages.index.amneziawgEvents')}</div>
- <div className={`log-container ${isMobile ? 'log-container-mobile' : ''}`}>
- {events.length === 0 ? (
- <div className="log-empty">{t('pages.index.amneziawgNoEvents')}</div>
- ) : (
- events.map((line, idx) => (
- <div key={idx} className="awglog-event-line">
- {line}
- </div>
- ))
- )}
- </div>
- </Modal>
- );
- }
|