| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- import React, { useState, useMemo } from 'react';
- import {
- Table, Button, Space, message, Layout, Typography,
- Card, Tooltip, Badge, Row, Col, Statistic, Dropdown, Avatar
- } from 'antd';
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
- import {
- PlayCircleFilled,
- PauseCircleFilled,
- ReloadOutlined,
- SettingOutlined,
- CloudUploadOutlined,
- FileTextOutlined,
- PlusOutlined,
- MoreOutlined,
- GlobalOutlined,
- UserOutlined,
- AppstoreOutlined,
- ThunderboltFilled
- } from '@ant-design/icons';
- // API & Components
- import { getStatus, startGroup, stopGroup, restartGroup } from '../api';
- import ConfigModal from '../components/ConfigModal';
- import UpgradeModal from '../components/UpgradeModal';
- import LogViewer from '../components/LogViewer';
- import CreateGroupModal from '../components/CreateGroupModal';
- const { Header, Content } = Layout;
- const { Title, Text } = Typography;
- const Dashboard = () => {
- const queryClient = useQueryClient();
-
- // State
- const [configGroupId, setConfigGroupId] = useState(null);
- const [logGroupId, setLogGroupId] = useState(null);
- const [isUpgradeOpen, setIsUpgradeOpen] = useState(false);
- const [isCreateOpen, setIsCreateOpen] = useState(false);
- // 1. Data Fetching
- const { data: statusResp, isLoading } = useQuery({
- queryKey: ['status'],
- queryFn: getStatus,
- refetchInterval: 3000,
- refetchOnWindowFocus: true,
- });
- const dataSource = statusResp?.data || [];
- // 2. Statistics Calculation
- const stats = useMemo(() => {
- const total = dataSource.length;
- const running = dataSource.filter(d => d.running).length;
- const stopped = total - running;
- const totalInstances = dataSource.reduce((acc, curr) => acc + (curr.instances || 0), 0);
- return { total, running, stopped, totalInstances };
- }, [dataSource]);
- // 3. Actions
- const actionMutation = useMutation({
- mutationFn: ({ fn, id }) => fn(id),
- onSuccess: () => {
- message.success('Command sent successfully');
- queryClient.invalidateQueries(['status']);
- },
- onError: (err) => message.error('Operation failed: ' + err.message),
- });
- const handleAction = (fn, id) => actionMutation.mutate({ fn, id });
- // 4. Columns Definition
- const columns = [
- {
- title: 'Identity',
- key: 'identity',
- width: 220,
- render: (_, record) => (
- <Space>
- <Avatar shape="square" size="large" icon={<AppstoreOutlined />} style={{ backgroundColor: record.running ? '#e6f7ff' : '#f5f5f5', color: record.running ? '#1890ff' : '#ccc' }} />
- <div style={{ display: 'flex', flexDirection: 'column' }}>
- <Text strong style={{ fontSize: '15px' }}>{record.id}</Text>
- <Text type="secondary" style={{ fontSize: '12px' }}>{record.plugin}</Text>
- </div>
- </Space>
- )
- },
- {
- title: 'Status',
- key: 'status',
- width: 120,
- render: (_, record) => (
- <Badge
- status={record.running ? "processing" : "default"}
- text={
- <span style={{
- color: record.running ? '#52c41a' : '#999',
- fontWeight: 500
- }}>
- {record.running ? 'Running' : 'Stopped'}
- </span>
- }
- />
- ),
- },
- {
- title: 'Target',
- dataIndex: 'instances',
- key: 'instances',
- width: 100,
- align: 'center',
- render: (val) => <TagPill value={val} label="Inst" />
- },
- {
- title: 'Resources',
- key: 'pools',
- render: (_, record) => (
- <Space direction="vertical" size={0}>
- <Space size={4}>
- <UserOutlined style={{ color: '#8c8c8c', fontSize: '12px' }} />
- <Text style={{ fontSize: '13px', color: record.local_account_pool ? '#595959' : '#d9d9d9' }}>
- {record.local_account_pool || 'N/A'}
- </Text>
- </Space>
- <Space size={4}>
- <GlobalOutlined style={{ color: '#8c8c8c', fontSize: '12px' }} />
- <Text style={{ fontSize: '13px', color: record.proxies_pool ? '#595959' : '#d9d9d9' }}>
- {record.proxies_pool || 'N/A'}
- </Text>
- </Space>
- </Space>
- )
- },
- {
- title: 'Actions',
- key: 'action',
- width: 200,
- align: 'right',
- render: (_, record) => {
- // Dropdown Menu for secondary actions
- const menuItems = [
- { key: 'logs', label: 'View Logs', icon: <FileTextOutlined />, onClick: () => setLogGroupId(record.id) },
- { key: 'config', label: 'Configuration', icon: <SettingOutlined />, onClick: () => setConfigGroupId(record.id) },
- ];
- return (
- <Space size="small">
- {record.running ? (
- <Tooltip title="Stop">
- <Button
- type="text"
- danger
- icon={<PauseCircleFilled style={{ fontSize: '18px' }} />}
- onClick={() => handleAction(stopGroup, record.id)}
- style={{ background: '#fff1f0', border: '1px solid #ffa39e' }}
- />
- </Tooltip>
- ) : (
- <Tooltip title="Start">
- <Button
- type="text"
- icon={<PlayCircleFilled style={{ fontSize: '18px', color: '#52c41a' }} />}
- onClick={() => handleAction(startGroup, record.id)}
- style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}
- />
- </Tooltip>
- )}
-
- <Tooltip title="Restart">
- <Button
- icon={<ReloadOutlined />}
- onClick={() => handleAction(restartGroup, record.id)}
- />
- </Tooltip>
- <Dropdown menu={{ items: menuItems }} trigger={['click']}>
- <Button icon={<MoreOutlined />} />
- </Dropdown>
- </Space>
- );
- },
- },
- ];
- return (
- <Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
- {/* 1. Modern Header */}
- <Header style={{
- background: '#fff',
- padding: '0 24px',
- boxShadow: '0 2px 8px #f0f1f2',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- zIndex: 10
- }}>
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
- <div style={{
- width: '36px', height: '36px',
- background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
- borderRadius: '8px',
- display: 'flex', alignItems: 'center', justifyContent: 'center',
- color: '#fff', fontSize: '20px'
- }}>
- <ThunderboltFilled />
- </div>
- <Title level={4} style={{ margin: 0, fontWeight: 600 }}>Visa Manager</Title>
- </div>
- <Space size="middle">
- <Button
- onClick={() => setIsUpgradeOpen(true)}
- icon={<CloudUploadOutlined />}
- >
- OTA Update
- </Button>
- <Button
- type="primary"
- icon={<PlusOutlined />}
- onClick={() => setIsCreateOpen(true)}
- style={{ borderRadius: '6px', height: '36px', padding: '0 20px' }}
- >
- Create Group
- </Button>
- </Space>
- </Header>
- <Content style={{ padding: '24px', maxWidth: '1600px', margin: '0 auto', width: '100%' }}>
-
- {/* 2. Stats Overview Cards */}
- <Row gutter={16} style={{ marginBottom: '24px' }}>
- <Col span={6}>
- <StatCard title="Total Groups" value={stats.total} icon={<AppstoreOutlined />} color="#1890ff" />
- </Col>
- <Col span={6}>
- <StatCard title="Running" value={stats.running} icon={<PlayCircleFilled />} color="#52c41a" />
- </Col>
- <Col span={6}>
- <StatCard title="Stopped" value={stats.stopped} icon={<PauseCircleFilled />} color="#ff4d4f" />
- </Col>
- <Col span={6}>
- <StatCard title="Total Instances" value={stats.totalInstances} icon={<ThunderboltFilled />} color="#faad14" />
- </Col>
- </Row>
- {/* 3. Main Data Table */}
- <Card
- bordered={false}
- bodyStyle={{ padding: '0' }}
- style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.04)', overflow: 'hidden' }}
- >
- <Table
- dataSource={dataSource}
- columns={columns}
- loading={isLoading}
- rowKey="id"
- pagination={false}
- rowClassName="align-middle"
- size="middle"
- />
- </Card>
- </Content>
- {/* Modals */}
- {configGroupId && <ConfigModal groupId={configGroupId} open={!!configGroupId} onClose={() => setConfigGroupId(null)} />}
- {logGroupId && <LogViewer groupId={logGroupId} open={!!logGroupId} onClose={() => setLogGroupId(null)} />}
- <UpgradeModal open={isUpgradeOpen} onClose={() => setIsUpgradeOpen(false)} />
- <CreateGroupModal open={isCreateOpen} onClose={() => setIsCreateOpen(false)} />
- </Layout>
- );
- };
- // --- Helper Components ---
- const StatCard = ({ title, value, icon, color }) => (
- <Card bordered={false} style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.02)' }}>
- <Statistic
- title={<span style={{ fontSize: '13px', fontWeight: 500, color: '#8c8c8c' }}>{title}</span>}
- value={value}
- valueStyle={{ fontWeight: 'bold', fontSize: '24px', color: '#262626' }}
- prefix={<span style={{ color, marginRight: '8px', fontSize: '20px', position: 'relative', top: '2px' }}>{icon}</span>}
- />
- </Card>
- );
- const TagPill = ({ value, label }) => (
- <div style={{
- background: '#f5f5f5', borderRadius: '4px',
- padding: '2px 8px', display: 'inline-block',
- fontSize: '12px', color: '#595959', border: '1px solid #d9d9d9'
- }}>
- <span style={{ fontWeight: 'bold', marginRight: '4px' }}>{value}</span>
- <span style={{ fontSize: '10px', color: '#8c8c8c' }}>{label}</span>
- </div>
- );
- export default Dashboard;
|