'use client';
import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { EmptyState } from '@/components/feedback/empty-state';
import { ErrorState } from '@/components/feedback/error-state';
import { InlineMessage } from '@/components/feedback/inline-message';
import { LoadingState } from '@/components/feedback/loading-state';
import { PageHeader } from '@/components/layout/page-header';
import { AppCard } from '@/components/ui/app-card';
import { StatusBadge } from '@/components/ui/status-badge';
import {
activateConfigVersion,
getActiveConfigVersion,
getConfigVersionDiff,
getConfigVersionPreview,
getConfigVersions,
publishConfigVersion,
cleanupConfigVersions,
} from '@/features/config-versions/api/config-versions';
import { ConfigVersionSnapshotModal } from '@/features/config-versions/components/config-version-snapshot-modal';
import { CleanupConfigVersionsModal } from '@/features/config-versions/components/cleanup-config-versions-modal';
import type {
ConfigOptionDiffItem,
ConfigDiffResult,
ConfigPreviewResult,
ConfigVersionDetail,
ConfigVersionSummary,
SupportFile,
} from '@/features/config-versions/types';
import {
CodeBlock,
PrimaryButton,
SecondaryButton,
} from '@/features/shared/components/resource-primitives';
import { formatDateTime } from '@/lib/utils/date';
const versionsQueryKey = ['config-versions'];
type FeedbackState = {
tone: 'info' | 'success' | 'danger';
message: string;
};
function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
}
function truncateChecksum(checksum: string) {
if (!checksum) {
return '—';
}
return checksum.length > 16 ? `${checksum.slice(0, 16)}...` : checksum;
}
function hasConfigDiff(diff: ConfigDiffResult) {
return (
diff.added_domains.length > 0 ||
diff.removed_domains.length > 0 ||
diff.modified_domains.length > 0 ||
diff.main_config_changed ||
diff.changed_option_keys.length > 0 ||
!diff.active_version
);
}
function DiffList({ title, items }: { title: string; items: string[] }) {
return (
{title}
0 ? 'info' : 'warning'}
/>
{items.length > 0 ? (
{items.map((item) => (
{item}
))}
) : (
当前无相关变更。
)}
);
}
function SupportFilesList({ files }: { files: SupportFile[] }) {
if (files.length === 0) {
return (
当前发布不需要额外支持文件。
);
}
return (
{files.map((file) => (
{file.path}
{file.content}
))}
);
}
function renderOptionValue(value: string) {
return value === '' ? '空' : value;
}
function OptionDiffTable({ items }: { items: ConfigOptionDiffItem[] }) {
if (items.length === 0) {
return (
当前无 OpenResty 性能参数变化。
);
}
return (
| 参数 |
当前激活值 |
待发布值 |
{items.map((item) => (
|
{item.key}
|
{renderOptionValue(item.previous_value)}
|
{renderOptionValue(item.current_value)}
|
))}
);
}
function PublishPreviewCard({
preview,
diff,
activeVersionMeta,
activeVersionDetail,
isActiveVersionDetailLoading,
activeVersionDetailError,
isPublishing,
onConfirm,
onCancel,
}: {
preview: ConfigPreviewResult;
diff: ConfigDiffResult;
activeVersionMeta: ConfigVersionSummary | null;
activeVersionDetail: ConfigVersionDetail | null;
isActiveVersionDetailLoading: boolean;
activeVersionDetailError: string | null;
isPublishing: boolean;
onConfirm: () => void;
onCancel: () => void;
}) {
const canPublish = preview.route_count > 0 && hasConfigDiff(diff);
return (
}
>
当前激活版本
{diff.active_version || '无'}
新增域名
{diff.added_domains.length}
删除域名
{diff.removed_domains.length}
修改域名
{diff.modified_domains.length}
主配置变化
{diff.main_config_changed ? '已变化' : '无变化'}
{!canPublish ? (
) : null}
OpenResty 参数变化
0 ? 'info' : 'warning'}
/>
{diff.main_config_changed && activeVersionMeta ? (
Current Active Main Config
{isActiveVersionDetailLoading ? (
) : activeVersionDetailError ? (
) : activeVersionDetail ? (
{activeVersionDetail.main_config}
) : (
)}
) : null}
{/* Legacy duplicated main config preview block kept commented out while
we replace it with an explicit active-vs-pending comparison above.
{diff.main_config_changed && activeVersion ? (
{activeVersion.main_config}
寰呭彂甯冧富閰嶇疆
Checksum锛歿preview.checksum}
{preview.main_config}
) : null}
*/}
主配置
Checksum:{preview.checksum}
{preview.main_config}
{preview.rendered_config}
{isPublishing ? '发布中...' : '确认发布'}
取消预览
);
}
export function ConfigVersionsPage() {
const queryClient = useQueryClient();
const [feedback, setFeedback] = useState(null);
const [selectedVersionId, setSelectedVersionId] = useState(
null,
);
const [publishPreview, setPublishPreview] = useState<{
preview: ConfigPreviewResult;
diff: ConfigDiffResult;
} | null>(null);
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
const [isCleanupModalOpen, setIsCleanupModalOpen] = useState(false);
const versionsQuery = useQuery({
queryKey: versionsQueryKey,
queryFn: getConfigVersions,
});
const versions = useMemo(
() => versionsQuery.data ?? [],
[versionsQuery.data],
);
const activeVersion = useMemo(
() => versions.find((item) => item.is_active) ?? null,
[versions],
);
const selectedVersion = useMemo(
() => versions.find((item) => item.id === selectedVersionId) ?? null,
[selectedVersionId, versions],
);
const shouldLoadActiveVersionDetail = Boolean(
activeVersion?.id && publishPreview?.diff.main_config_changed,
);
const activeVersionDetailQuery = useQuery({
queryKey: ['config-versions', 'active-detail', activeVersion?.id ?? 0],
queryFn: getActiveConfigVersion,
enabled: shouldLoadActiveVersionDetail,
});
const publishMutation = useMutation({
mutationFn: (force?: boolean) => publishConfigVersion(force),
onSuccess: async (version) => {
setFeedback({
tone: 'success',
message: `发布成功,版本 ${version.version}`,
});
setPublishPreview(null);
await queryClient.invalidateQueries({ queryKey: versionsQueryKey });
},
onError: (error) => {
setFeedback({ tone: 'danger', message: getErrorMessage(error) });
},
});
const activateMutation = useMutation({
mutationFn: activateConfigVersion,
onSuccess: async (version) => {
setFeedback({
tone: 'success',
message: `已激活版本 ${version.version}`,
});
setSelectedVersionId(version.id);
await queryClient.invalidateQueries({ queryKey: versionsQueryKey });
},
onError: (error) => {
setFeedback({ tone: 'danger', message: getErrorMessage(error) });
},
});
const cleanupMutation = useMutation({
mutationFn: cleanupConfigVersions,
onSuccess: async (data) => {
setFeedback({
tone: 'success',
message: `成功清理 ${data.deleted_count} 个历史快照`,
});
setIsCleanupModalOpen(false);
await queryClient.invalidateQueries({ queryKey: versionsQueryKey });
},
onError: (error) => {
setFeedback({ tone: 'danger', message: getErrorMessage(error) });
},
});
const handleOpenPublishPreview = async () => {
setFeedback(null);
setIsPreviewLoading(true);
try {
const [preview, diff] = await Promise.all([
getConfigVersionPreview(),
getConfigVersionDiff(),
]);
setPublishPreview({ preview, diff });
} catch (error) {
setFeedback({ tone: 'danger', message: getErrorMessage(error) });
} finally {
setIsPreviewLoading(false);
}
};
const handleActivate = (version: ConfigVersionSummary) => {
if (version.is_active) {
return;
}
if (!window.confirm(`确认激活版本 ${version.version} 吗?`)) {
return;
}
setFeedback(null);
activateMutation.mutate(version.id);
};
return (
<>
{
if (
window.confirm(
'确认强制重新发布吗?这会忽略配置变化检查并立即生成一个新版本。',
)
) {
publishMutation.mutate(true);
}
}}
disabled={publishMutation.isPending}
>
重新发布
{isPreviewLoading ? '加载预览中...' : '预览并发布'}
}
/>
{feedback ? (
) : null}
{publishPreview ? (
publishMutation.mutate(false)}
onCancel={() => setPublishPreview(null)}
/>
) : null}
setIsCleanupModalOpen(true)}
>
清理旧版本
void queryClient.invalidateQueries({
queryKey: versionsQueryKey,
})
}
>
刷新列表
}
>
{versionsQuery.isLoading ? (
) : versionsQuery.isError ? (
) : versions.length === 0 ? (
) : (
| 版本号 |
状态 |
创建人 |
Checksum |
创建时间 |
操作 |
{versions.map((version) => (
|
{version.version}
|
|
{version.created_by || '系统'}
|
{truncateChecksum(version.checksum)}
|
{formatDateTime(version.created_at)}
|
setSelectedVersionId(version.id)}
className="px-3 py-2 text-xs"
>
查看快照
{!version.is_active ? (
handleActivate(version)}
disabled={activateMutation.isPending}
className="px-3 py-2 text-xs"
>
重新激活
) : null}
|
))}
)}
setSelectedVersionId(null)}
/>
setIsCleanupModalOpen(false)}
onConfirm={(keepCount) =>
cleanupMutation.mutate({ keep_count: keepCount })
}
isPending={cleanupMutation.isPending}
/>
>
);
}