diff --git a/frontend/src/views/Dashboard/Dashboard.css b/frontend/src/views/Dashboard/Dashboard.css index a93976e..dbe04e1 100644 --- a/frontend/src/views/Dashboard/Dashboard.css +++ b/frontend/src/views/Dashboard/Dashboard.css @@ -729,6 +729,45 @@ code { width: min(100%, 680px); } +.certificate-history-modal { + width: min(100%, 940px); +} + +.certificate-history-list { + display: grid; + gap: 12px; +} + +.certificate-history-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + padding: 16px 18px; + border: 1px solid #dfe8f2; + border-radius: 14px; + background: #fbfdff; +} + +.certificate-history-row > div { + display: grid; + gap: 9px; + min-width: 0; +} + +.certificate-history-row .domain-meta { + row-gap: 7px; +} + +.certificate-history-row .domain-meta span:last-child { + overflow-wrap: anywhere; +} + +.certificate-history-row .secondary-button { + flex: 0 0 auto; + align-self: center; +} + .import-result { display: grid; gap: 6px; @@ -1045,4 +1084,13 @@ code { .settings-web-controls { grid-template-columns: 1fr; } + + .certificate-history-row { + flex-direction: column; + } + + .certificate-history-row .secondary-button, + .certificate-history-row > .muted { + align-self: flex-start; + } } diff --git a/frontend/src/views/Dashboard/Dashboard.js b/frontend/src/views/Dashboard/Dashboard.js index 00f7fcd..a88f073 100644 --- a/frontend/src/views/Dashboard/Dashboard.js +++ b/frontend/src/views/Dashboard/Dashboard.js @@ -904,7 +904,7 @@ export function useDashboard() { notice.value = `证书历史版本恢复任务已提交:${result.task.id},恢复完成后请另行确认远端部署状态`; showCertificateVersions.value = false; await loadData(); - await openTaskDetails(result.task); + await taskMonitor.openTaskDetails(result.task); } catch (err) { versionsError.value = err.message || "提交证书历史版本恢复失败"; } finally { diff --git a/frontend/src/views/Dashboard/Dashboard.test.js b/frontend/src/views/Dashboard/Dashboard.test.js index 6cec9e8..b6b47ae 100644 --- a/frontend/src/views/Dashboard/Dashboard.test.js +++ b/frontend/src/views/Dashboard/Dashboard.test.js @@ -99,6 +99,91 @@ test("证书导入使用组合式状态导出,避免点击时缺失处理函 assert.ok(script.includes(` ${name},`)); } }); + +test("证书历史版本使用稳定标识并提供独立恢复弹窗样式", async () => { + const { readFile } = await import("node:fs/promises"); + const template = await readFile(new URL("./Dashboard.vue", import.meta.url), "utf8"); + const script = await readFile(new URL("./Dashboard.js", import.meta.url), "utf8"); + const stylesheet = await readFile(new URL("./Dashboard.css", import.meta.url), "utf8"); + for (const name of [ + "showCertificateVersions", + "openCertificateVersions", + "closeCertificateVersions", + "restoreCertificateVersion", + ]) { + assert.ok(template.includes(` ${name},`)); + } + assert.match(script, /certificate\/versions/); + assert.match(template, /version\.history_id/); + assert.match(template, /certificate-history-modal/); + for (const selector of [ + ".certificate-history-modal", + ".certificate-history-list", + ".certificate-history-row", + ]) { + assert.match(stylesheet, new RegExp(`${selector.replaceAll(".", "\\.")}\\s*\\{`)); + } +}); + +test("证书历史版本读取列表并阻止恢复当前版本", async (t) => { + const requests = []; + const domain = { id: "domain-history", name: "example.test" }; + const versions = [ + { history_id: "history-current", current: true, version: 2 }, + { history_id: "history-old", current: false, version: 1 }, + ]; + const { state } = await mountComposable(t, useDashboard, async (path, options = {}) => { + requests.push({ path, options }); + if (path === `/api/v1/domains/${domain.id}/certificate/versions`) { + return Response.json({ items: versions }); + } + return Response.json({ items: [] }); + }); + await state.openCertificateVersions(domain); + assert.equal(state.showCertificateVersions.value, true); + assert.deepEqual(state.certificateVersions.value, versions); + assert.equal( + requests.some(({ path }) => path === `/api/v1/domains/${domain.id}/certificate/versions`), + true, + ); + window.confirm = () => assert.fail("当前版本不应触发恢复确认"); + await state.restoreCertificateVersion(versions[0]); + assert.equal( + requests.some(({ path }) => path.includes("/restore")), + false, + ); +}); + +test("证书历史版本恢复使用 history_id、幂等键并打开任务详情", async (t) => { + const requests = []; + const domain = { id: "domain-history", name: "example.test" }; + const task = { id: "task-history", task_type: "certificate_restore", status: "queued" }; + const version = { history_id: "history-old", current: false, version: 1 }; + const { state } = await mountComposable(t, useDashboard, async (path, options = {}) => { + requests.push({ path, options }); + if (path.endsWith("/certificate/versions")) return Response.json({ items: [version] }); + if (path.endsWith("/restore")) return Response.json({ accepted: true, task }, { status: 202 }); + if (path === `/api/v1/tasks/${task.id}`) return Response.json(task); + if (path.endsWith(`/api/v1/tasks/${task.id}/attempts`)) return Response.json({ items: [] }); + if (path.endsWith(`/api/v1/tasks/${task.id}/deployment-results`)) return Response.json({ items: [] }); + return Response.json({ items: [] }); + }); + window.confirm = () => true; + await state.openCertificateVersions(domain); + await state.restoreCertificateVersion(version); + const restoreRequest = requests.find(({ path }) => path.endsWith("/restore")); + assert.ok(restoreRequest); + assert.equal( + restoreRequest.path, + `/api/v1/domains/${domain.id}/certificate/versions/${version.history_id}/restore`, + ); + assert.match(restoreRequest.options.headers["Idempotency-Key"], /^certificate-restore-domain-history-/); + assert.equal(state.showCertificateVersions.value, false); + assert.equal(state.showTaskDetails.value, true); + assert.equal(state.taskDetails.value.id, task.id); + assert.match(state.notice.value, /恢复完成后请另行确认远端部署状态/); +}); + test("完整页面逐个渲染六个板块,每次只有一个内容标题", async (t) => { const { readFile } = await import("node:fs/promises"); const { parse, compileTemplate } = await import("@vue/compiler-sfc");