仓库集成流水线

This commit is contained in:
黄心宇 2025-03-27 16:53:41 +08:00
parent d3f7a7942f
commit 0b81808835
12 changed files with 484 additions and 41 deletions

View File

@ -179,7 +179,7 @@
"eslintConfig": {
"extends": "react-app"
},
"proxy": "http://localhost:3000",
"proxy": "http://172.20.32.201:4000",
"port": "3007",
"devDependencies": {
"@babel/runtime": "7.0.0-beta.51",

66
src/forge/DevOps/api.js Normal file
View File

@ -0,0 +1,66 @@
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

@ -1,15 +1,18 @@
import { message, Popover } from "antd";
import { message, Popover, Button, Divider, Switch, Modal } from "antd";
import React, { useEffect, useState } from "react";
import './index.scss';
import axios from "axios";
import { secondsToTimeFormat, timeAgo } from "../../../common/DateUtil";
import { getPipelineIdByFileName } from "../../Information/api";
import { runPipeline, delPipelines, stopPipelines, startPipelines, getCardList } from '../api'
import Nodata from "../../Nodata";
import New from "./newModal";
const { confirm } = Modal;
function CardDevops(props) {
const { project, history, match, mygetHelmetapi } = props || {};
const { project, match } = props || {};
const {owner, projectsId} = match.params;
const [list, setList] = useState([])
const [ visible , setVisible ] = useState(false);
useEffect(() => {
const { author, name } = project || {}
@ -25,13 +28,63 @@ function CardDevops(props) {
}, [])
function getList(){
axios.get(`/v1/${owner}/${projectsId}/actions/new_index.json`).then(res=>{
if(res && res.data){
setList(res.data.data);
getCardList(owner, projectsId).then(res => {
if (res.status === 0) {
setList(res.pipelines.map(e => {
e.run_data = e.run_data || {}
return e
}))
}
})
}
// 线
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('运行成功!')
window.open(`http://localhost:8000/devops/${owner}/${projectsId}/${item.id}`)
}
})
}
// /线
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: '图形化',
@ -45,35 +98,55 @@ function CardDevops(props) {
}
return <div className="cardList">
{list && list.map(e=>{
return <div className={`card developStatus${e.status}`} onClick={()=>{
getPipelineIdByFileName(owner, owner, projectsId, e.filename).then(res=>{
if(res.data && res.data.code === 200){
window.location.href = `${mygetHelmetapi && mygetHelmetapi.common.zone}/${owner}/pipeline/${res.data.data.pipelineId}`
}else{
message.error("发生错误,请联系平台管理员")
}
})
}}>
<div className="statusBar"></div>
<div className="cardContent">
<div className="cartHead">
<div className={e.pipeline_type === 1 ? "f36" : ''}>{listType[e.pipeline_type]}</div>
<Popover content={e.name} ><span className="task-hide weight">{e.name}</span></Popover>
</div>
<div className="cardBody">
<p>
<div className={`statusIcon developStatus${e.status}`}></div>
{statusEnum[e.status] || e.filename ? "待启动" : "待设计"}
{!!e.index && <span>#{e.index} &nbsp;&nbsp;&nbsp;{timeAgo(e.stopped)}&nbsp;执行时长{secondsToTimeFormat(e.length)}</span>}
</p>
<p className="mt5"><i className="iconfont icon-a-fenzhi1 font-12 mr4 mb3"></i> 分支{e.branch}</p>
<div className="mt5">{e.schedule && <span className="scheduleBox mr10">定时</span>}历史共执行{e.total || 0}成功{e.success || 0}失败{e.failure || 0}</div>
<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=>{
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(`http://localhost:8000/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.file_name ? "待启动" : "待设计")}
{!!e.run_data.index && <span>#{e.run_data.index} &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(`http://localhost:8000/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>
</div>
})}
{list && !list.length && <div style={{margin: '100px auto'}}><Nodata _html="暂无数据"/></div>}
})}
{list && !list.length && <div style={{margin: '100px auto'}}><Nodata _html="暂无数据"/></div>}
</div>
</div>
}

View File

@ -1,7 +1,14 @@
.cardList {
display: flex;
flex-wrap: wrap;
margin: 30px 20px;
display: flex;
flex-direction: column;
.ant-btn-primary {
margin-left: auto;
}
.listData {
display: flex;
flex-wrap: wrap;
}
.card {
width: 333px;
border-radius: 6px;
@ -50,16 +57,23 @@
}
.cardBody {
margin-top: 12px;
background: rgba(212,223,241,0.24);
border-radius: 6px 6px 6px 6px;
padding: 15px;
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;
height: 75%;
p {
display: flex;
align-items: baseline;
line-height: 1;
}
div {
line-height: 22px;
}
.statusIcon {
width: 4px;
@ -82,6 +96,20 @@
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;
}
}
}
}
}
}
@ -121,3 +149,95 @@
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/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,177 @@
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);
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(`http://localhost:8000/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
>
{
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 => {
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

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

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

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

BIN
src/images/modal-back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/images/template.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB