Pipeline.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <template>
  2. <ElTabs class="tabs" v-model="activeTab" v-loading.lock="loading">
  3. <ElTabPane label="流水线" name="pipeline">
  4. <ElTable :data="pipelines" width="100%" :default-sort="{prop: 'id', order: 'descending'}">
  5. <ElTableColumn
  6. prop="id"
  7. label="ID"
  8. align="center"
  9. width="60"
  10. ></ElTableColumn>
  11. <ElTableColumn
  12. prop="name"
  13. label="名称"
  14. align="center"
  15. width="180"
  16. ></ElTableColumn>
  17. <ElTableColumn
  18. prop="username"
  19. label="最近执行人"
  20. align="center"
  21. width="180"
  22. ></ElTableColumn>
  23. <ElTableColumn
  24. label="最近执行时间"
  25. align="center"
  26. :formatter="(row) => formatter(row.startTime)"
  27. ></ElTableColumn>
  28. <ElTableColumn
  29. prop="result"
  30. label="执行状态"
  31. align="center"
  32. width="110"
  33. :filters="[{ text: '成功', value: '成功' }, { text: '失败', value: '失败' }]"
  34. :filter-method="(value, row) => {
  35. return row.result === value;
  36. }"
  37. filter-placement="bottom-end"
  38. >
  39. <template #default="scope">
  40. <template v-if="scope.row.result === '成功'">
  41. <ElTag type="success">{{scope.row.result}}</ElTag>
  42. </template>
  43. <template v-else-if="scope.row.result === '失败'">
  44. <ElTag type="danger">{{scope.row.result}}</ElTag>
  45. </template>
  46. <template v-else-if="scope.row.result === '执行中'">
  47. <ElTag>{{scope.row.result}}<i class="el-icon-loading"></i></ElTag>
  48. </template>
  49. <template v-else-if="scope.row.result === '无执行记录'">
  50. <ElTag>{{scope.row.result}}</ElTag>
  51. </template>
  52. <template v-else>
  53. <ElTag type="warning">未知状态</ElTag>
  54. </template>
  55. </template>
  56. </ElTableColumn>
  57. <ElTableColumn
  58. label="详情"
  59. align="center"
  60. >
  61. <template #default="scope">
  62. <ElButton
  63. @click="handleClick('detail', scope.row.recordId)"
  64. type="text"
  65. size="small"
  66. :disabled="scope.row.result==='执行中'"
  67. >最近执行详情</ElButton>
  68. <ElButton @click="handleClick('history', scope.row.id)" type="text" size="small">历史</ElButton>
  69. </template>
  70. </ElTableColumn>
  71. <ElTableColumn
  72. align="center"
  73. >
  74. <template #header>
  75. <span>操作</span>
  76. <ElButton @click="handleClick('create')" type="text" class="add-button">新建</ElButton>
  77. </template>
  78. <template #default="scope">
  79. <ElButton @click="handleClick('trigger', scope.row.id)" type="text" size="small">触发</ElButton>
  80. <ElButton @click="handleClick('edit', scope.row.id)" type="text" size="small">编辑</ElButton>
  81. <ElButton @click="handleClick('delete', scope.row.id)" type="text" size="small">删除</ElButton>
  82. </template>
  83. </ElTableColumn>
  84. </ElTable>
  85. <ElDialog v-model="showPipelineDetailModal" destroy-on-close>
  86. <template #title>历史执行结果详情</template>
  87. <pre class="log" v-loading="dialogLoading" >{{detail}}</pre>
  88. </ElDialog>
  89. <ElDialog v-model="showPipelineHistoryModal" width="60%" destroy-on-close>
  90. <template #title>流水线构建历史</template>
  91. <ElTable
  92. :data="pipelineHistory"
  93. :default-sort="{prop: 'id', order: 'descending'}"
  94. v-loading="dialogLoading"
  95. height="400"
  96. >
  97. <ElTableColumn
  98. prop="id"
  99. label="构建ID"
  100. align="center"
  101. width="100"
  102. ></ElTableColumn>
  103. <ElTableColumn
  104. prop="pipelineId"
  105. label="流水线ID"
  106. align="center"
  107. width="100"
  108. ></ElTableColumn>
  109. <ElTableColumn
  110. prop="username"
  111. label="触发人"
  112. align="center"
  113. width="180"
  114. ></ElTableColumn>
  115. <ElTableColumn
  116. label="执行时间"
  117. align="center"
  118. :formatter="(row) => formatter(row.startTime)"
  119. ></ElTableColumn>
  120. <ElTableColumn
  121. prop="result"
  122. label="执行状态"
  123. align="center"
  124. width="100"
  125. :filters="[{ text: '成功', value: '成功' }, { text: '失败', value: '失败' }]"
  126. :filter-method="(value, row) => {
  127. return row.result === value;
  128. }"
  129. filter-placement="bottom-end"
  130. >
  131. <template #default="scope">
  132. <template v-if="scope.row.result === '成功'">
  133. <ElTag type="success">{{scope.row.result}}</ElTag>
  134. </template>
  135. <template v-else-if="scope.row.result === '失败'">
  136. <ElTag type="danger">{{scope.row.result}}</ElTag>
  137. </template>
  138. <template v-else-if="scope.row.result === '执行中'">
  139. <ElTag>{{scope.row.result}}<i class="el-icon-loading"></i></ElTag>
  140. </template>
  141. <template v-else-if="scope.row.result === '无执行记录'">
  142. <ElTag>{{scope.row.result}}</ElTag>
  143. </template>
  144. <template v-else>
  145. <ElTag type="warning">未知状态</ElTag>
  146. </template>
  147. </template>
  148. </ElTableColumn>
  149. <ElTableColumn>
  150. <template #default="scope">
  151. <ElButton @click="handleClick('detail', scope.row.id)" type="text" size="small">查看详情</ElButton>
  152. </template>
  153. </ElTableColumn>
  154. </ElTable>
  155. </ElDialog>
  156. <ElDialog
  157. v-model="showPipelineEditModal"
  158. destroy-on-close
  159. >
  160. <template #title>
  161. <span>编辑流水线配置</span>
  162. </template>
  163. <ElSteps
  164. :active="activeStep"
  165. v-loading="dialogLoading"
  166. style="width: 80%;margin: 0 auto;"
  167. align-center>
  168. <ElStep
  169. v-for="(config, index) in pipelineConfig"
  170. :key="index"
  171. class="step"
  172. @click="() => activeStep = index"
  173. >
  174. <template #title>
  175. <span>{{getStage(config.name).title}}</span>
  176. <ElTooltip
  177. class="tooltip"
  178. effect="dark"
  179. :content="getStage(config.name).description"
  180. placement="right-end"
  181. >
  182. <i class="el-icon-question"></i>
  183. </ElTooltip>
  184. </template>
  185. </ElStep>
  186. </ElSteps>
  187. <div class="edit-pipeline-form" v-for="(config, index) in pipelineConfig" :key="index" v-show="activeStep === index">
  188. <ElForm label-width="160px">
  189. <div style="margin: 20px 0 20px 160px;">
  190. <ElCheckbox
  191. :disabled="!getStage(config.name).skippable"
  192. v-model="config.active"
  193. >启用该阶段</ElCheckbox>
  194. </div>
  195. <template v-for="(val, key) in config.configs" :key="key" placeholder="请选择">
  196. <ElFormItem :label="key">
  197. <ElSelect v-if="key === 'apiTestIds'" v-model="config.configs[key]" multiple >
  198. <el-option
  199. v-for="item in apiTestOptions"
  200. :key="item.testId"
  201. :label="item.testName"
  202. :value="item.testId">
  203. </el-option>
  204. </ElSelect>
  205. <ElInput v-else
  206. :disabled="!getStage(config.name).configs.find((config) => config.name === key).editable"
  207. v-model="config.configs[key]"
  208. ></ElInput>
  209. </ElFormItem>
  210. </template>
  211. </ElForm>
  212. </div>
  213. <template #footer>
  214. <ElButton :disabled="activeStep == 0" @click="() => activeStep -= 1">上一步</ElButton>
  215. <ElButton :disabled="isLastStep" @click="() => activeStep += 1">下一步</ElButton>
  216. <ElButton type="primary" @click="handlePipelineUpdateConfirm">确定</ElButton>
  217. </template>
  218. </ElDialog>
  219. <ElDialog
  220. v-model="showPipelineCreateModal"
  221. :before-close="(done) => {
  222. $refs['createPipelineForm'].resetFields();
  223. done();
  224. }"
  225. destroy-on-close
  226. >
  227. <template #title>
  228. <span>创建流水线</span>
  229. <span class="button-group">
  230. <ElButton type="text">模板详解</ElButton>
  231. </span>
  232. </template>
  233. <ElForm
  234. :model="createPipelineForm"
  235. ref="createPipelineForm"
  236. label-position="top"
  237. label-suffix=":"
  238. class="create-pipeline-form"
  239. v-loading="dialogLoading"
  240. >
  241. <ElFormItem label="名称" prop="name" required>
  242. <ElInput v-model="createPipelineForm.name" placeholder="请输入流水线名称"></ElInput>
  243. </ElFormItem>
  244. <ElFormItem label="模板" prop="template" required>
  245. <ElSelect v-model="createPipelineForm.template" placeholder="请选择模板">
  246. <ElOption
  247. v-for="(template, index) in templates"
  248. :key="index" :label="template"
  249. :value="template"
  250. ></ElOption>
  251. </ElSelect>
  252. </ElFormItem>
  253. </ElForm>
  254. <template #footer>
  255. <span class="dialog-footer">
  256. <el-button type="primary" v-loading="confirmLoading" @click="handlePipelineCreateConfirm">确 定</el-button>
  257. </span>
  258. </template>
  259. </ElDialog>
  260. </ElTabPane>
  261. <ElTabPane label="部署产物" name="production">
  262. <ElTable :data="productions" width="100%" :default-sort="{prop: 'id', order: 'descending'}">
  263. <ElTableColumn
  264. prop="id"
  265. label="ID"
  266. align="center"
  267. width="60"
  268. ></ElTableColumn>
  269. <ElTableColumn
  270. prop="name"
  271. label="名称"
  272. align="center"
  273. width="100"
  274. ></ElTableColumn>
  275. <ElTableColumn
  276. prop="pipelineId"
  277. label="流水线ID"
  278. align="center"
  279. width="60"
  280. ></ElTableColumn>
  281. <ElTableColumn
  282. label="部署时间"
  283. align="center"
  284. width="320"
  285. :formatter="(row) => formatter(row.deployedTime)"
  286. ></ElTableColumn>
  287. <ElTableColumn
  288. label="URL"
  289. align="center"
  290. >
  291. <template #default="scope">
  292. <ElTooltip effect="dark" :content="scope.row.accessUrl" placement="top-start">
  293. <ElButton type="text" @click="open(scope.row.accessUrl, '_blank')">
  294. {{scope.row.accessUrl}}
  295. </ElButton>
  296. </ElTooltip>
  297. </template>
  298. </ElTableColumn>
  299. <ElTableColumn
  300. label="运行状态"
  301. align="center"
  302. width="100"
  303. >
  304. <template #default="scope">
  305. <ElTag :type="scope.row.status.indexOf('正常') > -1 ? 'success' : 'danger'">{{scope.row.status}}</ElTag>
  306. </template>
  307. </ElTableColumn>
  308. <ElTableColumn
  309. label="运行状态"
  310. align="center"
  311. width="100"
  312. >
  313. <template #default="scope">
  314. <ElButton type="text" @click="showLog(scope.row.pipelineId)">查看日志</ElButton>
  315. </template>
  316. </ElTableColumn>
  317. <ElTableColumn width="60">
  318. <template #default="scope">
  319. <ElButton type="text" @click="deleteProduction(scope.row.pipelineId)">
  320. <i class="el-icon-delete"></i>
  321. </ElButton>
  322. </template>
  323. </ElTableColumn>
  324. </ElTable>
  325. <ElDialog v-model="showProductionLogModal" destroy-on-close>
  326. <template #title>
  327. 产物日志
  328. </template>
  329. <pre class="log" v-loading="dialogLoading" >{{currentLog}}</pre>
  330. <template #footer>
  331. <ElButton @click="refreshLog">刷新</ElButton>
  332. </template>
  333. </ElDialog>
  334. </ElTabPane>
  335. </ElTabs>
  336. </template>
  337. <script>
  338. import { APITestAPI, PipelineAPI } from '@/api';
  339. import { formatter } from '@/utils/time';
  340. export default {
  341. data() {
  342. return {
  343. loading: false,
  344. dialogLoading: false,
  345. confirmLoading: false,
  346. activeTab: 'pipeline',
  347. pipelines: [],
  348. productions: [],
  349. showPipelineCreateModal: false,
  350. showPipelineEditModal: false,
  351. showPipelineHistoryModal: false,
  352. showPipelineDetailModal: false,
  353. detail: '',
  354. pipelineHistory: [],
  355. pipelineConfig: '',
  356. jsonValid: true,
  357. editingPipelineId: -1,
  358. templates: [],
  359. createPipelineForm: {
  360. name: '',
  361. template: '',
  362. },
  363. stages: [],
  364. activeStep: 0,
  365. showProductionLogModal: false,
  366. currentLogId: -1,
  367. currentLog: '',
  368. apiTestOptions: [],
  369. pipelineConfigTemplate: '',
  370. };
  371. },
  372. computed: {
  373. isLastStep() {
  374. return this.activeStep === this.stages.length - 1;
  375. },
  376. stageConfigsBase() {
  377. return this.stages.map((stage) => ({
  378. name: stage.name,
  379. active: true,
  380. configs: stage.configs.map(({ name, value, editable }) => ({
  381. [name]: value,
  382. editable,
  383. })).reduce((prev, curr) => Object.assign(prev, curr), {}),
  384. }));
  385. },
  386. stageConfigs() {
  387. return this.stageConfigsBase;
  388. },
  389. },
  390. async mounted() {
  391. this.loading = true;
  392. this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
  393. this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
  394. this.templates = await PipelineAPI.getPipelineTemplates();
  395. this.stages = await PipelineAPI.getStages();
  396. this.apiTestOptions = await APITestAPI.getAPITestInfosByProjectId(this.$store.state.workspace.currentProjectId);
  397. this.loading = false;
  398. },
  399. methods: {
  400. formatter,
  401. async handleClick(action, id) {
  402. switch (action) {
  403. case 'create': {
  404. this.showPipelineCreateModal = true;
  405. break;
  406. }
  407. case 'trigger': {
  408. this.loading = true;
  409. await PipelineAPI.triggerDeployment({
  410. projectId: this.$store.state.workspace.currentProjectId,
  411. pipelineId: id,
  412. });
  413. PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId)
  414. .then(
  415. (res) => {
  416. this.productions = res;
  417. },
  418. );
  419. this.loading = true;
  420. this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
  421. this.loading = false;
  422. break;
  423. }
  424. case 'detail': {
  425. this.showPipelineDetailModal = true;
  426. this.dialogLoading = true;
  427. this.detail = await PipelineAPI.getPipelineRecordDetail(id);
  428. this.dialogLoading = false;
  429. break;
  430. }
  431. case 'history': {
  432. this.showPipelineHistoryModal = true;
  433. this.dialogLoading = true;
  434. this.pipelineHistory = await PipelineAPI.getPipelineRecordList({
  435. projectId: this.$store.state.workspace.currentProjectId,
  436. pipelineId: id,
  437. });
  438. this.dialogLoading = false;
  439. break;
  440. }
  441. case 'edit': {
  442. this.showPipelineEditModal = true;
  443. this.dialogLoading = true;
  444. this.editingPipelineId = id;
  445. const config = (await PipelineAPI.getPipeline({
  446. projectId: this.$store.state.workspace.currentProjectId,
  447. pipelineId: id,
  448. })).configJson;
  449. this.pipelineConfig = JSON.parse(config || '{}');
  450. this.pipelineConfigTemplate = JSON.parse(config || '{}');
  451. this.pipelineConfig.forEach((item) => {
  452. const targetStage = this.stages.find((stage) => stage.name === item.name);
  453. if (targetStage) {
  454. targetStage.configs.forEach((configTemplate) => {
  455. if (!item.configs[configTemplate.name]) {
  456. item.configs[configTemplate.name] = this.parseConfigValue(configTemplate.value);
  457. }
  458. });
  459. }
  460. });
  461. this.dialogLoading = false;
  462. break;
  463. }
  464. case 'delete': {
  465. this.loading = true;
  466. PipelineAPI.deletePipeline({
  467. projectId: this.$store.state.workspace.currentProjectId,
  468. pipelineId: id,
  469. });
  470. this.loading = false;
  471. break;
  472. }
  473. default: {
  474. console.warn('Unknown type of action.');
  475. }
  476. }
  477. },
  478. async handlePipelineUpdateConfirm() {
  479. this.confirmLoading = true;
  480. this.pipelineConfigTemplate.forEach((item) => {
  481. const target = this.pipelineConfig.find((i) => i.name === item.name);
  482. Object.keys(item.configs).forEach((key) => {
  483. item.configs[key] = target.configs[key];
  484. });
  485. });
  486. await PipelineAPI.updatePipeline({
  487. projectId: this.$store.state.workspace.currentProjectId,
  488. pipelineId: this.editingPipelineId,
  489. configJson: JSON.stringify(this.pipelineConfigTemplate),
  490. });
  491. this.confirmLoading = false;
  492. this.showPipelineEditModal = false;
  493. this.loading = true;
  494. this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
  495. this.loading = false;
  496. },
  497. async handlePipelineCreateConfirm() {
  498. this.confirmLoading = true;
  499. this.$refs.createPipelineForm.validate(async (valid) => {
  500. if (valid) {
  501. await PipelineAPI.createPipeline({
  502. projectId: this.$store.state.workspace.currentProjectId,
  503. name: this.createPipelineForm.name,
  504. templateName: this.createPipelineForm.template,
  505. });
  506. this.showPipelineCreateModal = false;
  507. }
  508. });
  509. this.confirmLoading = false;
  510. this.loading = true;
  511. this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
  512. this.loading = false;
  513. },
  514. open(url, target) {
  515. window.open(url, target);
  516. },
  517. async deleteProduction(pipelineId) {
  518. this.loading = true;
  519. await PipelineAPI.deleteInstance({
  520. projectId: this.$store.state.workspace.currentProjectId,
  521. pipelineId,
  522. });
  523. this.loading = false;
  524. },
  525. getStage(stageName) {
  526. return this.stages.find((stage) => stage.name === stageName);
  527. },
  528. async showLog(pipelineId) {
  529. this.currentLogId = pipelineId;
  530. this.showProductionLogModal = true;
  531. this.dialogLoading = true;
  532. this.currentLog = await PipelineAPI.getDeploymentLog({
  533. projectId: this.$store.state.workspace.currentProjectId,
  534. pipelineId,
  535. });
  536. this.dialogLoading = false;
  537. },
  538. async refreshLog() {
  539. this.dialogLoading = true;
  540. this.currentLog = await PipelineAPI.getDeploymentLog({
  541. projectId: this.$store.state.workspace.currentProjectId,
  542. pipelineId: this.currentLogId,
  543. });
  544. this.dialogLoading = false;
  545. },
  546. parseConfigValue(str) {
  547. const project = this.$store.state.workspace.projectList.find(
  548. (item) => item.id === this.$store.state.workspace.currentProjectId,
  549. );
  550. const pipeline = this.pipelines.find((item) => item.id === this.editingPipelineId);
  551. return str.replaceAll('{projectName}', project.name).replaceAll('{projectId}', project.id)
  552. .replaceAll('{pipelineName}', pipeline.name);
  553. },
  554. },
  555. };
  556. </script>
  557. <style scoped>
  558. .tabs {
  559. width: 80%;
  560. margin: auto auto;
  561. }
  562. .add-button {
  563. margin-left: 14px;
  564. }
  565. .log {
  566. width: 100%;
  567. word-break: break-word;
  568. white-space: pre-wrap;
  569. }
  570. .create-pipeline-form {
  571. width: 60%;
  572. margin: 0 auto;
  573. }
  574. .edit-pipeline-form {
  575. margin-top: 20px;
  576. }
  577. .button-group {
  578. margin-left: 40px;
  579. }
  580. .step {
  581. cursor: pointer;
  582. }
  583. </style>