Pipeline.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <template>
  2. <ElTabs class="tabs" v-model="activeTab">
  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) => timeFormatter(row, 'startTime')"
  27. ></ElTableColumn>
  28. <ElTableColumn
  29. prop="result"
  30. label="执行状态"
  31. align="center"
  32. width="100"
  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>
  50. <ElTag type="warning">未知状态</ElTag>
  51. </template>
  52. </template>
  53. </ElTableColumn>
  54. <ElTableColumn
  55. label="详情"
  56. align="center"
  57. >
  58. <template #default="scope">
  59. <ElButton
  60. @click="handleClick('detail', scope.row.historyId)"
  61. type="text"
  62. size="small"
  63. :disabled="scope.row.result==='执行中'"
  64. >最近执行详情</ElButton>
  65. <ElButton @click="handleClick('history', scope.row.id)" type="text" size="small">历史</ElButton>
  66. </template>
  67. </ElTableColumn>
  68. <ElTableColumn
  69. align="center"
  70. >
  71. <template #header>
  72. <span>操作</span>
  73. <ElButton @click="handleClick('create')" type="text" class="add-button">新建</ElButton>
  74. </template>
  75. <template #default="scope">
  76. <ElButton @click="handleClick('trigger', scope.row.id)" type="text" size="small">触发</ElButton>
  77. <ElButton @click="handleClick('edit', scope.row.id)" type="text" size="small">编辑</ElButton>
  78. <ElButton @click="handleClick('delete', scope.row.id)" type="text" size="small">删除</ElButton>
  79. </template>
  80. </ElTableColumn>
  81. </ElTable>
  82. <ElDialog v-model="showPipelineDetailModal" destroy-on-close>
  83. <template #title>历史执行结果详情</template>
  84. <pre class="pipeline-detail">{{detail}}</pre>
  85. </ElDialog>
  86. <ElDialog v-model="showPipelineHistoryModal" destroy-on-close>
  87. <template #title>流水线构建历史</template>
  88. <ElTable
  89. :data="pipelineHistory"
  90. :default-sort="{prop: 'id', order: 'descending'}"
  91. height="400"
  92. >
  93. <ElTableColumn
  94. prop="id"
  95. label="构建ID"
  96. align="center"
  97. width="100"
  98. ></ElTableColumn>
  99. <ElTableColumn
  100. prop="pipelineId"
  101. label="流水线ID"
  102. align="center"
  103. width="100"
  104. ></ElTableColumn>
  105. <ElTableColumn
  106. prop="username"
  107. label="触发人"
  108. align="center"
  109. width="180"
  110. ></ElTableColumn>
  111. <ElTableColumn
  112. label="执行时间"
  113. align="center"
  114. :formatter="(row) => timeFormatter(row, 'startTime')"
  115. ></ElTableColumn>
  116. <ElTableColumn>
  117. <template #default="scope">
  118. <ElButton @click="handleClick('detail', scope.row.id)" type="text" size="small">查看详情</ElButton>
  119. </template>
  120. </ElTableColumn>
  121. </ElTable>
  122. </ElDialog>
  123. <ElDialog
  124. v-model="showPipelineEditModal"
  125. destroy-on-close
  126. >
  127. <template #title>
  128. <span>编辑流水线配置</span>
  129. </template>
  130. <ElSteps :active="activeStep">
  131. <ElStep
  132. v-for="(config, index) in pipelineConfig"
  133. :key="index"
  134. :title="getStage(config.name).title"
  135. :description="getStage(config.name).description"
  136. >
  137. </ElStep>
  138. </ElSteps>
  139. <div class="edit-pipeline-form" v-for="(config, index) in pipelineConfig" :key="index" v-show="activeStep === index">
  140. <ElForm label-width="160px">
  141. <div style="margin: 20px 0 20px 160px;">
  142. <ElCheckbox
  143. :disabled="!getStage(config.name).skippable"
  144. v-model="config.active"
  145. >启用该阶段</ElCheckbox>
  146. </div>
  147. <ElFormItem v-for="(val, key) in config.configs" :key="key" :label="key">
  148. <ElInput
  149. :disabled="!getStage(config.name).configs.find((config) => config.name === key).editable"
  150. v-model="config.configs[key]"
  151. ></ElInput>
  152. </ElFormItem>
  153. </ElForm>
  154. </div>
  155. <template #footer>
  156. <ElButton :disabled="activeStep == 0" @click="() => activeStep -= 1">上一步</ElButton>
  157. <ElButton :disabled="isLastStep" @click="() => activeStep += 1">下一步</ElButton>
  158. <ElButton type="primary" :disabled="!isLastStep" @click="handlePipelineUpdateConfirm">确定</ElButton>
  159. </template>
  160. </ElDialog>
  161. <ElDialog
  162. v-model="showPipelineCreateModal"
  163. :before-close="(done) => {
  164. $refs['createPipelineForm'].resetFields();
  165. done();
  166. }"
  167. destroy-on-close
  168. >
  169. <template #title>
  170. <span>创建流水线</span>
  171. <span class="button-group">
  172. <ElButton type="text">模板详解</ElButton>
  173. </span>
  174. </template>
  175. <ElForm
  176. :model="createPipelineForm"
  177. ref="createPipelineForm"
  178. label-position="top"
  179. label-suffix=":"
  180. class="create-pipeline-form"
  181. >
  182. <ElFormItem label="名称" prop="name" required>
  183. <ElInput v-model="createPipelineForm.name" placeholder="请输入流水线名称"></ElInput>
  184. </ElFormItem>
  185. <ElFormItem label="模板" prop="template" required>
  186. <ElSelect v-model="createPipelineForm.template" placeholder="请选择模板">
  187. <ElOption
  188. v-for="(template, index) in templates"
  189. :key="index" :label="template"
  190. :value="template"
  191. ></ElOption>
  192. </ElSelect>
  193. </ElFormItem>
  194. </ElForm>
  195. <template #footer>
  196. <span class="dialog-footer">
  197. <el-button type="primary" @click="handlePipelineCreateConfirm">确 定</el-button>
  198. </span>
  199. </template>
  200. </ElDialog>
  201. </ElTabPane>
  202. <ElTabPane label="部署产物" name="production">
  203. <ElTable :data="productions" width="100%" :default-sort="{prop: 'id', order: 'descending'}">
  204. <ElTableColumn
  205. prop="id"
  206. label="ID"
  207. align="center"
  208. width="60"
  209. ></ElTableColumn>
  210. <ElTableColumn
  211. prop="name"
  212. label="名称"
  213. align="center"
  214. ></ElTableColumn>
  215. <ElTableColumn
  216. prop="pipelineId"
  217. label="流水线ID"
  218. align="center"
  219. width="100"
  220. ></ElTableColumn>
  221. <ElTableColumn
  222. label="部署时间"
  223. align="center"
  224. :formatter="(row) => timeFormatter(row, 'deployedTime')"
  225. ></ElTableColumn>
  226. <ElTableColumn
  227. label="URL"
  228. align="center"
  229. >
  230. <template #default="scope">
  231. <ElButton type="text" @click="open(scope.row.accessUrl, '_blank')">
  232. {{scope.row.accessUrl}}
  233. </ElButton>
  234. </template>
  235. </ElTableColumn>
  236. <ElTableColumn
  237. label="运行状态"
  238. align="center"
  239. >
  240. <template #default="scope">
  241. <template v-if="scope.row.status === '正常'">
  242. <ElTag type="success">{{scope.row.status}}</ElTag>
  243. </template>
  244. <template v-else>
  245. <ElTag type="danger">{{scope.row.status}}</ElTag>
  246. </template>
  247. </template>
  248. </ElTableColumn>
  249. <ElTableColumn>
  250. <template #default="scope">
  251. <ElButton type="text" @click="deleteProduction(scope.row.id)">
  252. <i class="el-icon-delete"></i>
  253. </ElButton>
  254. </template>
  255. </ElTableColumn>
  256. </ElTable>
  257. </ElTabPane>
  258. </ElTabs>
  259. </template>
  260. <script>
  261. import { PipelineAPI } from '@/api';
  262. export default {
  263. data() {
  264. return {
  265. activeTab: 'pipeline',
  266. pipelines: [],
  267. productions: [],
  268. showPipelineCreateModal: false,
  269. showPipelineEditModal: false,
  270. showPipelineHistoryModal: false,
  271. showPipelineDetailModal: false,
  272. detail: '',
  273. pipelineHistory: [],
  274. pipelineConfig: '',
  275. jsonValid: true,
  276. editingPipelineId: -1,
  277. templates: [],
  278. createPipelineForm: {
  279. name: '',
  280. template: '',
  281. },
  282. stages: [],
  283. activeStep: 0,
  284. };
  285. },
  286. computed: {
  287. isLastStep() {
  288. return this.activeStep === this.stages.length - 1;
  289. },
  290. stageConfigsBase() {
  291. return this.stages.map((stage) => ({
  292. name: stage.name,
  293. active: true,
  294. configs: stage.configs.map(({ name, value, editable }) => ({
  295. [name]: value,
  296. editable,
  297. })).reduce((prev, curr) => Object.assign(prev, curr), {}),
  298. }));
  299. },
  300. stageConfigs() {
  301. return this.stageConfigsBase;
  302. },
  303. },
  304. async mounted() {
  305. this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
  306. this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
  307. this.templates = await PipelineAPI.getPipelineTemplates();
  308. this.stages = await PipelineAPI.getStages();
  309. },
  310. methods: {
  311. timeFormatter: (row, property) => `${row[property].year}/${row[property].month}/${row[property].day} `
  312. + `${row[property].hours}:${row[property].minutes}:${row[property].seconds}`,
  313. async handleClick(action, id) {
  314. switch (action) {
  315. case 'create': {
  316. this.showPipelineCreateModal = true;
  317. break;
  318. }
  319. case 'trigger': {
  320. await PipelineAPI.triggerDeployment({
  321. projectId: this.$store.state.workspace.currentProjectId,
  322. pipelineId: id,
  323. });
  324. this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
  325. break;
  326. }
  327. case 'detail': {
  328. this.showPipelineDetailModal = true;
  329. this.detail = await PipelineAPI.getPipelineHistoryDetail(id);
  330. break;
  331. }
  332. case 'history': {
  333. this.showPipelineHistoryModal = true;
  334. this.pipelineHistory = await PipelineAPI.getPipelineHistory({
  335. projectId: this.$store.state.workspace.currentProjectId,
  336. pipelineId: id,
  337. });
  338. break;
  339. }
  340. case 'edit': {
  341. this.showPipelineEditModal = true;
  342. this.editingPipelineId = id;
  343. const config = (await PipelineAPI.getPipeline({
  344. projectId: this.$store.state.workspace.currentProjectId,
  345. pipelineId: id,
  346. })).configJson;
  347. this.pipelineConfig = JSON.parse(config || '{}');
  348. break;
  349. }
  350. case 'delete': {
  351. PipelineAPI.deletePipeline({
  352. projectId: this.$store.state.workspace.currentProjectId,
  353. pipelineId: id,
  354. });
  355. break;
  356. }
  357. default: {
  358. console.warn('Unknown type of action.');
  359. }
  360. }
  361. },
  362. async handlePipelineUpdateConfirm() {
  363. await PipelineAPI.updatePipeline({
  364. projectId: this.$store.state.workspace.currentProjectId,
  365. pipelineId: this.editingPipelineId,
  366. configJson: JSON.stringify(this.pipelineConfig),
  367. });
  368. this.showPipelineEditModal = false;
  369. },
  370. async handlePipelineCreateConfirm() {
  371. this.$refs.createPipelineForm.validate(async (valid) => {
  372. if (valid) {
  373. await PipelineAPI.createPipeline({
  374. projectId: this.$store.state.workspace.currentProjectId,
  375. pipelineId: this.editingPipelineId,
  376. name: this.createPipelineForm.name,
  377. templateName: this.createPipelineForm.template,
  378. });
  379. this.showPipelineCreateModal = false;
  380. }
  381. });
  382. },
  383. open(url, target) {
  384. window.open(url, target);
  385. },
  386. async deleteProduction(id) {
  387. await PipelineAPI.deleteInstance(id);
  388. },
  389. getStage(stageName) {
  390. return this.stages.find((stage) => stage.name === stageName);
  391. },
  392. },
  393. };
  394. </script>
  395. <style scoped>
  396. .tabs {
  397. width: 80%;
  398. margin: auto auto;
  399. }
  400. .add-button {
  401. margin-left: 14px;
  402. }
  403. .pipeline-detail {
  404. width: 100%;
  405. word-break: break-word;
  406. white-space: pre-wrap;
  407. }
  408. .create-pipeline-form {
  409. width: 60%;
  410. margin: 0 auto;
  411. }
  412. .edit-pipeline-form {
  413. margin-top: 20px;
  414. }
  415. .button-group {
  416. margin-left: 40px;
  417. }
  418. </style>