仓库集成流水线
This commit is contained in:
parent
d3f7a7942f
commit
0b81808835
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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} {timeAgo(e.stopped)} 执行时长{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} {timeAgo(e.run_data.stopped)} 执行时长{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>
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 483 B |
Binary file not shown.
After Width: | Height: | Size: 698 B |
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
Loading…
Reference in New Issue