perf: 证书直接查看

pull/213/head
xiaojunnuo 2024-10-15 17:12:42 +08:00
parent 64c4933645
commit 5dde5bd3f7
13 changed files with 189 additions and 98 deletions

View File

@ -307,13 +307,15 @@ export class Executor {
//更新pipeline vars //更新pipeline vars
if (Object.keys(instance._result.pipelineVars).length > 0) { if (Object.keys(instance._result.pipelineVars).length > 0) {
// 判断 pipelineVars 有值时更新 // 判断 pipelineVars 有值时更新
const vars = await this.pipelineContext.getObj("vars"); let vars = await this.pipelineContext.getObj("vars");
vars = vars || {};
merge(vars, instance._result.pipelineVars); merge(vars, instance._result.pipelineVars);
await this.pipelineContext.setObj("vars", vars); await this.pipelineContext.setObj("vars", vars);
} }
if (Object.keys(instance._result.pipelinePrivateVars).length > 0) { if (Object.keys(instance._result.pipelinePrivateVars).length > 0) {
// 判断 pipelineVars 有值时更新 // 判断 pipelineVars 有值时更新
const vars = await this.pipelineContext.getObj("privateVars"); let vars = await this.pipelineContext.getObj("privateVars");
vars = vars || {};
merge(vars, instance._result.pipelinePrivateVars); merge(vars, instance._result.pipelinePrivateVars);
await this.pipelineContext.setObj("privateVars", vars); await this.pipelineContext.setObj("privateVars", vars);
} }

View File

@ -38,7 +38,7 @@ export class RunHistory {
start(runnable: Runnable): HistoryResult { start(runnable: Runnable): HistoryResult {
const now = new Date().getTime(); const now = new Date().getTime();
this.logs[runnable.id] = []; this.logs[runnable.id] = [];
this._loggers[runnable.id] = buildLogger((text) => { this._loggers[runnable.id] = buildLogger((text: string) => {
this.logs[runnable.id].push(text); this.logs[runnable.id].push(text);
}); });
const status: HistoryResult = { const status: HistoryResult = {

View File

@ -166,7 +166,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
mergeScript: ` mergeScript: `
return { return {
show: ctx.compute(({form})=>{ show: ctx.compute(({form})=>{
return (form.sslProvider === 'zerossl' || !form.zerosslCommonEabAccessId) || (form.sslProvider === 'google' && !form.googleCommonEabAccessId) return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId) || (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
}) })
} }
`, `,

View File

@ -1,12 +1,13 @@
<template> <template>
<a-config-provider :locale="locale" :theme="settingStore.themeToken"> <a-config-provider :locale="locale" :theme="settingStore.themeToken">
<contextHolder />
<fs-form-provider> <fs-form-provider>
<router-view v-if="routerEnabled" /> <router-view v-if="routerEnabled" />
</fs-form-provider> </fs-form-provider>
</a-config-provider> </a-config-provider>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import zhCN from "ant-design-vue/es/locale/zh_CN"; import zhCN from "ant-design-vue/es/locale/zh_CN";
import enUS from "ant-design-vue/es/locale/en_US"; import enUS from "ant-design-vue/es/locale/en_US";
import { nextTick, provide, ref } from "vue"; import { nextTick, provide, ref } from "vue";
@ -16,44 +17,39 @@ import { useSettingStore } from "/@/store/modules/settings";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import "dayjs/locale/en"; import "dayjs/locale/en";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Modal } from "ant-design-vue";
export default { defineOptions({
name: "App", name: "App"
setup() { });
// const [modal, contextHolder] = Modal.useModal();
const routerEnabled = ref(true); provide("modal", modal);
const locale = ref(zhCN); //
async function reload() { const routerEnabled = ref(true);
// routerEnabled.value = false; const locale = ref(zhCN);
// await nextTick(); async function reload() {
// routerEnabled.value = true; // routerEnabled.value = false;
} // await nextTick();
function localeChanged(value: any) { // routerEnabled.value = true;
console.log("locale changed:", value); }
if (value === "zh-cn") { function localeChanged(value: any) {
locale.value = zhCN; console.log("locale changed:", value);
dayjs.locale("zh-cn"); if (value === "zh-cn") {
} else if (value === "en") { locale.value = zhCN;
locale.value = enUS; dayjs.locale("zh-cn");
dayjs.locale("en"); } else if (value === "en") {
} locale.value = enUS;
} dayjs.locale("en");
localeChanged("zh-cn");
provide("fn:router.reload", reload);
provide("fn:locale.changed", localeChanged);
//
const resourceStore = useResourceStore();
resourceStore.init();
const pageStore = usePageStore();
pageStore.init();
const settingStore = useSettingStore();
return {
routerEnabled,
locale,
settingStore
};
} }
}; }
localeChanged("zh-cn");
provide("fn:router.reload", reload);
provide("fn:locale.changed", localeChanged);
//
const resourceStore = useResourceStore();
resourceStore.init();
const pageStore = usePageStore();
pageStore.init();
const settingStore = useSettingStore();
</script> </script>

View File

@ -43,7 +43,6 @@ async function loginWithOTPCode(otpCode: string) {
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
async function getDeviceId() { async function getDeviceId() {
// //
modal.confirm({ modal.confirm({
title: "请输入OTP验证码", title: "请输入OTP验证码",
maskClosable: true, maskClosable: true,

View File

@ -213,6 +213,7 @@ h1, h2, h3, h4, h5, h6 {
.fs-copyable { .fs-copyable {
display: inline-flex;
.text { .text {
flex: 1 flex: 1
} }

View File

@ -0,0 +1,7 @@
import { inject } from "vue";
import type { ModalStaticFunctions } from "ant-design-vue/es/modal/confirm";
import { ModalFuncWithRef } from "ant-design-vue/es/modal/useModal";
export function useModal(): ModalStaticFunctions<ModalFuncWithRef> {
return inject("modal");
}

View File

@ -0,0 +1,50 @@
<template>
<div class="cert-view">
<a-list item-layout="vertical" :data-source="certFiles">
<template #renderItem="{ item }">
<a-list-item key="item.key">
<a-list-item-meta>
<template #title>
<div class="title">
<div>{{ item.name }}({{ item.fileName }})</div>
<fs-copyable :model-value="item.content" :button="{ show: false }">
<a-tag type="success">复制</a-tag>
</fs-copyable>
</div>
</template>
</a-list-item-meta>
<div>
<a-textarea :value="item.content" rows="5" />
</div>
</a-list-item>
</template>
</a-list>
</div>
</template>
<script setup lang="ts">
import { CertInfo } from "/@/views/certd/pipeline/api";
import { ref } from "vue";
const props = defineProps<{
cert: CertInfo;
}>();
const certFiles = ref([
{ name: "证书", fileName: "fullchain.pem", key: "crt", content: props.cert.crt },
{ name: "私钥", fileName: "private.pem", key: "key", content: props.cert.key },
{ name: "中间证书", fileName: "chain.pem", key: "ic", content: props.cert.ic }
]);
</script>
<style lang="less">
.cert-view {
.title {
display: flex;
justify-content: space-between;
}
.ant-list-item-meta {
margin-block-end: 0px !important;
margin-top: 10px;
}
}
</style>

View File

@ -11,7 +11,9 @@ import { useUserStore } from "/@/store/modules/user";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useSettingStore } from "/@/store/modules/settings"; import { useSettingStore } from "/@/store/modules/settings";
import _ from "lodash-es"; import _ from "lodash-es";
import { useModal } from "/@/use/use-modal";
import CertView from "./cert-view.vue";
import { eachRunnable, eachStages } from "./utils";
export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
@ -149,6 +151,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}); });
} }
const model = useModal();
const viewCert = async (row: any) => { const viewCert = async (row: any) => {
const cert = await api.GetCert(row.id); const cert = await api.GetCert(row.id);
if (!cert) { if (!cert) {
@ -156,34 +159,20 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
return; return;
} }
Modal.success({ model.success({
title: "查看证书", title: "查看证书",
maskClosable: true, maskClosable: true,
okText: "关闭", okText: "关闭",
width: 800,
content: () => { content: () => {
return ( return <CertView cert={cert}></CertView>;
<div class={"view-cert"}>
<div class={"cert-txt"}>
<h3>fullchain.pem</h3>
<div>{cert.crt}</div>
</div>
<div class={"cert-txt"}>
<h3>private.pem</h3>
<div>{cert.key}</div>
</div>
<div class={"cert-txt"}>
<h3>.pem</h3>
<div>{cert.ic}</div>
</div>
</div>
);
} }
}); });
}; };
const downloadCert = async (row: any) => { const downloadCert = async (row: any) => {
const files = await api.GetFiles(row.id); const files = await api.GetFiles(row.id);
Modal.success({ model.success({
title: "文件下载", title: "文件下载",
maskClosable: true, maskClosable: true,
okText: "↑↑↑ 点击上面链接下载", okText: "↑↑↑ 点击上面链接下载",
@ -198,11 +187,6 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
{file.filename} {file.filename}
</a> </a>
</div> </div>
<div>
<a-button type={"primary"} onClick={viewCert}>
</a-button>
</div>
</div> </div>
); );
} }
@ -253,7 +237,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
buttons: { buttons: {
play: { play: {
order: -999, order: -999,
title: null, title: "运行流水线",
type: "link", type: "link",
icon: "ant-design:play-circle-outlined", icon: "ant-design:play-circle-outlined",
click({ row }) { click({ row }) {
@ -268,6 +252,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
} }
}, },
view: { view: {
show: false,
click({ row }) { click({ row }) {
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } }); router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } });
} }
@ -289,7 +274,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}, },
config: { config: {
order: 1, order: 1,
title: null, title: "修改流水线内容",
type: "link", type: "link",
icon: "ant-design:edit-outlined", icon: "ant-design:edit-outlined",
click({ row }) { click({ row }) {
@ -298,12 +283,22 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}, },
edit: { edit: {
order: 2, order: 2,
title: "修改流水线运行配置",
icon: "ant-design:setting-outlined" icon: "ant-design:setting-outlined"
}, },
download: { viewCert: {
order: 3, order: 3,
title: null, title: "查看证书",
type: "link", type: "link",
icon: "ph:certificate",
async click({ row }) {
viewCert(row);
}
},
download: {
order: 4,
type: "link",
title: "下载证书",
icon: "ant-design:download-outlined", icon: "ant-design:download-outlined",
async click({ row }) { async click({ row }) {
downloadCert(row); downloadCert(row);
@ -373,32 +368,53 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
} }
}, },
content: { content: {
title: "定时任务数", title: "流水线内容",
type: "number", form: { show: false },
column: { column: {
align: "center", show: false
width: 100,
cellRender({ value }) {
if (value && value.triggers) {
return value.triggers?.length > 0 ? value.triggers.length : "-";
}
return "-";
}
}, },
valueBuilder({ row }) { valueBuilder({ row }) {
if (row.content) { if (row.content) {
row.content = JSON.parse(row.content); row.content = JSON.parse(row.content);
const pipeline = row.content;
let stepCount = 0;
eachStages(pipeline.stages, (item, runnableType) => {
if (runnableType === "step") {
stepCount++;
}
});
row._stepCount = stepCount;
if (pipeline.triggers) {
row._triggerCount = pipeline.triggers?.length > 0 ? pipeline.triggers.length : "-";
}
} }
}, },
valueResolve({ row }) { valueResolve({ row }) {
if (row.content) { if (row.content) {
row.content = JSON.stringify(row.content); row.content = JSON.stringify(row.content);
} }
}
},
_triggerCount: {
title: "定时任务数",
type: "number",
column: {
align: "center",
width: 100
}, },
form: { form: {
show: false show: false
} }
}, },
_stepCount: {
title: "部署任务数",
type: "number",
form: { show: false },
column: {
align: "center",
width: 100
}
},
lastVars: { lastVars: {
title: "到期剩余", title: "到期剩余",
type: "number", type: "number",
@ -418,18 +434,6 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
width: 150 width: 150
} }
}, },
lastHistoryTime: {
title: "最后运行",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 150,
align: "center"
}
},
status: { status: {
title: "状态", title: "状态",
type: "dict-select", type: "dict-select",
@ -445,7 +449,18 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
align: "center" align: "center"
} }
}, },
lastHistoryTime: {
title: "最后运行",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 150,
align: "center"
}
},
disabled: { disabled: {
title: "启用", title: "启用",
type: "dict-switch", type: "dict-switch",

View File

@ -0,0 +1,15 @@
import { forEach } from "lodash-es";
export function eachStages(list: any[], exec: (item: any, runnableType: string) => void, runnableType: string = "stage") {
if (!list || list.length <= 0) {
return;
}
forEach(list, (item) => {
exec(item, runnableType);
if (runnableType === "stage") {
eachStages(item.tasks, exec, "task");
} else if (runnableType === "task") {
eachStages(item.steps, exec, "step");
}
});
}

View File

@ -15,7 +15,7 @@ export class CertController extends BaseController {
async getCert(@Query('id') id: number) { async getCert(@Query('id') id: number) {
const userId = this.getUserId(); const userId = this.getUserId();
await this.pipelineService.checkUserId(id, userId); await this.pipelineService.checkUserId(id, userId);
const privateVars = this.storeService.getPipelinePrivateVars(id); const privateVars = await this.storeService.getPipelinePrivateVars(id);
return this.ok(privateVars); return this.ok(privateVars.cert);
} }
} }

View File

@ -60,12 +60,17 @@ export class StorageService extends BaseService<StorageEntity> {
if (pipelineId == null) { if (pipelineId == null) {
throw new Error('pipelineId 不能为空'); throw new Error('pipelineId 不能为空');
} }
return await this.repository.find({ const res = await this.repository.findOne({
where: { where: {
scope: 'pipeline', scope: 'pipeline',
namespace: pipelineId + '', namespace: pipelineId + '',
key: 'privateVars', key: 'privateVars',
}, },
}); });
if (!res) {
return {};
}
const value = JSON.parse(res.value);
return value.value;
} }
} }

View File

@ -31,6 +31,7 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
name: 'a-textarea', name: 'a-textarea',
vModel: 'value', vModel: 'value',
rows: 6, rows: 6,
placeholder: 'systemctl restart nginx',
}, },
helper: '注意如果目标主机是windows且终端是cmd系统会自动将多行命令通过“&&”连接成一行', helper: '注意如果目标主机是windows且终端是cmd系统会自动将多行命令通过“&&”连接成一行',
required: true, required: true,