From 9ced0eb6f0ae8d24a21909901177b6130970280b Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Mar 2026 15:19:39 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8A=9F=E8=83=BD]=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=85=8D=E7=BD=AE=E7=89=88=E6=9C=AC=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E7=9A=84API=EF=BC=8C=E4=BC=98=E5=8C=96=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_server/controller/config_version.go | 38 ++++++++++++- openflare_server/model/config_version.go | 16 +++++- openflare_server/router/api-router.go | 1 + openflare_server/router/api_phase1_test.go | 24 ++++++++- openflare_server/service/config_version.go | 14 +++-- .../config-versions/api/config-versions.ts | 47 +++++++++------- .../config-version-snapshot-modal.tsx | 48 +++++++++++++---- .../components/config-versions-page.tsx | 54 +++++++++++++++---- .../web/features/config-versions/types.ts | 13 +++-- .../nodes/components/node-detail-page.tsx | 4 +- 10 files changed, 206 insertions(+), 53 deletions(-) diff --git a/openflare_server/controller/config_version.go b/openflare_server/controller/config_version.go index bd348294..c8f5e53d 100644 --- a/openflare_server/controller/config_version.go +++ b/openflare_server/controller/config_version.go @@ -1,10 +1,11 @@ package controller import ( - "github.com/gin-gonic/gin" "net/http" "openflare/service" "strconv" + + "github.com/gin-gonic/gin" ) // GetConfigVersions godoc @@ -30,6 +31,39 @@ func GetConfigVersions(c *gin.Context) { }) } +// GetConfigVersion godoc +// @Summary Get config version detail +// @Tags ConfigVersions +// @Produce json +// @Security BearerAuth +// @Param id path int true "Version ID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/config-versions/{id} [get] +func GetConfigVersion(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "invalid id", + }) + return + } + version, err := service.GetConfigVersionDetail(uint(id)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": version, + }) +} + // GetActiveConfigVersion godoc // @Summary Get active config version // @Tags ConfigVersions @@ -137,7 +171,7 @@ func ActivateConfigVersion(c *gin.Context) { if err != nil || id == 0 { c.JSON(http.StatusBadRequest, gin.H{ "success": false, - "message": "无效的参数", + "message": "invalid id", }) return } diff --git a/openflare_server/model/config_version.go b/openflare_server/model/config_version.go index 3670e142..ca290485 100644 --- a/openflare_server/model/config_version.go +++ b/openflare_server/model/config_version.go @@ -2,6 +2,15 @@ package model import "time" +type ConfigVersionSummary struct { + ID uint `json:"id"` + Version string `json:"version"` + Checksum string `json:"checksum"` + IsActive bool `json:"is_active"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` +} + type ConfigVersion struct { ID uint `json:"id" gorm:"primaryKey"` Version string `json:"version" gorm:"uniqueIndex;size:32;not null"` @@ -15,8 +24,11 @@ type ConfigVersion struct { CreatedAt time.Time `json:"created_at"` } -func ListConfigVersions() (versions []*ConfigVersion, err error) { - err = DB.Order("id desc").Find(&versions).Error +func ListConfigVersionSummaries() (versions []*ConfigVersionSummary, err error) { + err = DB.Model(&ConfigVersion{}). + Select("id", "version", "checksum", "is_active", "created_by", "created_at"). + Order("id desc"). + Find(&versions).Error return versions, err } diff --git a/openflare_server/router/api-router.go b/openflare_server/router/api-router.go index eed9ab30..304b3a27 100644 --- a/openflare_server/router/api-router.go +++ b/openflare_server/router/api-router.go @@ -108,6 +108,7 @@ func SetApiRouter(router *gin.Engine) { configVersionRoute.GET("/active", controller.GetActiveConfigVersion) configVersionRoute.GET("/preview", controller.PreviewConfigVersion) configVersionRoute.GET("/diff", controller.DiffConfigVersion) + configVersionRoute.GET("/:id", controller.GetConfigVersion) configVersionRoute.POST("/publish", controller.PublishConfigVersion) configVersionRoute.POST("/:id/activate", controller.ActivateConfigVersion) } diff --git a/openflare_server/router/api_phase1_test.go b/openflare_server/router/api_phase1_test.go index 7a00caa5..92b337ff 100644 --- a/openflare_server/router/api_phase1_test.go +++ b/openflare_server/router/api_phase1_test.go @@ -127,11 +127,33 @@ func TestPhase1PublishLifecycle(t *testing.T) { } resp = performJSONRequest(t, engine, token, http.MethodGet, "/api/config-versions/", nil) - var versions []model.ConfigVersion + var versions []map[string]any decodeResponseData(t, resp, &versions) if len(versions) != 2 { t.Fatalf("expected 2 versions, got %d", len(versions)) } + if _, ok := versions[0]["snapshot_json"]; ok { + t.Fatal("expected config version list to omit snapshot_json") + } + if _, ok := versions[0]["main_config"]; ok { + t.Fatal("expected config version list to omit main_config") + } + if _, ok := versions[0]["rendered_config"]; ok { + t.Fatal("expected config version list to omit rendered_config") + } + if _, ok := versions[0]["support_files_json"]; ok { + t.Fatal("expected config version list to omit support_files_json") + } + + detailResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/config-versions/"+toString(version2.ID), nil) + var versionDetail model.ConfigVersion + decodeResponseData(t, detailResp, &versionDetail) + if versionDetail.ID != version2.ID { + t.Fatalf("expected config version detail %d, got %d", version2.ID, versionDetail.ID) + } + if versionDetail.SnapshotJSON == "" || versionDetail.MainConfig == "" || versionDetail.RenderedConfig == "" { + t.Fatal("expected config version detail endpoint to include full payload") + } activeResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/config-versions/active", nil) var activeVersion model.ConfigVersion diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go index 0f30a669..19d11909 100644 --- a/openflare_server/service/config_version.go +++ b/openflare_server/service/config_version.go @@ -37,6 +37,10 @@ type ConfigPreviewResult struct { RouteCount int `json:"route_count"` } +type ConfigVersionSummary = model.ConfigVersionSummary + +type ConfigVersionDetail = model.ConfigVersion + type ConfigDiffResult struct { ActiveVersion string `json:"active_version,omitempty"` AddedDomains []string `json:"added_domains"` @@ -158,11 +162,15 @@ var requiredMainConfigTemplatePlaceholders = []string{ "{{OpenRestyRouteConfigInclude}}", } -func ListConfigVersions() ([]*model.ConfigVersion, error) { - return model.ListConfigVersions() +func ListConfigVersions() ([]*ConfigVersionSummary, error) { + return model.ListConfigVersionSummaries() } -func GetActiveConfigVersion() (*model.ConfigVersion, error) { +func GetConfigVersionDetail(id uint) (*ConfigVersionDetail, error) { + return model.GetConfigVersionByID(id) +} + +func GetActiveConfigVersion() (*ConfigVersionDetail, error) { return model.GetActiveConfigVersion() } diff --git a/openflare_server/web/features/config-versions/api/config-versions.ts b/openflare_server/web/features/config-versions/api/config-versions.ts index 49532441..cf9259a8 100644 --- a/openflare_server/web/features/config-versions/api/config-versions.ts +++ b/openflare_server/web/features/config-versions/api/config-versions.ts @@ -1,14 +1,23 @@ -import { apiRequest } from '@/lib/api/client'; - -import type { - ConfigDiffResult, - ConfigPreviewResult, - ConfigVersionItem, -} from '@/features/config-versions/types'; - -export function getConfigVersions() { - return apiRequest('/config-versions/'); -} +import { apiRequest } from '@/lib/api/client'; + +import type { + ConfigDiffResult, + ConfigPreviewResult, + ConfigVersionDetail, + ConfigVersionSummary, +} from '@/features/config-versions/types'; + +export function getConfigVersions() { + return apiRequest('/config-versions/'); +} + +export function getConfigVersion(id: number) { + return apiRequest(`/config-versions/${id}`); +} + +export function getActiveConfigVersion() { + return apiRequest('/config-versions/active'); +} export function getConfigVersionPreview() { return apiRequest('/config-versions/preview'); @@ -18,14 +27,14 @@ export function getConfigVersionDiff() { return apiRequest('/config-versions/diff'); } -export function publishConfigVersion() { - return apiRequest('/config-versions/publish', { - method: 'POST', - }); -} - -export function activateConfigVersion(id: number) { - return apiRequest(`/config-versions/${id}/activate`, { +export function publishConfigVersion() { + return apiRequest('/config-versions/publish', { + method: 'POST', + }); +} + +export function activateConfigVersion(id: number) { + return apiRequest(`/config-versions/${id}/activate`, { method: 'POST', }); } diff --git a/openflare_server/web/features/config-versions/components/config-version-snapshot-modal.tsx b/openflare_server/web/features/config-versions/components/config-version-snapshot-modal.tsx index 672e4ca4..4c29508c 100644 --- a/openflare_server/web/features/config-versions/components/config-version-snapshot-modal.tsx +++ b/openflare_server/web/features/config-versions/components/config-version-snapshot-modal.tsx @@ -1,20 +1,41 @@ 'use client'; +import { useQuery } from '@tanstack/react-query'; + +import { ErrorState } from '@/components/feedback/error-state'; +import { LoadingState } from '@/components/feedback/loading-state'; import { AppModal } from '@/components/ui/app-modal'; -import type { ConfigVersionItem } from '@/features/config-versions/types'; +import { getConfigVersion } from '@/features/config-versions/api/config-versions'; +import type { ConfigVersionSummary } from '@/features/config-versions/types'; import { CodeBlock, SecondaryButton, } from '@/features/shared/components/resource-primitives'; import { formatDateTime } from '@/lib/utils/date'; +function getErrorMessage(error: unknown) { + return error instanceof Error ? error.message : '请求失败,请稍后重试。'; +} + export function ConfigVersionSnapshotModal({ version, onClose, }: { - version: ConfigVersionItem | null; + version: ConfigVersionSummary | null; onClose: () => void; }) { + const versionDetailQuery = useQuery({ + queryKey: ['config-versions', 'detail', version?.id ?? 0], + queryFn: () => { + if (!version) { + throw new Error('missing config version'); + } + return getConfigVersion(version.id); + }, + enabled: Boolean(version?.id), + }); + const versionDetail = versionDetailQuery.data ?? null; + return ( } > - {version ? ( + {!version ? null : versionDetailQuery.isLoading && !versionDetail ? ( + + ) : versionDetailQuery.isError ? ( + + ) : versionDetail ? (
@@ -38,7 +66,7 @@ export function ConfigVersionSnapshotModal({ Checksum

- {version.checksum} + {versionDetail.checksum}

@@ -46,7 +74,7 @@ export function ConfigVersionSnapshotModal({ 创建人

- {version.created_by || '系统'} + {versionDetail.created_by || '系统'}

@@ -54,7 +82,7 @@ export function ConfigVersionSnapshotModal({ 创建时间

- {formatDateTime(version.created_at)} + {formatDateTime(versionDetail.created_at)}

@@ -64,23 +92,25 @@ export function ConfigVersionSnapshotModal({ 快照 JSON

- {version.snapshot_json} + {versionDetail.snapshot_json}
+

主配置

- {version.main_config} + {versionDetail.main_config}
+

路由配置

- {version.rendered_config} + {versionDetail.rendered_config}
diff --git a/openflare_server/web/features/config-versions/components/config-versions-page.tsx b/openflare_server/web/features/config-versions/components/config-versions-page.tsx index f2f13200..b11f845c 100644 --- a/openflare_server/web/features/config-versions/components/config-versions-page.tsx +++ b/openflare_server/web/features/config-versions/components/config-versions-page.tsx @@ -12,6 +12,7 @@ import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { activateConfigVersion, + getActiveConfigVersion, getConfigVersionDiff, getConfigVersionPreview, getConfigVersions, @@ -22,7 +23,8 @@ import type { ConfigOptionDiffItem, ConfigDiffResult, ConfigPreviewResult, - ConfigVersionItem, + ConfigVersionDetail, + ConfigVersionSummary, SupportFile, } from '@/features/config-versions/types'; import { @@ -168,14 +170,20 @@ function OptionDiffTable({ items }: { items: ConfigOptionDiffItem[] }) { function PublishPreviewCard({ preview, diff, - activeVersion, + activeVersionMeta, + activeVersionDetail, + isActiveVersionDetailLoading, + activeVersionDetailError, isPublishing, onConfirm, onCancel, }: { preview: ConfigPreviewResult; diff: ConfigDiffResult; - activeVersion: ConfigVersionItem | null; + activeVersionMeta: ConfigVersionSummary | null; + activeVersionDetail: ConfigVersionDetail | null; + isActiveVersionDetailLoading: boolean; + activeVersionDetailError: string | null; isPublishing: boolean; onConfirm: () => void; onCancel: () => void; @@ -263,18 +271,29 @@ function PublishPreviewCard({ - {diff.main_config_changed && activeVersion ? ( + {diff.main_config_changed && activeVersionMeta ? (

Current Active Main Config

- +
- - {activeVersion.main_config} - + {isActiveVersionDetailLoading ? ( + + ) : activeVersionDetailError ? ( + + ) : activeVersionDetail ? ( + + {activeVersionDetail.main_config} + + ) : ( + + )}
@@ -406,6 +425,14 @@ export function ConfigVersionsPage() { () => 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: publishConfigVersion, @@ -455,7 +482,7 @@ export function ConfigVersionsPage() { } }; - const handleActivate = (version: ConfigVersionItem) => { + const handleActivate = (version: ConfigVersionSummary) => { if (version.is_active) { return; } @@ -493,7 +520,14 @@ export function ConfigVersionsPage() { publishMutation.mutate()} onCancel={() => setPublishPreview(null)} diff --git a/openflare_server/web/features/config-versions/types.ts b/openflare_server/web/features/config-versions/types.ts index cc7c3306..b5607ecb 100644 --- a/openflare_server/web/features/config-versions/types.ts +++ b/openflare_server/web/features/config-versions/types.ts @@ -1,16 +1,19 @@ -export interface ConfigVersionItem { +export interface ConfigVersionSummary { id: number; version: string; - snapshot_json: string; - main_config: string; - rendered_config: string; - support_files_json: string; checksum: string; is_active: boolean; created_by: string; created_at: string; } +export interface ConfigVersionDetail extends ConfigVersionSummary { + snapshot_json: string; + main_config: string; + rendered_config: string; + support_files_json: string; +} + export interface SupportFile { path: string; content: string; diff --git a/openflare_server/web/features/nodes/components/node-detail-page.tsx b/openflare_server/web/features/nodes/components/node-detail-page.tsx index dc8b4c6e..646f2e17 100644 --- a/openflare_server/web/features/nodes/components/node-detail-page.tsx +++ b/openflare_server/web/features/nodes/components/node-detail-page.tsx @@ -17,7 +17,7 @@ import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { getConfigVersions } from '@/features/config-versions/api/config-versions'; import { ConfigVersionSnapshotModal } from '@/features/config-versions/components/config-version-snapshot-modal'; -import type { ConfigVersionItem } from '@/features/config-versions/types'; +import type { ConfigVersionSummary } from '@/features/config-versions/types'; import { getApplyLogs } from '@/features/apply-logs/api/apply-logs'; import { deleteNode, @@ -416,7 +416,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { } }; - const activeConfigVersion = useMemo(() => { + const activeConfigVersion = useMemo(() => { return ( (configVersionsQuery.data ?? []).find((item) => item.is_active) ?? null );