| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- import { DeleteOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons';
- import { Button, Card, Input, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
- import type { ColumnsType } from 'antd/es/table';
- import { useMemo, useState } from 'react';
- import { buildDomainHealth } from '../../frontend/domain-model.js';
- import { useI18n } from '../../frontend/i18n/react';
- import type { DnsCredential, Domain, SendEvent } from '../../frontend/types';
- import { StatusTag } from '../../components/common/StatusTag';
- interface DomainsPageProps {
- domains: Domain[];
- events: SendEvent[];
- dnsCredentials: DnsCredential[];
- actionLoading?: boolean;
- onViewDetail: (domain: Domain) => void;
- onApplyDns: (domain: Domain) => void;
- onCheck: (domain: Domain) => void;
- onSendTest: (domain: Domain) => void;
- onDelete: (domain: Domain) => void;
- onAddDomain: () => void;
- }
- export default function DomainsPage({
- domains,
- events,
- dnsCredentials,
- actionLoading,
- onViewDetail,
- onApplyDns,
- onCheck,
- onSendTest,
- onDelete,
- onAddDomain
- }: DomainsPageProps) {
- const { t } = useI18n();
- const [query, setQuery] = useState('');
- const [status, setStatus] = useState<string>();
- const credentialName = new Map(dnsCredentials.map((item) => [item.id, item.name]));
- const filtered = useMemo(() => {
- return domains.filter((domain) => {
- const health = buildDomainHealth(domain);
- const matchesQuery = !query || domain.domain.includes(query) || domain.senderHost.includes(query);
- const matchesStatus = !status || health.status === status;
- return matchesQuery && matchesStatus;
- });
- }, [domains, query, status]);
- const columns: ColumnsType<Domain> = [
- {
- title: t('domains.domain'),
- dataIndex: 'domain',
- fixed: 'left',
- width: 190,
- render: (value, domain) => (
- <Button type="link" className="table-link" onClick={() => onViewDetail(domain)}>
- {value}
- </Button>
- )
- },
- { title: t('domains.senderHost'), dataIndex: 'senderHost', width: 190, ellipsis: true },
- { title: t('domains.sendingIp'), dataIndex: 'sendingIp', width: 140 },
- {
- title: t('domains.dnsApi'),
- dataIndex: 'dnsCredentialId',
- width: 150,
- render: (value: number | null) => value ? <Tag>{credentialName.get(value) || value}</Tag> : <Tag>{t('common.manual')}</Tag>
- },
- recordColumn('DKIM', 'dkim'),
- recordColumn('SPF', 'spf'),
- recordColumn('DMARC', 'dmarc'),
- {
- title: t('domains.smtp'),
- width: 110,
- render: (_, domain) => <Tag color={domain.status?.verified ? 'success' : 'warning'}>{domain.status?.verified ? t('domains.sendable') : t('domains.waitingVerify')}</Tag>
- },
- {
- title: t('domains.lastSent'),
- width: 180,
- render: (_, domain) => {
- const event = events.find((item) => item.domain === domain.domain);
- return event ? new Date(event.createdAt).toLocaleString() : t('common.notFound');
- }
- },
- {
- title: t('domains.overallStatus'),
- width: 130,
- render: (_, domain) => {
- const health = buildDomainHealth(domain);
- return <Tag color={health.status === 'success' ? 'success' : health.status === 'warning' ? 'warning' : 'error'}>{domainHealthLabel(health.status, t)}</Tag>;
- }
- },
- {
- title: t('domains.actions'),
- width: 360,
- fixed: 'right',
- render: (_, domain) => (
- <Space size={8} wrap>
- <Button icon={<EyeOutlined />} onClick={() => onViewDetail(domain)}>
- {t('common.details')}
- </Button>
- <Button type="primary" disabled={!domain.dnsCredentialId} loading={actionLoading} onClick={() => onApplyDns(domain)}>
- {t('domains.oneClickDns')}
- </Button>
- <Button loading={actionLoading} onClick={() => onCheck(domain)}>
- {t('domains.check')}
- </Button>
- <Button onClick={() => onSendTest(domain)}>{t('domains.test')}</Button>
- <Popconfirm title={t('domains.deleteConfirm')} onConfirm={() => onDelete(domain)}>
- <Button danger icon={<DeleteOutlined />} />
- </Popconfirm>
- </Space>
- )
- }
- ];
- return (
- <Space direction="vertical" size={16} className="full-width">
- <Card>
- <div className="page-toolbar">
- <Space wrap>
- <Input
- allowClear
- prefix={<SearchOutlined />}
- placeholder={t('domains.searchPlaceholder')}
- value={query}
- onChange={(event) => setQuery(event.target.value)}
- className="toolbar-search"
- />
- <Select
- allowClear
- placeholder={t('domains.statusPlaceholder')}
- value={status}
- onChange={setStatus}
- options={[
- { value: 'success', label: t('domains.healthy') },
- { value: 'warning', label: t('domains.pending') },
- { value: 'error', label: t('domains.needsAction') }
- ]}
- className="toolbar-select"
- />
- </Space>
- <Button type="primary" onClick={onAddDomain}>
- {t('common.addDomain')}
- </Button>
- </div>
- </Card>
- <Card
- title={t('domains.title')}
- extra={
- <Typography.Text type="secondary">
- {filtered.length} / {domains.length}
- </Typography.Text>
- }
- >
- <Table rowKey="id" columns={columns} dataSource={filtered} scroll={{ x: 1800 }} />
- </Card>
- </Space>
- );
- }
- function recordColumn(title: string, key: string): ColumnsType<Domain>[number] {
- return {
- title,
- width: 110,
- render: (_, domain) => {
- const record = domain.status?.records?.find((item) => item.key === key);
- if (!record) return <StatusTag status="missing" />;
- return <StatusTag record={record} />;
- }
- };
- }
- function domainHealthLabel(status: string, t: (key: string) => string) {
- if (status === 'success') return t('domains.healthy');
- if (status === 'warning') return t('domains.waitingDns');
- return t('domains.needsAction');
- }
|