mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
添加代理节点、配置版本、节点状态和应用记录页面,更新路由和样式,完善相关功能
This commit is contained in:
@@ -16,6 +16,10 @@ import PasswordResetConfirm from './components/PasswordResetConfirm';
|
|||||||
import { UserContext } from './context/User';
|
import { UserContext } from './context/User';
|
||||||
import { StatusContext } from './context/Status';
|
import { StatusContext } from './context/Status';
|
||||||
import File from './pages/File';
|
import File from './pages/File';
|
||||||
|
import ProxyRoute from './pages/ProxyRoute';
|
||||||
|
import ConfigVersion from './pages/ConfigVersion';
|
||||||
|
import Node from './pages/Node';
|
||||||
|
import ApplyLog from './pages/ApplyLog';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const About = lazy(() => import('./pages/About'));
|
const About = lazy(() => import('./pages/About'));
|
||||||
@@ -72,6 +76,38 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/proxy-route'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<ProxyRoute />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='/config-version'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<ConfigVersion />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='/node'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Node />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='/apply-log'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<ApplyLog />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/file'
|
path='/file'
|
||||||
element={
|
element={
|
||||||
@@ -170,7 +206,7 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path='*' element={NotFound} />
|
<Route path='*' element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,30 @@ const headerButtons = [
|
|||||||
to: '/',
|
to: '/',
|
||||||
icon: 'home',
|
icon: 'home',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '规则',
|
||||||
|
to: '/proxy-route',
|
||||||
|
icon: 'sitemap',
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '版本',
|
||||||
|
to: '/config-version',
|
||||||
|
icon: 'history',
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '节点',
|
||||||
|
to: '/node',
|
||||||
|
icon: 'server',
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '记录',
|
||||||
|
to: '/apply-log',
|
||||||
|
icon: 'tasks',
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '文件',
|
name: '文件',
|
||||||
to: '/file',
|
to: '/file',
|
||||||
|
|||||||
@@ -144,3 +144,10 @@ export function timestamp2string(timestamp) {
|
|||||||
year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
|
year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(value) {
|
||||||
|
if (!value) return '无';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
return date.toLocaleString('zh-CN', { hour12: false });
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,8 +28,47 @@ code {
|
|||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.atsf-table td {
|
||||||
|
vertical-align: top !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.atsf-pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border: 1px solid #e3e6ea;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: rgba(0, 0, 0, 0.58);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
.hide-on-mobile {
|
.hide-on-mobile {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Form, Header, Label, Segment, Table } from 'semantic-ui-react';
|
||||||
|
import { API, formatDateTime, showError } from '../../helpers';
|
||||||
|
|
||||||
|
const ApplyLog = () => {
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadLogs = async (nodeId = '') => {
|
||||||
|
setLoading(true);
|
||||||
|
const path = nodeId ? `/api/apply-logs/?node_id=${encodeURIComponent(nodeId)}` : '/api/apply-logs/';
|
||||||
|
const res = await API.get(path);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setLogs(data || []);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLogs().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
await loadLogs(keyword.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment loading={loading}>
|
||||||
|
<Header as='h3'>应用记录</Header>
|
||||||
|
<p className='page-subtitle'>按节点查看配置应用成功或失败记录。</p>
|
||||||
|
|
||||||
|
<Form onSubmit={submit}>
|
||||||
|
<Form.Input
|
||||||
|
icon='search'
|
||||||
|
iconPosition='left'
|
||||||
|
placeholder='输入 node_id 过滤应用记录'
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e, { value }) => setKeyword(value)}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<Table celled stackable className='atsf-table'>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.HeaderCell>Node ID</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>版本</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>结果</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>信息</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>时间</Table.HeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{logs.map((log) => (
|
||||||
|
<Table.Row key={log.id}>
|
||||||
|
<Table.Cell>{log.node_id}</Table.Cell>
|
||||||
|
<Table.Cell>{log.version}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{log.result === 'success' ? <Label color='green'>成功</Label> : <Label color='red'>失败</Label>}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{log.message || '无'}</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(log.created_at)}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApplyLog;
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
Segment,
|
||||||
|
Table,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
|
import { API, formatDateTime, showError, showSuccess } from '../../helpers';
|
||||||
|
|
||||||
|
const ConfigVersion = () => {
|
||||||
|
const [versions, setVersions] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [publishing, setPublishing] = useState(false);
|
||||||
|
const [preview, setPreview] = useState(null);
|
||||||
|
|
||||||
|
const loadVersions = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await API.get('/api/config-versions/');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setVersions(data || []);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadVersions().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const publishConfig = async () => {
|
||||||
|
setPublishing(true);
|
||||||
|
const res = await API.post('/api/config-versions/publish');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess(`发布成功,版本 ${data.version}`);
|
||||||
|
await loadVersions();
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setPublishing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activateVersion = async (id) => {
|
||||||
|
const res = await API.put(`/api/config-versions/${id}/activate`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess(`已激活版本 ${data.version}`);
|
||||||
|
await loadVersions();
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment loading={loading}>
|
||||||
|
<div className='page-toolbar'>
|
||||||
|
<div>
|
||||||
|
<Header as='h3'>版本发布</Header>
|
||||||
|
<p className='page-subtitle'>查看历史快照,发布新版本,或重新激活旧版本。</p>
|
||||||
|
</div>
|
||||||
|
<Button primary icon labelPosition='left' loading={publishing} onClick={publishConfig}>
|
||||||
|
<Icon name='plus' />
|
||||||
|
生成新版本
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table celled stackable className='atsf-table'>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.HeaderCell>版本号</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>状态</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>创建人</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>Checksum</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>创建时间</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{versions.map((version) => (
|
||||||
|
<Table.Row key={version.id}>
|
||||||
|
<Table.Cell>{version.version}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{version.is_active ? <Label color='green'>当前激活</Label> : <Label>历史版本</Label>}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{version.created_by}</Table.Cell>
|
||||||
|
<Table.Cell title={version.checksum}>{(version.checksum || '').slice(0, 16)}...</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(version.created_at)}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Button size='small' onClick={() => setPreview(version)}>
|
||||||
|
预览
|
||||||
|
</Button>
|
||||||
|
{!version.is_active ? (
|
||||||
|
<Button size='small' positive onClick={() => activateVersion(version.id)}>
|
||||||
|
激活
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<Modal open={!!preview} onClose={() => setPreview(null)} closeIcon>
|
||||||
|
<Modal.Header>版本预览</Modal.Header>
|
||||||
|
<Modal.Content scrolling>
|
||||||
|
{preview ? (
|
||||||
|
<>
|
||||||
|
<Header as='h4'>快照 JSON</Header>
|
||||||
|
<pre className='atsf-pre'>{preview.snapshot_json}</pre>
|
||||||
|
<Header as='h4'>渲染结果</Header>
|
||||||
|
<pre className='atsf-pre'>{preview.rendered_config}</pre>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigVersion;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Header, Label, Segment, Table } from 'semantic-ui-react';
|
||||||
|
import { API, formatDateTime, showError } from '../../helpers';
|
||||||
|
|
||||||
|
const renderStatus = (status) => {
|
||||||
|
if (status === 'online') {
|
||||||
|
return <Label color='green'>在线</Label>;
|
||||||
|
}
|
||||||
|
return <Label color='grey'>离线</Label>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderApply = (result) => {
|
||||||
|
if (result === 'success') {
|
||||||
|
return <Label color='green'>成功</Label>;
|
||||||
|
}
|
||||||
|
if (result === 'failed') {
|
||||||
|
return <Label color='red'>失败</Label>;
|
||||||
|
}
|
||||||
|
return <Label>暂无</Label>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Node = () => {
|
||||||
|
const [nodes, setNodes] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadNodes = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await API.get('/api/nodes/');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setNodes(data || []);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadNodes().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment loading={loading}>
|
||||||
|
<Header as='h3'>节点状态</Header>
|
||||||
|
<p className='page-subtitle'>查看节点在线状态、当前版本和最近一次应用结果。</p>
|
||||||
|
|
||||||
|
<Table celled stackable className='atsf-table'>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.HeaderCell>节点名</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>Node ID</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>IP</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>状态</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>Agent / Nginx</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>当前版本</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>最近应用</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>最近心跳</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>错误</Table.HeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<Table.Row key={node.id}>
|
||||||
|
<Table.Cell>{node.name}</Table.Cell>
|
||||||
|
<Table.Cell>{node.node_id}</Table.Cell>
|
||||||
|
<Table.Cell>{node.ip}</Table.Cell>
|
||||||
|
<Table.Cell>{renderStatus(node.status)}</Table.Cell>
|
||||||
|
<Table.Cell>{node.agent_version} / {node.nginx_version || 'unknown'}</Table.Cell>
|
||||||
|
<Table.Cell>{node.current_version || '未应用'}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{renderApply(node.latest_apply_result)}
|
||||||
|
<div className='table-meta'>{node.latest_apply_message || '暂无记录'}</div>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(node.last_seen_at)}</Table.Cell>
|
||||||
|
<Table.Cell>{node.last_error || '无'}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Node;
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
Label,
|
||||||
|
Segment,
|
||||||
|
Table,
|
||||||
|
TextArea,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
|
import { API, showError, showSuccess, formatDateTime } from '../../helpers';
|
||||||
|
|
||||||
|
const initialForm = {
|
||||||
|
domain: '',
|
||||||
|
origin_url: '',
|
||||||
|
enabled: true,
|
||||||
|
remark: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProxyRoute = () => {
|
||||||
|
const [routes, setRoutes] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [publishing, setPublishing] = useState(false);
|
||||||
|
const [form, setForm] = useState(initialForm);
|
||||||
|
const [editingId, setEditingId] = useState(null);
|
||||||
|
|
||||||
|
const loadRoutes = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await API.get('/api/proxy-routes/');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setRoutes(data || []);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadRoutes().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setForm(initialForm);
|
||||||
|
setEditingId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitRoute = async () => {
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
domain: form.domain.trim(),
|
||||||
|
origin_url: form.origin_url.trim(),
|
||||||
|
remark: form.remark.trim(),
|
||||||
|
};
|
||||||
|
const res = editingId
|
||||||
|
? await API.put(`/api/proxy-routes/${editingId}`, payload)
|
||||||
|
: await API.post('/api/proxy-routes/', payload);
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess(editingId ? '规则已更新' : '规则已创建');
|
||||||
|
resetForm();
|
||||||
|
await loadRoutes();
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRoute = async (id) => {
|
||||||
|
const res = await API.delete(`/api/proxy-routes/${id}`);
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess('规则已删除');
|
||||||
|
await loadRoutes();
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishConfig = async () => {
|
||||||
|
setPublishing(true);
|
||||||
|
const res = await API.post('/api/config-versions/publish');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess(`发布成功,版本 ${data.version}`);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setPublishing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beginEdit = (route) => {
|
||||||
|
setEditingId(route.id);
|
||||||
|
setForm({
|
||||||
|
domain: route.domain,
|
||||||
|
origin_url: route.origin_url,
|
||||||
|
enabled: route.enabled,
|
||||||
|
remark: route.remark || '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment loading={loading}>
|
||||||
|
<div className='page-toolbar'>
|
||||||
|
<div>
|
||||||
|
<Header as='h3'>反代规则</Header>
|
||||||
|
<p className='page-subtitle'>维护 Host 到 Origin 的映射,并可直接触发发布。</p>
|
||||||
|
</div>
|
||||||
|
<Button primary icon labelPosition='left' loading={publishing} onClick={publishConfig}>
|
||||||
|
<Icon name='cloud upload' />
|
||||||
|
发布当前规则
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form onSubmit={submitRoute}>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='域名'
|
||||||
|
placeholder='example.com'
|
||||||
|
value={form.domain}
|
||||||
|
onChange={(e, { value }) => setForm({ ...form, domain: value })}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='源站地址'
|
||||||
|
placeholder='https://origin.internal'
|
||||||
|
value={form.origin_url}
|
||||||
|
onChange={(e, { value }) => setForm({ ...form, origin_url: value })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Field
|
||||||
|
control={TextArea}
|
||||||
|
label='备注'
|
||||||
|
placeholder='可选备注'
|
||||||
|
value={form.remark}
|
||||||
|
onChange={(e, { value }) => setForm({ ...form, remark: value })}
|
||||||
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
toggle
|
||||||
|
label='启用规则'
|
||||||
|
checked={form.enabled}
|
||||||
|
onChange={(e, { checked }) => setForm({ ...form, enabled: checked })}
|
||||||
|
style={{ alignSelf: 'flex-end', marginBottom: '1rem' }}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Button primary type='submit'>
|
||||||
|
{editingId ? '保存修改' : '新增规则'}
|
||||||
|
</Button>
|
||||||
|
{editingId ? (
|
||||||
|
<Button type='button' onClick={resetForm}>
|
||||||
|
取消编辑
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<Table celled stackable className='atsf-table'>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.HeaderCell>域名</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>源站地址</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>状态</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>备注</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>更新时间</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{routes.map((route) => (
|
||||||
|
<Table.Row key={route.id}>
|
||||||
|
<Table.Cell>{route.domain}</Table.Cell>
|
||||||
|
<Table.Cell>{route.origin_url}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{route.enabled ? <Label color='green'>启用</Label> : <Label>停用</Label>}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{route.remark || '无'}</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(route.updated_at)}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Button size='small' onClick={() => beginEdit(route)}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button size='small' negative onClick={() => deleteRoute(route.id)}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProxyRoute;
|
||||||
Reference in New Issue
Block a user