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