上线actions流水线

This commit is contained in:
谢思 2025-09-19 14:11:23 +08:00
parent c12369d373
commit 171b9973b4
14 changed files with 735 additions and 9 deletions

View File

@ -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;
}

View File

@ -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
})
}

View File

@ -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';

View File

@ -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 <div className="cardList">
<New
{...props}
open={visible}
onCancel={()=>setVisible(false)}
onOk={()=>{setVisible(false);getList()}}
/>
<Button onClick={()=>{setVisible(true)}} type='primary' className="mb20" >新建流水线</Button>
<div className="listData">
{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 <div className={`card developStatus${e.run_data.status}`} >
<div className="statusBar"></div>
<div className="cardContent">
<div onClick={()=>{
// window.location.href = `${mygetHelmetapi && mygetHelmetapi.common.zone}/${owner}/pipeline/${e.id}`
window.open(`${ pipelineUrl }/devops/${owner}/${projectsId}/${e.id}`)
}}>
<div className="cartHead">
<div className={e.pipeline_type === 1 ? "f36" : ''}>{listType[e.pipeline_type]}</div>
<Popover content={e.pipeline_name} ><span className="task-hide weight">{e.pipeline_name}</span></Popover>
</div>
<div className="cardBody">
<p>
<div className={`statusIcon developStatus${e.run_data.status}`}></div>
{statusEnum[+e.run_data.status] || ((!e.json && !e.yaml) ? "待设计" : "待启动")}
{!!e.run_data.total && <span>#{e.run_data.total} &nbsp;&nbsp;&nbsp;{timeAgo(e.run_data.stopped)}&nbsp;执行时长{secondsToTimeFormat(e.run_data.length)}</span>}
</p>
<p className="mt5"><i className="iconfont icon-a-fenzhi1 font-12 mr4 mb3"></i> 分支{e.branch}</p>
<div className="mt5">{e.run_data.schedule && <span className="scheduleBox mr10">定时</span>}历史共执行{e.run_data.total || 0}成功{e.run_data.success || 0}失败{e.run_data.failure || 0}</div>
</div>
</div>
<Divider dashed/>
<div className="cartBottom" >
<div className="leftButton">
<img src={ require('../../../images/devOps/start.png') } alt="" onClick={ () => run(e) }/>
<img src={ require('../../../images/devOps/edit.png') } alt="" onClick={ () => window.open(`${ pipelineUrl }/devops/${owner}/${projectsId}/${e.id}/edit`) }/>
<img src={ require('../../../images/devOps/delete.png') } alt="" onClick={ () => delPipelineFunc(e.id) }/>
</div>
<div className="rightButton">
<div className="status-switch">
<Switch size="small" className='mr10' checked={ !e.disable } onChange={()=>stopPipelineFunc(e.id, e.disable, e.file_name)} />
{ e.disable? '禁用' : '启用' }
</div>
</div>
</div>
</div>
</div>
})}
{list && !list.length && <div style={{margin: '100px auto'}}><Nodata _html="暂无数据"/></div>}
</div>
{
total > pageSize &&
<div style={{textAlign:'right',paddingTop:20}}>
<Pagination
simple
defaultCurrent={pageNum}
total={total}
pageSize={pageSize}
onChange={setPageNum}
></Pagination>
</div>
}
</div>
}
export default CardDevops

View File

@ -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;
}
}
}

View File

