feat: add preview and diff endpoints for config versions

- Implemented PreviewConfigVersion and DiffConfigVersion functions to provide configuration previews and differences.
- Updated API router to include new endpoints for preview and diff.
- Enhanced config version publishing to include custom headers in the rendered configuration.
- Added tests for preview and diff functionalities, ensuring correct behavior with custom headers.
- Updated frontend to support previewing and publishing configurations with a detailed change summary.
This commit is contained in:
ryan
2026-03-10 16:23:49 +08:00
parent b76fb822f8
commit 08e4de4898
10 changed files with 830 additions and 187 deletions
@@ -5,6 +5,7 @@ import {
Form,
Header,
Icon,
Input,
Label,
Segment,
Table,
@@ -12,6 +13,23 @@ import {
} from 'semantic-ui-react';
import { API, showError, showSuccess, formatDateTime } from '../../helpers';
const emptyHeader = () => ({ key: '', value: '' });
const parseCustomHeaders = (rawValue) => {
if (Array.isArray(rawValue)) {
return rawValue;
}
if (!rawValue) {
return [];
}
try {
const parsed = JSON.parse(rawValue);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
return [];
}
};
const initialForm = {
domain: '',
origin_url: '',
@@ -19,6 +37,7 @@ const initialForm = {
enable_https: false,
cert_id: '',
redirect_http: false,
custom_headers: [emptyHeader()],
remark: '',
};
@@ -106,6 +125,7 @@ const ProxyRoute = () => {
domain: form.domain.trim(),
origin_url: form.origin_url.trim(),
cert_id: form.enable_https && form.cert_id ? Number(form.cert_id) : null,
custom_headers: (form.custom_headers || []).filter((item) => item.key.trim() || item.value.trim()),
remark: form.remark.trim(),
};
const res = editingId
@@ -145,6 +165,7 @@ const ProxyRoute = () => {
};
const beginEdit = (route) => {
const customHeaders = parseCustomHeaders(route.custom_headers);
setEditingId(route.id);
setForm({
domain: route.domain,
@@ -153,11 +174,38 @@ const ProxyRoute = () => {
enable_https: route.enable_https || false,
cert_id: route.cert_id || '',
redirect_http: route.redirect_http || false,
custom_headers: customHeaders.length > 0 ? customHeaders : [emptyHeader()],
remark: route.remark || '',
});
setMatchResult(null);
};
const updateCustomHeader = (index, field, value) => {
setForm((current) => ({
...current,
custom_headers: current.custom_headers.map((item, itemIndex) => (
itemIndex === index ? { ...item, [field]: value } : item
)),
}));
};
const addCustomHeader = () => {
setForm((current) => ({
...current,
custom_headers: [...(current.custom_headers || []), emptyHeader()],
}));
};
const removeCustomHeader = (index) => {
setForm((current) => {
const nextHeaders = (current.custom_headers || []).filter((item, itemIndex) => itemIndex !== index);
return {
...current,
custom_headers: nextHeaders.length > 0 ? nextHeaders : [emptyHeader()],
};
});
};
const certificateOptions = certificates.map((certificate) => ({
key: certificate.id,
text: `${certificate.name} (${certificate.not_after ? formatDateTime(certificate.not_after) : 'unknown'})`,
@@ -254,6 +302,34 @@ const ProxyRoute = () => {
style={{ alignSelf: 'flex-end', marginBottom: '1rem' }}
/>
</Form.Group>
<Form.Field>
<label>自定义请求头</label>
{(form.custom_headers || []).map((header, index) => (
<Form.Group widths='equal' key={`header-${index}`}>
<Form.Field
control={Input}
label={index === 0 ? 'Header 名称' : undefined}
placeholder='X-Trace-Id'
value={header.key}
onChange={(e, { value }) => updateCustomHeader(index, 'key', value)}
/>
<Form.Field
control={Input}
label={index === 0 ? 'Header 值' : undefined}
placeholder='$request_id'
value={header.value}
onChange={(e, { value }) => updateCustomHeader(index, 'value', value)}
/>
<Form.Field style={{ display: 'flex', alignItems: index === 0 ? 'flex-end' : 'center' }}>
<Button type='button' icon='trash' negative basic onClick={() => removeCustomHeader(index)} />
</Form.Field>
</Form.Group>
))}
<Button type='button' basic icon labelPosition='left' onClick={addCustomHeader}>
<Icon name='plus' />
添加请求头
</Button>
</Form.Field>
<Button primary type='submit'>
{editingId ? '保存修改' : '新增规则'}
</Button>
@@ -270,6 +346,7 @@ const ProxyRoute = () => {
<Table.HeaderCell>域名</Table.HeaderCell>
<Table.HeaderCell>源站地址</Table.HeaderCell>
<Table.HeaderCell>HTTPS</Table.HeaderCell>
<Table.HeaderCell>自定义头</Table.HeaderCell>
<Table.HeaderCell>状态</Table.HeaderCell>
<Table.HeaderCell>备注</Table.HeaderCell>
<Table.HeaderCell>更新时间</Table.HeaderCell>
@@ -288,6 +365,13 @@ const ProxyRoute = () => {
<Label>HTTP</Label>
)}
</Table.Cell>
<Table.Cell>
{parseCustomHeaders(route.custom_headers).length > 0 ? (
<Label color='teal'>{parseCustomHeaders(route.custom_headers).length} 条</Label>
) : (
<Label>无</Label>
)}
</Table.Cell>
<Table.Cell>
{route.enabled ? <Label color='green'>启用</Label> : <Label>停用</Label>}
</Table.Cell>