| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- <template>
- <ElTabs class="tabs" v-model="activeTab" v-loading.lock="loading">
- <ElTabPane label="流水线" name="pipeline">
- <ElTable :data="pipelines" width="100%" :default-sort="{prop: 'id', order: 'descending'}">
- <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) => formatter(row.startTime)"
- ></ElTableColumn>
- <ElTableColumn
- prop="result"
- label="执行状态"
- align="center"
- width="110"
- :filters="[
- { text: '成功', value: '部署成功' },
- { text: '失败', value: '部署失败' },
- { 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-if="scope.row.result === '无执行记录'">
- <ElTag>{{scope.row.result}}</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.recordId)"
- 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="log" v-loading="dialogLoading" >{{detail}}</pre>
- </ElDialog>
- <ElDialog v-model="showPipelineHistoryModal" width="60%" destroy-on-close>
- <template #title>流水线构建历史</template>
- <ElTable
- :data="pipelineHistory"
- :default-sort="{prop: 'id', order: 'descending'}"
- v-loading="dialogLoading"
- height="400"
- >
- <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) => formatter(row.startTime)"
- ></ElTableColumn>
- <ElTableColumn
- prop="result"
- label="执行状态"
- align="center"
- width="100"
- :filters="[
- { text: '成功', value: '部署成功' },
- { text: '失败', value: '部署失败' },
- { 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-if="scope.row.result === '无执行记录'">
- <ElTag>{{scope.row.result}}</ElTag>
- </template>
- <template v-else>
- <ElTag type="warning">未知状态</ElTag>
- </template>
- </template>
- </ElTableColumn>
- <ElTableColumn>
- <template #default="scope">
- <ElButton @click="handleClick('detail', scope.row.id)" type="text" size="small">查看详情</ElButton>
- </template>
- </ElTableColumn>
- </ElTable>
- </ElDialog>
- <ElDialog
- v-model="showPipelineEditModal"
- destroy-on-close
- >
- <template #title>
- <span>编辑流水线配置</span>
- </template>
- <ElSteps
- :active="activeStep"
- v-loading="dialogLoading"
- style="width: 80%;margin: 0 auto;"
- align-center>
- <ElStep
- v-for="(config, index) in pipelineConfig"
- :key="index"
- class="step"
- @click="() => activeStep = index"
- >
- <template #title>
- <span>{{getStage(config.name).title}}</span>
- <ElTooltip
- class="tooltip"
- effect="dark"
- :content="getStage(config.name).description"
- placement="right-end"
- >
- <i class="el-icon-question"></i>
- </ElTooltip>
- </template>
- </ElStep>
- </ElSteps>
- <div class="edit-pipeline-form" v-for="(config, index) in pipelineConfig" :key="index" v-show="activeStep === index">
- <ElForm label-width="160px">
- <div style="margin: 20px 0 20px 160px;">
- <ElCheckbox
- :disabled="!getStage(config.name).skippable"
- v-model="config.active"
- >启用该阶段</ElCheckbox>
- </div>
- <template v-for="(val, key) in config.configs" :key="key" placeholder="请选择">
- <ElFormItem :label="key">
- <ElSelect v-if="key === 'apiTestIds'" v-model="config.configs[key]" multiple >
- <el-option
- v-for="item in apiTestOptions"
- :key="item.testId"
- :label="item.testName"
- :value="item.testId">
- </el-option>
- </ElSelect>
- <ElInput v-else
- :disabled="!getStage(config.name).configs.find((config) => config.name === key).editable"
- v-model="config.configs[key]"
- ></ElInput>
- </ElFormItem>
- </template>
- </ElForm>
- </div>
- <template #footer>
- <ElButton :disabled="activeStep == 0" @click="() => activeStep -= 1">上一步</ElButton>
- <ElButton :disabled="isLastStep" @click="() => activeStep += 1">下一步</ElButton>
- <ElButton type="primary" @click="handlePipelineUpdateConfirm">确定</ElButton>
- </template>
- </ElDialog>
- <ElDialog
- v-model="showPipelineCreateModal"
- :before-close="(done) => {
- $refs['createPipelineForm'].resetFields();
- done();
- }"
- destroy-on-close
- >
- <template #title>
- <span>创建流水线</span>
- <span class="button-group">
- <ElButton type="text">模板详解</ElButton>
- </span>
- </template>
- <ElForm
- :model="createPipelineForm"
- ref="createPipelineForm"
- label-position="top"
- label-suffix=":"
- class="create-pipeline-form"
- v-loading="dialogLoading"
- >
- <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" v-loading="confirmLoading" @click="handlePipelineCreateConfirm">确 定</el-button>
- </span>
- </template>
- </ElDialog>
- </ElTabPane>
- <ElTabPane label="部署产物" name="production">
- <ElTable :data="productions" width="100%" :default-sort="{prop: 'id', order: 'descending'}">
- <ElTableColumn
- prop="id"
- label="ID"
- align="center"
- width="60"
- ></ElTableColumn>
- <ElTableColumn
- prop="name"
- label="名称"
- align="center"
- width="100"
- ></ElTableColumn>
- <ElTableColumn
- prop="pipelineId"
- label="流水线ID"
- align="center"
- width="60"
- ></ElTableColumn>
- <ElTableColumn
- label="部署时间"
- align="center"
- width="320"
- :formatter="(row) => formatter(row.deployedTime)"
- ></ElTableColumn>
- <ElTableColumn
- label="URL"
- align="center"
- >
- <template #default="scope">
- <ElTooltip effect="dark" :content="scope.row.accessUrl" placement="top-start">
- <ElButton type="text" @click="open(scope.row.accessUrl, '_blank')">
- {{scope.row.accessUrl}}
- </ElButton>
- </ElTooltip>
- </template>
- </ElTableColumn>
- <ElTableColumn
- label="运行状态"
- align="center"
- width="100"
- >
- <template #default="scope">
- <ElTag :type="scope.row.status.indexOf('正常') > -1 ? 'success' : 'danger'">{{scope.row.status}}</ElTag>
- </template>
- </ElTableColumn>
- <ElTableColumn
- label="运行状态"
- align="center"
- width="100"
- >
- <template #default="scope">
- <ElButton type="text" @click="showLog(scope.row.pipelineId)">查看日志</ElButton>
- </template>
- </ElTableColumn>
- <ElTableColumn width="60">
- <template #default="scope">
- <ElButton type="text" @click="deleteProduction(scope.row.pipelineId)">
- <i class="el-icon-delete"></i>
- </ElButton>
- </template>
- </ElTableColumn>
- </ElTable>
- <ElDialog v-model="showProductionLogModal" destroy-on-close>
- <template #title>
- 产物日志
- </template>
- <pre class="log" v-loading="dialogLoading" >{{currentLog}}</pre>
- <template #footer>
- <ElButton @click="refreshLog">刷新</ElButton>
- </template>
- </ElDialog>
- </ElTabPane>
- </ElTabs>
- </template>
- <script>
- import { APITestAPI, PipelineAPI } from '@/api';
- import { formatter } from '@/utils/time';
- export default {
- data() {
- return {
- loading: false,
- dialogLoading: false,
- confirmLoading: false,
- activeTab: 'pipeline',
- pipelines: [],
- productions: [],
- showPipelineCreateModal: false,
- showPipelineEditModal: false,
- showPipelineHistoryModal: false,
- showPipelineDetailModal: false,
- detail: '',
- pipelineHistory: [],
- pipelineConfig: '',
- jsonValid: true,
- editingPipelineId: -1,
- templates: [],
- createPipelineForm: {
- name: '',
- template: '',
- },
- stages: [],
- activeStep: 0,
- showProductionLogModal: false,
- currentLogId: -1,
- currentLog: '',
- apiTestOptions: [],
- pipelineConfigTemplate: '',
- };
- },
- computed: {
- isLastStep() {
- return this.activeStep === this.stages.length - 1;
- },
- stageConfigsBase() {
- return this.stages.map((stage) => ({
- name: stage.name,
- active: true,
- configs: stage.configs.map(({ name, value, editable }) => ({
- [name]: value,
- editable,
- })).reduce((prev, curr) => Object.assign(prev, curr), {}),
- }));
- },
- stageConfigs() {
- return this.stageConfigsBase;
- },
- },
- async mounted() {
- this.loading = true;
- this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
- this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
- this.templates = await PipelineAPI.getPipelineTemplates();
- this.stages = await PipelineAPI.getStages();
- this.apiTestOptions = await APITestAPI.getAPITestInfosByProjectId(this.$store.state.workspace.currentProjectId);
- this.loading = false;
- },
- methods: {
- formatter,
- async handleClick(action, id) {
- switch (action) {
- case 'create': {
- this.showPipelineCreateModal = true;
- break;
- }
- case 'trigger': {
- this.loading = true;
- await PipelineAPI.triggerDeployment({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId: id,
- });
- PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId)
- .then(
- (res) => {
- this.productions = res;
- },
- );
- this.loading = true;
- this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
- this.loading = false;
- break;
- }
- case 'detail': {
- this.showPipelineDetailModal = true;
- this.dialogLoading = true;
- this.detail = await PipelineAPI.getPipelineRecordDetail(id);
- this.dialogLoading = false;
- break;
- }
- case 'history': {
- this.showPipelineHistoryModal = true;
- this.dialogLoading = true;
- this.pipelineHistory = await PipelineAPI.getPipelineRecordList({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId: id,
- });
- this.dialogLoading = false;
- break;
- }
- case 'edit': {
- this.showPipelineEditModal = true;
- this.dialogLoading = true;
- this.editingPipelineId = id;
- const config = (await PipelineAPI.getPipeline({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId: id,
- })).configJson;
- this.pipelineConfig = JSON.parse(config || '{}');
- this.pipelineConfigTemplate = JSON.parse(config || '{}');
- this.pipelineConfig.forEach((item) => {
- const targetStage = this.stages.find((stage) => stage.name === item.name);
- if (targetStage) {
- targetStage.configs.forEach((configTemplate) => {
- if (!item.configs[configTemplate.name]) {
- item.configs[configTemplate.name] = this.parseConfigValue(configTemplate.value);
- }
- });
- }
- });
- this.dialogLoading = false;
- break;
- }
- case 'delete': {
- this.loading = true;
- PipelineAPI.deletePipeline({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId: id,
- });
- this.loading = false;
- break;
- }
- default: {
- console.warn('Unknown type of action.');
- }
- }
- },
- async handlePipelineUpdateConfirm() {
- this.confirmLoading = true;
- this.pipelineConfigTemplate.forEach((item) => {
- const target = this.pipelineConfig.find((i) => i.name === item.name);
- item.active = target.active;
- Object.keys(item.configs).forEach((key) => {
- item.configs[key] = target.configs[key];
- });
- });
- await PipelineAPI.updatePipeline({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId: this.editingPipelineId,
- configJson: JSON.stringify(this.pipelineConfigTemplate),
- });
- this.confirmLoading = false;
- this.showPipelineEditModal = false;
- this.loading = true;
- this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
- this.loading = false;
- },
- async handlePipelineCreateConfirm() {
- this.confirmLoading = true;
- this.$refs.createPipelineForm.validate(async (valid) => {
- if (valid) {
- await PipelineAPI.createPipeline({
- projectId: this.$store.state.workspace.currentProjectId,
- name: this.createPipelineForm.name,
- templateName: this.createPipelineForm.template,
- });
- this.showPipelineCreateModal = false;
- }
- });
- this.confirmLoading = false;
- this.loading = true;
- this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
- this.loading = false;
- },
- open(url, target) {
- window.open(url, target);
- },
- async deleteProduction(pipelineId) {
- this.loading = true;
- await PipelineAPI.deleteInstance({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId,
- });
- this.loading = false;
- },
- getStage(stageName) {
- return this.stages.find((stage) => stage.name === stageName);
- },
- async showLog(pipelineId) {
- this.currentLogId = pipelineId;
- this.showProductionLogModal = true;
- this.dialogLoading = true;
- this.currentLog = await PipelineAPI.getDeploymentLog({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId,
- });
- this.dialogLoading = false;
- },
- async refreshLog() {
- this.dialogLoading = true;
- this.currentLog = await PipelineAPI.getDeploymentLog({
- projectId: this.$store.state.workspace.currentProjectId,
- pipelineId: this.currentLogId,
- });
- this.dialogLoading = false;
- },
- parseConfigValue(str) {
- const project = this.$store.state.workspace.projectList.find(
- (item) => item.id === this.$store.state.workspace.currentProjectId,
- );
- const pipeline = this.pipelines.find((item) => item.id === this.editingPipelineId);
- return str.replaceAll('{projectName}', project.name).replaceAll('{projectId}', project.id)
- .replaceAll('{pipelineName}', pipeline.name);
- },
- },
- };
- </script>
- <style scoped>
- .tabs {
- width: 80%;
- margin: auto auto;
- }
- .add-button {
- margin-left: 14px;
- }
- .log {
- width: 100%;
- word-break: break-word;
- white-space: pre-wrap;
- }
- .create-pipeline-form {
- width: 60%;
- margin: 0 auto;
- }
- .edit-pipeline-form {
- margin-top: 20px;
- }
- .button-group {
- margin-left: 40px;
- }
- .step {
- cursor: pointer;
- }
- </style>
|