@ -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(
<Modal
visible={open}
width={"686px"}
className="format-style"
title={"新建流水线"}
onCancel={close}
footer={
<div style={{textAlign:"center"}}>
<Button style={{width:"90px"}} onClick={close}>取消</Button>
<Button type={"primary"} className="ml25" onClick={onSubmit} style={{width:"90px"}}>保存</Button>
</div>
}
>
<Form layout={"vertical"}>
<Form.Item label="流水线名称">
{getFieldDecorator("pipeline_name",{
rules:[{ required: true, message: '请输入流水线名称' }]
})(
<Input placeholder={"请输入流水线名称"} maxLength={50}/>
)}
</Form.Item>
<Form.Item label="代码库分支">
{getFieldDecorator("branch",{
rules: [{ required: true, message: '请选择代码库分支' }]
})(
<Select
placeholder={"请选择代码库分支"}
filterOption={(input, option) =>
option.title.indexOf(input.toLowerCase()) >= 0
}
showSearch
getPopupContainer={trigger => trigger.parentNode}
>
{
branchList.map((item)=>{
return <Select.Option key={item.name} value={item.name} title={item.name}>
<span><i className="iconfont icon-a-fenzhi1 font14 mr5"/>{item.name}</span>
</Select.Option>
})
}
</Select>
)}
</Form.Item>
<Form.Item label="流水线类型">
{getFieldDecorator("pipeline_type",{
rules: [{ required: true, message: '请选择流水线类型' }]
})(
<Select
placeholder={"请选择流水线类型"}
onChange={e => { setPipelineType(e) }}
>
<Option value={ pipType.code }>声明式流水线</Option>
<Option value={ pipType.image }>图形化流水线</Option>
</Select>
)}
</Form.Item>
{
pipelineType === pipType.image &&
<Form.Item label="请选择流水线模板" >
<div className='templateContainer'>
{
templateList.map(e => {
console.log('e.nodes', e.nodes);
return <div className={`template ${ e.id === templateSelected ? 'templateSelected' : '' }`} onClick={()=>{
setTemplateSelected(e.id)
}}>
<span className='col0d0Text font16'><i className="iconfont icon-moban mr10 themeColorSpan font14"/>{e.name}</span>
<div className='mt15 nodes'>
{e.nodes && e.nodes.map((item, index)=>{
const nextItem = e.nodes[index+1]
return <React.Fragment>
<span className={`node mb10`}>{item.name}</span>
{nextItem && <Divider className='mb10' style={{marginTop: '0', minWidth: '45px', width: '45px', borderTop: '1px solid #B8C1CF', display: 'inline-flex'}}/>}
</React.Fragment>
})}
</div>
</div>
})
}
</div>
</Form.Item>
}
</Form>
</Modal>
)
}
export default Form.create()(New)

View File

@ -151,6 +151,10 @@ const Review = Loadable({
loader: () => import('../Newfile/codeReview'),
loading: Loading,
});
const DevopsCard = Loadable({
loader: () => import('../DevOps/cardList'),
loading: Loading,
});
/**
* permissionManager:管理员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 {
() => (<DevIndex {...this.props} {...this.state} {...common} />)
}
></Route>
{/* 流水线 */}
<Route path="/:owner/:projectsId/actions"
render={
() => (<DevopsCard {...this.props} {...this.state} {...common} />)
}
></Route>
{/* 标签列表 */}
{/* <Route path="/:owner/:projectsId/issues/tags"
render={

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Skeleton , Tooltip} from 'antd';
import { Dropdown, Menu, Skeleton , Tooltip} from 'antd';
import { Link } from 'react-router-dom';
import { numFormat } from 'educoder';
import QuitBox from './quit';
@ -96,13 +96,28 @@ function DetailBanner({ history,list , owner , projectsId ,showNotification , ur
{/* 引擎仅对当前仓库管理员可见 */}
{
item.menu_name === "devops" ?
<li className={pathname==="devops" ? "active" : ""}>
{/* <Link to={{ pathname: `/${owner}/${projectsId}/devops${open_devops ? `/dispose`:""}`, state }}> */}
<Link className='newTab' to={{ pathname: `/${owner}/${projectsId}/devops`, state:{...state,open_devops} }}>
<i className="iconfont icon-gongzuoliuicon font-13 mr5 color-grey-3"></i>引擎(Engine)
{projectDetail && projectDetail.ops_count ? <span>{projectDetail.ops_count}</span> : ""}
</Link>
</li>
<Dropdown overlay={
<Menu>
<Menu.Item>
<Link className='color-dark' to={{ pathname: `/${owner}/${projectsId}/devops`, state:{...state,open_devops} }} style={{padding: '9px 12px'}}>
<i className='iconfont icon-lianluzhuizong1 color-grey-3 font-14 mr3'></i>引擎(Engine)
{projectDetail && projectDetail.ops_count ? <span>{projectDetail.ops_count}</span> : ""}
</Link>
</Menu.Item>
<Menu.Item>
<Link className='color-dark' to={{ pathname: `/${owner}/${projectsId}/actions`, state:{...state,open_devops} }} style={{padding: '9px 12px'}}>
<i className='iconfont icon-lianluzhuizong1 color-grey-3 font-14 mr3'></i>引擎(Actions)
</Link>
</Menu.Item>
</Menu>
}>
<li className={pathname==="devops" ? "active" : ""} onClick={e => e.preventDefault()}>
<a>
<i className="iconfont icon-gongzuoliuicon font-13 mr5 color-grey-3"></i>
流水线(devops)
</a>
</li>
</Dropdown>
:""
}
{

View File

@ -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"},

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

BIN
src/images/devOps/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/images/devOps/start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB