Ver código fonte

feat: 基本完成pipeline部分页面,部分API待进一步对接

Jinyao 5 anos atrás
pai
commit
d4fb5becfb

+ 2 - 0
mock/index.js

@@ -22,6 +22,8 @@ const prefix = '/api';
 
 app.use(`${prefix}/users`, require('./routers/user'));
 app.use(`${prefix}/project`, require('./routers/project'));
+app.use(`${prefix}/pipelines`, require('./routers/pipeline'));
+app.use(`${prefix}/deployments`, require('./routers/deployment'));
 
 app.listen(4000);
 

+ 45 - 0
mock/routers/deployment.js

@@ -0,0 +1,45 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* eslint-disable @typescript-eslint/no-var-requires */
+const router = require('express').Router();
+const Mock = require('mockjs');
+const wrapper = require('../wrapper');
+
+const { Random } = Mock;
+
+router.delete((_, res) => {
+  res.json(wrapper('success'));
+});
+
+router.post((_, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('/list', (req, res) => {
+  res.json(wrapper(
+    Mock.mock(
+      {
+        'data|10': [{
+          accessUrl: Random.url('http', 'production.seecoder.cn'),
+          'name|1': ['MySQL', 'Java', 'Node'],
+          status: '正常',
+          'id|+5': 101,
+          pipelineId: 100,
+          deployedTime: {
+            date: 0,
+            day: 1,
+            'hours|+1': 12,
+            'minutes|+1': 30,
+            month: 3,
+            nanos: 0,
+            'seconds|+1': 15,
+            time: 0,
+            timezoneOffset: 0,
+            year: 2021,
+          },
+        }],
+      },
+    ).data,
+  ));
+});
+
+module.exports = router;

+ 110 - 0
mock/routers/pipeline.js

@@ -0,0 +1,110 @@
+/* eslint-disable max-len */
+/* eslint-disable import/no-extraneous-dependencies */
+/* eslint-disable @typescript-eslint/no-var-requires */
+const router = require('express').Router();
+const Mock = require('mockjs');
+const wrapper = require('../wrapper');
+
+const { Random } = Mock;
+
+router.delete((req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get((req, res) => {
+  res.json(wrapper({
+    configJson: {
+      code: 0,
+      data: {
+        configJson: 'string',
+        id: 0,
+      },
+      msg: 'string',
+    },
+    id: req.params.pipelineId,
+  }));
+});
+
+router.post((req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.put('/config', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('/history/list', (req, res) => {
+  res.json(wrapper(
+    Mock.mock(
+      {
+        'data|10': [{
+          details: `2021-03-16 00:01:58.621  INFO 20584 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
+2021-03-16 00:01:58.622  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
+2021-03-16 00:01:58.634  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
+2021-03-16 00:01:58.641  INFO 20584 --- [           main] s.d.s.w.s.ApiListingReferenceScanner     : Scanning for api listing references
+2021-03-16 00:01:58.974  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
+2021-03-16 00:01:58.975  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
+2021-03-16 00:01:58.993  INFO 20584 --- [           main] s.devcloud.web.DevcloudWebApplication    : Started DevcloudWebApplication in 6.659 seconds (JVM running for 8.913)
+2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
+2021-03-16 00:12:39.222  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 11 ms`,
+          'id|+5': 101,
+          pipelineId: 100,
+          'result|1': ['成功', '失败', '执行中'],
+          startTime: {
+            date: 0,
+            day: 1,
+            'hours|+1': 12,
+            'minutes|+1': 30,
+            month: 3,
+            nanos: 0,
+            'seconds|+1': 15,
+            time: 0,
+            timezoneOffset: 0,
+            year: 2021,
+          },
+          username: 'hkshu12',
+        }],
+      },
+    ).data,
+  ));
+});
+
+router.get('/list', (req, res) => {
+  res.json(wrapper(
+    Mock.mock(
+      {
+        'data|10': [{
+          details: `2021-03-16 00:01:58.621  INFO 20584 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
+2021-03-16 00:01:58.622  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
+2021-03-16 00:01:58.634  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
+2021-03-16 00:01:58.641  INFO 20584 --- [           main] s.d.s.w.s.ApiListingReferenceScanner     : Scanning for api listing references
+2021-03-16 00:01:58.974  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
+2021-03-16 00:01:58.975  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
+2021-03-16 00:01:58.993  INFO 20584 --- [           main] s.devcloud.web.DevcloudWebApplication    : Started DevcloudWebApplication in 6.659 seconds (JVM running for 8.913)
+2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
+2021-03-16 00:12:39.222  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 11 ms`,
+          'id|+1': 100,
+          name: Random.cname(),
+          'result|1': ['成功', '失败', '执行中'],
+          startTime: {
+            date: 0,
+            day: 1,
+            'hours|+1': 12,
+            'minutes|+1': 30,
+            month: 3,
+            nanos: 0,
+            'seconds|+1': 15,
+            time: 0,
+            timezoneOffset: 0,
+            year: 2021,
+          },
+          username: 'hkshu12',
+        }],
+      },
+    ).data,
+  ));
+});
+
+module.exports = router;

+ 5 - 6
src/api/pipeline.ts

@@ -1,10 +1,10 @@
-import axios from 'axios';
+import axios from '@/utils/axios';
 
 import { PIPELINE_PREFIX, DEPLOYMENT_PREFIX } from './prefix';
 
 interface PipelinePayload {
   projectId: number;
-  pipeline: number;
+  pipelineId: number;
 }
 
 type CreatePipelinePayload = PipelinePayload & {
@@ -26,9 +26,8 @@ export const getPipeline = (pipeline: PipelinePayload) => axios.get(`${PIPELINE_
 
 export const createPipeline = (createPipelinePayload: CreatePipelinePayload) => axios.post(`${PIPELINE_PREFIX}`, createPipelinePayload);
 
-export const updatePipeline = (updatePipelinePayload: UpdatePipelinePayload) => axios.put(`${PIPELINE_PREFIX}/config`, {
-  params: updatePipelinePayload,
-});
+export const updatePipeline = (updatePipelinePayload: UpdatePipelinePayload) => axios.put(`${PIPELINE_PREFIX}/config`,
+  updatePipelinePayload);
 
 export const getPipelineHistory = (pipeline: PipelinePayload) => axios.get(`${PIPELINE_PREFIX}/history/list`, {
   params: pipeline,
@@ -50,7 +49,7 @@ export const triggerDeployment = (pipeline: PipelinePayload) => axios.post(`${DE
   params: pipeline,
 });
 
-export const getAllDeployment = (projectId: number) => axios.get(`${PIPELINE_PREFIX}/list`, {
+export const getDeploymentsByProjectId = (projectId: number) => axios.get(`${DEPLOYMENT_PREFIX}/list`, {
   params: {
     projectId,
   },

+ 1 - 1
src/components/SideBar.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-menu default-active="schedule" class="side-bar" collapse="true" router="true">
+  <el-menu default-active="schedule" class="side-bar" :collapse="true" :router="true">
     <el-menu-item index="schedule">
       <i class="icon" target="schedule"></i>
     </el-menu-item>

+ 12 - 0
src/element-plus.ts

@@ -9,11 +9,17 @@ import {
   ElLink,
   ElMain,
   ElMenu,
+  ElDialog,
+  ElIcon,
+  ElInput,
   ElSubmenu,
   ElMenuItem,
+  ElForm,
+  ElFormItem,
   ElRow,
   ElTable,
   ElTableColumn,
+  ElTag,
   ElPagination,
   ElButton,
   ElSelect,
@@ -29,14 +35,20 @@ export const Components = [
   ElHeader,
   ElMain,
   ElMenu,
+  ElDialog,
   ElLink,
   ElMenuItem,
   ElCarousel,
+  ElForm,
+  ElFormItem,
+  ElIcon,
+  ElInput,
   ElCarouselItem,
   ElDivider,
   ElCard,
   ElTable,
   ElTableColumn,
+  ElTag,
   ElPagination,
   ElButton,
   ElSelect,

+ 1 - 1
src/store/modules/user.ts

@@ -13,7 +13,7 @@ const DEFAULT: UserState = {
 const actions: ActionTree<UserState, RootState> = {
   async updateUserInfo({ commit }) {
     const res = (await UserAPI.getUserInfo()) as unknown as {
-      user_info: any;
+      user_info: UserState;
     };
     commit('updateUserInfo', res.user_info);
   },

+ 18 - 1
src/typings/vuex.d.ts

@@ -3,7 +3,24 @@ import { Store } from 'vuex';
 
 declare module '@vue/runtime-core' {
   // declare your own store states
-  import RootState from '@/store/typings';
+  interface State {
+    token: {
+      token: string;
+    };
+    user: {
+      email: string;
+      id: number;
+      phone: string;
+      role: Role;
+      username: string;
+    };
+    workspace: {
+      currentGroupId: GroupId;
+      currentProjectId: ProjectId;
+      groupList: Array<Group>;
+      projectList: Array<Project>;
+    };
+  }
 
   // provide typings for `this.$store`
   interface ComponentCustomProperties {

+ 15 - 0
src/utils/debounce.ts

@@ -0,0 +1,15 @@
+/* eslint-disable */
+const debounce = (fn: Function, delay: number) => {
+  let timer: NodeJS.Timeout | null = null;
+  return function () {
+    // @ts-ignore
+    const context = this;
+    const args = arguments;
+    if (timer) {
+      clearTimeout(timer);
+    }
+    timer = setTimeout(fn.apply(context, args), delay);
+  };
+};
+
+export default debounce;

Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
src/utils/jsonlint.js


+ 386 - 4
src/views/Project/Pipeline.vue

@@ -1,13 +1,395 @@
 <template>
-  project-pipeline
+  <ElTabs class="tabs" v-model="activeTab">
+    <ElTabPane label="流水线" name="pipeline">
+      <ElTable :data="pipelines" width="100%">
+        <ElTableColumn
+          prop="id"
+          label="ID"
+          align="center"
+          width="60"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="name"
+          label="名称"
+          align="center"
+          width="180"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="username"
+          label="最近执行人"
+          align="center"
+          width="180"
+        ></ElTableColumn>
+        <ElTableColumn
+          label="最近执行时间"
+          align="center"
+          :formatter="(row) => timeFormatter(row, 'startTime')"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="result"
+          label="执行状态"
+          align="center"
+          width="100"
+          :filters="[{ text: '成功', value: '成功' }, { text: '失败', value: '失败' }]"
+          :filter-method="(value, row) => {
+            return row.result === value;
+          }"
+          filter-placement="bottom-end"
+        >
+          <template #default="scope">
+            <template v-if="scope.row.result === '成功'">
+              <ElTag type="success">{{scope.row.result}}</ElTag>
+            </template>
+            <template v-else-if="scope.row.result === '失败'">
+              <ElTag type="danger">{{scope.row.result}}</ElTag>
+            </template>
+            <template v-else-if="scope.row.result === '执行中'">
+              <ElTag>{{scope.row.result}}<i class="el-icon-loading"></i></ElTag>
+            </template>
+            <template v-else>
+              <ElTag type="warning">未知状态</ElTag>
+            </template>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn
+          label="详情"
+          align="center"
+        >
+          <template #default="scope">
+            <ElButton
+              @click="handleClick('detail', scope.row.id)"
+              type="text"
+              size="small"
+              :disabled="scope.row.result==='执行中'"
+            >最近执行详情</ElButton>
+            <ElButton @click="handleClick('history', scope.row.id)" type="text" size="small">历史</ElButton>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn
+          align="center"
+        >
+          <template #header>
+            <span>操作</span>
+            <ElButton @click="handleClick('create')" type="text" class="add-button">新建</ElButton>
+          </template>
+          <template #default="scope">
+            <ElButton @click="handleClick('trigger', scope.row.id)" type="text" size="small">触发</ElButton>
+            <ElButton @click="handleClick('edit', scope.row.id)" type="text" size="small">编辑</ElButton>
+            <ElButton @click="handleClick('delete', scope.row.id)" type="text" size="small">删除</ElButton>
+          </template>
+        </ElTableColumn>
+      </ElTable>
+      <ElDialog v-model="showPipelineDetailModal" destroy-on-close>
+        <template #title>最近一次流水线构建的执行结果详情</template>
+        <pre class="pipeline-detail">{{detail}}</pre>
+      </ElDialog>
+      <ElDialog v-model="showPipelineHistoryModal" destroy-on-close>
+        <template #title>流水线构建历史</template>
+        <ElTable
+          :data="pipelineHistory"
+          :default-sort="{
+            props: 'id',
+          }"
+          height="400"
+        >
+          <ElTableColumn type="expand">
+            <template #default="scope">
+              <pre class="pipeline-detail">{{scope.row.details}}</pre>
+            </template>
+          </ElTableColumn>
+          <ElTableColumn
+            prop="id"
+            label="构建ID"
+            align="center"
+            width="100"
+          ></ElTableColumn>
+          <ElTableColumn
+            prop="pipelineId"
+            label="流水线ID"
+            align="center"
+            width="100"
+          ></ElTableColumn>
+          <ElTableColumn
+            prop="username"
+            label="触发人"
+            align="center"
+            width="180"
+          ></ElTableColumn>
+          <ElTableColumn
+            label="执行时间"
+            align="center"
+            :formatter="(row) => timeFormatter(row, 'startTime')"
+          ></ElTableColumn>
+        </ElTable>
+      </ElDialog>
+      <ElDialog
+        v-model="showPipelineEditModal"
+        :before-close="(done) => {
+          jsonValid=true;
+          done();
+        }"
+        destroy-on-close
+      >
+        <template #title>
+          <span>编辑流水线配置</span>
+          <span class="button-group">
+            <ElButton type="text">脚本教程</ElButton>
+            <ElButton type="text">模板详解</ElButton>
+          </span>
+        </template>
+        <ElInput
+          type="textarea"
+          :rows="16"
+          placeholder="请输入流水线配置"
+          @input="lintJson"
+          v-model="pipelineConfig">
+        ></ElInput>
+        <pre v-if="!jsonValid" :innerText="hint"></pre>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="primary" @click="handlePipelineUpdateConfirm" :disabled="!jsonValid">确 定</el-button>
+          </span>
+        </template>
+      </ElDialog>
+      <ElDialog
+        v-model="showPipelineCreateModal"
+        :before-close="(done) => {
+          $refs['createPipelineForm'].resetFields();
+          done();
+        }"
+        destroy-on-close
+      >
+        <template #title>创建流水线</template>
+        <ElForm
+          :model="createPipelineForm"
+          ref="createPipelineForm"
+          label-position="top"
+          label-suffix=":"
+          class="create-pipeline-form"
+        >
+          <ElFormItem label="名称" prop="name" required>
+            <ElInput v-model="createPipelineForm.name" placeholder="请输入流水线名称"></ElInput>
+          </ElFormItem>
+          <ElFormItem label="模板" prop="template" required>
+            <ElSelect v-model="createPipelineForm.template" placeholder="请选择模板">
+              <ElOption
+                v-for="(template, index) in templates"
+                :key="index" :label="template"
+                :value="template"
+              ></ElOption>
+            </ElSelect>
+          </ElFormItem>
+        </ElForm>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="primary" @click="handlePipelineCreateConfirm">确 定</el-button>
+          </span>
+        </template>
+      </ElDialog>
+    </ElTabPane>
+    <ElTabPane label="部署产物" name="production">
+      <ElTable :data="productions" width="100%">
+        <ElTableColumn
+          prop="id"
+          label="ID"
+          align="center"
+          width="60"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="name"
+          label="名称"
+          align="center"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="pipelineId"
+          label="流水线ID"
+          align="center"
+          width="100"
+        ></ElTableColumn>
+        <ElTableColumn
+          label="部署时间"
+          align="center"
+          :formatter="(row) => timeFormatter(row, 'deployedTime')"
+        ></ElTableColumn>
+        <ElTableColumn
+          label="URL"
+          align="center"
+        >
+          <template #default="scope">
+              <ElButton type="text" @click="open(scope.row.accessUrl, '_blank')">
+                {{scope.row.accessUrl}}
+              </ElButton>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn
+          label="运行状态"
+          align="center"
+        >
+          <template #default="scope">
+            <template v-if="scope.row.status === '正常'">
+              <ElTag type="success">{{scope.row.status}}</ElTag>
+            </template>
+            <template v-else>
+              <ElTag type="danger">{{scope.row.status}}</ElTag>
+            </template>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn>
+          <template #default="scope">
+            <ElButton type="text" @click="deleteProduction(scope.row.id)">
+              <i class="el-icon-delete-solid"></i>
+            </ElButton>
+          </template>
+        </ElTableColumn>
+      </ElTable>
+    </ElTabPane>
+  </ElTabs>
 </template>
 
 <script>
-export default {
+import jsonlint from '@/utils/jsonlint';
+
+import { PipelineAPI } from '@/api';
+import debounce from '@/utils/debounce';
 
+export default {
+  data() {
+    return {
+      activeTab: 'pipeline',
+      pipelines: [],
+      productions: [],
+      showPipelineCreateModal: false,
+      showPipelineEditModal: false,
+      showPipelineHistoryModal: false,
+      showPipelineDetailModal: false,
+      detail: '',
+      pipelineHistory: [],
+      pipelineConfig: '',
+      jsonValid: true,
+      editingPipelineId: -1,
+      templates: ['Java项目', 'MySQL'],
+      createPipelineForm: {
+        name: '',
+        template: '',
+      },
+    };
+  },
+  async mounted() {
+    this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
+    this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
+  },
+  methods: {
+    timeFormatter: (row, property) => `${row[property].year}/${row[property].month}/${row[property].day} `
+            + `${row[property].hours}:${row[property].minutes}:${row[property].seconds}`,
+    async handleClick(action, id) {
+      switch (action) {
+        case 'create': {
+          this.showPipelineCreateModal = true;
+          break;
+        }
+        case 'trigger': {
+          await PipelineAPI.triggerDeployment({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          });
+          this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
+          break;
+        }
+        case 'detail': {
+          this.showPipelineDetailModal = true;
+          this.detail = Array.prototype.find.call(this.pipelines, (pipeline) => pipeline.id === id).details;
+          break;
+        }
+        case 'history': {
+          this.showPipelineHistoryModal = true;
+          this.pipelineHistory = await PipelineAPI.getPipelineHistory({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          });
+          break;
+        }
+        case 'edit': {
+          this.showPipelineEditModal = true;
+          this.editingPipelineId = id;
+          this.pipelineConfig = await PipelineAPI.getPipeline({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          }).configJson || '{}';
+          break;
+        }
+        case 'delete': {
+          PipelineAPI.deletePipeline({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          });
+          break;
+        }
+        default: {
+          console.warn('Unknown type of action.');
+        }
+      }
+    },
+    // eslint-disable-next-line func-names
+    // FIXME: debounce效果有点问题,待排查
+    lintJson: debounce(function (value) {
+      try {
+        const result = jsonlint.parse(value);
+        this.pipelineConfig = JSON.stringify(result, null, '  ');
+        this.jsonValid = true;
+      } catch (e) {
+        this.jsonValid = false;
+        this.hint = e;
+      }
+    }, 500),
+    async handlePipelineUpdateConfirm() {
+      await PipelineAPI.updatePipeline({
+        projectId: this.$store.state.workspace.currentProjectId,
+        pipelineId: this.editingPipelineId,
+        configJson: JSON.stringify(JSON.parse(this.pipelineConfig)),
+      });
+      this.showPipelineEditModal = false;
+    },
+    async handlePipelineCreateConfirm() {
+      this.$refs.createPipelineForm.validate(async (valid) => {
+        if (valid) {
+          await PipelineAPI.createPipeline({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: this.editingPipelineId,
+            name: this.createPipelineForm.name,
+            templateName: this.createPipelineForm.template,
+          });
+          this.showPipelineCreateModal = false;
+        }
+      });
+    },
+    open(url, target) {
+      window.open(url, target);
+    },
+    async deleteProduction(id) {
+      await PipelineAPI.deleteInstance(id);
+    },
+  },
 };
 </script>
 
-<style>
-
+<style scoped>
+.tabs {
+  width: 80%;
+  margin: auto auto;
+}
+.add-button {
+  margin-left: 14px;
+}
+.pipeline-detail {
+  width: 100%;
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+.create-pipeline-form {
+  width: 60%;
+  margin: 0 auto;
+}
+.button-group {
+  margin-left: 40px;
+}
 </style>

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff