diff --git a/src/common/DateUtil.js b/src/common/DateUtil.js index 4808ae5a9..72f8fc3e6 100644 --- a/src/common/DateUtil.js +++ b/src/common/DateUtil.js @@ -135,4 +135,25 @@ export function timeAgo(backDate) { // return seconds + "秒前"; // } return "刚刚"; +} + +// seconds: 秒数,返回格式为 xxhxxmxxs 的字符串 +export function secondsToTimeFormat(seconds) { + const hours = Math.floor(seconds / 3600); // 计算小时数 + const minutes = Math.floor((seconds % 3600) / 60); // 计算分钟数 + const remainingSeconds = seconds % 60; // 计算剩余秒数 + + let result = ""; + + if (hours > 0) { + result += `${hours}h`; + } + if (minutes > 0) { + result += `${minutes}m`; + } + if (remainingSeconds > 0 || result === "") { + result += `${remainingSeconds}s`; + } + + return result; } \ No newline at end of file diff --git a/src/forge/DevOps/cardList/api.js b/src/forge/DevOps/cardList/api.js new file mode 100644 index 000000000..7dce8c03c --- /dev/null +++ b/src/forge/DevOps/cardList/api.js @@ -0,0 +1,65 @@ +import fetch from './fetch'; +export function addPipelines(owner, repo, data) { + return fetch({ + url:`/v1/${ owner }/${ repo }/pipelines`, + method: 'POST', + data + }) +} + +export function getCardList(owner, repo, params) { + return fetch({ + url:`/v1/${ owner }/${ repo }/pipelines`, + method: 'get', + params + }) +} + +export function getTemplateList() { + return fetch({ + url:`/action/templates.json`, + method: 'get', + }) +} + +export function getProjectBranch(owner, repo) { + return fetch({ + url:`/${ owner }/${ repo }/branches.json`, + method: 'GET', + }) +} + +export function runPipeline(owner, repo, params) { + return fetch({ + url:`/v1/${ owner }/${ repo }/actions/runs.json`, + method: 'post', + data: params + }) +} + +export function delPipelines(owner, repo, id) { + return fetch({ + url:`/v1/${ owner }/${ repo }/pipelines/${ id }.json`, + method: 'DELETE', + }) +} + +export function stopPipelines(owner, repo, params) { + return fetch({ + url:`/v1/${ owner }/${ repo }/actions/disable.json`, + method: 'POST', + data: params + }) +} + +export function startPipelines(owner, repo, params) { + return fetch({ + url:`/v1/${ owner }/${ repo }/actions/enable.json`, + method: 'POST', + data: params + }) +} + + + + diff --git a/src/forge/DevOps/cardList/fetch.js b/src/forge/DevOps/cardList/fetch.js new file mode 100644 index 000000000..babbe58fe --- /dev/null +++ b/src/forge/DevOps/cardList/fetch.js @@ -0,0 +1,7 @@ +import javaFetch from '../../javaFetch'; + +// let settings = JSON.parse(localStorage.chromesetting); + +const service = javaFetch('/api'); +export default service; +export const TokenKey = 'autologin_trustie'; \ No newline at end of file diff --git a/src/forge/DevOps/cardList/index.jsx b/src/forge/DevOps/cardList/index.jsx new file mode 100644 index 000000000..e14bb2172 --- /dev/null +++ b/src/forge/DevOps/cardList/index.jsx @@ -0,0 +1,178 @@ +import { message, Popover, Button, Divider, Switch, Modal, Pagination } from "antd"; +import React, { useEffect, useState } from "react"; +import './index.scss'; +import { secondsToTimeFormat, timeAgo } from "../../../common/DateUtil"; +import { runPipeline, delPipelines, stopPipelines, startPipelines, getCardList } from './api' +import Nodata from "../../Nodata"; +import New from "./newModal"; + +const { confirm } = Modal; + +function CardDevops(props) { + const { project, match } = props || {}; + const {owner, projectsId} = match.params; + const [list, setList] = useState([]) + const [ visible , setVisible ] = useState(false); + const [ pageNum , setPageNum ] = useState(1); + const [ total , setTotal ] = useState(0); + const pageSize = 20; + + let settings = localStorage.chromesetting && JSON.parse(localStorage.chromesetting); + let pipelineUrl = settings && settings.common.pipeline; + + + useEffect(() => { + document.title = `流水线-${owner}/${projectsId}`; + }, []) + + useEffect(()=>{ + getList(); + const intervalId = setInterval(() => { + getList(); + }, 10000); + return () => clearInterval(intervalId); + }, [pageNum]) + + function getList(){ + getCardList(owner, projectsId, { limit: pageSize, page: pageNum }).then(res => { + if (res.status === 0) { + setList(res.pipelines.map(e => { + e.run_data = e.run_data || {} + return e + })) + setTotal(res.count) + } + }) + } + + // 删除流水线 + async function delPipelineFunc(id) { + confirm({ + title: '提示', + content: '确认删除该流水线?', + onOk() { + delPipelines( owner, projectsId, id).then(res => { + if(res && res.status === 0){ + message.success(`流水线删除成功!`); + getList(); + } + }) + }, + onCancel() { + }, + }); + + } + + // 手动运行流水线 + async function run (item) { + if (!item.file_name) { + message.warning('请先编辑流水线内容!') + return + } + runPipeline( owner, projectsId, { ref: item.branch, workflow: item.file_name.split('/').pop() }).then(res => { + if(res && res.status === 0) { + message.success('运行成功!') + getList(); + } + }) + } + + // 禁用/启用流水线 + async function stopPipelineFunc(id,status, fileName = undefined) { + if (!fileName) { + message.warning('请先编辑流水线内容!') + return + } + let res = !status ? await stopPipelines(owner, projectsId, { id, workflow: fileName.split('/').pop() }) : await startPipelines(owner, projectsId, { id, workflow: fileName.split('/').pop() }) + + if(res && res.status === 0){ + message.info(`流水线${ !status ? "禁用":"启用"}成功!`); + getList(); + } + } + + const listType = { + 1: '声明式', + 2: '图形化', + } + let toBeDesigned = -1 + + const statusEnum = { + 1: '成功', + 2: '失败', + 5: '等待', + 6: '运行中', + 0: '待启动', + [toBeDesigned]: '待设计' + } + + return
+ setVisible(false)} + onOk={()=>{setVisible(false);getList()}} + /> + +
+ {list && list.map(e=>{ + if (!e.run_data.status && e.run_data.status !== 0) { + e.run_data.status = (!e.json && !e.yaml) ? -1 : 0 + } + return
+
+
+
{ + // window.location.href = `${mygetHelmetapi && mygetHelmetapi.common.zone}/${owner}/pipeline/${e.id}` + window.open(`${ pipelineUrl }/devops/${owner}/${projectsId}/${e.id}`) + }}> +
+
{listType[e.pipeline_type]}
+ {e.pipeline_name} +
+
+

+

+ {statusEnum[+e.run_data.status] || ((!e.json && !e.yaml) ? "待设计" : "待启动")} + {!!e.run_data.total && #{e.run_data.total}    {timeAgo(e.run_data.stopped)} 执行时长{secondsToTimeFormat(e.run_data.length)}} +

+

分支:{e.branch}

+
{e.run_data.schedule && 定时}历史共执行{e.run_data.total || 0}次(成功{e.run_data.success || 0}次,失败{e.run_data.failure || 0}次)
+
+
+ +
+
+ run(e) }/> + window.open(`${ pipelineUrl }/devops/${owner}/${projectsId}/${e.id}/edit`) }/> + delPipelineFunc(e.id) }/> +
+
+
+ stopPipelineFunc(e.id, e.disable, e.file_name)} /> + { e.disable? '禁用' : '启用' } +
+
+
+
+
+ })} + {list && !list.length &&
} +
+ { + total > pageSize && +
+ +
+ } +
+} + +export default CardDevops \ No newline at end of file diff --git a/src/forge/DevOps/cardList/index.scss b/src/forge/DevOps/cardList/index.scss new file mode 100644 index 000000000..3c06a7502 --- /dev/null +++ b/src/forge/DevOps/cardList/index.scss @@ -0,0 +1,245 @@ +.cardList { + margin: 30px auto; + width: 1200px; + padding-bottom: 50px; + display: flex; + flex-direction: column; + .ant-btn-primary { + margin-left: auto; + } + .listData { + display: flex; + flex-wrap: wrap; + } + .card { + width: 333px; + border-radius: 6px; + margin-right: 31px; + margin-bottom: 25px; + padding: 0 1px 1px 1px; + display: flex; + flex-direction: column; + cursor: pointer; + &:hover{ + box-shadow: 0px 0px 15px 1px rgba(109,130,170,0.44); + } + .statusBar{ + height: 6px; + margin: 0 4px; + } + .cardContent { + flex-grow: 2; + background: #FFFFFF; + border-radius: 6px 6px 6px 6px; + padding: 15px 17px 20px; + box-sizing: border-box; + .cartHead { + display: flex; + align-items: center; + div { + width: 57px; + height: 21px; + border-radius: 4px 4px 4px 4px; + border: 1px solid $primary-color; + font-size: 13px; + color: $primary-color; + line-height: 21px; + text-align: center; + margin-right: 11px; + } + .f36{ + color: #F3651C; + border-color: #F3651C; + } + span { + font-family: 'alibaba-medium'; + font-size: 16px; + color: #1E1E1E; + } + } + .cardBody { + margin-top: 12px; + height: 133px; + background: #d4dff13d; + border-radius: 6px; + padding: 20px 15px; + display: flex; + flex-direction: column; + justify-content: space-between; + font-size: 14px; + color: #082340; + box-sizing: border-box; + p { + display: flex; + align-items: baseline; + line-height: 1; + } + div { + line-height: 22px; + } + .statusIcon { + width: 4px; + height: 12px; + border-radius: 6px 6px 6px 6px; + margin-right: 12px; + } + } + .cartBottom { + display: flex; + align-items: center; + .leftButton { + img { + width: 26px; + margin-right: 20px; + cursor: pointer; + } + } + .rightButton { + margin-left: auto; + font-size: 14px; + color: $primary-color; + .status-switch { + display: flex; + align-items: center; + justify-content: center; + button { + margin-right: 10px; + } + .ant-switch-checked { + background: #07A35A; + &:hover { + background: #07A35A !important; + } + } + } + } + } + } + .scheduleBox{ + border-radius: 4px; + border: 1px solid #F3651C; + color: #F3651C; + padding: 3px 4px; + } + } + .statusIcon, .card, .statusBar{ + background: #979797; + } + .developStatus2.statusIcon, .card.developStatus2, .developStatus2 .statusBar{ + background: #EC6E66; + } + .developStatus1.statusIcon, .card.developStatus1, .developStatus1 .statusBar{ + background: #07A35A; + } + .developStatus5.statusIcon, .card.developStatus5, .developStatus5 .statusBar{ + background: #979797; + } + .developStatus6.statusIcon, .card.developStatus6, .developStatus6 .statusBar{ + background: #2D74FB; + } + .card.developStatus6 .statusBar { + background-image: repeating-linear-gradient(115deg, #ffffff63 0, #2D74FB 1px, #2D74FB 10px, #ffffff63 11px, #ffffff63 20px); + animation: 3s linear 0s infinite normal none running workflow-running; + } +} + +@keyframes workflow-running{ + 0% { + background-position-x: -53.5px; + } + 100% { + background-position-x: 0; + } +} + +.templateContainer { + height: 380px; + overflow-y: auto; +} +.template{ + padding: 15px 18px; + background-size: 100% 100%; + background-image: url('../../../images/devOps/template.png'); + border-radius: 4px; + border: 1px solid rgba(158,169,185,0.23); + margin-bottom: 15px; + font-size: 16px; + cursor: pointer; + &:hover{ + border-color: #0d5ef8; + } + .nodes { + display: flex; + align-items: center; + flex-wrap: wrap; + font-size: 14px; + } + .node{ + padding: 1px 12px; + background: rgba(70,106,255,0.05); + border-radius: 4px; + display: inline-block; + } +} +.templateSelected { + border-color: #0d5ef8; +} +.themeColorSpan { + color: #0d5ef8; +} + +.format-style{ + div[class~="ant-modal-title"]{ + font-size: 18px; + color: #0d0f12; + } + div[class~='ant-modal-content']{ + position: relative; + &::before{ + position: absolute; + top: 0px; + width: 100%; + height: 120px; + left: 0px; + content: ""; + z-index: 1; + border-radius: 9px; + background: linear-gradient(179.62deg,#c0ceff 0%,rgba(214, 233, 255, 0.78) 49.37%,#fbfcff 92%,#fff 100%); + } + &::after{ + position: absolute; + top: 30px; + width: 180px; + height: 140px; + content: ""; + left: 55px; + z-index: 2; + background-image: url('../../../images/devOps/modal-back.png'); + } + div[class~="ant-modal-body"]{ + min-height: 100px; + position: relative; + z-index: 3; + padding: 0 24px; + } + div[class~="ant-modal-header"]{ + text-align: center; + margin-bottom: 30px; + background-color: transparent; + position: relative; + z-index: 3; + border-bottom: unset; + } + div[class~="ant-modal-footer"]{ + border-top: unset; + } + } + div[class~='format-button']{ + text-align: center; + padding:20px 0; + button{ + width: 100px; + margin:0px 16px; + } + } +} diff --git a/src/forge/DevOps/cardList/newModal.jsx b/src/forge/DevOps/cardList/newModal.jsx new file mode 100644 index 000000000..186d9eaaa --- /dev/null +++ b/src/forge/DevOps/cardList/newModal.jsx @@ -0,0 +1,183 @@ +import React , { useState , useEffect } from 'react'; +import { getProjectBranch, addPipelines, getTemplateList } from './api'; +import './index.scss'; +import { Modal , Form , Input, Select , Button, Divider } from 'antd'; + +const modalList = [ + { + name: '空白模板', + id: -1, + nodes: [{ name: '请自定义您的流水线模板' }] + }, +] + +const pipType = { + all: 0, + code: 1, + image: 2 +} + +const { Option } = Select; + + +const New =({ + open,onCancel,form,match, onOk +}) => { + + const { getFieldDecorator } = form; + const {owner, projectsId} = match.params; + // const [ loading, setLoading ] = useState(false); + const [ pipelineType, setPipelineType ] = useState(pipType.code); + const [ branchList, setBranchList] = useState([]); + const [ templateList, setTemplateList ] = useState([]); + const [ templateSelected, setTemplateSelected ] = useState(-1); + + let settings = JSON.parse(localStorage.chromesetting); + let pipelineUrl = settings && settings.common.pipeline; + + useEffect(() => { + if(open){ + getTemplates(); + getBranchList() + } + }, [open]); + + function getTemplates(){ + // 获取流水线模板列表 + getTemplateList().then(res=>{ + + if(res.templates){ + res.templates.map((item)=>{ + return item.nodes = JSON.parse(item.json).nodes + }) + setTemplateList(modalList.concat(res.templates)); + } + }) + } + + function getBranchList () { + getProjectBranch(owner, projectsId).then(res => { + setBranchList(res) + }) + } + + async function onSubmit() { + await form.validateFields(); + const { branch , pipeline_name, pipeline_type } = form.getFieldsValue(); + const params = { + branch,pipeline_name, pipeline_type + } + if (pipelineType === pipType.image) params.pipelineTemplateJson = templateSelected !== -1 ? getTemplateJson(templateSelected) : {nodes: [], edges: [], combos: []} + addPipelines(owner, projectsId, params).then(res=>{ + if(res.id){ + form.resetFields(); + onOk() + window.open(`${ pipelineUrl }/devops/${owner}/${projectsId}/${res.id}/edit`) + } + }) + } + + const getTemplateJson = (id) => { + let json = {} + for (let i = 0; i < templateList.length; i++) { + const element = templateList[i]; + if (element.id === id) json = JSON.parse(element.json) + } + return json + } + + function close() { + form.resetFields(); + onCancel(); + } + + return( + + + + + } + > +
+ + + {getFieldDecorator("pipeline_name",{ + rules:[{ required: true, message: '请输入流水线名称' }] + })( + + )} + + + {getFieldDecorator("branch",{ + rules: [{ required: true, message: '请选择代码库分支' }] + })( + + )} + + + {getFieldDecorator("pipeline_type",{ + rules: [{ required: true, message: '请选择流水线类型' }] + })( + + )} + + { + pipelineType === pipType.image && + +
+ { + templateList.map(e => { + console.log('e.nodes', e.nodes); + + return
{ + setTemplateSelected(e.id) + }}> + {e.name} +
+ {e.nodes && e.nodes.map((item, index)=>{ + const nextItem = e.nodes[index+1] + return + {item.name} + {nextItem && } + + })} +
+
+ }) + } +
+
+ } + +
+ ) +} +export default Form.create()(New) \ No newline at end of file diff --git a/src/forge/Main/Detail.js b/src/forge/Main/Detail.js index ce033477c..1c29f5842 100644 --- a/src/forge/Main/Detail.js +++ b/src/forge/Main/Detail.js @@ -151,6 +151,10 @@ const Review = Loadable({ loader: () => import('../Newfile/codeReview'), loading: Loading, }); +const DevopsCard = Loadable({ + loader: () => import('../DevOps/cardList'), + loading: Loading, +}); /** * permission:Manager:管理员,Reporter:报告人员(只有读取权限),Developer:开发人员(除不能设置仓库信息外) */ @@ -176,6 +180,8 @@ function checkPathname(projectsId, owner, pathname) { name = "dataset" } else if (url.indexOf(`/devops`) > -1) { name = "devops" + } else if (url.indexOf(`/actions`) > -1) { + name = "devops" } else if (url.indexOf(`/source`) > -1) { name = "source" } else if (url.indexOf(`/wiki`) > -1) { @@ -839,6 +845,12 @@ class Detail extends Component { () => () } > + {/* 流水线 */} + () + } + > {/* 标签列表 */} {/* - {/* */} - - 引擎(Engine) - {projectDetail && projectDetail.ops_count ? {projectDetail.ops_count} : ""} - - + + + + 引擎(Engine) + {projectDetail && projectDetail.ops_count ? {projectDetail.ops_count} : ""} + + + + + 引擎(Actions) + + + + }> +
  • e.preventDefault()}> + + + 流水线(devops) + +
  • +
    :"" } { diff --git a/src/forge/Settings/Setting.js b/src/forge/Settings/Setting.js index 544ac6521..ef3c7c95b 100644 --- a/src/forge/Settings/Setting.js +++ b/src/forge/Settings/Setting.js @@ -17,7 +17,7 @@ const menu = [ {name:"代码库",index:"code"}, {name:"疑修 (Issue)",index:"issues"}, {name:"合并请求 (PR)",index:"pulls"}, - {name:"引擎 (Engine)",index:"devops"}, + {name:"流水线 (Devops)",index:"devops"}, {name:"数据集",index:"dataset"}, // {name:"资源库",index:"resources"}, {name:"里程碑",index:"versions"}, diff --git a/src/images/devOps/delete.png b/src/images/devOps/delete.png new file mode 100644 index 000000000..8845d164d Binary files /dev/null and b/src/images/devOps/delete.png differ diff --git a/src/images/devOps/edit.png b/src/images/devOps/edit.png new file mode 100644 index 000000000..de8162d64 Binary files /dev/null and b/src/images/devOps/edit.png differ diff --git a/src/images/devOps/modal-back.png b/src/images/devOps/modal-back.png new file mode 100644 index 000000000..409116835 Binary files /dev/null and b/src/images/devOps/modal-back.png differ diff --git a/src/images/devOps/start.png b/src/images/devOps/start.png new file mode 100644 index 000000000..e5a41c72b Binary files /dev/null and b/src/images/devOps/start.png differ diff --git a/src/images/devOps/template.png b/src/images/devOps/template.png new file mode 100644 index 000000000..3877e1b64 Binary files /dev/null and b/src/images/devOps/template.png differ