拆分部署服务器弹窗并压缩单屏布局及说明提示
This commit is contained in:
parent
7ba299d607
commit
1f8495ad49
@ -364,3 +364,72 @@ test("domainModal.compilesAndRendersSanFieldsWithoutLiteralNewline", async () =>
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("serverModal.compilesAndPreservesDeploymentFields", async () => {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const { parse, compileScript, compileTemplate } = await import("@vue/compiler-sfc");
|
||||
const { renderToString } = await import("@vue/server-renderer");
|
||||
const Vue = await import("vue");
|
||||
const source = await readFile(new URL("./ServerModal.vue", import.meta.url), "utf8");
|
||||
const { descriptor, errors } = parse(source);
|
||||
assert.deepEqual(errors, []);
|
||||
assert.ok(compileScript(descriptor, { id: "server-modal" }));
|
||||
const compiled = compileTemplate({
|
||||
source: descriptor.template.content,
|
||||
filename: "ServerModal.vue",
|
||||
id: "server-modal",
|
||||
compilerOptions: { mode: "function" },
|
||||
});
|
||||
assert.deepEqual(compiled.errors, []);
|
||||
const render = new Function("Vue", compiled.code)(Vue);
|
||||
for (const webServer of ["nginx", "caddy", "apache", "custom"]) {
|
||||
const app = Vue.createSSRApp({
|
||||
render,
|
||||
setup: () => ({
|
||||
showServerForm: true,
|
||||
closeServerForm: () => {},
|
||||
serverEditingId: null,
|
||||
serverForm: {
|
||||
name: "test-server",
|
||||
web_server: webServer,
|
||||
address: "example.test",
|
||||
port: 22,
|
||||
username: "deploy",
|
||||
private_key_ref: "key-ref",
|
||||
host_key_fingerprint: "SHA256:test-fingerprint",
|
||||
certificate_path: "/test/cert.pem",
|
||||
certificate_chain_path: "/test/fullchain.pem",
|
||||
private_key_path: "/test/key.pem",
|
||||
validation_command: "nginx\n-t",
|
||||
reload_command: "nginx\n-s\nreload",
|
||||
},
|
||||
sshKeys: [],
|
||||
openSSHKeyCreate: () => {},
|
||||
defaultCertificatePath: "",
|
||||
defaultCertificateChainPath: "",
|
||||
defaultPrivateKeyPath: "",
|
||||
serverFormError: "test-validation-error",
|
||||
serverSaving: false,
|
||||
saveServer: () => {},
|
||||
}),
|
||||
});
|
||||
app.component("HelpTip", {
|
||||
setup: (_, { slots }) => () => Vue.h("span", { class: "test-help-tip" }, slots.default?.()),
|
||||
});
|
||||
const html = await renderToString(app);
|
||||
assert.match(html, /class="modal-card server-modal"/);
|
||||
assert.match(html, /class="server-paths"/);
|
||||
assert.match(html, /type="submit"/);
|
||||
assert.match(html, /SHA256:test-fingerprint/);
|
||||
assert.match(html, /test-validation-error/);
|
||||
assert.match(html, /value="22"/);
|
||||
assert.match(html, /min="1"/);
|
||||
assert.match(html, /max="65535"/);
|
||||
assert.equal((html.match(/class="test-help-tip"/g) || []).length, 4);
|
||||
for (const path of ["/test/cert.pem", "/test/fullchain.pem", "/test/key.pem"]) {
|
||||
assert.ok(html.includes(path));
|
||||
}
|
||||
assert.ok(html.includes("nginx\n-t"));
|
||||
assert.ok(html.includes("nginx\n-s\nreload"));
|
||||
}
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import ChangePassword from "../components/ChangePassword/ChangePassword.vue";
|
||||
import UserManagement from "../UserManagement/UserManagement.vue";
|
||||
import SettingsModal from "./SettingsModal.vue";
|
||||
import DomainModal from "./DomainModal.vue";
|
||||
import ServerModal from "./ServerModal.vue";
|
||||
import { useDashboard } from "./Dashboard.js";
|
||||
|
||||
const dashboard = useDashboard();
|
||||
@ -471,177 +472,7 @@ const {
|
||||
|
||||
<DomainModal v-if="showForm" :state="dashboard" />
|
||||
|
||||
<div
|
||||
v-if="showServerForm"
|
||||
class="modal-backdrop"
|
||||
@click.self="closeServerForm"
|
||||
>
|
||||
<section
|
||||
class="modal-card server-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="服务器配置"
|
||||
>
|
||||
<div class="modal-heading">
|
||||
<div>
|
||||
<p class="eyebrow">远程部署</p>
|
||||
<h2>{{ serverEditingId ? "编辑部署服务器" : "添加部署服务器" }}</h2>
|
||||
</div>
|
||||
<button
|
||||
class="close-button"
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
@click="closeServerForm"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<form @submit.prevent="saveServer">
|
||||
<div class="form-grid">
|
||||
<label class="field"
|
||||
><span>服务器名称</span
|
||||
><input
|
||||
v-model="serverForm.name"
|
||||
required
|
||||
type="text"
|
||||
placeholder="例如生产 Nginx" /></label
|
||||
><label class="field"
|
||||
><span>网站服务</span
|
||||
><select v-model="serverForm.web_server">
|
||||
<option value="nginx">Nginx</option>
|
||||
<option value="caddy">Caddy</option>
|
||||
<option value="apache">Apache</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select></label
|
||||
>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="field"
|
||||
><span>服务器地址</span
|
||||
><input
|
||||
v-model="serverForm.address"
|
||||
required
|
||||
type="text"
|
||||
placeholder="IP 或主机名" /></label
|
||||
><label class="field"
|
||||
><span>SSH 端口</span
|
||||
><input
|
||||
v-model.number="serverForm.port"
|
||||
required
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="field"
|
||||
><span>SSH 用户</span
|
||||
><input
|
||||
v-model="serverForm.username"
|
||||
required
|
||||
type="text" /></label
|
||||
><label class="field"
|
||||
><span class="field-label-row"
|
||||
><span>SSH 私钥</span
|
||||
><button
|
||||
class="inline-button"
|
||||
type="button"
|
||||
@click="openSSHKeyCreate"
|
||||
>
|
||||
新建或导入
|
||||
</button></span
|
||||
><select v-model="serverForm.private_key_ref" required>
|
||||
<option value="">请选择已加密保存的私钥</option>
|
||||
<option
|
||||
v-for="key in sshKeys"
|
||||
:key="key.id"
|
||||
:value="key.reference"
|
||||
>
|
||||
{{ key.name }} · {{ key.algorithm }}
|
||||
</option></select
|
||||
><small v-if="sshKeys.length === 0"
|
||||
>还没有私钥?点击“新建或导入”,按照引导生成、粘贴或读取私钥文件。</small
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
<label class="field"
|
||||
><span>SSH 主机 SHA256 指纹</span
|
||||
><input
|
||||
v-model="serverForm.host_key_fingerprint"
|
||||
required
|
||||
type="text"
|
||||
placeholder="SHA256:..."
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span>远程证书路径</span
|
||||
><input
|
||||
v-model="serverForm.certificate_path"
|
||||
required
|
||||
type="text"
|
||||
:placeholder="defaultCertificatePath"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span>远程完整证书链路径</span
|
||||
><input
|
||||
v-model="serverForm.certificate_chain_path"
|
||||
required
|
||||
type="text"
|
||||
:placeholder="defaultCertificateChainPath"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span>远程私钥路径</span
|
||||
><input
|
||||
v-model="serverForm.private_key_path"
|
||||
required
|
||||
type="text"
|
||||
:placeholder="defaultPrivateKeyPath"
|
||||
/></label>
|
||||
<p class="field-hint">
|
||||
保存的是路径模板,部署时会将 {域名} 和 {证书类型}
|
||||
替换为实际值。证书类型目录固定为 single、wildcard 或 san。
|
||||
</p>
|
||||
<label class="field"
|
||||
><span
|
||||
>配置校验命令
|
||||
<small
|
||||
>每行一个参数;Nginx、Caddy、Apache 留空使用默认命令,自定义服务必须填写</small
|
||||
></span
|
||||
><textarea
|
||||
v-model="serverForm.validation_command"
|
||||
rows="3"
|
||||
placeholder="nginx -t"
|
||||
></textarea>
|
||||
</label>
|
||||
<label class="field"
|
||||
><span
|
||||
>重载命令
|
||||
<small>每行一个参数;留空使用网站服务默认命令</small></span
|
||||
><textarea
|
||||
v-model="serverForm.reload_command"
|
||||
rows="3"
|
||||
placeholder="nginx -s reload"
|
||||
></textarea>
|
||||
</label>
|
||||
<p v-if="serverFormError" class="form-error">{{ serverFormError }}</p>
|
||||
<div class="modal-actions">
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="button"
|
||||
:disabled="serverSaving"
|
||||
@click="closeServerForm"
|
||||
>
|
||||
取消</button
|
||||
><button
|
||||
class="primary-button"
|
||||
type="submit"
|
||||
:disabled="serverSaving"
|
||||
>
|
||||
{{ serverSaving ? "保存中……" : "保存服务器" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<ServerModal v-if="showServerForm" :state="dashboard" />
|
||||
|
||||
<div
|
||||
v-if="showSSHKeyForm"
|
||||
|
||||
105
frontend/src/views/Dashboard/ServerModal.css
Normal file
105
frontend/src/views/Dashboard/ServerModal.css
Normal file
@ -0,0 +1,105 @@
|
||||
.modal-backdrop.server-backdrop {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.server-modal {
|
||||
box-sizing: border-box;
|
||||
width: min(100%, 1120px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
padding: 22px 28px;
|
||||
}
|
||||
|
||||
.server-modal .modal-heading {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.server-modal form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0 18px;
|
||||
}
|
||||
|
||||
.server-modal form > .form-grid,
|
||||
.server-modal form > .field {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.server-modal .form-grid {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.server-modal .server-paths,
|
||||
.server-modal .form-error,
|
||||
.server-modal .modal-actions {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.server-modal .server-paths {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.server-modal .field {
|
||||
min-width: 0;
|
||||
margin-bottom: 10px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.server-modal .field > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.server-modal .field input,
|
||||
.server-modal .field select,
|
||||
.server-modal .field textarea {
|
||||
min-height: 36px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.server-modal .field textarea {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.server-modal .form-error {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.server-modal .modal-actions {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #dce6f1;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.server-modal form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.server-modal form > .field {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.server-modal .server-paths {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.server-modal {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.server-modal form,
|
||||
.server-modal .form-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.server-modal form > .form-grid,
|
||||
.server-modal form > .field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
198
frontend/src/views/Dashboard/ServerModal.vue
Normal file
198
frontend/src/views/Dashboard/ServerModal.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<script setup>
|
||||
import HelpTip from "../components/HelpTip/HelpTip.vue";
|
||||
|
||||
const props = defineProps({ state: { type: Object, required: true } });
|
||||
const {
|
||||
showServerForm,
|
||||
closeServerForm,
|
||||
serverEditingId,
|
||||
serverForm,
|
||||
sshKeys,
|
||||
openSSHKeyCreate,
|
||||
defaultCertificatePath,
|
||||
defaultCertificateChainPath,
|
||||
defaultPrivateKeyPath,
|
||||
serverFormError,
|
||||
serverSaving,
|
||||
saveServer,
|
||||
} = props.state;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="showServerForm"
|
||||
class="modal-backdrop server-backdrop"
|
||||
@click.self="closeServerForm"
|
||||
>
|
||||
<section
|
||||
class="modal-card server-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="服务器配置"
|
||||
>
|
||||
<div class="modal-heading">
|
||||
<div>
|
||||
<p class="eyebrow">远程部署</p>
|
||||
<h2>{{ serverEditingId ? "编辑部署服务器" : "添加部署服务器" }}</h2>
|
||||
</div>
|
||||
<button
|
||||
class="close-button"
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
@click="closeServerForm"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<form @submit.prevent="saveServer">
|
||||
<div class="form-grid">
|
||||
<label class="field"
|
||||
><span>服务器名称</span
|
||||
><input
|
||||
v-model="serverForm.name"
|
||||
required
|
||||
type="text"
|
||||
placeholder="例如生产 Nginx" /></label
|
||||
><label class="field"
|
||||
><span>网站服务</span
|
||||
><select v-model="serverForm.web_server">
|
||||
<option value="nginx">Nginx</option>
|
||||
<option value="caddy">Caddy</option>
|
||||
<option value="apache">Apache</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select></label
|
||||
>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="field"
|
||||
><span>服务器地址</span
|
||||
><input
|
||||
v-model="serverForm.address"
|
||||
required
|
||||
type="text"
|
||||
placeholder="IP 或主机名" /></label
|
||||
><label class="field"
|
||||
><span>SSH 端口</span
|
||||
><input
|
||||
v-model.number="serverForm.port"
|
||||
required
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="field"
|
||||
><span>SSH 用户</span
|
||||
><input
|
||||
v-model="serverForm.username"
|
||||
required
|
||||
type="text" /></label
|
||||
><label class="field"
|
||||
><span class="field-label-row"
|
||||
><span>SSH 私钥</span
|
||||
><button
|
||||
class="inline-button"
|
||||
type="button"
|
||||
@click="openSSHKeyCreate"
|
||||
>
|
||||
新建或导入
|
||||
</button></span
|
||||
><select v-model="serverForm.private_key_ref" required>
|
||||
<option value="">请选择已加密保存的私钥</option>
|
||||
<option
|
||||
v-for="key in sshKeys"
|
||||
:key="key.id"
|
||||
:value="key.reference"
|
||||
>
|
||||
{{ key.name }} · {{ key.algorithm }}
|
||||
</option></select
|
||||
><HelpTip v-if="sshKeys.length === 0"
|
||||
>还没有私钥?点击“新建或导入”,按照引导生成、粘贴或读取私钥文件。</HelpTip
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
<label class="field"
|
||||
><span>SSH 主机 SHA256 指纹</span
|
||||
><input
|
||||
v-model="serverForm.host_key_fingerprint"
|
||||
required
|
||||
type="text"
|
||||
placeholder="SHA256:..."
|
||||
/></label>
|
||||
<div class="server-paths">
|
||||
<label class="field"
|
||||
><span>远程证书路径</span
|
||||
><input
|
||||
v-model="serverForm.certificate_path"
|
||||
required
|
||||
type="text"
|
||||
:placeholder="defaultCertificatePath"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span>远程完整证书链路径</span
|
||||
><input
|
||||
v-model="serverForm.certificate_chain_path"
|
||||
required
|
||||
type="text"
|
||||
:placeholder="defaultCertificateChainPath"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span>远程私钥路径 <HelpTip>
|
||||
保存的是路径模板,部署时会将 {域名} 和 {证书类型}
|
||||
替换为实际值。证书类型目录固定为 single、wildcard 或 san。
|
||||
</HelpTip></span
|
||||
><input
|
||||
v-model="serverForm.private_key_path"
|
||||
required
|
||||
type="text"
|
||||
:placeholder="defaultPrivateKeyPath"
|
||||
/></label>
|
||||
</div>
|
||||
|
||||
<label class="field"
|
||||
><span
|
||||
>配置校验命令
|
||||
<HelpTip
|
||||
>每行一个参数;Nginx、Caddy、Apache 留空使用默认命令,自定义服务必须填写</HelpTip
|
||||
></span
|
||||
><textarea
|
||||
v-model="serverForm.validation_command"
|
||||
rows="2"
|
||||
placeholder="nginx -t"
|
||||
></textarea>
|
||||
</label>
|
||||
<label class="field"
|
||||
><span
|
||||
>重载命令
|
||||
<HelpTip>每行一个参数;留空使用网站服务默认命令</HelpTip></span
|
||||
><textarea
|
||||
v-model="serverForm.reload_command"
|
||||
rows="2"
|
||||
placeholder="nginx -s reload"
|
||||
></textarea>
|
||||
</label>
|
||||
<p v-if="serverFormError" class="form-error">{{ serverFormError }}</p>
|
||||
<div class="modal-actions">
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="button"
|
||||
:disabled="serverSaving"
|
||||
@click="closeServerForm"
|
||||
>
|
||||
取消</button
|
||||
><button
|
||||
class="primary-button"
|
||||
type="submit"
|
||||
:disabled="serverSaving"
|
||||
>
|
||||
{{ serverSaving ? "保存中……" : "保存服务器" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped src="./Dashboard.css"></style>
|
||||
<style scoped src="./ServerModal.css"></style>
|
||||
Loading…
Reference in New Issue
Block a user