Pipeline.vue 20 KB

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