feat: 完善发布运维前端体验
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
- 状态管理使用 Pinia。
|
||||
- 路由使用 Vue Router。
|
||||
- 请求封装使用 Axios,并集中在 `src/services/**`。
|
||||
- BPMN 展示使用 `bpmn-js`,不手写替代渲染器。
|
||||
- 发布流程展示以后端 `run.steps` 为准,前端只展示步骤状态、节点详情和日志,不维护流程图 XML 渲染器。
|
||||
- 不新增前端 mock 数据或接口失败降级;项目、发布、refs、流程和 Agent 调用统一走后端 API。
|
||||
|
||||
## API 与安全边界
|
||||
@@ -25,9 +25,9 @@
|
||||
- 前端不得直接调用 Jenkins、Gitea、企微、飞书或模型供应商;统一通过后端 API。
|
||||
- API 契约以 `/Users/mac033/Desktop/my-project/devops-platform-docs/devops-platform-implementation-tracker.md` 和后端 OpenAPI 为准。
|
||||
|
||||
## BPMN 与 Agent 边界
|
||||
## 发布流程与 Agent 边界
|
||||
|
||||
- BPMN 组件只展示流程图、节点状态和节点详情,不推进状态机。
|
||||
- `step.bpmnNodeId` 必须与 BPMN XML 节点 id 一致。
|
||||
- 发布步骤组件只展示后端步骤、节点状态和节点详情,不推进状态机。
|
||||
- 后端返回的节点标识在前端统一映射为 `step.nodeId`,仅用于展示和定位步骤。
|
||||
- Agent 只能作为全局右侧抽屉工具存在,不作为独立侧边栏路由;能力只允许 DevOps 场景:发布风险摘要、失败诊断、Runbook 问答、发布说明草稿和事故复盘草稿。
|
||||
- 禁止在前端新增泛聊天入口。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DevOps 运维平台前端
|
||||
|
||||
Vue 3 + Vite + TypeScript 实现的 DevOps 运维控制台第一阶段,用于管理本地项目和 DevOps 平台自身的发布状态、Jenkins/Gitea/通知平台/LLM 接入状态、BPMN 流程展示和 Agent 辅助能力。
|
||||
Vue 3 + Vite + TypeScript 实现的 DevOps 运维控制台第一阶段,用于管理本地项目和 DevOps 平台自身的发布状态、Jenkins/Gitea/通知平台/LLM 接入状态、发布步骤轨迹和 Agent 辅助能力。
|
||||
|
||||
## 当前范围
|
||||
|
||||
@@ -12,7 +12,7 @@ Vue 3 + Vite + TypeScript 实现的 DevOps 运维控制台第一阶段,用于
|
||||
- 发布中心包含项目、环境、后端返回的 ref/tag、备注、发布前检查、生产发布回滚/窗口/通知确认、触发、取消、重试和 Jenkins 状态同步入口。
|
||||
- 运行记录展示状态概览、项目/环境/状态/关键词筛选、Jenkins queue/build、Build URL、开始/结束时间、执行提示、失败摘要和日志摘要,并提供超级管理员导入 Jenkins 直接构建记录的入口。
|
||||
- 系统配置页展示集成配置、Agent 密钥状态、通知 outbox 和审计日志概览,支持通知投递与审计记录筛选,用于定位配置缺失、通知失败和敏感操作。
|
||||
- BPMN 状态区使用后端流程 XML 和 run steps 作为状态来源,左侧展示节点输出或 Jenkins 构建日志,右侧粘性展示进度统计、节点时间线和详情面板。
|
||||
- 发布步骤轨迹区使用后端 run steps 作为状态来源,左侧展示节点输出或 Jenkins 构建日志,右侧粘性展示进度统计、节点时间线和详情面板。
|
||||
- Agent 作为全局工具从顶部打开,只包含发布风险摘要、失败诊断、Runbook 问答、发布说明草稿和事故复盘草稿,并通过后端 Agent API 创建调用记录。
|
||||
|
||||
## 技术栈
|
||||
@@ -24,7 +24,6 @@ Vue 3 + Vite + TypeScript 实现的 DevOps 运维控制台第一阶段,用于
|
||||
- Pinia
|
||||
- Vue Router
|
||||
- Axios
|
||||
- bpmn-js
|
||||
|
||||
## 本地启动
|
||||
|
||||
@@ -54,10 +53,10 @@ pnpm build
|
||||
- 通知 webhook
|
||||
- LLM API key/baseURL/model
|
||||
|
||||
## BPMN 边界
|
||||
## 发布步骤边界
|
||||
|
||||
- 前端只负责 BPMN XML 展示和节点状态高亮。
|
||||
- `run.steps[].bpmnNodeId` 必须与 BPMN XML 节点 id 一致。
|
||||
- 前端只负责后端 `run.steps` 的状态、节点详情和日志展示,不渲染流程图 XML。
|
||||
- 后端返回的节点标识在前端统一映射为 `run.steps[].nodeId`,用于步骤选择、详情定位和日志联动。
|
||||
- 流程推进、失败停留、审计日志和状态机由后端负责。
|
||||
|
||||
## Agent 边界
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
- `/overview`:总览,展示值班焦点、优先 Run 队列、项目发布健康、服务状态和项目环境快捷入口。
|
||||
- `/projects`:项目配置,展示项目可发布性概览、发布态/权限筛选、Gitea 仓库、Jenkins Job、通知通道、环境策略和上下文跳转。
|
||||
- `/members`:成员权限,仅超级管理员可见,展示账号/构建权限/安全状态概览,支持成员筛选,并维护普通成员、项目权限和密码重置消息。
|
||||
- `/releases`:发布中心,提供发布参数、后端 ref/tag 选择、发布前检查、生产发布确认、触发、取消、重试和 BPMN 流程状态。
|
||||
- `/runs`:运行记录,展示 run 状态概览、筛选列表、执行详情、流程轨迹,并支持超级管理员导入 Jenkins 直接构建记录。
|
||||
- `/releases`:发布中心,提供发布参数、后端 ref/tag 选择、发布前检查、生产发布确认、触发、取消、重试和发布步骤轨迹。
|
||||
- `/runs`:运行记录,展示 run 状态概览、筛选列表、执行详情、步骤轨迹,并支持超级管理员导入 Jenkins 直接构建记录。
|
||||
- 顶部 Agent 按钮:打开全局右侧抽屉,只保留 DevOps 场景能力;`/agent` 仅重定向到总览,不作为独立侧边栏入口。
|
||||
- `/settings`:系统配置,展示集成配置、Agent、通知 outbox 和审计日志概览,支持通知/审计筛选,并维护后端 Agent 模型配置。
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
- 发布中心触发按钮接入后端发布单创建接口。
|
||||
- 运行记录接入后端 run/step 查询接口。
|
||||
- Jenkins 直接构建不由前端直连 Jenkins;运行记录页调用后端 `/deploy-runs/import-jenkins` 扫描已纳管 job 并导入 run。
|
||||
- BPMN XML 由后端流程定义接口返回。
|
||||
- 发布步骤轨迹以后端 run steps 为准,前端不再拉取或渲染流程图 XML。
|
||||
- Agent 面板调用后端 Agent proxy,前端只传 DevOps 场景参数。
|
||||
- Agent 配置页调用后端 `/agent/config`,前端不持久化 key、baseURL 或 model。
|
||||
- Agent 能力限定为发布风险摘要、失败诊断、Runbook 问答、发布说明草稿和事故复盘草稿。
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.17.0",
|
||||
"bpmn-js": "^18.18.0",
|
||||
"element-plus": "^2.14.1",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.34",
|
||||
|
||||
Generated
-155
@@ -14,9 +14,6 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.17.0
|
||||
version: 1.17.0
|
||||
bpmn-js:
|
||||
specifier: ^18.18.0
|
||||
version: 18.18.0
|
||||
element-plus:
|
||||
specifier: ^2.14.1
|
||||
version: 2.14.1(vue@3.5.38(typescript@6.0.3))
|
||||
@@ -89,9 +86,6 @@ packages:
|
||||
resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==}
|
||||
engines: {node: ^22.18.0 || >=24.11.0}
|
||||
|
||||
'@bpmn-io/diagram-js-ui@0.2.4':
|
||||
resolution: {integrity: sha512-I678ZGtoH7z7FgzFhzzppM9ThsJ3Lh83kW3GjhjG3xyYciIPYHMaHK1MABP8F69H9pFZpViM6t4qUo8dwrVz0w==}
|
||||
|
||||
'@ctrl/tinycolor@4.2.0':
|
||||
resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -388,13 +382,6 @@ packages:
|
||||
birpc@2.9.0:
|
||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||
|
||||
bpmn-js@18.18.0:
|
||||
resolution: {integrity: sha512-3pTiqc5M+CRtXCvBjRBFEewqNKDnx0ytuCLucvMRfdhes/EuVr4memcsi6ql+q/1RUbycOL9MyfNcgrwSfnA1w==}
|
||||
|
||||
bpmn-moddle@10.0.0:
|
||||
resolution: {integrity: sha512-vXePD5jkatcILmM3zwJG/m6IIHIghTGB7WvgcdEraEw8E8VdJHrTgrvBUhbzqaXJpnsGQz15QS936xeBY6l9aA==}
|
||||
engines: {node: '>= 20.12'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -403,10 +390,6 @@ packages:
|
||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -444,22 +427,6 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
diagram-js-direct-editing@3.4.0:
|
||||
resolution: {integrity: sha512-eXfd+nxJr5xt3YlCrs+E53mPi+gqBWRBkAXkR99ZPL7Pl0B3H5nkv/uiwx4Q4vJU/Sp8Jy3zVdhP/Y2c6loTjw==}
|
||||
peerDependencies:
|
||||
diagram-js: '*'
|
||||
|
||||
diagram-js@15.17.0:
|
||||
resolution: {integrity: sha512-wOlnfzd7MmcUMwIpJiCiFScQ04hQMx4Ribir0ky4cfNgzwr7V1dhi7MdFuLyaMdseglMkBU7YYoqmzCDR8+JCw==}
|
||||
|
||||
didi@11.0.0:
|
||||
resolution: {integrity: sha512-PzCfRzQttvFpVcYMbSF7h8EsWjeJpVjWH4qDhB5LkMi1ILvHq4Ob0vhM2wLFziPkbUBi+PAo7ODbe2sacR7nJQ==}
|
||||
engines: {node: '>= 20.12'}
|
||||
|
||||
domify@3.0.0:
|
||||
resolution: {integrity: sha512-bs2yO68JDFOm6rKv8f0EnrM2cENduhRkpqOtt/s5l5JBA/eqGBZCzLPmdYoHtJ6utgLGgcBajFsEQbl12pT0lQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -552,20 +519,10 @@ packages:
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
htm@3.1.1:
|
||||
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
ids@3.0.2:
|
||||
resolution: {integrity: sha512-t6YJP4mdC+GHF96Nbis/4FEANhP/8VWmYMvUuYpXvSdrhg5hpIVbq2XZlOA3UWTbtdwPCi0q7jEXOdHkAnqOnw==}
|
||||
engines: {node: '>= 20.12'}
|
||||
|
||||
inherits-browser@0.1.0:
|
||||
resolution: {integrity: sha512-CJHHvW3jQ6q7lzsXPpapLdMx5hDpSF3FSh45pwsj6bKxJJ8Nl8v43i5yXnr3BdfOimGHKyniewQtnAIp3vyJJw==}
|
||||
|
||||
is-what@5.5.0:
|
||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -689,27 +646,12 @@ packages:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
min-dash@5.0.0:
|
||||
resolution: {integrity: sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==}
|
||||
|
||||
min-dom@5.3.0:
|
||||
resolution: {integrity: sha512-0w5FEBgPAyHhmFojW3zxd7we3D+m5XYS3E/06OyvxmbHJoiQVa4Nagj6RWvoAKYRw5xth6cP5TMePc5cR1M9hA==}
|
||||
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mlly@1.8.2:
|
||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||
|
||||
moddle-xml@12.0.0:
|
||||
resolution: {integrity: sha512-NJc2+sCe4tvuGlaUBcoZcYf6j9f+z+qxHOyGm/LB3ZrlJXVPPHoBTg/KXgDRCufdBJhJ3AheFs3QU/abABNzRg==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
moddle: '>= 6.2.0'
|
||||
|
||||
moddle@8.1.0:
|
||||
resolution: {integrity: sha512-dBddc1CNuZHgro8nQWwfPZ2BkyLWdnxoNpPu9d+XKPN96DAiiBOeBw527ft++ebDuFez5PMdaR3pgUgoOaUGrA==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -724,16 +666,9 @@ packages:
|
||||
normalize-wheel-es@1.2.0:
|
||||
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
|
||||
|
||||
object-refs@0.4.0:
|
||||
resolution: {integrity: sha512-6kJqKWryKZmtte6QYvouas0/EIJKPI1/MMIuRsiBlNuhIMfqYTggzX2F1AJ2+cDs288xyi9GL7FyasHINR98BQ==}
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
path-intersection@4.1.0:
|
||||
resolution: {integrity: sha512-urUP6WvhnxbHPdHYl6L7Yrc6+1ny6uOFKPCzPxTSUSYGHG0o94RmI7SvMMaScNAM5RtTf08bg4skc6/kjfne3A==}
|
||||
engines: {node: '>= 14.20'}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@@ -769,9 +704,6 @@ packages:
|
||||
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
preact@10.29.2:
|
||||
resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==}
|
||||
|
||||
proxy-from-env@2.1.0:
|
||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -791,10 +723,6 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
saxen@11.0.2:
|
||||
resolution: {integrity: sha512-WDb4gqac8uiJzOdOdVpr9NWh9NrJMm7Brn5GX2Poj+mjE/QTXqYQENr8T/mom54dDDgbd3QjwTg23TRHYiWXRA==}
|
||||
engines: {node: '>= 20.12'}
|
||||
|
||||
scule@1.3.0:
|
||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||
|
||||
@@ -810,10 +738,6 @@ packages:
|
||||
resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tiny-svg@4.1.4:
|
||||
resolution: {integrity: sha512-cBaEACCbouYrQc9RG+eTXnPYosX1Ijqty/I6DdXovwDd89Pwu4jcmpOR7BuFEF9YCcd7/AWwasE0207WMK7hdw==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -966,11 +890,6 @@ snapshots:
|
||||
'@babel/helper-string-parser': 8.0.0-rc.6
|
||||
'@babel/helper-validator-identifier': 8.0.0-rc.6
|
||||
|
||||
'@bpmn-io/diagram-js-ui@0.2.4':
|
||||
dependencies:
|
||||
htm: 3.1.1
|
||||
preact: 10.29.2
|
||||
|
||||
'@ctrl/tinycolor@4.2.0': {}
|
||||
|
||||
'@element-plus/icons-vue@2.3.2(vue@3.5.38(typescript@6.0.3))':
|
||||
@@ -1282,23 +1201,6 @@ snapshots:
|
||||
|
||||
birpc@2.9.0: {}
|
||||
|
||||
bpmn-js@18.18.0:
|
||||
dependencies:
|
||||
bpmn-moddle: 10.0.0
|
||||
diagram-js: 15.17.0
|
||||
diagram-js-direct-editing: 3.4.0(diagram-js@15.17.0)
|
||||
ids: 3.0.2
|
||||
inherits-browser: 0.1.0
|
||||
min-dash: 5.0.0
|
||||
min-dom: 5.3.0
|
||||
tiny-svg: 4.1.4
|
||||
|
||||
bpmn-moddle@10.0.0:
|
||||
dependencies:
|
||||
min-dash: 5.0.0
|
||||
moddle: 8.1.0
|
||||
moddle-xml: 12.0.0(moddle@8.1.0)
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -1308,8 +1210,6 @@ snapshots:
|
||||
dependencies:
|
||||
readdirp: 5.0.0
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
@@ -1334,28 +1234,6 @@ snapshots:
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
diagram-js-direct-editing@3.4.0(diagram-js@15.17.0):
|
||||
dependencies:
|
||||
diagram-js: 15.17.0
|
||||
min-dash: 5.0.0
|
||||
min-dom: 5.3.0
|
||||
|
||||
diagram-js@15.17.0:
|
||||
dependencies:
|
||||
'@bpmn-io/diagram-js-ui': 0.2.4
|
||||
clsx: 2.1.1
|
||||
didi: 11.0.0
|
||||
inherits-browser: 0.1.0
|
||||
min-dash: 5.0.0
|
||||
min-dom: 5.3.0
|
||||
object-refs: 0.4.0
|
||||
path-intersection: 4.1.0
|
||||
tiny-svg: 4.1.4
|
||||
|
||||
didi@11.0.0: {}
|
||||
|
||||
domify@3.0.0: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -1453,8 +1331,6 @@ snapshots:
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
htm@3.1.1: {}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
@@ -1462,10 +1338,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ids@3.0.2: {}
|
||||
|
||||
inherits-browser@0.1.0: {}
|
||||
|
||||
is-what@5.5.0: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -1555,13 +1427,6 @@ snapshots:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
min-dash@5.0.0: {}
|
||||
|
||||
min-dom@5.3.0:
|
||||
dependencies:
|
||||
domify: 3.0.0
|
||||
min-dash: 5.0.0
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mlly@1.8.2:
|
||||
@@ -1571,16 +1436,6 @@ snapshots:
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.4
|
||||
|
||||
moddle-xml@12.0.0(moddle@8.1.0):
|
||||
dependencies:
|
||||
min-dash: 5.0.0
|
||||
moddle: 8.1.0
|
||||
saxen: 11.0.2
|
||||
|
||||
moddle@8.1.0:
|
||||
dependencies:
|
||||
min-dash: 5.0.0
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
@@ -1589,12 +1444,8 @@ snapshots:
|
||||
|
||||
normalize-wheel-es@1.2.0: {}
|
||||
|
||||
object-refs@0.4.0: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-intersection@4.1.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
perfect-debounce@1.0.0: {}
|
||||
@@ -1630,8 +1481,6 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
preact@10.29.2: {}
|
||||
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
quansync@0.2.11: {}
|
||||
@@ -1661,8 +1510,6 @@ snapshots:
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
||||
|
||||
saxen@11.0.2: {}
|
||||
|
||||
scule@1.3.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
@@ -1673,8 +1520,6 @@ snapshots:
|
||||
dependencies:
|
||||
copy-anything: 4.0.5
|
||||
|
||||
tiny-svg@4.1.4: {}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { JenkinsBuildLog, PipelineStep, ReleaseRun, StepStatus } from '../t
|
||||
import StatusPill from './StatusPill.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
xml: string
|
||||
steps: PipelineStep[]
|
||||
run?: ReleaseRun
|
||||
}>()
|
||||
@@ -22,7 +21,7 @@ type StatusMeta = {
|
||||
type OutputMode = 'step' | 'jenkins'
|
||||
const maxRenderedLogLines = 520
|
||||
|
||||
const selectedNodeId = ref('')
|
||||
const selectedStepNodeId = ref('')
|
||||
const terminalOutputEl = ref<HTMLElement>()
|
||||
const outputMode = ref<OutputMode>('step')
|
||||
const logLoading = ref(false)
|
||||
@@ -104,11 +103,11 @@ const currentStep = computed(() => {
|
||||
})
|
||||
|
||||
const selectedStep = computed(() => {
|
||||
if (!selectedNodeId.value) {
|
||||
if (!selectedStepNodeId.value) {
|
||||
return currentStep.value
|
||||
}
|
||||
|
||||
return findStep(selectedNodeId.value) ?? currentStep.value
|
||||
return findStep(selectedStepNodeId.value) ?? currentStep.value
|
||||
})
|
||||
|
||||
const stepStats = computed(() => {
|
||||
@@ -140,8 +139,6 @@ const isActiveRun = computed(
|
||||
() => props.run?.status === 'queued' || props.run?.status === 'running',
|
||||
)
|
||||
|
||||
const processSourceText = computed(() => (props.xml ? 'BPMN XML 已加载' : '等待 BPMN XML'))
|
||||
|
||||
const terminalTitle = computed(() => {
|
||||
if (outputMode.value === 'jenkins') {
|
||||
return hasJenkinsBuildNumber.value
|
||||
@@ -160,14 +157,14 @@ const terminalSubtitle = computed(() => {
|
||||
|
||||
const buildText = hasJenkinsBuildNumber.value ? `Build #${props.run?.jenkinsBuildNumber}` : ''
|
||||
const queueText = props.run?.jenkinsQueueId ? `Queue ${props.run.jenkinsQueueId}` : ''
|
||||
return [buildText, queueText, processSourceText.value].filter(Boolean).join(' · ')
|
||||
return [buildText, queueText].filter(Boolean).join(' · ') || '等待 Jenkins 日志来源'
|
||||
}
|
||||
|
||||
if (!selectedStep.value) {
|
||||
return processSourceText.value
|
||||
return '等待后端步骤'
|
||||
}
|
||||
|
||||
return `${selectedStep.value.owner} · ${selectedStep.value.bpmnNodeId}`
|
||||
return `${selectedStep.value.owner} · 节点 ${selectedStep.value.nodeId}`
|
||||
})
|
||||
|
||||
const logRefreshStateText = computed(() => {
|
||||
@@ -193,7 +190,7 @@ const stepOutputText = computed(() => {
|
||||
`# ${step.name}`,
|
||||
`owner=${step.owner}`,
|
||||
`status=${statusMeta[step.status].label}`,
|
||||
`bpmnNodeId=${step.bpmnNodeId}`,
|
||||
`nodeId=${step.nodeId}`,
|
||||
`duration=${step.duration}`,
|
||||
step.startedAt ? `startedAt=${step.startedAt}` : '',
|
||||
step.finishedAt ? `finishedAt=${step.finishedAt}` : '',
|
||||
@@ -238,7 +235,7 @@ const terminalEmptyHint = computed(() => {
|
||||
})
|
||||
|
||||
function findStep(nodeId: string) {
|
||||
return props.steps.find((step) => step.bpmnNodeId === nodeId)
|
||||
return props.steps.find((step) => step.nodeId === nodeId)
|
||||
}
|
||||
|
||||
function statusClass(status: StepStatus): string {
|
||||
@@ -246,11 +243,11 @@ function statusClass(status: StepStatus): string {
|
||||
}
|
||||
|
||||
function isSelectedStep(step: PipelineStep): boolean {
|
||||
return selectedStep.value?.bpmnNodeId === step.bpmnNodeId
|
||||
return selectedStep.value?.nodeId === step.nodeId
|
||||
}
|
||||
|
||||
function selectStep(step: PipelineStep) {
|
||||
selectedNodeId.value = step.bpmnNodeId
|
||||
selectedStepNodeId.value = step.nodeId
|
||||
|
||||
if (step.owner === 'Jenkins') {
|
||||
outputMode.value = 'jenkins'
|
||||
@@ -401,7 +398,7 @@ watch(
|
||||
watch(
|
||||
() => `${props.run?.id ?? 'none'}:${currentStep.value?.id ?? 'none'}:${currentStep.value?.status ?? 'none'}`,
|
||||
() => {
|
||||
if (selectedNodeId.value || currentStep.value?.owner !== 'Jenkins') {
|
||||
if (selectedStepNodeId.value || currentStep.value?.owner !== 'Jenkins') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -430,7 +427,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bpmn-viewer">
|
||||
<div class="pipeline-viewer">
|
||||
<section class="execution-output">
|
||||
<div class="execution-toolbar">
|
||||
<div>
|
||||
@@ -484,7 +481,7 @@ onBeforeUnmount(() => {
|
||||
<div v-if="logError && outputMode === 'jenkins'" class="log-error">{{ logError }}</div>
|
||||
</section>
|
||||
|
||||
<aside class="bpmn-side">
|
||||
<aside class="pipeline-side">
|
||||
<div class="flow-stats">
|
||||
<div>
|
||||
<small>流程进度</small>
|
||||
@@ -535,7 +532,7 @@ onBeforeUnmount(() => {
|
||||
<div class="node-detail-head">
|
||||
<span>
|
||||
<strong>{{ selectedStep.name }}</strong>
|
||||
<small>{{ selectedStep.owner }} · {{ selectedStep.bpmnNodeId }}</small>
|
||||
<small>{{ selectedStep.owner }} · 节点 {{ selectedStep.nodeId }}</small>
|
||||
</span>
|
||||
<StatusPill :state="selectedStep.status" />
|
||||
</div>
|
||||
@@ -563,7 +560,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bpmn-viewer {
|
||||
.pipeline-viewer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 330px;
|
||||
gap: 16px;
|
||||
@@ -712,7 +709,7 @@ onBeforeUnmount(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bpmn-side {
|
||||
.pipeline-side {
|
||||
position: sticky;
|
||||
top: 86px;
|
||||
display: flex;
|
||||
@@ -972,7 +969,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.bpmn-viewer {
|
||||
.pipeline-viewer {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
@@ -989,7 +986,7 @@ onBeforeUnmount(() => {
|
||||
height: min(520px, 70vh);
|
||||
}
|
||||
|
||||
.bpmn-side {
|
||||
.pipeline-side {
|
||||
position: static;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
+73
-2
@@ -1,8 +1,79 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import {
|
||||
ElAlert,
|
||||
ElAside,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElContainer,
|
||||
ElDialog,
|
||||
ElDrawer,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElEmpty,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElHeader,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElLink,
|
||||
ElLoading,
|
||||
ElMain,
|
||||
ElMenu,
|
||||
ElMenuItem,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSegmented,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
} from 'element-plus'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).use(ElementPlus).mount('#app')
|
||||
const app = createApp(App)
|
||||
|
||||
for (const component of [
|
||||
ElAlert,
|
||||
ElAside,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElContainer,
|
||||
ElDialog,
|
||||
ElDrawer,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElEmpty,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElHeader,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElLink,
|
||||
ElMain,
|
||||
ElMenu,
|
||||
ElMenuItem,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSegmented,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
]) {
|
||||
app.use(component)
|
||||
}
|
||||
|
||||
app.use(ElLoading).use(createPinia()).use(router).mount('#app')
|
||||
|
||||
+12
-8
@@ -1,13 +1,14 @@
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||
import { useSessionStore } from '../stores/session'
|
||||
import MainLayout from '../layouts/MainLayout.vue'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import OverviewView from '../views/OverviewView.vue'
|
||||
import ProjectsView from '../views/ProjectsView.vue'
|
||||
import MembersView from '../views/MembersView.vue'
|
||||
import ReleaseCenterView from '../views/ReleaseCenterView.vue'
|
||||
import RunsView from '../views/RunsView.vue'
|
||||
import SettingsView from '../views/SettingsView.vue'
|
||||
|
||||
const MainLayout = () => import('../layouts/MainLayout.vue')
|
||||
const LoginView = () => import('../views/LoginView.vue')
|
||||
const OverviewView = () => import('../views/OverviewView.vue')
|
||||
const ProjectsView = () => import('../views/ProjectsView.vue')
|
||||
const MembersView = () => import('../views/MembersView.vue')
|
||||
const ReleaseCenterView = () => import('../views/ReleaseCenterView.vue')
|
||||
const RunsView = () => import('../views/RunsView.vue')
|
||||
const SettingsView = () => import('../views/SettingsView.vue')
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -59,6 +60,9 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'settings',
|
||||
name: 'settings',
|
||||
component: SettingsView,
|
||||
meta: {
|
||||
superAdminOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+12
-26
@@ -91,6 +91,7 @@ interface BackendProject {
|
||||
|
||||
interface BackendDeployRunStep {
|
||||
id?: string
|
||||
nodeId?: string
|
||||
bpmnNodeId?: string
|
||||
name?: string
|
||||
status?: BackendRunStatus
|
||||
@@ -151,13 +152,6 @@ interface BackendJenkinsImportSummary {
|
||||
}>
|
||||
}
|
||||
|
||||
interface BackendProcessDefinition {
|
||||
key: 'release'
|
||||
version: string
|
||||
xml: string
|
||||
nodeIds: string[]
|
||||
}
|
||||
|
||||
interface BackendIntegrationConfigItem {
|
||||
key: IntegrationConfigKey
|
||||
name: string
|
||||
@@ -231,7 +225,7 @@ const agentCards: AgentCard[] = [
|
||||
mode: 'incidentReview',
|
||||
title: '事故复盘草稿',
|
||||
description: '基于失败 run、流程耗时、Jenkins 摘要和 Runbook 生成复盘草稿。',
|
||||
inputScope: ['失败 run', 'BPMN 节点', '脱敏日志片段', 'Runbook'],
|
||||
inputScope: ['失败 run', '失败节点', '脱敏日志片段', 'Runbook'],
|
||||
outputPreview: ['时间线', '根因假设', '修复和预防项'],
|
||||
},
|
||||
]
|
||||
@@ -246,12 +240,12 @@ const settings: SystemSetting[] = [
|
||||
boundary: '前端不写死真实 token,不直接访问 Jenkins/Gitea/LLM。',
|
||||
},
|
||||
{
|
||||
key: 'bpmn',
|
||||
name: 'BPMN 流程定义',
|
||||
key: 'pipeline',
|
||||
name: '发布步骤流转',
|
||||
status: 'healthy',
|
||||
owner: '平台流程配置',
|
||||
contract: 'step.bpmnNodeId 必须和 BPMN XML 节点 id 一致。',
|
||||
boundary: '前端只展示流程和状态,不推进状态机。',
|
||||
owner: '后端发布状态机',
|
||||
contract: 'run.steps 由后端返回,前端只按节点顺序展示状态、耗时和日志摘要。',
|
||||
boundary: '前端只展示步骤和日志,不推进状态机。',
|
||||
},
|
||||
{
|
||||
key: 'agent',
|
||||
@@ -539,14 +533,6 @@ export const devopsApi = {
|
||||
return data
|
||||
},
|
||||
|
||||
async getReleaseProcessXml(): Promise<string> {
|
||||
const { data } = await http.get<BackendProcessDefinition>(
|
||||
'/process-definitions/release',
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return data.xml
|
||||
},
|
||||
|
||||
async createAgentInvocation(
|
||||
payload: CreateAgentInvocationPayload,
|
||||
): Promise<AgentInvocation> {
|
||||
@@ -1153,14 +1139,14 @@ function sanitizeNotificationOutboxMessage(
|
||||
}
|
||||
|
||||
function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
|
||||
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
|
||||
const name = step.name || bpmnNodeId
|
||||
const nodeId = step.nodeId || step.bpmnNodeId || step.id || 'unknown-step'
|
||||
const name = step.name || nodeId
|
||||
|
||||
return {
|
||||
id: step.id || bpmnNodeId,
|
||||
bpmnNodeId,
|
||||
id: step.id || nodeId,
|
||||
nodeId,
|
||||
name,
|
||||
owner: stepOwner(bpmnNodeId),
|
||||
owner: stepOwner(nodeId),
|
||||
status: toStepStatus(step.status),
|
||||
duration: formatDuration(step.startedAt, step.finishedAt, step.status),
|
||||
summary: step.errorSummary || step.message || name,
|
||||
|
||||
@@ -46,7 +46,6 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
checkedAt: '',
|
||||
integrations: [],
|
||||
})
|
||||
const releaseProcessXml = ref('')
|
||||
const projectRefs = ref<Record<string, ProjectRefs>>({})
|
||||
|
||||
const failedRuns = computed(() => runs.value.filter((run) => run.status === 'failed'))
|
||||
@@ -83,19 +82,16 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
agentCards.value = summary.agentCards
|
||||
settings.value = summary.settings
|
||||
integrationConfig.value = summary.integrationConfig
|
||||
const processXmlPromise = devopsApi.getReleaseProcessXml()
|
||||
const auditLogsPromise = includeAdminData
|
||||
? devopsApi.getAuditLogs()
|
||||
: Promise.resolve<AuditLogEntry[]>([])
|
||||
const notificationOutboxPromise = includeAdminData
|
||||
? devopsApi.getNotificationOutbox()
|
||||
: Promise.resolve<NotificationOutboxMessage[]>([])
|
||||
const [processXml, recentAuditLogs, recentNotificationOutbox] = await Promise.all([
|
||||
processXmlPromise,
|
||||
const [recentAuditLogs, recentNotificationOutbox] = await Promise.all([
|
||||
auditLogsPromise,
|
||||
notificationOutboxPromise,
|
||||
])
|
||||
releaseProcessXml.value = processXml
|
||||
auditLogs.value = recentAuditLogs
|
||||
notificationOutbox.value = recentNotificationOutbox
|
||||
applyProjectGiteaDiagnostics()
|
||||
@@ -358,7 +354,6 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
projectJenkinsDiagnostics,
|
||||
projectGiteaDiagnostics,
|
||||
integrationConfig,
|
||||
releaseProcessXml,
|
||||
projectRefs,
|
||||
failedRuns,
|
||||
runningRuns,
|
||||
|
||||
+1
-1
@@ -353,7 +353,7 @@ export interface ProjectRefItem {
|
||||
|
||||
export interface PipelineStep {
|
||||
id: string
|
||||
bpmnNodeId: string
|
||||
nodeId: string
|
||||
name: string
|
||||
owner: 'Gitea' | 'Jenkins' | 'HealthCheck' | 'Notification' | 'Platform'
|
||||
status: StepStatus
|
||||
|
||||
@@ -273,6 +273,16 @@ async function updatePermissions() {
|
||||
return
|
||||
}
|
||||
|
||||
const buildCount = Object.values(editPermissions).filter((level) => level === 'build').length
|
||||
const confirmed = await confirmMemberAction(
|
||||
'确认保存项目权限',
|
||||
`将保存 ${member.displayName} 的项目权限,其中 ${buildCount} 个项目具备构建权限。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
const updated = await devopsApi.updateMemberPermissions(member.id, {
|
||||
permissions: toPermissionPayload(editPermissions),
|
||||
})
|
||||
@@ -313,6 +323,16 @@ async function showTemporaryPassword(
|
||||
|
||||
async function toggleStatus(member: MemberSummary) {
|
||||
const nextStatus = member.status === 'active' ? 'disabled' : 'active'
|
||||
const actionText = nextStatus === 'active' ? '启用' : '禁用'
|
||||
const confirmed = await confirmMemberAction(
|
||||
`${actionText}成员`,
|
||||
`确认${actionText}成员 ${member.displayName}?${nextStatus === 'disabled' ? '禁用后该成员不能继续登录平台。' : ''}`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
const updated = await devopsApi.updateMemberStatus(member.id, {
|
||||
status: nextStatus,
|
||||
})
|
||||
@@ -336,12 +356,35 @@ async function deleteMember(member: MemberSummary) {
|
||||
}
|
||||
|
||||
async function markMessageHandled(message: PlatformMessageSummary) {
|
||||
const confirmed = await confirmMemberAction(
|
||||
'确认处理消息',
|
||||
`将密码重置消息“${message.title}”标记为已处理。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
await devopsApi.markMessageHandled(message.id)
|
||||
await loadMessages()
|
||||
await loadMembers()
|
||||
ElMessage.success('消息已处理')
|
||||
}
|
||||
|
||||
async function confirmMemberAction(title: string, message: string): Promise<boolean> {
|
||||
try {
|
||||
await ElMessageBox.confirm(message, title, {
|
||||
confirmButtonText: '确认执行',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
autofocus: false,
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMembers() {
|
||||
members.value = await devopsApi.getMembers()
|
||||
}
|
||||
|
||||
+106
-26
@@ -3,10 +3,10 @@
|
||||
* 发布中心负责创建发布单、展示真实 Gitea ref 摘要并同步 Jenkins 状态。
|
||||
*/
|
||||
import { CircleClose, Promotion, RefreshRight, VideoPause } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import BpmnStatusViewer from '../components/BpmnStatusViewer.vue'
|
||||
import PipelineStatusViewer from '../components/PipelineStatusViewer.vue'
|
||||
import RunExecutionDetails from '../components/RunExecutionDetails.vue'
|
||||
import StatusPill from '../components/StatusPill.vue'
|
||||
import { isDevopsApiError } from '../services/http'
|
||||
@@ -333,8 +333,16 @@ const preflightSummary = computed(() => {
|
||||
return '关键检查均已通过'
|
||||
})
|
||||
|
||||
const contextRuns = computed(() => {
|
||||
return platform.runs.filter((run) => matchesReleaseContext(run))
|
||||
})
|
||||
|
||||
const shownRun = computed(() => {
|
||||
return activeRun.value || platform.runs[0]
|
||||
if (activeRun.value && matchesReleaseContext(activeRun.value)) {
|
||||
return activeRun.value
|
||||
}
|
||||
|
||||
return contextRuns.value[0]
|
||||
})
|
||||
|
||||
const canSyncJenkins = computed(() => {
|
||||
@@ -357,6 +365,20 @@ const canRetryShownRun = computed(
|
||||
)
|
||||
|
||||
async function triggerRelease() {
|
||||
if (!canTrigger.value) {
|
||||
ElMessage.warning(triggerDisabledReason.value || '发布参数不完整')
|
||||
return
|
||||
}
|
||||
|
||||
const confirmed = await confirmReleaseAction(
|
||||
form.environment === 'production' ? '确认触发生产发布' : '确认触发发布',
|
||||
`即将发布 ${selectedProject.value?.name || form.projectKey} / ${form.environment} / ${form.ref}。提交后会创建后端发布单并进入 Jenkins 执行链路。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const run = await platform.createDeployRun({
|
||||
@@ -444,12 +466,22 @@ async function cancelRelease() {
|
||||
return
|
||||
}
|
||||
|
||||
const run = shownRun.value
|
||||
const confirmed = await confirmReleaseAction(
|
||||
'确认取消发布单',
|
||||
`将取消 ${run.projectName} / ${run.environment} 的发布单 ${run.id}。该操作会写入审计日志。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
const runId = shownRun.value.id
|
||||
const runId = run.id
|
||||
try {
|
||||
const run = await platform.cancelDeployRun(shownRun.value)
|
||||
activeRun.value = run
|
||||
localStatus.value = run.status
|
||||
const canceled = await platform.cancelDeployRun(run)
|
||||
activeRun.value = canceled
|
||||
localStatus.value = canceled.status
|
||||
ElMessage.success('发布单已取消')
|
||||
} catch (error) {
|
||||
await handleReleaseActionError(error, '发布单取消失败', runId)
|
||||
@@ -463,12 +495,22 @@ async function retryRelease() {
|
||||
return
|
||||
}
|
||||
|
||||
const run = shownRun.value
|
||||
const confirmed = await confirmReleaseAction(
|
||||
'确认重试发布单',
|
||||
`将基于 ${run.projectName} / ${run.environment} 的发布单 ${run.id} 发起重试。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
const runId = shownRun.value.id
|
||||
const runId = run.id
|
||||
try {
|
||||
const run = await platform.retryDeployRun(shownRun.value)
|
||||
activeRun.value = run
|
||||
localStatus.value = run.status
|
||||
const retried = await platform.retryDeployRun(run)
|
||||
activeRun.value = retried
|
||||
localStatus.value = retried.status
|
||||
ElMessage.success('已提交重试发布单')
|
||||
} catch (error) {
|
||||
await handleReleaseActionError(error, '发布单重试失败', runId)
|
||||
@@ -482,12 +524,22 @@ async function syncJenkinsRun() {
|
||||
return
|
||||
}
|
||||
|
||||
const run = shownRun.value
|
||||
const confirmed = await confirmReleaseAction(
|
||||
'确认同步 Jenkins 状态',
|
||||
`将从后端同步发布单 ${run.id} 的 Jenkins queue/build 状态。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
const runId = shownRun.value.id
|
||||
const runId = run.id
|
||||
try {
|
||||
const run = await platform.syncJenkinsRun(shownRun.value)
|
||||
activeRun.value = run
|
||||
localStatus.value = run.status
|
||||
const synced = await platform.syncJenkinsRun(run)
|
||||
activeRun.value = synced
|
||||
localStatus.value = synced.status
|
||||
ElMessage.success('已同步 Jenkins 状态')
|
||||
} catch (error) {
|
||||
await handleReleaseActionError(error, 'Jenkins 状态同步失败', runId)
|
||||
@@ -527,6 +579,24 @@ function queryProjectKey(): string {
|
||||
return typeof route.query.projectKey === 'string' ? route.query.projectKey : ''
|
||||
}
|
||||
|
||||
function matchesReleaseContext(run: ReleaseRun): boolean {
|
||||
return run.projectKey === form.projectKey && run.environment === form.environment
|
||||
}
|
||||
|
||||
async function confirmReleaseAction(title: string, message: string): Promise<boolean> {
|
||||
try {
|
||||
await ElMessageBox.confirm(message, title, {
|
||||
confirmButtonText: '确认执行',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
autofocus: false,
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReleaseActionError(
|
||||
error: unknown,
|
||||
fallbackMessage: string,
|
||||
@@ -536,7 +606,7 @@ async function handleReleaseActionError(
|
||||
const refreshedRuns = await platform.refreshRuns()
|
||||
const refreshedRun = runId
|
||||
? refreshedRuns.find((run) => run.id === runId)
|
||||
: refreshedRuns[0]
|
||||
: refreshedRuns.find((run) => matchesReleaseContext(run))
|
||||
|
||||
if (refreshedRun) {
|
||||
activeRun.value = refreshedRun
|
||||
@@ -805,20 +875,23 @@ watch(
|
||||
<div class="panel-body" v-if="shownRun">
|
||||
<RunExecutionDetails :run="shownRun" />
|
||||
</div>
|
||||
<div v-else class="panel-body release-empty-state">
|
||||
<ElEmpty description="当前项目和环境暂无发布单" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel bpmn-status-panel">
|
||||
<section class="panel pipeline-status-panel">
|
||||
<div class="panel-header">
|
||||
<h2>BPMN 流程状态</h2>
|
||||
<span>step.bpmnNodeId 与 BPMN XML 节点 id 对齐后高亮</span>
|
||||
<h2>发布步骤轨迹</h2>
|
||||
<span>以后端 run steps 为准展示节点状态、输出和 Jenkins 日志</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<BpmnStatusViewer
|
||||
v-if="platform.releaseProcessXml && shownRun"
|
||||
:xml="platform.releaseProcessXml"
|
||||
<PipelineStatusViewer
|
||||
v-if="shownRun"
|
||||
:run="shownRun"
|
||||
:steps="shownRun.steps"
|
||||
/>
|
||||
<ElEmpty v-else description="暂无可查看的发布步骤" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -835,11 +908,11 @@ watch(
|
||||
|
||||
.release-params-panel,
|
||||
.current-run-panel,
|
||||
.bpmn-status-panel {
|
||||
.pipeline-status-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bpmn-status-panel {
|
||||
.pipeline-status-panel {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@@ -858,10 +931,17 @@ watch(
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.bpmn-status-panel .panel-body {
|
||||
.pipeline-status-panel .panel-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.release-empty-state {
|
||||
display: flex;
|
||||
min-height: 180px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.current-run-panel :deep(.run-execution-details) {
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -1152,7 +1232,7 @@ watch(
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bpmn-status-panel {
|
||||
.pipeline-status-panel {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
|
||||
+76
-16
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 运行记录页展示历史发布单、Jenkins 同步入口和后端步骤流转图。
|
||||
* 运行记录页展示历史发布单、Jenkins 同步入口和后端步骤轨迹。
|
||||
*/
|
||||
import { CircleClose, Download, RefreshRight, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import BpmnStatusViewer from '../components/BpmnStatusViewer.vue'
|
||||
import PipelineStatusViewer from '../components/PipelineStatusViewer.vue'
|
||||
import RunExecutionDetails from '../components/RunExecutionDetails.vue'
|
||||
import StatusPill from '../components/StatusPill.vue'
|
||||
import { isDevopsApiError } from '../services/http'
|
||||
@@ -24,6 +24,7 @@ const session = useSessionStore()
|
||||
const route = useRoute()
|
||||
const selectedRunId = ref(platform.runs[0]?.id || '')
|
||||
const syncingAll = ref(false)
|
||||
const selectedActionSubmitting = ref(false)
|
||||
const importingJenkins = ref(false)
|
||||
const lastJenkinsImportSummary = ref<JenkinsImportSummary | null>(null)
|
||||
const filterProjectKey = ref(queryProjectKey())
|
||||
@@ -213,12 +214,25 @@ async function syncSelectedRun() {
|
||||
return
|
||||
}
|
||||
|
||||
const run = selectedRun.value
|
||||
const confirmed = await confirmRunAction(
|
||||
'确认同步 Jenkins 状态',
|
||||
`将同步发布单 ${run.id} 的 Jenkins queue/build 状态。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedActionSubmitting.value = true
|
||||
try {
|
||||
const run = await platform.syncJenkinsRun(selectedRun.value)
|
||||
selectedRunId.value = run.id
|
||||
const synced = await platform.syncJenkinsRun(run)
|
||||
selectedRunId.value = synced.id
|
||||
ElMessage.success('已同步 Jenkins 状态')
|
||||
} catch (error) {
|
||||
await handleRunActionError(error, 'Jenkins 状态同步失败')
|
||||
} finally {
|
||||
selectedActionSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,16 +241,38 @@ async function retrySelectedRun() {
|
||||
return
|
||||
}
|
||||
|
||||
const run = selectedRun.value
|
||||
const confirmed = await confirmRunAction(
|
||||
'确认重试发布单',
|
||||
`将基于 ${run.projectName} / ${run.environment} 的发布单 ${run.id} 发起重试。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedActionSubmitting.value = true
|
||||
try {
|
||||
const run = await platform.retryDeployRun(selectedRun.value)
|
||||
selectedRunId.value = run.id
|
||||
const retried = await platform.retryDeployRun(run)
|
||||
selectedRunId.value = retried.id
|
||||
ElMessage.success('已提交重试发布单')
|
||||
} catch (error) {
|
||||
await handleRunActionError(error, '发布单重试失败')
|
||||
} finally {
|
||||
selectedActionSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function syncAllJenkinsRuns() {
|
||||
const confirmed = await confirmRunAction(
|
||||
'确认批量同步 Jenkins',
|
||||
'将同步所有当前可同步的 Jenkins run 状态。该操作可能产生多条后端请求和审计记录。',
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
syncingAll.value = true
|
||||
try {
|
||||
const result = await platform.syncJenkinsRuns()
|
||||
@@ -260,6 +296,15 @@ async function syncAllJenkinsRuns() {
|
||||
}
|
||||
|
||||
async function importJenkinsBuilds() {
|
||||
const confirmed = await confirmRunAction(
|
||||
'确认导入 Jenkins 直构',
|
||||
'将扫描已纳管 Jenkins Job 并导入平台外直接触发的构建记录。',
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
importingJenkins.value = true
|
||||
try {
|
||||
const result = await platform.importJenkinsBuilds()
|
||||
@@ -291,6 +336,20 @@ async function importJenkinsBuilds() {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRunAction(title: string, message: string): Promise<boolean> {
|
||||
try {
|
||||
await ElMessageBox.confirm(message, title, {
|
||||
confirmButtonText: '确认执行',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
autofocus: false,
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRunActionError(error: unknown, fallbackMessage: string) {
|
||||
if (isDevopsApiError(error) && error.code === 'CONFLICT') {
|
||||
await platform.refreshRuns()
|
||||
@@ -367,13 +426,13 @@ function skippedReasonLabel(reason: string): string {
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>运行记录</h1>
|
||||
<p>查看发布 run、Jenkins 构建链接、耗时、失败节点和流程轨迹。</p>
|
||||
<p>查看发布 run、Jenkins 构建链接、耗时、失败节点和步骤轨迹。</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<ElButton
|
||||
:icon="Download"
|
||||
:loading="importingJenkins"
|
||||
:disabled="!canImportJenkinsBuilds"
|
||||
:disabled="!canImportJenkinsBuilds || syncingAll"
|
||||
@click="importJenkinsBuilds"
|
||||
>
|
||||
导入 Jenkins 直构
|
||||
@@ -381,7 +440,7 @@ function skippedReasonLabel(reason: string): string {
|
||||
<ElButton
|
||||
:icon="RefreshRight"
|
||||
:loading="syncingAll"
|
||||
:disabled="!canSyncAnyRun"
|
||||
:disabled="!canSyncAnyRun || importingJenkins"
|
||||
@click="syncAllJenkinsRuns"
|
||||
>
|
||||
同步全部 Jenkins
|
||||
@@ -531,7 +590,8 @@ function skippedReasonLabel(reason: string): string {
|
||||
<ElButton
|
||||
size="small"
|
||||
:icon="RefreshRight"
|
||||
:disabled="!canSyncSelectedRun"
|
||||
:disabled="!canSyncSelectedRun || selectedActionSubmitting"
|
||||
:loading="selectedActionSubmitting && canSyncSelectedRun"
|
||||
@click="syncSelectedRun"
|
||||
>
|
||||
同步 Jenkins
|
||||
@@ -539,7 +599,8 @@ function skippedReasonLabel(reason: string): string {
|
||||
<ElButton
|
||||
size="small"
|
||||
:icon="CircleClose"
|
||||
:disabled="!canRetrySelectedRun"
|
||||
:disabled="!canRetrySelectedRun || selectedActionSubmitting"
|
||||
:loading="selectedActionSubmitting && canRetrySelectedRun"
|
||||
@click="retrySelectedRun"
|
||||
>
|
||||
重试
|
||||
@@ -560,12 +621,11 @@ function skippedReasonLabel(reason: string): string {
|
||||
|
||||
<section class="panel" v-if="selectedRun">
|
||||
<div class="panel-header">
|
||||
<h2>{{ selectedRun.id }} 流程轨迹</h2>
|
||||
<h2>{{ selectedRun.id }} 发布步骤轨迹</h2>
|
||||
<StatusPill :state="selectedRun.status" />
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<BpmnStatusViewer
|
||||
:xml="platform.releaseProcessXml"
|
||||
<PipelineStatusViewer
|
||||
:run="selectedRun"
|
||||
:steps="selectedRun.steps"
|
||||
/>
|
||||
@@ -578,7 +638,7 @@ function skippedReasonLabel(reason: string): string {
|
||||
</div>
|
||||
<div class="panel-body run-empty-state">
|
||||
<strong>当前筛选没有可查看的 Run</strong>
|
||||
<span>调整项目、环境、状态或搜索条件后,再查看对应的执行详情与流程轨迹。</span>
|
||||
<span>调整项目、环境、状态或搜索条件后,再查看对应的执行详情与步骤轨迹。</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 系统配置页展示集成变量说明、配置边界和审计日志脱敏 JSON。
|
||||
*/
|
||||
import { Connection, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import AuditJsonCell from '../components/AuditJsonCell.vue'
|
||||
import StatusPill from '../components/StatusPill.vue'
|
||||
@@ -385,6 +385,15 @@ async function saveAgentConfig() {
|
||||
return
|
||||
}
|
||||
|
||||
const confirmed = await confirmSettingsAction(
|
||||
'确认保存 Agent 模型配置',
|
||||
'将把 Base URL、模型名称和可选 API Key 提交给后端保存;前端不会持久化密钥。',
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
agentConfigLoading.value = true
|
||||
|
||||
try {
|
||||
@@ -410,6 +419,15 @@ async function testAgentConfig() {
|
||||
return
|
||||
}
|
||||
|
||||
const confirmed = await confirmSettingsAction(
|
||||
'确认测试 Agent 连接',
|
||||
'后端将使用已保存的模型配置发起一次真实连通性测试。',
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
agentConfigTesting.value = true
|
||||
|
||||
try {
|
||||
@@ -432,6 +450,15 @@ async function refreshNotificationOutbox() {
|
||||
}
|
||||
|
||||
async function retryNotificationOutbox(record: NotificationOutboxMessage) {
|
||||
const confirmed = await confirmSettingsAction(
|
||||
'确认重发通知',
|
||||
`将重发通知 outbox ${record.id},渠道 ${record.channel},模板 ${record.template}。`,
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
setRetryingOutbox(record.id, true)
|
||||
|
||||
try {
|
||||
@@ -445,6 +472,20 @@ async function retryNotificationOutbox(record: NotificationOutboxMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSettingsAction(title: string, message: string): Promise<boolean> {
|
||||
try {
|
||||
await ElMessageBox.confirm(message, title, {
|
||||
confirmButtonText: '确认执行',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
autofocus: false,
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function applyAgentConfig(config: AgentConfigSummary) {
|
||||
agentConfig.value = config
|
||||
agentConfigForm.baseURL = config.baseURL
|
||||
@@ -607,12 +648,18 @@ function resetAuditFilters() {
|
||||
/>
|
||||
</ElFormItem>
|
||||
<div class="toolbar">
|
||||
<ElButton type="primary" :loading="agentConfigLoading" @click="saveAgentConfig">
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="agentConfigLoading"
|
||||
:disabled="agentConfigTesting"
|
||||
@click="saveAgentConfig"
|
||||
>
|
||||
保存配置
|
||||
</ElButton>
|
||||
<ElButton
|
||||
:icon="Connection"
|
||||
:loading="agentConfigTesting"
|
||||
:disabled="agentConfigLoading"
|
||||
@click="testAgentConfig"
|
||||
>
|
||||
测试连接
|
||||
|
||||
@@ -7,6 +7,31 @@ import vue from '@vitejs/plugin-vue'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
groups: [
|
||||
{
|
||||
name: 'element-plus-vendor',
|
||||
test: /node_modules[\\/](?:\.pnpm[\\/].*?[\\/]node_modules[\\/])?(?:element-plus|@element-plus|@vueuse)[\\/]/,
|
||||
priority: 30,
|
||||
},
|
||||
{
|
||||
name: 'vue-vendor',
|
||||
test: /node_modules[\\/](?:\.pnpm[\\/].*?[\\/]node_modules[\\/])?(?:vue|@vue|vue-router|pinia)[\\/]/,
|
||||
priority: 20,
|
||||
},
|
||||
{
|
||||
name: 'vendor',
|
||||
test: /node_modules[\\/]/,
|
||||
priority: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 4301,
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user