mirror of https://github.com/kubevela/velaux.git
Compare commits
11 Commits
Author | SHA1 | Date |
---|---|---|
|
6a0dc8c866 | |
|
2f68a03aa3 | |
|
67b8dc2afa | |
|
4f45d87afb | |
|
6548c3317c | |
|
e4c6008c73 | |
|
1a08d6888f | |
|
bd3f768293 | |
|
e4d73e1595 | |
|
fa4cbc0c82 | |
|
2d49a97045 |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "valaux",
|
"name": "valaux",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node scripts/start.js",
|
"start": "node scripts/start.js",
|
||||||
|
@ -69,6 +69,7 @@
|
||||||
"react-dnd": "^7.3.2",
|
"react-dnd": "^7.3.2",
|
||||||
"react-dnd-html5-backend": "^7.2.0",
|
"react-dnd-html5-backend": "^7.2.0",
|
||||||
"react-dom": "^16.3.0",
|
"react-dom": "^16.3.0",
|
||||||
|
"react-draggable": "^4.4.5",
|
||||||
"react-i18next": "11.13.0",
|
"react-i18next": "11.13.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"react-markdown": "7.1.0",
|
"react-markdown": "7.1.0",
|
||||||
|
|
|
@ -88,3 +88,8 @@ export function applyProjectConfigDistribution(
|
||||||
const urlPath = project + `/${projectName}/distributions`;
|
const urlPath = project + `/${projectName}/distributions`;
|
||||||
return post(urlPath, params).then((res) => res);
|
return post(urlPath, params).then((res) => res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteProjectConfigDistribution(projectName: string, name: string) {
|
||||||
|
const urlPath = project + `/${projectName}/distributions/${name}`;
|
||||||
|
return rdelete(urlPath, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { get, post, put, rdelete } from './request';
|
||||||
|
import { getDomain } from '../utils/common';
|
||||||
|
import type { CreatePipelineRequest } from '../interface/pipeline';
|
||||||
|
|
||||||
|
const baseURLOject = getDomain();
|
||||||
|
const base = baseURLOject.MOCK || baseURLOject.APIBASE;
|
||||||
|
|
||||||
|
export function listPipelines(params: { projectName?: string; query?: string }) {
|
||||||
|
const url = base + '/api/v1/pipelines';
|
||||||
|
return get(url, { params: params }).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPipeline(params: CreatePipelineRequest) {
|
||||||
|
const url = base + `/api/v1/projects/${params.project}/pipelines`;
|
||||||
|
return post(url, params).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePipeline(params: CreatePipelineRequest) {
|
||||||
|
const url = base + `/api/v1/projects/${params.project}/pipelines/${params.name}`;
|
||||||
|
return put(url, {
|
||||||
|
description: params.description,
|
||||||
|
alias: params.alias,
|
||||||
|
spec: params.spec,
|
||||||
|
}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPipeline(params: { projectName: string; pipelineName: string }) {
|
||||||
|
const url = base + `/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}`;
|
||||||
|
return get(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePipeline(params: { projectName: string; pipelineName: string }) {
|
||||||
|
const url = base + `/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}`;
|
||||||
|
return rdelete(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPipelineRuns(params: { projectName: string; pipelineName: string }) {
|
||||||
|
const url = base + `/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs`;
|
||||||
|
return get(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePipelineRun(params: {
|
||||||
|
projectName: string;
|
||||||
|
pipelineName: string;
|
||||||
|
runName: string;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
base +
|
||||||
|
`/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs/${params.runName}`;
|
||||||
|
return rdelete(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopPipelineRun(params: {
|
||||||
|
projectName: string;
|
||||||
|
pipelineName: string;
|
||||||
|
runName: string;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
base +
|
||||||
|
`/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs/${params.runName}/stop`;
|
||||||
|
return post(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPipelineRunBase(params: {
|
||||||
|
projectName: string;
|
||||||
|
pipelineName: string;
|
||||||
|
runName: string;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
base +
|
||||||
|
`/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs/${params.runName}`;
|
||||||
|
return get(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPipelineRunStatus(params: {
|
||||||
|
projectName: string;
|
||||||
|
pipelineName: string;
|
||||||
|
runName: string;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
base +
|
||||||
|
`/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs/${params.runName}/status`;
|
||||||
|
return get(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPipelineRunStepLogs(params: {
|
||||||
|
projectName: string;
|
||||||
|
pipelineName: string;
|
||||||
|
runName: string;
|
||||||
|
stepName: string;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
base +
|
||||||
|
`/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs/${params.runName}/log`;
|
||||||
|
return get(url, { params: { step: params.stepName } }).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listPipelineContexts(projectName: string, pipelineName: string) {
|
||||||
|
const url = base + `/api/v1/projects/${projectName}/pipelines/${pipelineName}/contexts`;
|
||||||
|
return get(url, {}).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPipelineRunStepOutputs(params: {
|
||||||
|
projectName: string;
|
||||||
|
pipelineName: string;
|
||||||
|
runName: string;
|
||||||
|
stepName: string;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
base +
|
||||||
|
`/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs/${params.runName}/output`;
|
||||||
|
return get(url, { params: { step: params.stepName } }).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPipelineRunStepInputs(params: {
|
||||||
|
projectName: string;
|
||||||
|
pipelineName: string;
|
||||||
|
runName: string;
|
||||||
|
stepName: string;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
base +
|
||||||
|
`/api/v1/projects/${params.projectName}/pipelines/${params.pipelineName}/runs/${params.runName}/input`;
|
||||||
|
return get(url, { params: { step: params.stepName } }).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPipelineContext(
|
||||||
|
projectName: string,
|
||||||
|
pipelineName: string,
|
||||||
|
context: {
|
||||||
|
name: string;
|
||||||
|
values: { key: string; value: string }[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const url = base + `/api/v1/projects/${projectName}/pipelines/${pipelineName}/contexts`;
|
||||||
|
return post(url, context).then((res) => res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runPipeline(projectName: string, pipelineName: string, contextName?: string) {
|
||||||
|
const url = base + `/api/v1/projects/${projectName}/pipelines/${pipelineName}/run`;
|
||||||
|
return post(url, contextName ? { contextName } : {}).then((res) => res);
|
||||||
|
}
|
|
@ -364,14 +364,30 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
.commonDialog {
|
.commonDialog {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
padding: 0 !important;
|
||||||
|
.next-dialog-header {
|
||||||
|
color: var(--grey-900);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.next-dialog-close {
|
||||||
|
.next-dialog-close-icon {
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.next-dialog-footer {
|
.next-dialog-footer {
|
||||||
display: flex;
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
|
width: 100%;
|
||||||
.next-btn {
|
.next-btn {
|
||||||
display: block;
|
display: block;
|
||||||
flex: none !important;
|
flex: none !important;
|
||||||
|
@ -380,8 +396,10 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
.next-dialog-body {
|
.next-dialog-body {
|
||||||
|
flex: 1;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
|
margin-bottom: 48px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basic {
|
.basic {
|
||||||
|
@ -423,10 +441,6 @@ a {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.next-dialog-body {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -474,10 +488,16 @@ a {
|
||||||
color: @dangerColor !important;
|
color: @dangerColor !important;
|
||||||
border: @dangerColor 1px solid !important;
|
border: @dangerColor 1px solid !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.next-btn.next-btn-text.danger-btn {
|
||||||
|
border: @dangerColor 0px solid !important;
|
||||||
|
}
|
||||||
|
|
||||||
.danger-btn:hover {
|
.danger-btn:hover {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
background: @dangerColor !important;
|
background: @dangerColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-icon:hover {
|
.danger-icon:hover {
|
||||||
color: @dangerColor !important;
|
color: @dangerColor !important;
|
||||||
}
|
}
|
||||||
|
@ -485,3 +505,32 @@ a {
|
||||||
.line {
|
.line {
|
||||||
border: solid 0.2px rgba(0, 0, 0, 0.1);
|
border: solid 0.2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
.next-breadcrumb {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.next-breadcrumb-text,
|
||||||
|
.next-breadcrumb-separator {
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
a {
|
||||||
|
color: @primarycolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn,
|
||||||
|
.inline-center {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-item {
|
||||||
|
svg {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
color: var(--grey-500);
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ type Props = {
|
||||||
id?: string;
|
id?: string;
|
||||||
onChange?: (params: any) => void;
|
onChange?: (params: any) => void;
|
||||||
onBlurEditor?: (value: string) => void;
|
onBlurEditor?: (value: string) => void;
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class DefinitionCode extends React.Component<Props> {
|
class DefinitionCode extends React.Component<Props> {
|
||||||
|
@ -88,8 +87,7 @@ class DefinitionCode extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { id, style } = this.props;
|
return null;
|
||||||
return <div style={style} id={id} />;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,4 @@
|
||||||
.drawer-footer {
|
.customDrawer.next-drawer {
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: calc(~'100% - 40px');
|
|
||||||
height: 60px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-drawer {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -27,6 +16,19 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
.next-drawer-close {
|
||||||
|
right: 26px !important;
|
||||||
|
|
||||||
|
.next-drawer-close-icon.next-icon::before {
|
||||||
|
color: #000;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
font-size: 24px !important;
|
||||||
|
line-height: 24px !important;
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.next-drawer-body {
|
.next-drawer-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -35,19 +37,15 @@
|
||||||
margin-top: 71px;
|
margin-top: 71px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
.drawer-footer {
|
||||||
}
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
.next-drawer-close {
|
display: flex;
|
||||||
right: 26px !important;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
.next-drawer-close-icon.next-icon::before {
|
width: calc(~'100% - 40px');
|
||||||
color: #000;
|
height: 60px;
|
||||||
font-weight: 800 !important;
|
background: #fff;
|
||||||
font-size: 24px !important;
|
|
||||||
line-height: 24px !important;
|
|
||||||
&:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ class DrawerWithFooter extends Component<Props, any> {
|
||||||
<Drawer
|
<Drawer
|
||||||
title={title}
|
title={title}
|
||||||
closeMode="close"
|
closeMode="close"
|
||||||
|
className="customDrawer"
|
||||||
closeable="close"
|
closeable="close"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
visible={true}
|
visible={true}
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 32px 16px;
|
||||||
svg {
|
svg {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
.message {
|
.message {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
color: var(--grey-500);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,60 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
import Translation from '../Translation';
|
import Translation from '../Translation';
|
||||||
|
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message?: string | React.ReactNode;
|
message?: string | React.ReactNode;
|
||||||
style?: {};
|
style?: React.CSSProperties;
|
||||||
iconWidth?: string;
|
iconWidth?: string;
|
||||||
|
hideIcon?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Empty = (props: Props) => {
|
const Empty = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="empty" style={props.style}>
|
<div className="empty" style={props.style}>
|
||||||
<div>
|
<div>
|
||||||
<svg
|
<If condition={!props.hideIcon}>
|
||||||
style={{ width: props.iconWidth }}
|
<svg
|
||||||
viewBox="0 0 1024 1024"
|
style={{ width: props.iconWidth }}
|
||||||
version="1.1"
|
viewBox="0 0 1024 1024"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
version="1.1"
|
||||||
p-id="1890"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
p-id="1890"
|
||||||
<path
|
>
|
||||||
d="M512 0C229.261369 0 0 229.261369 0 512c0 282.738631 229.261369 512 512 512 282.738631 0 512-229.261369 512-512C1024 229.261369 794.738631 0 512 0L512 0zM512 999.82009c-269.049475 0-487.948026-218.898551-487.948026-487.948026 0-269.049475 218.898551-487.948026 487.948026-487.948026 269.049475 0 487.948026 218.898551 487.948026 487.948026C999.82009 781.049475 781.049475 999.82009 512 999.82009L512 999.82009zM512 999.82009"
|
<path
|
||||||
p-id="1891"
|
d="M512 0C229.261369 0 0 229.261369 0 512c0 282.738631 229.261369 512 512 512 282.738631 0 512-229.261369 512-512C1024 229.261369 794.738631 0 512 0L512 0zM512 999.82009c-269.049475 0-487.948026-218.898551-487.948026-487.948026 0-269.049475 218.898551-487.948026 487.948026-487.948026 269.049475 0 487.948026 218.898551 487.948026 487.948026C999.82009 781.049475 781.049475 999.82009 512 999.82009L512 999.82009zM512 999.82009"
|
||||||
fill="#bfbfbf"
|
p-id="1891"
|
||||||
/>
|
fill="#bfbfbf"
|
||||||
<path
|
/>
|
||||||
d="M223.632184 430.888556c0 16.375812 6.78061 32.623688 18.294853 44.265867 11.642179 11.642179 27.890055 18.294853 44.265867 18.294853 16.375812 0 32.623688-6.78061 44.265867-18.294853 11.642179-11.642179 18.294853-27.890055 18.294853-44.265867 0-16.375812-6.78061-32.751624-18.294853-44.265867-11.642179-11.642179-27.890055-18.294853-44.265867-18.294853-16.375812 0-32.751624 6.78061-44.265867 18.294853C230.412794 398.264868 223.632184 414.512744 223.632184 430.888556L223.632184 430.888556zM223.632184 430.888556"
|
<path
|
||||||
p-id="1892"
|
d="M223.632184 430.888556c0 16.375812 6.78061 32.623688 18.294853 44.265867 11.642179 11.642179 27.890055 18.294853 44.265867 18.294853 16.375812 0 32.623688-6.78061 44.265867-18.294853 11.642179-11.642179 18.294853-27.890055 18.294853-44.265867 0-16.375812-6.78061-32.751624-18.294853-44.265867-11.642179-11.642179-27.890055-18.294853-44.265867-18.294853-16.375812 0-32.751624 6.78061-44.265867 18.294853C230.412794 398.264868 223.632184 414.512744 223.632184 430.888556L223.632184 430.888556zM223.632184 430.888556"
|
||||||
fill="#bfbfbf"
|
p-id="1892"
|
||||||
/>
|
fill="#bfbfbf"
|
||||||
<path
|
/>
|
||||||
d="M674.606697 424.61969c0 16.503748 6.78061 33.007496 18.550725 44.777611 11.642179 11.642179 28.145927 18.550725 44.649675 18.550725 16.503748 0 33.007496-6.78061 44.777611-18.550725 11.770115-11.770115 18.550725-28.145927 18.550725-44.777611 0-16.503748-6.78061-33.007496-18.550725-44.777611-11.642179-11.642179-28.145927-18.550725-44.777611-18.550725-16.503748 0-33.007496 6.78061-44.649675 18.550725C681.387306 391.612194 674.606697 408.115942 674.606697 424.61969L674.606697 424.61969zM674.606697 424.61969"
|
<path
|
||||||
p-id="1893"
|
d="M674.606697 424.61969c0 16.503748 6.78061 33.007496 18.550725 44.777611 11.642179 11.642179 28.145927 18.550725 44.649675 18.550725 16.503748 0 33.007496-6.78061 44.777611-18.550725 11.770115-11.770115 18.550725-28.145927 18.550725-44.777611 0-16.503748-6.78061-33.007496-18.550725-44.777611-11.642179-11.642179-28.145927-18.550725-44.777611-18.550725-16.503748 0-33.007496 6.78061-44.649675 18.550725C681.387306 391.612194 674.606697 408.115942 674.606697 424.61969L674.606697 424.61969zM674.606697 424.61969"
|
||||||
fill="#bfbfbf"
|
p-id="1893"
|
||||||
/>
|
fill="#bfbfbf"
|
||||||
<path
|
/>
|
||||||
d="M385.471264 275.702149c-4.605697-11.642179-17.3993-17.527236-28.785607-13.049475l-153.77911 60.257871c-11.386307 4.477761-16.75962 17.527236-12.281859 29.041479l4.093953 10.490755c4.605697 11.642179 17.3993 17.527236 28.785607 13.049475l153.77911-60.257871c11.386307-4.477761 16.75962-17.527236 12.281859-29.169415L385.471264 275.702149 385.471264 275.702149zM385.471264 275.702149"
|
<path
|
||||||
p-id="1894"
|
d="M385.471264 275.702149c-4.605697-11.642179-17.3993-17.527236-28.785607-13.049475l-153.77911 60.257871c-11.386307 4.477761-16.75962 17.527236-12.281859 29.041479l4.093953 10.490755c4.605697 11.642179 17.3993 17.527236 28.785607 13.049475l153.77911-60.257871c11.386307-4.477761 16.75962-17.527236 12.281859-29.169415L385.471264 275.702149 385.471264 275.702149zM385.471264 275.702149"
|
||||||
fill="#bfbfbf"
|
p-id="1894"
|
||||||
/>
|
fill="#bfbfbf"
|
||||||
<path
|
/>
|
||||||
d="M830.688656 318.816592l-156.465767-52.965517c-11.514243-3.838081-24.17991 2.558721-28.145927 14.328836l-3.582209 10.746627c-3.966017 11.898051 2.046977 24.563718 13.561219 28.529735l156.465767 52.965517c11.514243 3.966017 24.051974-2.558721 28.145927-14.328836l3.582209-10.746627C848.343828 335.448276 842.202899 322.654673 830.688656 318.816592L830.688656 318.816592zM830.688656 318.816592"
|
<path
|
||||||
p-id="1895"
|
d="M830.688656 318.816592l-156.465767-52.965517c-11.514243-3.838081-24.17991 2.558721-28.145927 14.328836l-3.582209 10.746627c-3.966017 11.898051 2.046977 24.563718 13.561219 28.529735l156.465767 52.965517c11.514243 3.966017 24.051974-2.558721 28.145927-14.328836l3.582209-10.746627C848.343828 335.448276 842.202899 322.654673 830.688656 318.816592L830.688656 318.816592zM830.688656 318.816592"
|
||||||
fill="#bfbfbf"
|
p-id="1895"
|
||||||
/>
|
fill="#bfbfbf"
|
||||||
<path
|
/>
|
||||||
d="M696.995502 663.604198c-6.268866-9.083458-18.550725-11.514243-27.378311-5.501249l-120.515742 82.902549c-8.827586 6.14093-11.002499 18.422789-4.733633 27.634183l5.629185 8.187906c6.268866 9.083458 18.550725 11.514243 27.378311 5.501249l120.515742-82.902549c8.827586-6.14093 11.002499-18.422789 4.733633-27.634183L696.995502 663.604198 696.995502 663.604198zM696.995502 663.604198"
|
<path
|
||||||
p-id="1896"
|
d="M696.995502 663.604198c-6.268866-9.083458-18.550725-11.514243-27.378311-5.501249l-120.515742 82.902549c-8.827586 6.14093-11.002499 18.422789-4.733633 27.634183l5.629185 8.187906c6.268866 9.083458 18.550725 11.514243 27.378311 5.501249l120.515742-82.902549c8.827586-6.14093 11.002499-18.422789 4.733633-27.634183L696.995502 663.604198 696.995502 663.604198zM696.995502 663.604198"
|
||||||
fill="#bfbfbf"
|
p-id="1896"
|
||||||
/>
|
fill="#bfbfbf"
|
||||||
</svg>
|
/>
|
||||||
|
</svg>
|
||||||
|
</If>
|
||||||
</div>
|
</div>
|
||||||
<div className="message">{props.message || <Translation>Empty Data</Translation>}</div>
|
<div className="message">{props.message || <Translation>Empty Data</Translation>}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
.title-wrapper {
|
.title-wrapper {
|
||||||
height: 45px;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 0;
|
||||||
|
line-height: 36px;
|
||||||
.title {
|
.title {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
@ -9,10 +13,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.float-right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 719px) and (min-width: 480px) {
|
@media (max-width: 719px) and (min-width: 480px) {
|
||||||
.title-wrapper {
|
.title-wrapper {
|
||||||
.subTitle {
|
.subTitle {
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Grid } from '@b-design/ui';
|
import { Button } from '@b-design/ui';
|
||||||
import Translation from '../Translation';
|
import Translation from '../Translation';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { If } from 'tsx-control-statements/components';
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
subTitle: string;
|
subTitle?: string;
|
||||||
extButtons?: [React.ReactNode];
|
extButtons?: [React.ReactNode];
|
||||||
addButtonTitle?: string;
|
addButtonTitle?: string;
|
||||||
addButtonClick?: () => void;
|
addButtonClick?: () => void;
|
||||||
|
buttonSize?: 'small' | 'medium' | 'large';
|
||||||
};
|
};
|
||||||
export default function (props: Props) {
|
export default function (props: Props) {
|
||||||
const { Row, Col } = Grid;
|
const { title, subTitle, extButtons, addButtonTitle, addButtonClick, buttonSize } = props;
|
||||||
const { title, subTitle, extButtons, addButtonTitle, addButtonClick } = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="title-wrapper">
|
||||||
<Row className="title-wrapper" wrap={true}>
|
<div>
|
||||||
<Col xl={15} xs={24}>
|
<span className="title font-size-20">
|
||||||
<span className="title font-size-20">
|
<Translation>{title}</Translation>
|
||||||
<Translation>{title}</Translation>
|
</span>
|
||||||
</span>
|
|
||||||
|
{subTitle && (
|
||||||
<span className="subTitle font-size-14">
|
<span className="subTitle font-size-14">
|
||||||
<Translation>{subTitle}</Translation>
|
<Translation>{subTitle}</Translation>
|
||||||
</span>
|
</span>
|
||||||
</Col>
|
)}
|
||||||
<Col xl={9} xs={24}>
|
</div>
|
||||||
<div className="float-right">
|
|
||||||
{extButtons &&
|
<div className="float-right">
|
||||||
extButtons.map((item) => {
|
{extButtons &&
|
||||||
return item;
|
extButtons.map((item) => {
|
||||||
})}
|
return item;
|
||||||
<If condition={addButtonTitle}>
|
})}
|
||||||
<Button type="primary" onClick={addButtonClick}>
|
<If condition={addButtonTitle}>
|
||||||
<Translation>{addButtonTitle ? addButtonTitle : ''}</Translation>
|
<Button size={buttonSize ? buttonSize : 'medium'} type="primary" onClick={addButtonClick}>
|
||||||
</Button>
|
<Translation>{addButtonTitle ? addButtonTitle : ''}</Translation>
|
||||||
</If>
|
</Button>
|
||||||
</div>
|
</If>
|
||||||
</Col>
|
</div>
|
||||||
</Row>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { WorkflowStepStatus } from '../../../interface/application';
|
||||||
|
|
||||||
|
export function renderStepStatusIcon(status: WorkflowStepStatus) {
|
||||||
|
switch (status.phase) {
|
||||||
|
case 'succeeded':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
className="step-icon color-fg-success"
|
||||||
|
aria-label="completed successfully"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8 16A8 8 0 108 0a8 8 0 000 16zm3.78-9.72a.75.75 0 00-1.06-1.06L6.75 9.19 5.28 7.72a.75.75 0 00-1.06 1.06l2 2a.75.75 0 001.06 0l4.5-4.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case 'failed':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
className="step-icon color-fg-danger"
|
||||||
|
aria-label="failed"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case 'skipped':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
className="step-icon neutral-check"
|
||||||
|
aria-label="cancelled"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.47.22A.75.75 0 015 0h6a.75.75 0 01.53.22l4.25 4.25c.141.14.22.331.22.53v6a.75.75 0 01-.22.53l-4.25 4.25A.75.75 0 0111 16H5a.75.75 0 01-.53-.22L.22 11.53A.75.75 0 010 11V5a.75.75 0 01.22-.53L4.47.22zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5H5.31zM8 4a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 4zm0 8a1 1 0 100-2 1 1 0 000 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case 'running':
|
||||||
|
return (
|
||||||
|
<div className="step-icon running-icon">
|
||||||
|
<svg
|
||||||
|
aria-label="currently running"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
className="icon-rotate"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
opacity=".5"
|
||||||
|
d="M8 15A7 7 0 108 1a7 7 0 000 14v0z"
|
||||||
|
stroke="#dbab0a"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<path d="M15 8a7 7 0 01-7 7" stroke="#dbab0a" stroke-width="2" />
|
||||||
|
<path d="M8 12a4 4 0 100-8 4 4 0 000 8z" fill="#dbab0a" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
width="16"
|
||||||
|
data-view-component="true"
|
||||||
|
className="step-icon pending-icon"
|
||||||
|
>
|
||||||
|
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
import type { WorkflowStepStatus } from '../../../interface/application';
|
||||||
|
import { timeDiff } from '../../../utils/common';
|
||||||
|
import { renderStepStatusIcon } from './step-icon';
|
||||||
|
|
||||||
|
export interface StepProps {
|
||||||
|
step: WorkflowStepStatus;
|
||||||
|
output: boolean;
|
||||||
|
input: boolean;
|
||||||
|
probeState: {
|
||||||
|
stepWidth: number;
|
||||||
|
stepInterval: number;
|
||||||
|
};
|
||||||
|
group: boolean;
|
||||||
|
onNodeClick: (step: WorkflowStepStatus) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Step = (props: StepProps) => {
|
||||||
|
const { step, output, input, onNodeClick, group } = props;
|
||||||
|
const { stepWidth, stepInterval } = props.probeState;
|
||||||
|
const [isActive, setActive] = useState(false);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('step', { active: isActive }, { group: group })}
|
||||||
|
onMouseEnter={() => setActive(true)}
|
||||||
|
onMouseLeave={() => setActive(false)}
|
||||||
|
style={{ marginRight: stepInterval + 'px' }}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!group) {
|
||||||
|
onNodeClick(props.step);
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<If condition={group}>
|
||||||
|
<div className="step-title">{step.name || step.id}</div>
|
||||||
|
<div className="groups" style={{ width: stepWidth + 'px' }}>
|
||||||
|
{step.subSteps?.map((subStep) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="step-status"
|
||||||
|
onClick={(event) => {
|
||||||
|
onNodeClick(subStep);
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{renderStepStatusIcon(subStep)}</div>
|
||||||
|
<div className="step-name">{subStep.name || subStep.id}</div>
|
||||||
|
<div>{timeDiff(subStep.firstExecuteTime, subStep.lastExecuteTime)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<If condition={!group}>
|
||||||
|
<div className="groups" style={{ width: stepWidth + 'px' }}>
|
||||||
|
<div className="step-status">
|
||||||
|
<div>{renderStepStatusIcon(step)}</div>
|
||||||
|
<div className="step-name">{step.name || step.id}</div>
|
||||||
|
<div className="">{timeDiff(step.firstExecuteTime, step.lastExecuteTime)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<If condition={output}>
|
||||||
|
<div className="workflow-step-port workflow-step-port-output step-circle" />
|
||||||
|
</If>
|
||||||
|
<If condition={input}>
|
||||||
|
<div className="workflow-step-port workflow-step-port-input step-circle" />
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,179 @@
|
||||||
|
.workflow-graph {
|
||||||
|
--color-workflow-step-bg: #fff;
|
||||||
|
--color-workflow-step-connector-bg: #85d4ff;
|
||||||
|
--color-border-default: #85d4ff;
|
||||||
|
--color-workflow-step-header-shadow: #fff;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
transform-origin: left top;
|
||||||
|
cursor: grab;
|
||||||
|
.step {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
background-color: #fff;
|
||||||
|
border: var(--color-border-default) 1px solid;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 8px 16px 0px #60617029;
|
||||||
|
&.active {
|
||||||
|
--color-border-default: var(--primary-color);
|
||||||
|
--color-workflow-step-connector-bg: var(--primary-color);
|
||||||
|
}
|
||||||
|
&.group {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
}
|
||||||
|
.step-title {
|
||||||
|
position: absolute;
|
||||||
|
top: -21px;
|
||||||
|
left: -1px;
|
||||||
|
padding: 4px var(--padding-common) 0 var(--padding-common);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
background-color: var(--color-workflow-step-bg);
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
box-shadow: inset 0 1px 0 var(--color-border-default),
|
||||||
|
inset 1px 0 0 var(--color-border-default), inset -1px 0 0 var(--color-border-default),
|
||||||
|
0 -1px 2px var(--color-workflow-step-header-shadow);
|
||||||
|
transition: background-color ease-out 0.12s, box-shadow ease-out 0.12s;
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: -19px;
|
||||||
|
width: 20px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 16px;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
box-shadow: inset 1px 0 0 var(--color-border-default),
|
||||||
|
inset 0 -1px 0 var(--color-border-default), -1px 3px var(--color-workflow-step-bg);
|
||||||
|
transition: box-shadow ease-out 0.12s;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.groups {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--padding-common);
|
||||||
|
transition: background-color ease-out 0.12s, border-color ease-out 0.12s,
|
||||||
|
box-shadow ease-out 0.12s;
|
||||||
|
}
|
||||||
|
.workflow-step-port {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background-color: var(--color-workflow-step-bg);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color ease-out 0.12s;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--color-workflow-step-connector-bg);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color ease-out 0.12s;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.workflow-step-port-output {
|
||||||
|
right: -8px;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-workflow-step-bg) 0%,
|
||||||
|
var(--color-workflow-step-bg) 50%,
|
||||||
|
var(--color-border-default) 50%,
|
||||||
|
var(--color-border-default) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.workflow-step-port-input {
|
||||||
|
left: -8px;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
270deg,
|
||||||
|
var(--color-workflow-step-bg) 0%,
|
||||||
|
var(--color-workflow-step-bg) 50%,
|
||||||
|
var(--color-border-default) 50%,
|
||||||
|
var(--color-border-default) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.workflow-connectors {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
transform-origin: left top;
|
||||||
|
}
|
||||||
|
.workflow-connector {
|
||||||
|
transition: stroke ease-out 0.12s, stroke-width ease-out 0.12s, opacity ease-out 0.12s;
|
||||||
|
stroke: var(--color-workflow-step-connector-bg);
|
||||||
|
stroke-width: 2px;
|
||||||
|
&:hover {
|
||||||
|
stroke: var(--color-workflow-step-connector-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-status {
|
||||||
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 6px;
|
||||||
|
&:hover {
|
||||||
|
background: #f6f4ed;
|
||||||
|
}
|
||||||
|
.step-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 18px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
overflow: visible;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
.step-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.color-fg-success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
.color-fg-danger {
|
||||||
|
color: var(--failed-color);
|
||||||
|
}
|
||||||
|
.icon-rotate {
|
||||||
|
animation: rotate-keyframes 1s linear infinite;
|
||||||
|
}
|
||||||
|
.pending-icon {
|
||||||
|
color: var(--running-color);
|
||||||
|
}
|
||||||
|
@keyframes rotate-keyframes {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
animation-duration: 1s;
|
||||||
|
animation-play-state: running;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: normal;
|
||||||
|
animation-fill-mode: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { WorkflowStepStatus } from '../../interface/application';
|
||||||
|
import type { PipelineRunStatus } from '../../interface/pipeline';
|
||||||
|
import Draggable from 'react-draggable';
|
||||||
|
|
||||||
|
import './index.less';
|
||||||
|
import { Step } from './components/step';
|
||||||
|
|
||||||
|
type PipelineGraphProps = {
|
||||||
|
pipeline: PipelineRunStatus;
|
||||||
|
zoom: number;
|
||||||
|
onNodeClick: (step: WorkflowStepStatus) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
stepWidth: number;
|
||||||
|
stepInterval: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PipelineGraph extends React.Component<PipelineGraphProps, State> {
|
||||||
|
constructor(props: PipelineGraphProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
stepWidth: 260,
|
||||||
|
stepInterval: 56,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderConnector(index: number, total: number, from: string, to: string) {
|
||||||
|
const { stepInterval, stepWidth } = this.state;
|
||||||
|
const startPoint = stepWidth + (index - 1) * (stepWidth + stepInterval);
|
||||||
|
const endPoint = startPoint + stepInterval;
|
||||||
|
const width = (stepInterval + stepWidth) * total;
|
||||||
|
return (
|
||||||
|
<svg className="workflow-connectors" width={width} height={300}>
|
||||||
|
<path
|
||||||
|
className="workflow-connector"
|
||||||
|
data-from={'step-' + from}
|
||||||
|
data-to={'step-' + to}
|
||||||
|
fill="none"
|
||||||
|
d={`M ${startPoint} 38 H ${endPoint}`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { pipeline, zoom } = this.props;
|
||||||
|
const steps = pipeline.steps;
|
||||||
|
return (
|
||||||
|
<Draggable>
|
||||||
|
<div
|
||||||
|
className="workflow-graph"
|
||||||
|
style={{
|
||||||
|
transform: `scale(${zoom})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{steps &&
|
||||||
|
steps.length > 1 &&
|
||||||
|
steps.map((step, i: number) => {
|
||||||
|
if (i < steps.length - 1) {
|
||||||
|
return this.renderConnector(i + 1, steps.length, step.id, steps[i + 1].id);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{steps &&
|
||||||
|
steps.map((step, i: number) => {
|
||||||
|
return (
|
||||||
|
<Step
|
||||||
|
probeState={this.state}
|
||||||
|
step={step}
|
||||||
|
group={step.type == 'step-group'}
|
||||||
|
output={i < steps.length - 1}
|
||||||
|
input={i !== 0}
|
||||||
|
onNodeClick={this.props.onNodeClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PipelineGraph;
|
|
@ -340,7 +340,7 @@ class PlatformSetting extends React.Component<Props, State> {
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<If condition={this.field.getValue('loginType') == 'dex'}>
|
<If condition={this.field.getValue('loginType') == 'dex'}>
|
||||||
<a href={this.generateDexAddress()} target="_blank">
|
<a href={this.generateDexAddress()} target="_blank" rel="noopener noreferrer">
|
||||||
<Translation>Click me to test open the dex page.</Translation>
|
<Translation>Click me to test open the dex page.</Translation>
|
||||||
</a>
|
</a>
|
||||||
</If>
|
</If>
|
||||||
|
@ -416,7 +416,11 @@ class PlatformSetting extends React.Component<Props, State> {
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Translation>User experience improvement plan</Translation>
|
<Translation>User experience improvement plan</Translation>
|
||||||
<a target="_blank" href="https://kubevela.io/docs/reference/user-improvement-plan">
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://kubevela.io/docs/reference/user-improvement-plan"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<Icon style={{ marginLeft: '4px' }} type="help" />
|
<Icon style={{ marginLeft: '4px' }} type="help" />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
.context-box {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.notice {
|
||||||
|
margin: var(--spacing-4) 0;
|
||||||
|
color: var(--grey-500);
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
&.success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.context-item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: var(--spacing-4);
|
||||||
|
padding: 16px;
|
||||||
|
border: solid 1px var(--grey-200);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
.context-name {
|
||||||
|
margin-right: var(--spacing-4);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
border: solid 2px var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Dialog, Loading, Tag } from '@b-design/ui';
|
||||||
|
import { listPipelineContexts, runPipeline } from '../../api/pipeline';
|
||||||
|
import i18n from '../../i18n';
|
||||||
|
import type { KeyValue, Pipeline } from '../../interface/pipeline';
|
||||||
|
import './index.less';
|
||||||
|
import locale from '../../utils/locale';
|
||||||
|
import ListTitle from '../ListTitle';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
import NewContext from './new-context';
|
||||||
|
|
||||||
|
export interface PipelineProps {
|
||||||
|
pipeline: Pipeline;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: (runName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunPipeline = (props: PipelineProps) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [contexts, setContexts] = useState<Record<string, KeyValue[]>>({});
|
||||||
|
const [contextName, setSelectContextName] = useState('');
|
||||||
|
const [addContext, showAddContext] = useState(false);
|
||||||
|
const { pipeline } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
listPipelineContexts(pipeline.project.name, pipeline.name)
|
||||||
|
.then((res) => {
|
||||||
|
setContexts(res && res.contexts ? res.contexts : []);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [pipeline, loading]);
|
||||||
|
|
||||||
|
const onRunPipeline = () => {
|
||||||
|
runPipeline(pipeline.project.name, pipeline.name, contextName).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
if (props.onSuccess) {
|
||||||
|
props.onSuccess(res.pipelineRunName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="commonDialog"
|
||||||
|
visible={true}
|
||||||
|
locale={locale().Dialog}
|
||||||
|
title={i18n.t('Run Pipeline')}
|
||||||
|
onClose={props.onClose}
|
||||||
|
onCancel={props.onClose}
|
||||||
|
onOk={onRunPipeline}
|
||||||
|
isFullScreen={true}
|
||||||
|
>
|
||||||
|
<Loading style={{ width: '100%' }} visible={loading}>
|
||||||
|
<div className="context-box">
|
||||||
|
<ListTitle
|
||||||
|
title={i18n.t('Select Contexts')}
|
||||||
|
subTitle={i18n.t('The context is the runtime inputs for the Pipeline')}
|
||||||
|
buttonSize={'small'}
|
||||||
|
addButtonTitle="New Context"
|
||||||
|
addButtonClick={() => {
|
||||||
|
showAddContext(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{Object.keys(contexts).map((key: string) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className={classNames('context-item', { active: contextName === key })}
|
||||||
|
onClick={() => {
|
||||||
|
if (contextName != key) {
|
||||||
|
setSelectContextName(key);
|
||||||
|
} else {
|
||||||
|
setSelectContextName('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="context-name">{key}</div>
|
||||||
|
<div className="context-values">
|
||||||
|
{Array.isArray(contexts[key]) &&
|
||||||
|
contexts[key].map((item) => {
|
||||||
|
return <Tag>{`${item.key}=${item.value}`}</Tag>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<If condition={addContext}>
|
||||||
|
<div className="context-item">
|
||||||
|
<NewContext
|
||||||
|
pipeline={props.pipeline}
|
||||||
|
onCancel={() => showAddContext(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setLoading(true);
|
||||||
|
showAddContext(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<If condition={contextName == ''}>
|
||||||
|
<span className="notice">
|
||||||
|
No context is required for the execution of this Pipeline
|
||||||
|
</span>
|
||||||
|
</If>
|
||||||
|
<If condition={contextName != ''}>
|
||||||
|
<span className="notice success">Selected a context: {contextName}</span>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</Loading>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RunPipeline;
|
|
@ -0,0 +1,100 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Field } from '@b-design/ui';
|
||||||
|
import { Button, Form, Grid, Input } from '@b-design/ui';
|
||||||
|
import KV from '../../extends/KV';
|
||||||
|
import i18n from '../../i18n';
|
||||||
|
import Translation from '../Translation';
|
||||||
|
import { createPipelineContext } from '../../api/pipeline';
|
||||||
|
import type { KeyValue, Pipeline } from '../../interface/pipeline';
|
||||||
|
import { checkName } from '../../utils/common';
|
||||||
|
|
||||||
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
export interface NewContextProps {
|
||||||
|
pipeline: Pipeline;
|
||||||
|
onSuccess: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewContext extends React.Component<NewContextProps> {
|
||||||
|
field: Field;
|
||||||
|
|
||||||
|
constructor(props: NewContextProps) {
|
||||||
|
super(props);
|
||||||
|
this.field = new Field(this);
|
||||||
|
}
|
||||||
|
submitContext = () => {
|
||||||
|
this.field.validate((errs, values: any) => {
|
||||||
|
if (errs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { project, name } = this.props.pipeline;
|
||||||
|
const keyValues: KeyValue[] = [];
|
||||||
|
Object.keys(values.values).map((key) => {
|
||||||
|
keyValues.push({ key: key, value: values.values[key] });
|
||||||
|
});
|
||||||
|
createPipelineContext(project.name, name, { name: values.name, values: keyValues }).then(
|
||||||
|
(res) => {
|
||||||
|
if (res) {
|
||||||
|
this.props.onSuccess();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
render() {
|
||||||
|
const { init } = this.field;
|
||||||
|
return (
|
||||||
|
<Form field={this.field} style={{ width: '100%' }}>
|
||||||
|
<Row style={{ width: '100%' }} wrap>
|
||||||
|
<Col span={24}>
|
||||||
|
<Form.Item label="Name" required>
|
||||||
|
<Input
|
||||||
|
{...init('name', {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: i18n.t('You must input a context name'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: checkName,
|
||||||
|
message: i18n.t('You must input a valid name'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
placeholder={i18n.t('Please input the context name')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Form.Item label="Values" required>
|
||||||
|
<KV
|
||||||
|
{...init('values', {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: i18n.t('You must input at least one value'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<div className="flexcenter">
|
||||||
|
<Button type="secondary" onClick={this.submitContext} style={{ marginRight: '16px' }}>
|
||||||
|
<Translation>Submit</Translation>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={this.props.onCancel}>
|
||||||
|
<Translation>Cancel</Translation>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewContext;
|
|
@ -250,7 +250,12 @@ class UISchema extends Component<Props, State> {
|
||||||
<If condition={definition}>
|
<If condition={definition}>
|
||||||
<p>
|
<p>
|
||||||
Refer to the document:
|
Refer to the document:
|
||||||
<a style={{ marginLeft: '8px' }} target="_blank" href={this.renderDocumentURL()}>
|
<a
|
||||||
|
style={{ marginLeft: '8px' }}
|
||||||
|
target="_blank"
|
||||||
|
href={this.renderDocumentURL()}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
click here
|
click here
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -419,6 +424,7 @@ class UISchema extends Component<Props, State> {
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
disabled={disableEdit}
|
disabled={disableEdit}
|
||||||
|
autoComplete="off"
|
||||||
{...init(param.jsonKey, {
|
{...init(param.jsonKey, {
|
||||||
initValue: initValue,
|
initValue: initValue,
|
||||||
rules: convertRule(param.validate),
|
rules: convertRule(param.validate),
|
||||||
|
@ -438,6 +444,7 @@ class UISchema extends Component<Props, State> {
|
||||||
<Input
|
<Input
|
||||||
disabled={disableEdit}
|
disabled={disableEdit}
|
||||||
htmlType="password"
|
htmlType="password"
|
||||||
|
autoComplete="off"
|
||||||
{...init(param.jsonKey, {
|
{...init(param.jsonKey, {
|
||||||
initValue: initValue,
|
initValue: initValue,
|
||||||
rules: convertRule(param.validate),
|
rules: convertRule(param.validate),
|
||||||
|
|
|
@ -73,7 +73,7 @@ class ImageSecretSelect extends Component<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
const { disabled, onChange, value } = this.props;
|
const { disabled, onChange, value } = this.props;
|
||||||
const { registries, loading, inputRepo } = this.state;
|
const { registries, loading, inputRepo } = this.state;
|
||||||
const dataSource = registries;
|
const dataSource = registries || [];
|
||||||
if (inputRepo) {
|
if (inputRepo) {
|
||||||
dataSource.unshift({ secretName: inputRepo, name: inputRepo });
|
dataSource.unshift({ secretName: inputRepo, name: inputRepo });
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,4 +79,5 @@ export interface EnableAddonRequest {
|
||||||
version: string;
|
version: string;
|
||||||
properties: Record<string, any>;
|
properties: Record<string, any>;
|
||||||
clusters?: string[];
|
clusters?: string[];
|
||||||
|
registry?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,17 +142,21 @@ export interface WorkflowStatus {
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowStepStatus {
|
interface StepStatus {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
phase: string;
|
phase: 'succeeded' | 'failed' | 'skipped' | 'stopped' | 'running' | 'pending';
|
||||||
message: string;
|
message?: string;
|
||||||
reason: string;
|
reason?: string;
|
||||||
firstExecuteTime?: string;
|
firstExecuteTime?: string;
|
||||||
lastExecuteTime?: string;
|
lastExecuteTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStepStatus extends StepStatus {
|
||||||
|
subSteps?: StepStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Trait {
|
export interface Trait {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -309,6 +313,7 @@ export interface Trigger {
|
||||||
workflowName: string;
|
workflowName: string;
|
||||||
type: 'webhook';
|
type: 'webhook';
|
||||||
payloadType?: 'custom' | 'dockerHub' | 'ACR' | 'harbor' | 'artifactory';
|
payloadType?: 'custom' | 'dockerHub' | 'ACR' | 'harbor' | 'artifactory';
|
||||||
|
registry?: string;
|
||||||
token: string;
|
token: string;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
import type { Condition, InputItem, OutputItem, WorkflowStepStatus } from './application';
|
||||||
|
import type { NameAlias } from './env';
|
||||||
|
|
||||||
|
export interface CreatePipelineRequest {
|
||||||
|
name: string;
|
||||||
|
project: string;
|
||||||
|
alias?: string;
|
||||||
|
description?: string;
|
||||||
|
spec: {
|
||||||
|
steps: WorkflowStep[];
|
||||||
|
mode?: PipelineRunMode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface Pipeline {
|
||||||
|
name: string;
|
||||||
|
alias?: string;
|
||||||
|
description?: string;
|
||||||
|
project: NameAlias;
|
||||||
|
createTime?: string;
|
||||||
|
info?: {
|
||||||
|
lastRun?: PipelineRun;
|
||||||
|
runStat?: {
|
||||||
|
activeNum: number;
|
||||||
|
total: RunStateInfo;
|
||||||
|
week: RunStateInfo[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineRun extends PipelineRunBase {
|
||||||
|
status?: PipelineRunStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineDetail {
|
||||||
|
alias: string;
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
project: NameAlias;
|
||||||
|
spec: {
|
||||||
|
steps: WorkflowStep[];
|
||||||
|
mode?: PipelineRunMode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunStateInfo {
|
||||||
|
total: number;
|
||||||
|
success: number;
|
||||||
|
fail: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineRunMeta {
|
||||||
|
pipelineName: string;
|
||||||
|
project: NameAlias;
|
||||||
|
pipelineRunName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineRunBase extends PipelineRunMeta {
|
||||||
|
record: string;
|
||||||
|
contextName?: string;
|
||||||
|
contextValues?: KeyValue[];
|
||||||
|
spec: PipelineRunSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyValue {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineRunBriefing {
|
||||||
|
pipelineRunName: string;
|
||||||
|
finished: boolean;
|
||||||
|
phase: RunPhase;
|
||||||
|
message: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
contextName: string;
|
||||||
|
contextValues: KeyValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineRunSpec {
|
||||||
|
context?: Record<string, any>;
|
||||||
|
mode?: PipelineRunMode;
|
||||||
|
workflowSpec: {
|
||||||
|
steps: WorkflowStep[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkflowStepBase {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
meta?: {
|
||||||
|
alias?: string;
|
||||||
|
};
|
||||||
|
if?: string;
|
||||||
|
timeout?: string;
|
||||||
|
dependsOn?: string[];
|
||||||
|
inputs?: InputItem[];
|
||||||
|
outputs?: OutputItem[];
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStep extends WorkflowStepBase {
|
||||||
|
subSteps?: WorkflowStepBase[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineRunMode {
|
||||||
|
steps?: 'DAG' | 'StepByStep';
|
||||||
|
subSteps?: 'DAG' | 'StepByStep';
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunPhase =
|
||||||
|
| 'initializing'
|
||||||
|
| 'executing'
|
||||||
|
| 'suspending'
|
||||||
|
| 'terminated'
|
||||||
|
| 'failed'
|
||||||
|
| 'succeeded'
|
||||||
|
| 'skipped';
|
||||||
|
|
||||||
|
export interface PipelineRunStatus {
|
||||||
|
conditions?: Condition[];
|
||||||
|
mode?: PipelineRunMode;
|
||||||
|
status: RunPhase;
|
||||||
|
message?: string;
|
||||||
|
suspend: boolean;
|
||||||
|
suspendState?: string;
|
||||||
|
terminated: boolean;
|
||||||
|
finished: boolean;
|
||||||
|
steps?: WorkflowStepStatus[];
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStepOutputs {
|
||||||
|
stepName: string;
|
||||||
|
stepID: string;
|
||||||
|
values?: WorkflowStepOutputValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStepInputs {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
values?: WorkflowStepInputValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStepOutputValue {
|
||||||
|
name: string;
|
||||||
|
valueFrom: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStepInputValue {
|
||||||
|
from: string;
|
||||||
|
parameterKey?: string;
|
||||||
|
value: string;
|
||||||
|
fromStep: string;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Grid, Card, Breadcrumb, Button, Message, Dialog } from '@b-design/ui';
|
import { Grid, Card, Breadcrumb, Button, Message, Dialog, Icon } from '@b-design/ui';
|
||||||
import { connect } from 'dva';
|
import { connect } from 'dva';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
|
@ -25,6 +25,7 @@ import locale from '../../../../utils/locale';
|
||||||
import DeployConfig from '../DeployConfig';
|
import DeployConfig from '../DeployConfig';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import Permission from '../../../../components/Permission';
|
import Permission from '../../../../components/Permission';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const { Row, Col } = Grid;
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
@ -160,11 +161,11 @@ class ApplicationHeader extends Component<Props, State> {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={6} className="padding16">
|
<Col span={6} className={classNames('padding16', 'breadcrumb')}>
|
||||||
|
<Link to={'/'}>
|
||||||
|
<Icon type="home" />
|
||||||
|
</Link>
|
||||||
<Breadcrumb separator="/">
|
<Breadcrumb separator="/">
|
||||||
<Breadcrumb.Item>
|
|
||||||
<Translation>Projects</Translation>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Link to={'/projects/' + projectName}>{projectName}</Link>
|
<Link to={'/projects/' + projectName}>{projectName}</Link>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
|
|
|
@ -119,6 +119,7 @@ class CloudShell extends Component<Props, State> {
|
||||||
className="full-screen"
|
className="full-screen"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={this.loadFullScreenAddress()}
|
href={this.loadFullScreenAddress()}
|
||||||
|
rel="noopener noreferrer"
|
||||||
title="Full Screen"
|
title="Full Screen"
|
||||||
>
|
>
|
||||||
<AiOutlineFullscreen color="#fff" />
|
<AiOutlineFullscreen color="#fff" />
|
||||||
|
|
|
@ -27,6 +27,8 @@ import DefinitionsLayout from '../Definitions';
|
||||||
import Definitions from '../../pages/Definitions';
|
import Definitions from '../../pages/Definitions';
|
||||||
import DefinitionDetails from '../DefinitionDetails';
|
import DefinitionDetails from '../DefinitionDetails';
|
||||||
import UiSchema from '../../pages/UiSchema';
|
import UiSchema from '../../pages/UiSchema';
|
||||||
|
import PipelineRunPage from '../../pages/PipelineRunPage';
|
||||||
|
import PipelineListPage from '../../pages/PipelineListPage';
|
||||||
|
|
||||||
export default function Content() {
|
export default function Content() {
|
||||||
return (
|
return (
|
||||||
|
@ -130,6 +132,12 @@ export default function Content() {
|
||||||
return <EnvPage {...props} />;
|
return <EnvPage {...props} />;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Route exact path="/pipelines" component={PipelineListPage} />
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/projects/:projectName/pipelines/:pipelineName/runs/:runName"
|
||||||
|
component={PipelineRunPage}
|
||||||
|
/>
|
||||||
<Route path="/targets" component={TargetList} />
|
<Route path="/targets" component={TargetList} />
|
||||||
<Route path="/clusters" component={Clusters} />
|
<Route path="/clusters" component={Clusters} />
|
||||||
<Route path="/addons/:addonName" component={Addons} />
|
<Route path="/addons/:addonName" component={Addons} />
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { connect } from 'dva';
|
import { connect } from 'dva';
|
||||||
import { Grid, Breadcrumb } from '@b-design/ui';
|
import { Grid, Breadcrumb, Icon } from '@b-design/ui';
|
||||||
import { Link } from 'dva/router';
|
import { Link } from 'dva/router';
|
||||||
import Translation from '../../components/Translation';
|
import Translation from '../../components/Translation';
|
||||||
import type { LoginUserInfo } from '../../interface/user';
|
import type { LoginUserInfo } from '../../interface/user';
|
||||||
import type { DefinitionMenuType } from '../../interface/definitions';
|
import type { DefinitionMenuType } from '../../interface/definitions';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const { Row, Col } = Grid;
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
@ -66,7 +67,10 @@ class DefinitionDetailsLayout extends Component<Props> {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={6} className="padding16">
|
<Col span={6} className={classNames('padding16', 'breadcrumb')}>
|
||||||
|
<Link to={'/'}>
|
||||||
|
<Icon type="home" />
|
||||||
|
</Link>
|
||||||
<Breadcrumb separator="/">
|
<Breadcrumb separator="/">
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Translation>Definitions</Translation>
|
<Translation>Definitions</Translation>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
isProjectPath,
|
isProjectPath,
|
||||||
isRolesPath,
|
isRolesPath,
|
||||||
isConfigPath,
|
isConfigPath,
|
||||||
|
isPipelinePath,
|
||||||
isDefinitionsPath,
|
isDefinitionsPath,
|
||||||
} from '../../utils/common';
|
} from '../../utils/common';
|
||||||
export function getLeftSlider(pathname) {
|
export function getLeftSlider(pathname) {
|
||||||
|
@ -16,6 +17,7 @@ export function getLeftSlider(pathname) {
|
||||||
const isAddons = isAddonsPath(pathname);
|
const isAddons = isAddonsPath(pathname);
|
||||||
const isTarget = isTargetURL(pathname);
|
const isTarget = isTargetURL(pathname);
|
||||||
const isEnv = isEnvPath(pathname);
|
const isEnv = isEnvPath(pathname);
|
||||||
|
const isPipeline = isPipelinePath(pathname);
|
||||||
const isUser = isUsersPath(pathname);
|
const isUser = isUsersPath(pathname);
|
||||||
const isProject = isProjectPath(pathname);
|
const isProject = isProjectPath(pathname);
|
||||||
const isRole = isRolesPath(pathname);
|
const isRole = isRolesPath(pathname);
|
||||||
|
@ -35,10 +37,17 @@ export function getLeftSlider(pathname) {
|
||||||
{
|
{
|
||||||
className: isEnv,
|
className: isEnv,
|
||||||
link: '/envs',
|
link: '/envs',
|
||||||
iconType: 'Directory-tree',
|
iconType: 'layer-group',
|
||||||
navName: 'Environments',
|
navName: 'Environments',
|
||||||
permission: { resource: 'project:?/environment:*', action: 'list' },
|
permission: { resource: 'project:?/environment:*', action: 'list' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
className: isPipeline,
|
||||||
|
link: '/pipelines',
|
||||||
|
iconType: 'Directory-tree',
|
||||||
|
navName: 'Pipelines',
|
||||||
|
permission: { resource: 'project:?/pipeline:*', action: 'list' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { connect } from 'dva';
|
import { connect } from 'dva';
|
||||||
import { Grid, Breadcrumb } from '@b-design/ui';
|
import { Grid, Breadcrumb, Icon } from '@b-design/ui';
|
||||||
import { Link } from 'dva/router';
|
import { Link } from 'dva/router';
|
||||||
import Translation from '../../components/Translation';
|
import Translation from '../../components/Translation';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import type { ProjectDetail } from '../../interface/project';
|
import type { ProjectDetail } from '../../interface/project';
|
||||||
import { checkPermission } from '../../utils/permission';
|
import { checkPermission } from '../../utils/permission';
|
||||||
import type { LoginUserInfo } from '../../interface/user';
|
import type { LoginUserInfo } from '../../interface/user';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const { Row, Col } = Grid;
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
@ -109,10 +110,13 @@ class ProjectLayout extends Component<Props> {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={6} className="padding16">
|
<Col span={6} className={classNames('padding16', 'breadcrumb')}>
|
||||||
|
<Link to={'/'}>
|
||||||
|
<Icon type="home" />
|
||||||
|
</Link>
|
||||||
<Breadcrumb separator="/">
|
<Breadcrumb separator="/">
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Translation>Projects</Translation>
|
<Link to={'/projects'}>Projects</Link>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<Link to={'/projects/' + projectDetails?.name}>
|
<Link to={'/projects/' + projectDetails?.name}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { Grid, Form, Input, Field, Button, Message, Icon, Dialog } from '@b-design/ui';
|
import { Grid, Form, Input, Field, Message, Icon, Dialog } from '@b-design/ui';
|
||||||
import { updateUser } from '../../../../api/users';
|
import { updateUser } from '../../../../api/users';
|
||||||
import type { LoginUserInfo } from '../../../../interface/user';
|
import type { LoginUserInfo } from '../../../../interface/user';
|
||||||
import { checkUserPassword } from '../../../../utils/common';
|
import { checkUserPassword } from '../../../../utils/common';
|
||||||
|
@ -63,15 +63,6 @@ class EditPlatFormUserDialog extends Component<Props, State> {
|
||||||
return i18n.t('Reset the password and email for the administrator account');
|
return i18n.t('Reset the password and email for the administrator account');
|
||||||
}
|
}
|
||||||
|
|
||||||
showClickButtons = () => {
|
|
||||||
const { isLoading } = this.state;
|
|
||||||
return [
|
|
||||||
<Button type="primary" onClick={this.onUpdateUser} loading={isLoading}>
|
|
||||||
{i18n.t('Update')}
|
|
||||||
</Button>,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClickLook = () => {
|
handleClickLook = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLookPassword: !this.state.isLookPassword,
|
isLookPassword: !this.state.isLookPassword,
|
||||||
|
@ -80,6 +71,7 @@ class EditPlatFormUserDialog extends Component<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const init = this.field.init;
|
const init = this.field.init;
|
||||||
|
const { isLoading } = this.state;
|
||||||
const { Row, Col } = Grid;
|
const { Row, Col } = Grid;
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
const formItemLayout = {
|
const formItemLayout = {
|
||||||
|
@ -95,12 +87,13 @@ class EditPlatFormUserDialog extends Component<Props, State> {
|
||||||
<Dialog
|
<Dialog
|
||||||
visible={true}
|
visible={true}
|
||||||
title={this.showTitle()}
|
title={this.showTitle()}
|
||||||
|
className={'commonDialog'}
|
||||||
style={{ width: '600px' }}
|
style={{ width: '600px' }}
|
||||||
onOk={this.onUpdateUser}
|
onOk={this.onUpdateUser}
|
||||||
locale={locale().Dialog}
|
locale={locale().Dialog}
|
||||||
footerActions={['ok']}
|
footerActions={['ok']}
|
||||||
>
|
>
|
||||||
<Form {...formItemLayout} field={this.field}>
|
<Form loading={isLoading} {...formItemLayout} field={this.field}>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={24} style={{ padding: '0 8px' }}>
|
<Col span={24} style={{ padding: '0 8px' }}>
|
||||||
<FormItem label={<Translation>Password</Translation>} required>
|
<FormItem label={<Translation>Password</Translation>} required>
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.item {
|
.item {
|
||||||
|
margin-right: var(--spacing-6);
|
||||||
a {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -266,7 +266,12 @@ class TopBar extends Component<Props, State> {
|
||||||
</Permission>
|
</Permission>
|
||||||
|
|
||||||
<div className="vela-item">
|
<div className="vela-item">
|
||||||
<a title="KubeVela Documents" href="https://kubevela.io" target="_blank">
|
<a
|
||||||
|
title="KubeVela Documents"
|
||||||
|
href="https://kubevela.io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<Icon size={14} type="help1" />
|
<Icon size={14} type="help1" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -59,7 +59,9 @@
|
||||||
.layout-shell {
|
.layout-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.layout {
|
||||||
|
min-width: var(--min-layout-width);
|
||||||
|
}
|
||||||
.layout-navigation {
|
.layout-navigation {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
|
|
43
src/lib.less
43
src/lib.less
|
@ -1,8 +1,49 @@
|
||||||
:root {
|
:root {
|
||||||
|
--min-layout-width: 1400px;
|
||||||
--primary-color: #1b58f4;
|
--primary-color: #1b58f4;
|
||||||
--warning-color: var(--message-warning-color-icon-inline, #fac800);
|
--warning-color: var(--message-warning-color-icon-inline, #fac800);
|
||||||
--hover-color: #f7bca9;
|
--hover-color: #f7bca9;
|
||||||
|
--border-default-color: #30363d;
|
||||||
|
--success-color: #3fb950;
|
||||||
|
--failed-color: #f85149;
|
||||||
|
--running-color: #dbab0a;
|
||||||
|
--grey-200: #d9dae5;
|
||||||
|
--grey-300: #b0b1c4;
|
||||||
|
--grey-400: #9293ab;
|
||||||
|
--grey-500: #6b6d85;
|
||||||
|
--grey-700: #383946;
|
||||||
|
--grey-800: #22222a;
|
||||||
|
--grey-900: #0b0b0d;
|
||||||
|
--red-100: #fbe6e4;
|
||||||
|
--red-600: #da291d;
|
||||||
|
--red-900: #b41710;
|
||||||
|
|
||||||
|
--font-size-normal: 14px;
|
||||||
|
--font-size-large: 20px;
|
||||||
|
--font-size-x-large: 24px;
|
||||||
|
--font-size-medium: 18px;
|
||||||
|
--font-size-small: 12px;
|
||||||
|
|
||||||
|
--spacing-0: 0;
|
||||||
|
--spacing-1: 0.125rem;
|
||||||
|
--spacing-2: 0.25rem;
|
||||||
|
--spacing-3: 0.5rem;
|
||||||
|
--spacing-4: 0.75rem;
|
||||||
|
--spacing-5: 1rem;
|
||||||
|
--spacing-6: 1.25rem;
|
||||||
|
--spacing-7: 1.5rem;
|
||||||
|
--spacing-8: 2rem;
|
||||||
|
--spacing-9: 2.5rem;
|
||||||
|
--spacing-10: 3rem;
|
||||||
|
--spacing-11: 4rem;
|
||||||
|
--spacing-12: 4.5rem;
|
||||||
|
--spacing-13: 6rem;
|
||||||
|
--spacing-14: 10rem;
|
||||||
|
--spacing: var(--spacing-5);
|
||||||
|
|
||||||
|
--padding-common: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@border-radius-8: 8px;
|
@border-radius-8: 8px;
|
||||||
@F7F7F7: #f7f7f7;
|
@F7F7F7: #f7f7f7;
|
||||||
@F9F8FF: #f9f8ff;
|
@F9F8FF: #f9f8ff;
|
||||||
|
@ -11,7 +52,7 @@
|
||||||
@dangerColor: rgb(248, 81, 73);
|
@dangerColor: rgb(248, 81, 73);
|
||||||
@warningColor: var(--message-warning-color-icon-inline, #fac800);
|
@warningColor: var(--message-warning-color-icon-inline, #fac800);
|
||||||
@colorRED: red;
|
@colorRED: red;
|
||||||
@colorGREEN: green;
|
@colorGREEN: #3fb950;
|
||||||
@colorBLUE: blue;
|
@colorBLUE: blue;
|
||||||
@colorFFF: #fff;
|
@colorFFF: #fff;
|
||||||
@color333: #333;
|
@color333: #333;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"App Name-": "应用名称-",
|
"App Name-": "应用名称-",
|
||||||
"Cluster Bind": "集群绑定",
|
"Cluster Bind": "集群绑定",
|
||||||
"App Describe": "应用备注",
|
"App Describe": "应用备注",
|
||||||
"Confirm": "确认",
|
"OK": "确认",
|
||||||
"Visit": "访问",
|
"Visit": "访问",
|
||||||
"Workflow": "工作流",
|
"Workflow": "工作流",
|
||||||
"Enter template name": "输入模版名称",
|
"Enter template name": "输入模版名称",
|
||||||
|
@ -447,7 +447,8 @@
|
||||||
"Are you sure you want to delete the project": "你确定想删除这个项目吗",
|
"Are you sure you want to delete the project": "你确定想删除这个项目吗",
|
||||||
"Please enter a project name": "请输入符合规范的项目名称",
|
"Please enter a project name": "请输入符合规范的项目名称",
|
||||||
"Name(Alias)": "名称(别名)",
|
"Name(Alias)": "名称(别名)",
|
||||||
"Configs": "集成配置",
|
"Configs": "配置",
|
||||||
|
"Config Distributions": "配置分发",
|
||||||
"Summary": "概览",
|
"Summary": "概览",
|
||||||
"Roles": "角色",
|
"Roles": "角色",
|
||||||
"Members": "成员",
|
"Members": "成员",
|
||||||
|
@ -580,5 +581,16 @@
|
||||||
"This policy is being used by workflow, do you want to force delete it?": "此策略正被工作流引用,是否强制删除它?",
|
"This policy is being used by workflow, do you want to force delete it?": "此策略正被工作流引用,是否强制删除它?",
|
||||||
"The application is synchronizing from the cluster.": "该应用正在从集群同步配置和状态",
|
"The application is synchronizing from the cluster.": "该应用正在从集群同步配置和状态",
|
||||||
"Once deployed, VelaUX hosts this application and no longer syncs the configuration from the cluster.": "执行部署后,控制台将接管该应用且不再从集群同步配置。",
|
"Once deployed, VelaUX hosts this application and no longer syncs the configuration from the cluster.": "执行部署后,控制台将接管该应用且不再从集群同步配置。",
|
||||||
"This application is synchronizing from cluster, recycling from this environment means this application will be deleted.": "该应用正在从集群同步配置,如果回收意味着该应用将被删除"
|
"This application is synchronizing from cluster, recycling from this environment means this application will be deleted.": "该应用正在从集群同步配置,如果回收意味着该应用将被删除",
|
||||||
}
|
"Pipelines": "流水线",
|
||||||
|
"Orchestrate your multiple cloud native app release processes or model your cloud native pipeline.": "编排你的多个云原生应用发布过程或为你的任何云原生流水线建模。",
|
||||||
|
"New Pipeline": "创建流水线",
|
||||||
|
"Distribute": "分发",
|
||||||
|
"Distribution": "分发状态",
|
||||||
|
"Last Run": "上一次执行",
|
||||||
|
"Recent Runs(Last 7-Days)": "最近执行(过去 7 天)",
|
||||||
|
"Clone": "克隆",
|
||||||
|
"Run": "执行",
|
||||||
|
"View Runs": "执行记录",
|
||||||
|
"Tags": "标签"
|
||||||
|
}
|
||||||
|
|
|
@ -219,6 +219,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
||||||
name: this.props.addonName,
|
name: this.props.addonName,
|
||||||
version: this.state.version,
|
version: this.state.version,
|
||||||
properties: values.properties,
|
properties: values.properties,
|
||||||
|
registry: this.state.addonDetailInfo?.registryName,
|
||||||
};
|
};
|
||||||
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
|
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
|
||||||
params.clusters = this.state.clusters;
|
params.clusters = this.state.clusters;
|
||||||
|
@ -248,6 +249,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
||||||
name: this.props.addonName,
|
name: this.props.addonName,
|
||||||
version: this.state.version,
|
version: this.state.version,
|
||||||
properties: properties,
|
properties: properties,
|
||||||
|
registry: this.state.addonDetailInfo?.registryName,
|
||||||
};
|
};
|
||||||
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
|
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
|
||||||
params.clusters = this.state.clusters;
|
params.clusters = this.state.clusters;
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
.addon-search {
|
.addon-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
.tag-search {
|
.tag-search {
|
||||||
|
|
|
@ -71,58 +71,60 @@ class SelectSearch extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-radius-8 addon-search">
|
<div className="border-radius-8 addon-search">
|
||||||
<Row wrap={true}>
|
<div>
|
||||||
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
|
<Row wrap={true}>
|
||||||
<Select
|
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
|
||||||
locale={locale().Select}
|
<Select
|
||||||
mode="single"
|
locale={locale().Select}
|
||||||
size="large"
|
mode="single"
|
||||||
onChange={this.handleChangRegistry}
|
size="large"
|
||||||
className="item"
|
onChange={this.handleChangRegistry}
|
||||||
value={registryValue}
|
className="item"
|
||||||
>
|
value={registryValue}
|
||||||
<Option value="">
|
>
|
||||||
<Translation>All</Translation>
|
<Option value="">
|
||||||
</Option>
|
<Translation>All</Translation>
|
||||||
{registries?.map((item: any) => {
|
</Option>
|
||||||
return (
|
{registries?.map((item: any) => {
|
||||||
<Option key={item.name} value={item.name}>
|
return (
|
||||||
{item.name}
|
<Option key={item.name} value={item.name}>
|
||||||
</Option>
|
{item.name}
|
||||||
);
|
</Option>
|
||||||
})}
|
);
|
||||||
</Select>
|
})}
|
||||||
</Col>
|
</Select>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
|
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
|
||||||
<Input
|
<Input
|
||||||
innerAfter={
|
innerAfter={
|
||||||
<Icon
|
<Icon
|
||||||
type="search"
|
type="search"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={this.handleClickSearch}
|
onClick={this.handleClickSearch}
|
||||||
style={{ margin: 4 }}
|
style={{ margin: 4 }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
placeholder={queryPlaceholder}
|
placeholder={queryPlaceholder}
|
||||||
onChange={this.handleChangName}
|
onChange={this.handleChangName}
|
||||||
onPressEnter={this.handleClickSearch}
|
onPressEnter={this.handleClickSearch}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
className="item"
|
className="item"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<div className="tag-search">
|
<div className="tag-search">
|
||||||
<div className="tag-name">
|
<div className="tag-name">
|
||||||
<Translation>Tags</Translation>
|
<Translation>Tags</Translation>
|
||||||
</div>
|
</div>
|
||||||
<div className="tag-list">
|
<div className="tag-list">
|
||||||
<Checkbox.Group
|
<Checkbox.Group
|
||||||
dataSource={this.generateTagList()}
|
dataSource={this.generateTagList()}
|
||||||
onChange={(tags) => {
|
onChange={(tags) => {
|
||||||
this.props.onTagChange(tags);
|
this.props.onTagChange(tags);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -102,6 +102,7 @@ class TriggerDialog extends React.Component<Props, State> {
|
||||||
payloadType = '',
|
payloadType = '',
|
||||||
workflowName = '',
|
workflowName = '',
|
||||||
componentName = '',
|
componentName = '',
|
||||||
|
registry = '',
|
||||||
} = values;
|
} = values;
|
||||||
const query = { appName };
|
const query = { appName };
|
||||||
const params: Trigger = {
|
const params: Trigger = {
|
||||||
|
@ -113,13 +114,13 @@ class TriggerDialog extends React.Component<Props, State> {
|
||||||
workflowName,
|
workflowName,
|
||||||
token: '',
|
token: '',
|
||||||
componentName,
|
componentName,
|
||||||
|
registry,
|
||||||
};
|
};
|
||||||
createTriggers(params, query).then((res: any) => {
|
createTriggers(params, query).then((res: any) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
Message.success({
|
Message.success({
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
title: 'Trigger create success.',
|
content: 'Trigger created successfully.',
|
||||||
content: 'Trigger create success.',
|
|
||||||
});
|
});
|
||||||
this.props.onOK(res);
|
this.props.onOK(res);
|
||||||
}
|
}
|
||||||
|
@ -266,7 +267,7 @@ class TriggerDialog extends React.Component<Props, State> {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row>
|
<Row wrap>
|
||||||
<Col span={12} style={{ padding: '0 8px' }}>
|
<Col span={12} style={{ padding: '0 8px' }}>
|
||||||
<FormItem label={<Translation>Type</Translation>} required>
|
<FormItem label={<Translation>Type</Translation>} required>
|
||||||
<Select
|
<Select
|
||||||
|
@ -307,6 +308,34 @@ class TriggerDialog extends React.Component<Props, State> {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</If>
|
</If>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={12} style={{ padding: '0 8px' }}>
|
||||||
|
<If
|
||||||
|
condition={
|
||||||
|
this.field.getValue('type') === 'webhook' &&
|
||||||
|
this.field.getValue('payloadType') === 'acr'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormItem label={<Translation>Registry</Translation>}>
|
||||||
|
<Input
|
||||||
|
name="registry"
|
||||||
|
locale={locale().Input}
|
||||||
|
{...init('registry', {
|
||||||
|
initValue: '',
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
'^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$',
|
||||||
|
message: 'This is a invalid domain',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
placeholder={i18n.t(
|
||||||
|
'For the ACR Enterprise Edition, you should set the domain of the registry.',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</If>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
|
|
|
@ -236,6 +236,9 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
||||||
(c) => c.workloadType?.type == 'configurations.terraform.core.oam.dev',
|
(c) => c.workloadType?.type == 'configurations.terraform.core.oam.dev',
|
||||||
);
|
);
|
||||||
const showCloudInstance = cloudComponents?.length && cloudComponents?.length > 0;
|
const showCloudInstance = cloudComponents?.length && cloudComponents?.length > 0;
|
||||||
|
const queryPod =
|
||||||
|
cloudComponents?.length == undefined ||
|
||||||
|
(components?.length && components.length > cloudComponents?.length);
|
||||||
const { target, componentName } = this.state;
|
const { target, componentName } = this.state;
|
||||||
const envs = envbinding.filter((item) => item.name == envName);
|
const envs = envbinding.filter((item) => item.name == envName);
|
||||||
if (applicationDetail && applicationDetail.name && envs.length > 0) {
|
if (applicationDetail && applicationDetail.name && envs.length > 0) {
|
||||||
|
@ -251,20 +254,22 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
||||||
param.clusterNs = target.cluster?.namespace || '';
|
param.clusterNs = target.cluster?.namespace || '';
|
||||||
}
|
}
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
listApplicationPods(param)
|
if (queryPod) {
|
||||||
.then((re) => {
|
listApplicationPods(param)
|
||||||
if (re && re.podList) {
|
.then((re) => {
|
||||||
re.podList.map((item: any) => {
|
if (re && re.podList) {
|
||||||
item.primaryKey = item.metadata.name;
|
re.podList.map((item: any) => {
|
||||||
});
|
item.primaryKey = item.metadata.name;
|
||||||
this.setState({ podList: re.podList });
|
});
|
||||||
} else {
|
this.setState({ podList: re.podList });
|
||||||
this.setState({ podList: [] });
|
} else {
|
||||||
}
|
this.setState({ podList: [] });
|
||||||
})
|
}
|
||||||
.finally(() => {
|
})
|
||||||
this.setState({ loading: false });
|
.finally(() => {
|
||||||
});
|
this.setState({ loading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
if (showCloudInstance) {
|
if (showCloudInstance) {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
listCloudResources(param)
|
listCloudResources(param)
|
||||||
|
@ -587,7 +592,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
||||||
cell={(value: string, index: number, record: CloudInstance) => {
|
cell={(value: string, index: number, record: CloudInstance) => {
|
||||||
if (record.url) {
|
if (record.url) {
|
||||||
return (
|
return (
|
||||||
<a target="_blank" href={record.url}>
|
<a target="_blank" href={record.url} rel="noopener noreferrer">
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -625,7 +630,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
||||||
cell={(value: string, index: number, record: CloudInstance) => {
|
cell={(value: string, index: number, record: CloudInstance) => {
|
||||||
if (record.instanceName) {
|
if (record.instanceName) {
|
||||||
return (
|
return (
|
||||||
<a target="_blank" href={value}>
|
<a target="_blank" href={value} rel="noopener noreferrer">
|
||||||
<Translation>Console</Translation>
|
<Translation>Console</Translation>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
.dialog-app-wraper {
|
|
||||||
width: 1200px;
|
|
||||||
.guide-code {
|
|
||||||
height: 340px;
|
|
||||||
overflow: hidden;
|
|
||||||
:global {
|
|
||||||
.overflow-guard {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
.margin {
|
|
||||||
top: 16px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.next-select-trigger {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.next-dialog-body {
|
|
||||||
width: 100% !important;
|
|
||||||
max-height: none !important;
|
|
||||||
}
|
|
||||||
.next-dialog-footer {
|
|
||||||
width: 280px !important;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.radio-group-wraper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.add-btn {
|
|
||||||
margin: 20px 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hiddenEnvbind {
|
|
||||||
position: absolute;
|
|
||||||
right: -2000px;
|
|
||||||
}
|
|
|
@ -10,7 +10,6 @@ import type { DefinitionDetail } from '../../../../interface/application';
|
||||||
import UISchema from '../../../../components/UISchema';
|
import UISchema from '../../../../components/UISchema';
|
||||||
import DrawerWithFooter from '../../../../components/Drawer';
|
import DrawerWithFooter from '../../../../components/Drawer';
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
import './index.less';
|
|
||||||
import type { Project } from '../../../../interface/project';
|
import type { Project } from '../../../../interface/project';
|
||||||
import { connect } from 'dva';
|
import { connect } from 'dva';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
|
@ -96,8 +95,8 @@ class AppDialog extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
if (defaultProject != '') {
|
if (projectName || defaultProject) {
|
||||||
this.setState({ project: defaultProject }, () => {
|
this.setState({ project: projectName ? projectName : defaultProject }, () => {
|
||||||
this.loadEnvs();
|
this.loadEnvs();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { checkPermission } from '../../../../utils/permission';
|
||||||
import Permission from '../../../../components/Permission';
|
import Permission from '../../../../components/Permission';
|
||||||
import type { ShowMode } from '../..';
|
import type { ShowMode } from '../..';
|
||||||
import type { Project } from '../../../../interface/project';
|
import type { Project } from '../../../../interface/project';
|
||||||
|
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||||
const { Column } = Table;
|
const { Column } = Table;
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -79,6 +80,7 @@ class CardContent extends React.Component<Props, State> {
|
||||||
this.onEditAppPlan(item);
|
this.onEditAppPlan(item);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<AiFillSetting />
|
||||||
<Translation>Edit</Translation>
|
<Translation>Edit</Translation>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -89,7 +91,10 @@ class CardContent extends React.Component<Props, State> {
|
||||||
this.onEditAppPlan(item);
|
this.onEditAppPlan(item);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Translation>Edit</Translation>
|
<div className="dropdown-menu-item inline-center">
|
||||||
|
<AiFillSetting />
|
||||||
|
<Translation>Edit</Translation>
|
||||||
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,14 +121,23 @@ class CardContent extends React.Component<Props, State> {
|
||||||
if (checkPermission(request, project, userInfo)) {
|
if (checkPermission(request, project, userInfo)) {
|
||||||
if (button) {
|
if (button) {
|
||||||
return (
|
return (
|
||||||
<Button text size={'medium'} ghost={true} component={'a'} onClick={onClick}>
|
<Button
|
||||||
<Translation>Remove</Translation>
|
text
|
||||||
|
size={'medium'}
|
||||||
|
className="danger-btn"
|
||||||
|
ghost={true}
|
||||||
|
component={'a'}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<AiFillDelete /> <Translation>Remove</Translation>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Menu.Item onClick={onClick}>
|
<Menu.Item onClick={onClick}>
|
||||||
<Translation>Remove</Translation>
|
<div className="dropdown-menu-item inline-center">
|
||||||
|
<AiFillDelete /> <Translation>Remove</Translation>
|
||||||
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -171,9 +185,9 @@ class CardContent extends React.Component<Props, State> {
|
||||||
cell: (v: string, i: number, record: ApplicationBase) => {
|
cell: (v: string, i: number, record: ApplicationBase) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.isDeletePermission(record, true)}
|
|
||||||
<span className="line" />
|
|
||||||
{this.isEditPermission(record, true)}
|
{this.isEditPermission(record, true)}
|
||||||
|
<span className="line" />
|
||||||
|
{this.isDeletePermission(record, true)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,16 +1,5 @@
|
||||||
.application-logs-wrapper {
|
.application-logs-wrapper {
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
.logBox {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0%;
|
|
||||||
flex-direction: column;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: calc(~'100vh - 410px');
|
|
||||||
padding: 16px;
|
|
||||||
overflow: scroll;
|
|
||||||
color: #fff;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
.logDate {
|
.logDate {
|
||||||
text-decoration: underline dotted;
|
text-decoration: underline dotted;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -23,3 +12,15 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logBox {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: calc(~'100vh - 410px');
|
||||||
|
padding: 16px;
|
||||||
|
overflow: scroll;
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
.dialog-clust-wraper {
|
|
||||||
width: 800px;
|
|
||||||
.guide-code {
|
|
||||||
min-height: 340px;
|
|
||||||
:global {
|
|
||||||
.overflow-guard {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
.margin {
|
|
||||||
top: 16px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.next-dialog-body {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
.next-dialog-footer {
|
|
||||||
width: 450px !important;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.add-btn {
|
|
||||||
margin: 20px 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
import DefinitionCode from '../../../../components/DefinitionCode';
|
import DefinitionCode from '../../../../components/DefinitionCode';
|
||||||
import { checkName } from '../../../../utils/common';
|
import { checkName } from '../../../../utils/common';
|
||||||
import { getClusterDetails, updateCluster } from '../../../../api/cluster';
|
import { getClusterDetails, updateCluster } from '../../../../api/cluster';
|
||||||
import './index.less';
|
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
import type { Cluster } from '../../../../interface/cluster';
|
import type { Cluster } from '../../../../interface/cluster';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
|
|
|
@ -108,7 +108,12 @@ class CardContent extends React.Component<Props, State> {
|
||||||
<Row className="content-title">
|
<Row className="content-title">
|
||||||
<Col span={16} className="font-size-16 color1A1A1A">
|
<Col span={16} className="font-size-16 color1A1A1A">
|
||||||
<If condition={dashboardURL}>
|
<If condition={dashboardURL}>
|
||||||
<a title={name} target="_blank" href={dashboardURL}>
|
<a
|
||||||
|
title={name}
|
||||||
|
target="_blank"
|
||||||
|
href={dashboardURL}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{showName}
|
{showName}
|
||||||
</a>
|
</a>
|
||||||
</If>
|
</If>
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
.dialog-cloudService-wrapper {
|
|
||||||
width: 1000px;
|
|
||||||
.cloud-server-wrapper {
|
|
||||||
.next-select-trigger {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.next-dialog-body {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
.next-dialog-footer {
|
|
||||||
width: 280px !important;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.next-table-cell.last .next-table-cell-wrapper {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cluster-cloud-pagination-wrapper {
|
|
||||||
float: right;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,9 +12,8 @@ import {
|
||||||
} from '@b-design/ui';
|
} from '@b-design/ui';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import { If } from 'tsx-control-statements/components';
|
import { If } from 'tsx-control-statements/components';
|
||||||
import { ACKCLusterStatus } from '../../../../utils/common';
|
import { ACKClusterStatus } from '../../../../utils/common';
|
||||||
import { getCloudClustersList } from '../../../../api/cluster';
|
import { getCloudClustersList } from '../../../../api/cluster';
|
||||||
import './index.less';
|
|
||||||
import { handleError } from '../../../../utils/errors';
|
import { handleError } from '../../../../utils/errors';
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
|
@ -113,7 +112,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connectcloudCluster = (record: Record) => {
|
connectCloudCluster = (record: Record) => {
|
||||||
const { id = '', description = '', icon = '', name = '' } = record;
|
const { id = '', description = '', icon = '', name = '' } = record;
|
||||||
const { accessKeyID, accessKeySecret, provider } = this.field.getValues();
|
const { accessKeyID, accessKeySecret, provider } = this.field.getValues();
|
||||||
const params = {
|
const params = {
|
||||||
|
@ -212,7 +211,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
|
||||||
title: <Translation>Cluster Status</Translation>,
|
title: <Translation>Cluster Status</Translation>,
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
cell: (v: string) => {
|
cell: (v: string) => {
|
||||||
const findArr = ACKCLusterStatus.filter((item) => {
|
const findArr = ACKClusterStatus.filter((item) => {
|
||||||
return item.key == v;
|
return item.key == v;
|
||||||
});
|
});
|
||||||
return <span style={{ color: findArr[0].color || '' }}> {v} </span>;
|
return <span style={{ color: findArr[0].color || '' }}> {v} </span>;
|
||||||
|
@ -254,7 +253,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
|
||||||
ghost={true}
|
ghost={true}
|
||||||
component={'a'}
|
component={'a'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.connectcloudCluster(record);
|
this.connectCloudCluster(record);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Translation>Connect</Translation>
|
<Translation>Connect</Translation>
|
||||||
|
@ -271,7 +270,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Dialog
|
<Dialog
|
||||||
locale={locale().Dialog}
|
locale={locale().Dialog}
|
||||||
className="dialog-cloudService-wrapper"
|
className="commonDialog"
|
||||||
title={<Translation>Connect Kubernetes Cluster From Cloud</Translation>}
|
title={<Translation>Connect Kubernetes Cluster From Cloud</Translation>}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Icon,
|
Icon,
|
||||||
Loading,
|
Loading,
|
||||||
Select,
|
Select,
|
||||||
|
Dialog,
|
||||||
} from '@b-design/ui';
|
} from '@b-design/ui';
|
||||||
import DrawerWithFooter from '../../../../components/Drawer';
|
import DrawerWithFooter from '../../../../components/Drawer';
|
||||||
import UISchema from '../../../../components/UISchema';
|
import UISchema from '../../../../components/UISchema';
|
||||||
|
@ -33,6 +34,7 @@ import type {
|
||||||
} from '../../../../interface/configs';
|
} from '../../../../interface/configs';
|
||||||
import Permission from '../../../../components/Permission';
|
import Permission from '../../../../components/Permission';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
|
import { connect } from 'dva';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -52,6 +54,7 @@ type State = {
|
||||||
templates?: ConfigTemplate[];
|
templates?: ConfigTemplate[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@connect()
|
||||||
class CreateConfigDialog extends React.Component<Props, State> {
|
class CreateConfigDialog extends React.Component<Props, State> {
|
||||||
field: Field;
|
field: Field;
|
||||||
uiSchemaRef: React.RefObject<UISchema>;
|
uiSchemaRef: React.RefObject<UISchema>;
|
||||||
|
@ -170,7 +173,23 @@ class CreateConfigDialog extends React.Component<Props, State> {
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
Message.success(<Translation>Config created successfully</Translation>);
|
Message.success(<Translation>Config created successfully</Translation>);
|
||||||
this.props.onSuccess();
|
if (
|
||||||
|
templateName &&
|
||||||
|
['image-registry', 'helm-repository', 'tls-certificate'].includes(templateName)
|
||||||
|
) {
|
||||||
|
Dialog.confirm({
|
||||||
|
content: i18n.t(
|
||||||
|
'This config needs to be distributed, you should go to the project summary page to do it before you want to use it.',
|
||||||
|
),
|
||||||
|
locale: locale().Dialog,
|
||||||
|
onOk: () => {
|
||||||
|
this.props.onSuccess();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
this.props.onSuccess();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
@ -265,7 +265,7 @@ class EnvDialog extends React.Component<Props, State> {
|
||||||
footer={
|
footer={
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={this.onOk} type="primary" loading={submitLoading}>
|
<Button onClick={this.onOk} type="primary" loading={submitLoading}>
|
||||||
<Translation>Confirm</Translation>
|
<Translation>OK</Translation>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Table, Message, Dialog, Tag } from '@b-design/ui';
|
import { Table, Message, Dialog, Tag, Button } from '@b-design/ui';
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
|
@ -11,6 +11,7 @@ import type { Project } from '../../../../interface/project';
|
||||||
import Permission from '../../../../components/Permission';
|
import Permission from '../../../../components/Permission';
|
||||||
import { checkPermission } from '../../../../utils/permission';
|
import { checkPermission } from '../../../../utils/permission';
|
||||||
import type { LoginUserInfo } from '../../../../interface/user';
|
import type { LoginUserInfo } from '../../../../interface/user';
|
||||||
|
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||||
const { Group: TagGroup } = Tag;
|
const { Group: TagGroup } = Tag;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -124,48 +125,58 @@ class TableList extends Component<Props> {
|
||||||
key: 'operation',
|
key: 'operation',
|
||||||
title: <Translation>Actions</Translation>,
|
title: <Translation>Actions</Translation>,
|
||||||
dataIndex: 'operation',
|
dataIndex: 'operation',
|
||||||
width: '120px',
|
width: '200px',
|
||||||
cell: (v: string, i: number, record: Env) => {
|
cell: (v: string, i: number, record: Env) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<If condition={record.targets?.length}>
|
<Permission
|
||||||
<Permission
|
request={{ resource: `environment:${record.name}`, action: 'update' }}
|
||||||
request={{ resource: `environment:${record.name}`, action: 'delete' }}
|
project={record.project.name}
|
||||||
project={record.project.name}
|
>
|
||||||
|
<Button
|
||||||
|
className="margin-left-10"
|
||||||
|
text={true}
|
||||||
|
component={'a'}
|
||||||
|
size={'medium'}
|
||||||
|
ghost={true}
|
||||||
|
onClick={() => {
|
||||||
|
this.onEdit(record);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<a
|
<AiFillSetting />
|
||||||
onClick={() => {
|
<Translation>Edit</Translation>
|
||||||
Dialog.confirm({
|
</Button>
|
||||||
type: 'confirm',
|
<span className="line" />
|
||||||
content: (
|
</Permission>
|
||||||
<Translation>
|
<Permission
|
||||||
Unrecoverable after deletion, are you sure to delete it?
|
request={{ resource: `environment:${record.name}`, action: 'delete' }}
|
||||||
</Translation>
|
project={record.project.name}
|
||||||
),
|
>
|
||||||
onOk: () => {
|
<Button
|
||||||
this.onDelete(record);
|
text={true}
|
||||||
},
|
component={'a'}
|
||||||
locale: locale().Dialog,
|
size={'medium'}
|
||||||
});
|
ghost={true}
|
||||||
}}
|
className={'danger-btn'}
|
||||||
>
|
onClick={() => {
|
||||||
<Translation>Remove</Translation>
|
Dialog.confirm({
|
||||||
</a>
|
type: 'confirm',
|
||||||
</Permission>
|
content: (
|
||||||
<Permission
|
<Translation>
|
||||||
request={{ resource: `environment:${record.name}`, action: 'update' }}
|
Unrecoverable after deletion, are you sure to delete it?
|
||||||
project={record.project.name}
|
</Translation>
|
||||||
|
),
|
||||||
|
onOk: () => {
|
||||||
|
this.onDelete(record);
|
||||||
|
},
|
||||||
|
locale: locale().Dialog,
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<a
|
<AiFillDelete />
|
||||||
className="margin-left-10"
|
<Translation>Remove</Translation>
|
||||||
onClick={() => {
|
</Button>
|
||||||
this.onEdit(record);
|
</Permission>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Translation>Edit</Translation>
|
|
||||||
</a>
|
|
||||||
</Permission>
|
|
||||||
</If>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -144,7 +144,12 @@ export default class LoginPage extends Component<Props, State> {
|
||||||
<div style={{ flex: '1 1 0%' }} />
|
<div style={{ flex: '1 1 0%' }} />
|
||||||
<div className="right">
|
<div className="right">
|
||||||
<div className="vela-item">
|
<div className="vela-item">
|
||||||
<a title="KubeVela Documents" href="https://kubevela.io" target="_blank">
|
<a
|
||||||
|
title="KubeVela Documents"
|
||||||
|
href="https://kubevela.io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<Icon size={14} type="help1" />
|
<Icon size={14} type="help1" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,353 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Field, Form, Grid, Icon, Input, Message, Select, Upload } from '@b-design/ui';
|
||||||
|
import DrawerWithFooter from '../../../../components/Drawer';
|
||||||
|
import Translation from '../../../../components/Translation';
|
||||||
|
import DefinitionCode from '../../../../components/DefinitionCode';
|
||||||
|
import i18n from '../../../../i18n';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import { connect } from 'dva';
|
||||||
|
import type { LoginUserInfo } from '../../../../interface/user';
|
||||||
|
import { checkPermission } from '../../../../utils/permission';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { createPipeline, loadPipeline, updatePipeline } from '../../../../api/pipeline';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import type { Pipeline, PipelineDetail } from '../../../../interface/pipeline';
|
||||||
|
import { checkName } from '../../../../utils/common';
|
||||||
|
import locale from '../../../../utils/locale';
|
||||||
|
import { templates } from './pipeline-template';
|
||||||
|
|
||||||
|
const FormItem = Form.Item;
|
||||||
|
|
||||||
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
export interface PipelineProps {
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
userInfo?: LoginUserInfo;
|
||||||
|
pipeline?: Pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
configError?: string[];
|
||||||
|
containerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@connect((store: any) => {
|
||||||
|
return { ...store.user };
|
||||||
|
})
|
||||||
|
class CreatePipeline extends React.Component<PipelineProps, State> {
|
||||||
|
field: Field;
|
||||||
|
DefinitionCodeRef: React.RefObject<DefinitionCode>;
|
||||||
|
constructor(props: PipelineProps) {
|
||||||
|
super(props);
|
||||||
|
this.field = new Field(this);
|
||||||
|
this.DefinitionCodeRef = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
containerId: uuid(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { pipeline } = this.props;
|
||||||
|
if (pipeline) {
|
||||||
|
loadPipeline({ projectName: pipeline.project.name, pipelineName: pipeline.name }).then(
|
||||||
|
(res: PipelineDetail) => {
|
||||||
|
this.field.setValues({
|
||||||
|
name: res.name,
|
||||||
|
project: res.project.name,
|
||||||
|
alias: res.alias,
|
||||||
|
description: res.description,
|
||||||
|
steps: yaml.dump(res.spec.steps),
|
||||||
|
stepMode: res.spec.mode?.steps,
|
||||||
|
subStepMode: res.spec.mode?.subSteps,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
const { pipeline } = this.props;
|
||||||
|
this.field.validate((errs: any, values: any) => {
|
||||||
|
if (errs) {
|
||||||
|
if (errs.config && Array.isArray(errs.config.errors) && errs.config.errors.length > 0) {
|
||||||
|
this.setState({ configError: errs.config.errors });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { name, steps, project, alias, description, stepMode, subStepMode } = values;
|
||||||
|
const stepArray: any = yaml.load(steps);
|
||||||
|
const request = {
|
||||||
|
project,
|
||||||
|
alias,
|
||||||
|
description,
|
||||||
|
name: name,
|
||||||
|
spec: {
|
||||||
|
steps: stepArray,
|
||||||
|
mode: {
|
||||||
|
steps: stepMode,
|
||||||
|
subSteps: subStepMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (pipeline) {
|
||||||
|
updatePipeline(request).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.success(i18n.t('Pipeline updated successfully'));
|
||||||
|
if (this.props.onSuccess) {
|
||||||
|
this.props.onSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createPipeline(request).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.success(i18n.t('Pipeline created successfully'));
|
||||||
|
if (this.props.onSuccess) {
|
||||||
|
this.props.onSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
customRequest = (option: any) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const fileSelect = option.file;
|
||||||
|
reader.readAsText(fileSelect);
|
||||||
|
reader.onload = () => {
|
||||||
|
this.field.setValues({
|
||||||
|
steps: reader.result?.toString() || '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
file: File,
|
||||||
|
onError: () => {},
|
||||||
|
abort() {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
checkStepConfig = (data: any) => {
|
||||||
|
if (!data || !Array.isArray(data)) {
|
||||||
|
return ['The YAML content is not a valid array.'];
|
||||||
|
}
|
||||||
|
const messages: string[] = [];
|
||||||
|
data.map((step, i) => {
|
||||||
|
if (!step) {
|
||||||
|
messages.push(`[${i}] Step is invalid.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!step.name) {
|
||||||
|
messages.push(`[${i}] Step is not named.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!step.type) {
|
||||||
|
messages.push(`[${i}] Step does not specify a type.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { init } = this.field;
|
||||||
|
const { userInfo, pipeline } = this.props;
|
||||||
|
let defaultProject = '';
|
||||||
|
const projectOptions: { label: string; value: string }[] = [];
|
||||||
|
(userInfo?.projects || []).map((project) => {
|
||||||
|
if (
|
||||||
|
checkPermission(
|
||||||
|
{ resource: `project:${project.name}/pipeline:*`, action: 'create' },
|
||||||
|
project.name,
|
||||||
|
userInfo,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (project.name === 'default') {
|
||||||
|
defaultProject = project.name;
|
||||||
|
}
|
||||||
|
projectOptions.push({
|
||||||
|
label: project.alias ? `${project.alias}(${project.name})` : project.name,
|
||||||
|
value: project.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { configError, containerId } = this.state;
|
||||||
|
const modeOptions = [{ value: 'StepByStep' }, { value: 'DAG' }];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerWithFooter
|
||||||
|
title={i18n.t(pipeline == undefined ? 'New Pipeline' : 'Edit Pipeline')}
|
||||||
|
onClose={this.props.onClose}
|
||||||
|
onOk={this.onSubmit}
|
||||||
|
>
|
||||||
|
<Form field={this.field}>
|
||||||
|
<Row wrap>
|
||||||
|
<Col span={12} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem required label={<Translation>Name</Translation>}>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
disabled={pipeline != undefined}
|
||||||
|
{...init('name', {
|
||||||
|
initValue: '',
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
pattern: checkName,
|
||||||
|
message: 'Please input a valid name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please input a name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={12} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem label={<Translation>Alias</Translation>}>
|
||||||
|
<Input
|
||||||
|
name="alias"
|
||||||
|
placeholder={i18n.t('Give your pipeline a more recognizable name').toString()}
|
||||||
|
{...init('alias', {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 64,
|
||||||
|
message: 'Enter a string of 2 to 64 characters.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={24} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem label={<Translation>Project</Translation>} required>
|
||||||
|
<Select
|
||||||
|
name="project"
|
||||||
|
placeholder={i18n.t('Please select a project').toString()}
|
||||||
|
dataSource={projectOptions}
|
||||||
|
filterLocal={true}
|
||||||
|
hasClear={true}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
{...init('project', {
|
||||||
|
initValue: defaultProject,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please select a project',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={12} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem required label={<Translation>Step Mode</Translation>}>
|
||||||
|
<Select
|
||||||
|
name="stepMode"
|
||||||
|
{...init('stepMode', {
|
||||||
|
initValue: 'StepByStep',
|
||||||
|
})}
|
||||||
|
locale={locale().Select}
|
||||||
|
dataSource={modeOptions}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={12} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem required label={<Translation>Sub Step Mode</Translation>}>
|
||||||
|
<Select
|
||||||
|
name="subStepMode"
|
||||||
|
{...init('subStepMode', {
|
||||||
|
initValue: 'DAG',
|
||||||
|
})}
|
||||||
|
locale={locale().Select}
|
||||||
|
dataSource={modeOptions}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={24} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem label={<Translation>Description</Translation>}>
|
||||||
|
<Input
|
||||||
|
name="description"
|
||||||
|
{...init('description', {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
maxLength: 128,
|
||||||
|
message: 'Enter a description less than 128 characters.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={24} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem label={<Translation>Template</Translation>}>
|
||||||
|
<Select
|
||||||
|
locale={locale().Select}
|
||||||
|
name="template"
|
||||||
|
dataSource={templates}
|
||||||
|
placeholder="Select a template"
|
||||||
|
onChange={(value) => {
|
||||||
|
this.field.setValue('steps', value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={24} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem
|
||||||
|
className={classNames({
|
||||||
|
'has-error': configError != undefined && configError.length > 0,
|
||||||
|
})}
|
||||||
|
label={<Translation>Steps</Translation>}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<Upload request={this.customRequest}>
|
||||||
|
<Button text type="normal" className="padding-left-0">
|
||||||
|
<Icon type="cloudupload" />
|
||||||
|
<Translation>Upload YAML File</Translation>
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
<div id={containerId} className="guide-code">
|
||||||
|
<DefinitionCode
|
||||||
|
containerId={containerId}
|
||||||
|
{...init<string>('steps', {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
validator: (rule, v: string, callback) => {
|
||||||
|
const message = 'Please input the valid Pipeline Steps YAML.';
|
||||||
|
try {
|
||||||
|
const pipelineSteps: any = yaml.load(v);
|
||||||
|
const stepMessage = this.checkStepConfig(pipelineSteps);
|
||||||
|
callback(stepMessage.toString());
|
||||||
|
this.setState({ configError: stepMessage });
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ configError: [message + err] });
|
||||||
|
callback(message + err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
language={'yaml'}
|
||||||
|
readOnly={false}
|
||||||
|
ref={this.DefinitionCodeRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{configError && (
|
||||||
|
<div className="next-form-item-help" style={{ marginTop: '24px' }}>
|
||||||
|
{configError.map((m) => {
|
||||||
|
return <p>{m}</p>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
</DrawerWithFooter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreatePipeline;
|
|
@ -0,0 +1,110 @@
|
||||||
|
export const templates = [
|
||||||
|
{
|
||||||
|
label: 'Observability Template',
|
||||||
|
value: `
|
||||||
|
- name: Enable Prism
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: vela-prism
|
||||||
|
|
||||||
|
- name: Enable o11y
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: o11y-definitions
|
||||||
|
operation: enable
|
||||||
|
args:
|
||||||
|
- --override-definitions
|
||||||
|
|
||||||
|
- name: Enable Exporter
|
||||||
|
type: step-group
|
||||||
|
subSteps:
|
||||||
|
- name: Node Exporter
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: node-exporter
|
||||||
|
operation: enable
|
||||||
|
|
||||||
|
- name: Kube State Exporter
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: kube-state-metrics
|
||||||
|
operation: enable
|
||||||
|
|
||||||
|
- name: Prepare Prometheus
|
||||||
|
type: step-group
|
||||||
|
subSteps:
|
||||||
|
- name: get-exist-prometheus
|
||||||
|
type: list-config
|
||||||
|
properties:
|
||||||
|
template: prometheus-server
|
||||||
|
namespace: vela-system
|
||||||
|
outputs:
|
||||||
|
- name: prometheus
|
||||||
|
valueFrom: "output.configs"
|
||||||
|
|
||||||
|
- name: prometheus-server
|
||||||
|
inputs:
|
||||||
|
- from: prometheus
|
||||||
|
parameterKey: configs
|
||||||
|
if: context.readConfig == "false" || len(inputs.prometheus) == 0
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: prometheus-server
|
||||||
|
operation: enable
|
||||||
|
args:
|
||||||
|
- memory=1024Mi
|
||||||
|
|
||||||
|
- name: Prepare Loki
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: loki
|
||||||
|
operation: enable
|
||||||
|
args:
|
||||||
|
- agent=vector
|
||||||
|
|
||||||
|
- name: Prepare Grafana
|
||||||
|
type: step-group
|
||||||
|
subSteps:
|
||||||
|
- name: get-exist-grafana
|
||||||
|
type: list-config
|
||||||
|
properties:
|
||||||
|
template: grafana
|
||||||
|
namespace: vela-system
|
||||||
|
outputs:
|
||||||
|
- name: grafana
|
||||||
|
valueFrom: "output.configs"
|
||||||
|
|
||||||
|
- name: Install Grafana & Init Dashboards
|
||||||
|
inputs:
|
||||||
|
- from: grafana
|
||||||
|
parameterKey: configs
|
||||||
|
if: context.readConfig == "false" || len(inputs.grafana) == 0
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: grafana
|
||||||
|
operation: enable
|
||||||
|
args:
|
||||||
|
- serviceType=LoadBalancer
|
||||||
|
|
||||||
|
- name: Init Dashboards
|
||||||
|
inputs:
|
||||||
|
- from: grafana
|
||||||
|
parameterKey: configs
|
||||||
|
if: "len(inputs.grafana) != 0"
|
||||||
|
type: addon-operation
|
||||||
|
properties:
|
||||||
|
addonName: grafana
|
||||||
|
operation: enable
|
||||||
|
args:
|
||||||
|
- install=false
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
type: clean-jobs
|
||||||
|
|
||||||
|
- name: print-message
|
||||||
|
type: print-message-in-status
|
||||||
|
properties:
|
||||||
|
message: "All addons have been enabled successfully, you can use 'vela addon list' to check them."
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,208 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Dialog, Field, Form, Grid, Input, Loading, Message, Select } from '@b-design/ui';
|
||||||
|
import i18n from '../../../../i18n';
|
||||||
|
import { connect } from 'dva';
|
||||||
|
import type { LoginUserInfo } from '../../../../interface/user';
|
||||||
|
import { checkPermission } from '../../../../utils/permission';
|
||||||
|
import { createPipeline, loadPipeline } from '../../../../api/pipeline';
|
||||||
|
import type { Pipeline, PipelineDetail } from '../../../../interface/pipeline';
|
||||||
|
import Translation from '../../../../components/Translation';
|
||||||
|
import { checkName } from '../../../../utils/common';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
|
||||||
|
import locale from '../../../../utils/locale';
|
||||||
|
|
||||||
|
const FormItem = Form.Item;
|
||||||
|
|
||||||
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
export interface PipelineProps {
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
userInfo?: LoginUserInfo;
|
||||||
|
pipeline?: Pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
loading: boolean;
|
||||||
|
pipelineDetail?: PipelineDetail;
|
||||||
|
};
|
||||||
|
|
||||||
|
@connect((store: any) => {
|
||||||
|
return { ...store.user };
|
||||||
|
})
|
||||||
|
class ClonePipeline extends React.Component<PipelineProps, State> {
|
||||||
|
field: Field;
|
||||||
|
constructor(props: PipelineProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
this.field = new Field(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { pipeline } = this.props;
|
||||||
|
if (pipeline) {
|
||||||
|
loadPipeline({ projectName: pipeline.project.name, pipelineName: pipeline.name })
|
||||||
|
.then((res: PipelineDetail) => {
|
||||||
|
this.setState({ pipelineDetail: res });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
const { pipelineDetail } = this.state;
|
||||||
|
if (pipelineDetail) {
|
||||||
|
this.field.validate((errs: any, values: any) => {
|
||||||
|
if (errs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { name, project, alias, description } = values;
|
||||||
|
const request = {
|
||||||
|
project,
|
||||||
|
alias,
|
||||||
|
description,
|
||||||
|
name: name,
|
||||||
|
spec: pipelineDetail?.spec,
|
||||||
|
};
|
||||||
|
createPipeline(request).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.success(i18n.t('Pipeline cloned successfully'));
|
||||||
|
if (this.props.onSuccess) {
|
||||||
|
this.props.onSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, pipelineDetail } = this.state;
|
||||||
|
const { userInfo } = this.props;
|
||||||
|
const { init } = this.field;
|
||||||
|
const projectOptions: { label: string; value: string }[] = [];
|
||||||
|
(userInfo?.projects || []).map((project) => {
|
||||||
|
if (
|
||||||
|
checkPermission(
|
||||||
|
{ resource: `project:${project.name}/pipeline:*`, action: 'create' },
|
||||||
|
project.name,
|
||||||
|
userInfo,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
projectOptions.push({
|
||||||
|
label: project.alias ? `${project.alias}(${project.name})` : project.name,
|
||||||
|
value: project.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
onOk={this.onSubmit}
|
||||||
|
onClose={this.props.onClose}
|
||||||
|
onCancel={this.props.onClose}
|
||||||
|
locale={locale().Dialog}
|
||||||
|
visible
|
||||||
|
className="commonDialog"
|
||||||
|
title="Clone Pipeline"
|
||||||
|
>
|
||||||
|
<Loading visible={loading}>
|
||||||
|
<If condition={pipelineDetail}>
|
||||||
|
<Message
|
||||||
|
type="success"
|
||||||
|
title={i18n.t('Pipeline loaded successfully and is ready to clone.')}
|
||||||
|
/>
|
||||||
|
<Form field={this.field}>
|
||||||
|
<Row wrap>
|
||||||
|
<Col span={8} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem required label={<Translation>Name</Translation>}>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
{...init('name', {
|
||||||
|
initValue: pipelineDetail?.name && pipelineDetail?.name + '-clone',
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
pattern: checkName,
|
||||||
|
message: 'Please input a valid name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please input a name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={8} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem label={<Translation>Project</Translation>} required>
|
||||||
|
<Select
|
||||||
|
name="project"
|
||||||
|
placeholder={i18n.t('Please select a project').toString()}
|
||||||
|
dataSource={projectOptions}
|
||||||
|
filterLocal={true}
|
||||||
|
hasClear={true}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
{...init('project', {
|
||||||
|
initValue: pipelineDetail?.project.name,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please select a project',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={8} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem label={<Translation>Alias</Translation>}>
|
||||||
|
<Input
|
||||||
|
name="alias"
|
||||||
|
placeholder={i18n.t('Give your pipeline a more recognizable name').toString()}
|
||||||
|
{...init('alias', {
|
||||||
|
initValue: pipelineDetail?.alias,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 64,
|
||||||
|
message: 'Enter a string of 2 to 64 characters.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={24} style={{ padding: '0 8px' }}>
|
||||||
|
<FormItem label={<Translation>Description</Translation>}>
|
||||||
|
<Input
|
||||||
|
name="description"
|
||||||
|
{...init('description', {
|
||||||
|
initValue: pipelineDetail?.description,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
maxLength: 128,
|
||||||
|
message: 'Enter a description less than 128 characters.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
</If>
|
||||||
|
<If condition={!pipelineDetail}>
|
||||||
|
<Message type="notice" title={i18n.t('Pipeline loading')} />
|
||||||
|
</If>
|
||||||
|
</Loading>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClonePipeline;
|
|
@ -0,0 +1,26 @@
|
||||||
|
.app-select-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
.item {
|
||||||
|
min-width: 100%;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.btn-wrapper {
|
||||||
|
float: right;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-right-20 {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-mode {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
float: right;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
.flexboth {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Grid, Icon, Select, Input, Button } from '@b-design/ui';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import './index.less';
|
||||||
|
import type { Project } from '../../../../interface/project';
|
||||||
|
import locale from '../../../../utils/locale';
|
||||||
|
import type { ShowMode } from '../..';
|
||||||
|
import i18n from '../../../../i18n';
|
||||||
|
|
||||||
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projects?: Project[];
|
||||||
|
getPipelines: (params: any) => void;
|
||||||
|
setMode: (mode: ShowMode) => void;
|
||||||
|
showMode: ShowMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
projectValue: string;
|
||||||
|
inputValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SelectSearch extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
projectValue: '',
|
||||||
|
inputValue: '',
|
||||||
|
};
|
||||||
|
this.onChangeProject = this.onChangeProject.bind(this);
|
||||||
|
this.handleChangName = this.handleChangName.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeProject(e: string) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
projectValue: e,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.getPipelines();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangName(e: string) {
|
||||||
|
this.setState({
|
||||||
|
inputValue: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickSearch = () => {
|
||||||
|
this.getPipelines();
|
||||||
|
};
|
||||||
|
|
||||||
|
getPipelines = async () => {
|
||||||
|
const { projectValue, inputValue } = this.state;
|
||||||
|
const params = {
|
||||||
|
project: projectValue,
|
||||||
|
query: inputValue,
|
||||||
|
};
|
||||||
|
this.props.getPipelines(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { projects } = this.props;
|
||||||
|
const { projectValue, inputValue } = this.state;
|
||||||
|
|
||||||
|
const projectPlaceholder = i18n.t('Search by Project').toString();
|
||||||
|
const appPlaceholder = i18n.t('Search by name and description etc').toString();
|
||||||
|
const projectSource = projects?.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.alias || item.name,
|
||||||
|
value: item.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Row className="app-select-wrapper border-radius-8" wrap={true}>
|
||||||
|
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
|
||||||
|
<Select
|
||||||
|
locale={locale().Select}
|
||||||
|
mode="single"
|
||||||
|
size="large"
|
||||||
|
onChange={this.onChangeProject}
|
||||||
|
dataSource={projectSource}
|
||||||
|
placeholder={projectPlaceholder}
|
||||||
|
className="item"
|
||||||
|
hasClear
|
||||||
|
value={projectValue}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
|
||||||
|
<Input
|
||||||
|
innerAfter={
|
||||||
|
<Icon
|
||||||
|
type="search"
|
||||||
|
size="xs"
|
||||||
|
onClick={this.handleClickSearch}
|
||||||
|
style={{ margin: 4 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hasClear
|
||||||
|
placeholder={appPlaceholder}
|
||||||
|
onChange={this.handleChangName}
|
||||||
|
onPressEnter={this.handleClickSearch}
|
||||||
|
value={inputValue}
|
||||||
|
className="item"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xl={6} className="flexboth">
|
||||||
|
<div className="padding16">
|
||||||
|
<Button type={'secondary'} onClick={() => this.getPipelines()}>
|
||||||
|
<Icon type="refresh" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation()(SelectSearch);
|
|
@ -0,0 +1,182 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Balloon, Button, Dialog, Table, Message } from '@b-design/ui';
|
||||||
|
import { deletePipelineRun, loadPipelineRuns } from '../../../../api/pipeline';
|
||||||
|
import type { Pipeline, PipelineRunBriefing } from '../../../../interface/pipeline';
|
||||||
|
import locale from '../../../../utils/locale';
|
||||||
|
import i18n from '../../../../i18n';
|
||||||
|
import { Link } from 'dva/router';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { momentDate, timeDiff } from '../../../../utils/common';
|
||||||
|
import { AiFillDelete } from 'react-icons/ai';
|
||||||
|
import Translation from '../../../../components/Translation';
|
||||||
|
import Permission from '../../../../components/Permission';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
|
||||||
|
export interface ViewRunsProps {
|
||||||
|
pipeline: Pipeline;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewRuns = (props: ViewRunsProps) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [runs, setRuns] = useState<PipelineRunBriefing[]>([]);
|
||||||
|
const { pipeline } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
loadPipelineRuns({ projectName: pipeline.project.name, pipelineName: pipeline.name })
|
||||||
|
.then((res: { runs?: PipelineRunBriefing[] }) => {
|
||||||
|
setRuns(res && res.runs ? res.runs : []);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [pipeline, loading]);
|
||||||
|
|
||||||
|
const deleteRun = (name: string) => {
|
||||||
|
if (name) {
|
||||||
|
Dialog.confirm({
|
||||||
|
type: 'confirm',
|
||||||
|
content: (
|
||||||
|
<Translation>Unrecoverable after deletion, are you sure to delete it?</Translation>
|
||||||
|
),
|
||||||
|
onOk: () => {
|
||||||
|
deletePipelineRun({
|
||||||
|
projectName: pipeline.project.name,
|
||||||
|
pipelineName: pipeline.name,
|
||||||
|
runName: name,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.success(i18n.t('The Pipeline Run removed successfully'));
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
locale: locale().Dialog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="commonDialog"
|
||||||
|
visible={true}
|
||||||
|
locale={locale().Dialog}
|
||||||
|
title={i18n.t('Pipeline Runs')}
|
||||||
|
onClose={props.onClose}
|
||||||
|
onCancel={props.onClose}
|
||||||
|
footerActions={['cancel']}
|
||||||
|
isFullScreen={true}
|
||||||
|
style={{ width: '1200px' }}
|
||||||
|
>
|
||||||
|
<Table loading={loading} dataSource={runs} locale={locale().Table}>
|
||||||
|
<Table.Column
|
||||||
|
key={'name'}
|
||||||
|
dataIndex="pipelineRunName"
|
||||||
|
title={i18n.t('Name')}
|
||||||
|
cell={(name: string) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/projects/${props.pipeline.project.name}/pipelines/${props.pipeline.name}/runs/${name}`}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'status'}
|
||||||
|
dataIndex="phase"
|
||||||
|
title={i18n.t('Status')}
|
||||||
|
cell={(phase: string, i: number, run: PipelineRunBriefing) => {
|
||||||
|
const show = (
|
||||||
|
<span
|
||||||
|
className={classNames({
|
||||||
|
colorRed: phase == 'failed',
|
||||||
|
colorGreen: phase == 'succeeded',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{phase.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
if (run.message) {
|
||||||
|
return <Balloon trigger={show}>{run.message}</Balloon>;
|
||||||
|
}
|
||||||
|
return show;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'startTime'}
|
||||||
|
dataIndex="startTime"
|
||||||
|
title={i18n.t('Start Time')}
|
||||||
|
cell={(value: string) => {
|
||||||
|
return momentDate(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'endTime'}
|
||||||
|
dataIndex="endTime"
|
||||||
|
title={i18n.t('End Time')}
|
||||||
|
cell={(value: string) => {
|
||||||
|
return momentDate(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Table.Column
|
||||||
|
key={'endTime'}
|
||||||
|
dataIndex="endTime"
|
||||||
|
title={i18n.t('Duration')}
|
||||||
|
cell={(value: string, i: number, run: PipelineRunBriefing) => {
|
||||||
|
return timeDiff(run.startTime, run.endTime);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'contextName'}
|
||||||
|
dataIndex="contextName"
|
||||||
|
title={i18n.t('Context Name')}
|
||||||
|
cell={(value: string) => {
|
||||||
|
return value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Table.Column
|
||||||
|
key={'actions'}
|
||||||
|
dataIndex="pipelineRunName"
|
||||||
|
title={i18n.t('Actions')}
|
||||||
|
cell={(name: string, i: number, run: PipelineRunBriefing) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<If condition={run.phase != 'executing'}>
|
||||||
|
<Permission
|
||||||
|
project={props.pipeline.project.name}
|
||||||
|
resource={{
|
||||||
|
resource: `project:${props.pipeline.project.name}/pipeline:${props.pipeline.name}/pipelineRun:${name}`,
|
||||||
|
action: 'delete',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
text
|
||||||
|
size={'small'}
|
||||||
|
ghost={true}
|
||||||
|
className={'danger-btn'}
|
||||||
|
component={'a'}
|
||||||
|
onClick={() => {
|
||||||
|
deleteRun(name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AiFillDelete />
|
||||||
|
<Translation>Remove</Translation>
|
||||||
|
</Button>
|
||||||
|
</Permission>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Table>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewRuns;
|
|
@ -0,0 +1,79 @@
|
||||||
|
.table-pipeline-list {
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
.next-table-cell.last .next-table-cell-wrapper {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-name,
|
||||||
|
.last-run .metadata {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
span {
|
||||||
|
color: var(--grey-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-run {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.metadata {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
margin-left: var(--spacing-4);
|
||||||
|
color: var(--execution-status-color);
|
||||||
|
--execution-status-color: var(--primary-color);
|
||||||
|
&.warning {
|
||||||
|
--execution-status-color: var(--failed-color);
|
||||||
|
}
|
||||||
|
&.success {
|
||||||
|
--execution-status-color: var(--success-color);
|
||||||
|
}
|
||||||
|
.status-text {
|
||||||
|
margin-left: var(--spacing-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
.active {
|
||||||
|
margin-left: var(--spacing-4);
|
||||||
|
font-size: var(--font-size-x-large);
|
||||||
|
}
|
||||||
|
.week {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
.rectangle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
width: 10px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
background: var(--grey-400);
|
||||||
|
&.success {
|
||||||
|
background: var(--success-color);
|
||||||
|
}
|
||||||
|
&.failure {
|
||||||
|
background: var(--failed-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-notice {
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 200px var(--spacing-8);
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
a {
|
||||||
|
margin: 0 var(--spacing-2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,456 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'dva';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
import Title from '../../components/ListTitle';
|
||||||
|
import { Loading, Button, Table, Dialog, Message, Balloon } from '@b-design/ui';
|
||||||
|
import SelectSearch from './components/SelectSearch';
|
||||||
|
import type { LoginUserInfo } from '../../interface/user';
|
||||||
|
import Permission from '../../components/Permission';
|
||||||
|
import Translation from '../../components/Translation';
|
||||||
|
import type { Pipeline, PipelineRun, RunStateInfo } from '../../interface/pipeline';
|
||||||
|
import locale from '../../utils/locale';
|
||||||
|
import { Link, routerRedux } from 'dva/router';
|
||||||
|
import type { NameAlias } from '../../interface/env';
|
||||||
|
import { deletePipeline, listPipelines } from '../../api/pipeline';
|
||||||
|
import { AiFillCaretRight, AiFillDelete } from 'react-icons/ai';
|
||||||
|
import { BiCopyAlt } from 'react-icons/bi';
|
||||||
|
import { HiViewList } from 'react-icons/hi';
|
||||||
|
import './index.less';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
import RunPipeline from '../../components/RunPipeline';
|
||||||
|
import ViewRuns from './components/ViewRuns';
|
||||||
|
import i18n from '../../i18n';
|
||||||
|
import CreatePipeline from './components/CreatePipeline';
|
||||||
|
import { beautifyTime, momentDate } from '../../utils/common';
|
||||||
|
import ClonePipeline from './components/PipelineClone';
|
||||||
|
import RunStatusIcon from '../PipelineRunPage/components/RunStatusIcon';
|
||||||
|
import type { AddonBaseStatus } from '../../interface/addon';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
targets?: [];
|
||||||
|
envs?: [];
|
||||||
|
history: any;
|
||||||
|
userInfo?: LoginUserInfo;
|
||||||
|
dispatch: Dispatch<any>;
|
||||||
|
enabledAddons?: AddonBaseStatus[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShowMode = 'table' | 'card' | string | null;
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
isLoading: boolean;
|
||||||
|
editItem?: Pipeline;
|
||||||
|
pipelines?: Pipeline[];
|
||||||
|
showMode: ShowMode;
|
||||||
|
showRunPipeline?: boolean;
|
||||||
|
pipeline?: Pipeline;
|
||||||
|
showRuns?: boolean;
|
||||||
|
showNewPipeline?: boolean;
|
||||||
|
showClonePipeline?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@connect((store: any) => {
|
||||||
|
return { ...store.user, ...store.addons };
|
||||||
|
})
|
||||||
|
class PipelineListPage extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isLoading: false,
|
||||||
|
showMode: 'list',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.getPipelines({});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPipelines = async (params: { projectName?: string; query?: string }) => {
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
listPipelines(params)
|
||||||
|
.then((res) => {
|
||||||
|
this.setState({
|
||||||
|
pipelines: res && Array.isArray(res.pipelines) ? res.pipelines : [],
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onShowPipelineRuns = (pipeline: Pipeline) => {
|
||||||
|
this.setState({ showRuns: true, pipeline: pipeline });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRunPipeline = (pipeline: Pipeline) => {
|
||||||
|
this.setState({ pipeline: pipeline, showRunPipeline: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onClonePipeline = (pipeline: Pipeline) => {
|
||||||
|
this.setState({ pipeline, showClonePipeline: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeletePipeline = (pipeline: Pipeline) => {
|
||||||
|
Dialog.confirm({
|
||||||
|
type: 'confirm',
|
||||||
|
content: <Translation>Unrecoverable after deletion, are you sure to delete it?</Translation>,
|
||||||
|
onOk: () => {
|
||||||
|
deletePipeline({
|
||||||
|
projectName: pipeline.project.name,
|
||||||
|
pipelineName: pipeline.name,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.success(i18n.t('The Pipeline removed successfully'));
|
||||||
|
this.getPipelines({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
locale: locale().Dialog,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderPipelineTable = () => {
|
||||||
|
const { pipelines } = this.state;
|
||||||
|
return (
|
||||||
|
<div className="table-pipeline-list">
|
||||||
|
<Table
|
||||||
|
className="customTable"
|
||||||
|
size="medium"
|
||||||
|
style={{ minWidth: '1400px' }}
|
||||||
|
locale={locale().Table}
|
||||||
|
dataSource={pipelines}
|
||||||
|
>
|
||||||
|
<Table.Column
|
||||||
|
key={'name'}
|
||||||
|
title={i18n.t('Name(Alias)')}
|
||||||
|
dataIndex="name"
|
||||||
|
cell={(name: string, i: number, pipeline: Pipeline) => {
|
||||||
|
let text = name;
|
||||||
|
if (pipeline.alias) {
|
||||||
|
text = `${name}(${pipeline.alias})`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="pipeline-name">
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({ pipeline: pipeline, showNewPipeline: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
<span>{pipeline.description}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'project'}
|
||||||
|
title={i18n.t('Project')}
|
||||||
|
dataIndex="project"
|
||||||
|
cell={(project: NameAlias) => {
|
||||||
|
let text = project.name;
|
||||||
|
if (project.alias) {
|
||||||
|
text = `${project.name}(${project.alias})`;
|
||||||
|
}
|
||||||
|
return <Link to={`/projects/${project.name}`}>{text}</Link>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'createTime'}
|
||||||
|
title={i18n.t('CreateTime')}
|
||||||
|
dataIndex="createTime"
|
||||||
|
width={160}
|
||||||
|
cell={(v: string) => {
|
||||||
|
return momentDate(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'runs'}
|
||||||
|
title={i18n.t('Recent Runs(Last 7-Days)')}
|
||||||
|
dataIndex="info.runStat"
|
||||||
|
width={'280px'}
|
||||||
|
cell={(runState: { activeNum: number; total: RunStateInfo; week?: RunStateInfo[] }) => {
|
||||||
|
if (!runState) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="run-state">
|
||||||
|
<div className="week">
|
||||||
|
{runState.week?.map((day) => {
|
||||||
|
const failure = day.total == 0 ? 0 : Math.floor((day.fail / day.total) * 100);
|
||||||
|
const success = 100 - failure;
|
||||||
|
return (
|
||||||
|
<Balloon
|
||||||
|
trigger={
|
||||||
|
<div className="rectangle">
|
||||||
|
<If condition={day.total > 0}>
|
||||||
|
<span className="failure" style={{ height: `${failure}%` }} />
|
||||||
|
<span className="success" style={{ height: `${success}%` }} />
|
||||||
|
</If>
|
||||||
|
<If condition={day.total == 0}>
|
||||||
|
<span style={{ height: `10px` }} />
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<If condition={day.total == 0}>No Run</If>
|
||||||
|
<If condition={day.total > 0}>
|
||||||
|
<p>Total: {day.total}</p>
|
||||||
|
<p>Success: {day.success}</p>
|
||||||
|
</If>
|
||||||
|
</Balloon>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<If condition={runState.total.total > 0}>
|
||||||
|
<div className="active">
|
||||||
|
<span>{runState.total.total}</span>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'lastRun'}
|
||||||
|
title={i18n.t('Last Run')}
|
||||||
|
dataIndex="info.lastRun"
|
||||||
|
cell={(run?: PipelineRun) => {
|
||||||
|
if (run) {
|
||||||
|
return (
|
||||||
|
<div className="last-run">
|
||||||
|
<div className="metadata">
|
||||||
|
<Link
|
||||||
|
to={`/projects/${run.project.name}/pipelines/${run.pipelineName}/runs/${run.pipelineRunName}`}
|
||||||
|
>
|
||||||
|
{run.pipelineRunName}
|
||||||
|
</Link>
|
||||||
|
<span>{beautifyTime(run.status?.startTime)}</span>
|
||||||
|
</div>
|
||||||
|
<RunStatusIcon runStatus={run.status} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span style={{ color: 'var(--grey-400)' }}>This Pipeline never ran.</span>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table.Column
|
||||||
|
key={'actions'}
|
||||||
|
title={i18n.t('Actions')}
|
||||||
|
dataIndex="name"
|
||||||
|
width={'360px'}
|
||||||
|
cell={(name: string, i: number, pipeline: Pipeline) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Permission
|
||||||
|
project={pipeline.project.name}
|
||||||
|
resource={{
|
||||||
|
resource: `project:${pipeline.project.name}/pipeline:${pipeline.name}`,
|
||||||
|
action: 'run',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
text
|
||||||
|
size={'medium'}
|
||||||
|
ghost={true}
|
||||||
|
component={'a'}
|
||||||
|
onClick={() => {
|
||||||
|
this.onRunPipeline(pipeline);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AiFillCaretRight /> <Translation>Run</Translation>
|
||||||
|
</Button>
|
||||||
|
<span className="line" />
|
||||||
|
</Permission>
|
||||||
|
<Permission
|
||||||
|
project={pipeline.project.name}
|
||||||
|
resource={{
|
||||||
|
resource: `project:${pipeline.project.name}/pipeline:${pipeline.name}/pipelineRun:*`,
|
||||||
|
action: 'list',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
text
|
||||||
|
size={'medium'}
|
||||||
|
ghost={true}
|
||||||
|
component={'a'}
|
||||||
|
onClick={() => {
|
||||||
|
this.onShowPipelineRuns(pipeline);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HiViewList />
|
||||||
|
<Translation>View Runs</Translation>
|
||||||
|
</Button>
|
||||||
|
<span className="line" />
|
||||||
|
</Permission>
|
||||||
|
<Permission
|
||||||
|
project={pipeline.project.name}
|
||||||
|
resource={{
|
||||||
|
resource: `project:${pipeline.project.name}/pipeline:*`,
|
||||||
|
action: 'create',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
text
|
||||||
|
size={'medium'}
|
||||||
|
ghost={true}
|
||||||
|
component={'a'}
|
||||||
|
onClick={() => {
|
||||||
|
this.onClonePipeline(pipeline);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BiCopyAlt /> <Translation>Clone</Translation>
|
||||||
|
</Button>
|
||||||
|
<span className="line" />
|
||||||
|
</Permission>
|
||||||
|
<Permission
|
||||||
|
project={pipeline.project.name}
|
||||||
|
resource={{
|
||||||
|
resource: `project:${pipeline.project.name}/pipeline:${pipeline.name}`,
|
||||||
|
action: 'delete',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
text
|
||||||
|
size={'medium'}
|
||||||
|
ghost={true}
|
||||||
|
className={'danger-btn'}
|
||||||
|
component={'a'}
|
||||||
|
onClick={() => {
|
||||||
|
this.onDeletePipeline(pipeline);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AiFillDelete />
|
||||||
|
<Translation>Remove</Translation>
|
||||||
|
</Button>
|
||||||
|
</Permission>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { userInfo } = this.props;
|
||||||
|
const {
|
||||||
|
showMode,
|
||||||
|
isLoading,
|
||||||
|
showRunPipeline,
|
||||||
|
pipeline,
|
||||||
|
showRuns,
|
||||||
|
showNewPipeline,
|
||||||
|
showClonePipeline,
|
||||||
|
} = this.state;
|
||||||
|
const { enabledAddons } = this.props;
|
||||||
|
const addonEnabled = enabledAddons?.filter((addon) => addon.name == 'vela-workflow').length;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title
|
||||||
|
title="Pipelines"
|
||||||
|
subTitle="Orchestrate your multiple cloud native app release processes or model your cloud native pipeline."
|
||||||
|
extButtons={[
|
||||||
|
<Permission
|
||||||
|
request={{ resource: 'project:?/pipeline:*', action: 'create' }}
|
||||||
|
project={'?'}
|
||||||
|
>
|
||||||
|
<If condition={addonEnabled}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({ showNewPipeline: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Translation>New Pipeline</Translation>
|
||||||
|
</Button>
|
||||||
|
</If>
|
||||||
|
</Permission>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectSearch
|
||||||
|
projects={userInfo?.projects}
|
||||||
|
showMode={showMode}
|
||||||
|
setMode={(mode: ShowMode) => {
|
||||||
|
this.setState({ showMode: mode });
|
||||||
|
if (mode) {
|
||||||
|
localStorage.setItem('application-list-mode', mode);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
getPipelines={(params: any) => {
|
||||||
|
this.getPipelines(params);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<If condition={addonEnabled}>
|
||||||
|
<Loading style={{ width: '100%' }} visible={isLoading}>
|
||||||
|
{this.renderPipelineTable()}
|
||||||
|
</Loading>
|
||||||
|
</If>
|
||||||
|
<If condition={!addonEnabled}>
|
||||||
|
<div className="addon-notice">
|
||||||
|
Please enable the <Link to="/addons/vela-workflow">vela-workflow</Link> Addon that
|
||||||
|
powers Pipeline.
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<If condition={showRunPipeline}>
|
||||||
|
{pipeline && (
|
||||||
|
<RunPipeline
|
||||||
|
onClose={() => {
|
||||||
|
this.setState({ showRunPipeline: false, pipeline: undefined });
|
||||||
|
}}
|
||||||
|
onSuccess={(runName) => {
|
||||||
|
this.props.dispatch(
|
||||||
|
routerRedux.push(
|
||||||
|
`/projects/${pipeline.project.name}/pipelines/${pipeline.name}/runs/${runName}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
pipeline={pipeline}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<If condition={showRuns}>
|
||||||
|
{pipeline && (
|
||||||
|
<ViewRuns
|
||||||
|
pipeline={pipeline}
|
||||||
|
onClose={() => {
|
||||||
|
this.setState({ showRuns: false, pipeline: undefined });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</If>
|
||||||
|
<If condition={showNewPipeline}>
|
||||||
|
<CreatePipeline
|
||||||
|
onClose={() => {
|
||||||
|
this.setState({ showNewPipeline: false, pipeline: undefined });
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
this.getPipelines({});
|
||||||
|
this.setState({ showNewPipeline: false, pipeline: undefined });
|
||||||
|
}}
|
||||||
|
pipeline={pipeline}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<If condition={showClonePipeline}>
|
||||||
|
<ClonePipeline
|
||||||
|
onClose={() => {
|
||||||
|
this.setState({ showClonePipeline: false, pipeline: undefined });
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
this.getPipelines({});
|
||||||
|
this.setState({ showClonePipeline: false, pipeline: undefined });
|
||||||
|
}}
|
||||||
|
pipeline={pipeline}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PipelineListPage;
|
|
@ -0,0 +1,272 @@
|
||||||
|
import { Grid, Breadcrumb, Button, Icon, Dialog, Message, Balloon } from '@b-design/ui';
|
||||||
|
import { connect } from 'dva';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
import Translation from '../../../../components/Translation';
|
||||||
|
import { Link, routerRedux } from 'dva/router';
|
||||||
|
import Permission from '../../../../components/Permission';
|
||||||
|
import type {
|
||||||
|
PipelineDetail,
|
||||||
|
PipelineRunBase,
|
||||||
|
PipelineRunStatus,
|
||||||
|
} from '../../../../interface/pipeline';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { momentDate, timeDiff } from '../../../../utils/common';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
import { runPipeline, stopPipelineRun } from '../../../../api/pipeline';
|
||||||
|
import locale from '../../../../utils/locale';
|
||||||
|
import RunStatusIcon from '../RunStatusIcon';
|
||||||
|
|
||||||
|
const { Row, Col } = Grid;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pipeline?: PipelineDetail;
|
||||||
|
runStatus?: PipelineRunStatus;
|
||||||
|
runBase?: PipelineRunBase;
|
||||||
|
loadRunStatus: () => void;
|
||||||
|
statusLoading: boolean;
|
||||||
|
dispatch?: Dispatch<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
stopLoading?: boolean;
|
||||||
|
reRunLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@connect((store: any) => {
|
||||||
|
return { ...store.user };
|
||||||
|
})
|
||||||
|
class Header extends Component<Props, State> {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
onRerun = () => {
|
||||||
|
Dialog.confirm({
|
||||||
|
type: 'alert',
|
||||||
|
content: 'Are you sure to rerun this Pipeline?',
|
||||||
|
locale: locale().Dialog,
|
||||||
|
onOk: () => {
|
||||||
|
const { pipeline, runBase } = this.props;
|
||||||
|
if (runBase && pipeline) {
|
||||||
|
this.setState({ reRunLoading: true });
|
||||||
|
runPipeline(runBase?.project.name, pipeline?.name, runBase.contextName)
|
||||||
|
.then((res) => {
|
||||||
|
debugger;
|
||||||
|
if (res && res.pipelineRunName && this.props.dispatch) {
|
||||||
|
this.props.dispatch(
|
||||||
|
routerRedux.push(
|
||||||
|
`/projects/${pipeline.project.name}/pipelines/${pipeline.name}/runs/${res.pipelineRunName}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ reRunLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onStop = () => {
|
||||||
|
Dialog.confirm({
|
||||||
|
type: 'alert',
|
||||||
|
content: 'Are you sure to stop this Pipeline?',
|
||||||
|
locale: locale().Dialog,
|
||||||
|
onOk: () => {
|
||||||
|
const { pipeline, runBase } = this.props;
|
||||||
|
if (runBase && pipeline) {
|
||||||
|
this.setState({ stopLoading: true });
|
||||||
|
stopPipelineRun({
|
||||||
|
pipelineName: pipeline?.name,
|
||||||
|
projectName: runBase?.project.name,
|
||||||
|
runName: runBase?.pipelineRunName,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.success('Pipeline stopped successfully.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ stopLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {}
|
||||||
|
|
||||||
|
componentWillUnmount() {}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { pipeline, runBase, runStatus, statusLoading } = this.props;
|
||||||
|
const projectName = (pipeline && pipeline.project?.name) || '';
|
||||||
|
const runCondition = runStatus?.conditions?.filter((con) => con.type === 'WorkflowRun');
|
||||||
|
const { reRunLoading, stopLoading } = this.state;
|
||||||
|
let message = runStatus?.message;
|
||||||
|
if (runStatus?.status == 'suspending' && !runStatus?.message) {
|
||||||
|
message = 'Pipeline need you approve.';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row style={{ marginBottom: '16px' }}>
|
||||||
|
<Col span={6} className={classNames('breadcrumb')}>
|
||||||
|
<Link to={'/'}>
|
||||||
|
<Icon type="home" />
|
||||||
|
</Link>
|
||||||
|
<Breadcrumb separator="/">
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<Link to={'/projects/' + projectName}>{projectName}</Link>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<Link to={`/pipelines`}>
|
||||||
|
<Translation>Pipelines</Translation>
|
||||||
|
</Link>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>{pipeline && (pipeline.alias || pipeline.name)}</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>{runBase?.pipelineRunName}</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
</Col>
|
||||||
|
<Col span={18} className="flexright" style={{ padding: '0 16px' }}>
|
||||||
|
<Permission
|
||||||
|
request={{
|
||||||
|
resource: `project:${projectName}/pipeline:${pipeline && pipeline.name}`,
|
||||||
|
action: 'run',
|
||||||
|
}}
|
||||||
|
project={projectName}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
loading={reRunLoading}
|
||||||
|
disabled={
|
||||||
|
runStatus?.status != 'failed' &&
|
||||||
|
runStatus?.status != 'succeeded' &&
|
||||||
|
runStatus?.status != 'terminated'
|
||||||
|
}
|
||||||
|
style={{ marginLeft: '16px' }}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => this.onRerun()}
|
||||||
|
>
|
||||||
|
<Translation>Rerun</Translation>
|
||||||
|
</Button>
|
||||||
|
</Permission>
|
||||||
|
<If condition={runStatus?.status == 'executing' || runStatus?.status == 'suspending'}>
|
||||||
|
<Permission
|
||||||
|
request={{
|
||||||
|
resource: `project:${projectName}/pipeline:${
|
||||||
|
pipeline && pipeline.name
|
||||||
|
}/pipelineRun:${runBase?.pipelineRunName}`,
|
||||||
|
action: 'run',
|
||||||
|
}}
|
||||||
|
project={projectName}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
loading={stopLoading}
|
||||||
|
style={{ marginLeft: '16px' }}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => this.onStop()}
|
||||||
|
>
|
||||||
|
<Translation>Stop</Translation>
|
||||||
|
</Button>
|
||||||
|
</Permission>
|
||||||
|
</If>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row className="description" wrap={true}>
|
||||||
|
<Col xl={16} xs={24}>
|
||||||
|
<div className="name_metadata">
|
||||||
|
<div>
|
||||||
|
<div className="name">{runBase?.pipelineRunName}</div>
|
||||||
|
<div className="metadata">
|
||||||
|
<div className="start_at">
|
||||||
|
<span className="label_key">Started at:</span>
|
||||||
|
<time className="label_value">{momentDate(runStatus?.startTime)}</time>
|
||||||
|
</div>
|
||||||
|
<div className="duration_time">
|
||||||
|
<span className="label_key">Duration:</span>
|
||||||
|
<time className="label_value">
|
||||||
|
{timeDiff(runStatus?.startTime, runStatus?.endTime)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
<div className="mode">
|
||||||
|
<span className="label_key">Mode:</span>
|
||||||
|
<time className="label_value">{runStatus?.mode?.steps || 'StepByStep'}</time>
|
||||||
|
</div>
|
||||||
|
<div className="mode">
|
||||||
|
<span className="label_key">Sub Step Mode:</span>
|
||||||
|
<time className="label_value">{runStatus?.mode?.subSteps || 'DAG'}</time>
|
||||||
|
</div>
|
||||||
|
<Balloon
|
||||||
|
trigger={
|
||||||
|
<div>
|
||||||
|
<span className="label_key">Context:</span>
|
||||||
|
<time className="label_value">{runBase?.contextName || '-'}</time>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{runBase?.contextValues?.map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.key}>
|
||||||
|
<span className="label_key">{item.key}=</span>
|
||||||
|
<span className="label_value">{item.value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Balloon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flexright">
|
||||||
|
<Button type="secondary" loading={statusLoading} onClick={this.props.loadRunStatus}>
|
||||||
|
<Icon type="refresh" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col xl={8} xs={24}>
|
||||||
|
<If
|
||||||
|
condition={
|
||||||
|
!runCondition || runCondition.length == 0 || runCondition[0].status == 'True'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'status',
|
||||||
|
{ warning: runStatus?.status == 'failed' },
|
||||||
|
{ success: runStatus?.status == 'succeeded' },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RunStatusIcon runStatus={runStatus} />
|
||||||
|
<If condition={message}>
|
||||||
|
<div className="message">
|
||||||
|
<div className="summary">
|
||||||
|
{runStatus?.status == 'failed' ? 'Error Summary' : 'Summary'}
|
||||||
|
</div>
|
||||||
|
<p className="text">{message}</p>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<If condition={runCondition && runCondition[0].status == 'False'}>
|
||||||
|
<div className={classNames('status', { warning: runStatus?.status == 'failed' })}>
|
||||||
|
<div className="icon">
|
||||||
|
<Icon type="wind-warning" />
|
||||||
|
<span>{(runStatus?.status || 'pending').toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
<If condition={runCondition && runCondition[0].message}>
|
||||||
|
<div className="message">
|
||||||
|
<div className="summary">Error Summary</div>
|
||||||
|
<p className="text">{runCondition && runCondition[0].message}</p>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Icon } from '@b-design/ui';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { FaStopCircle } from 'react-icons/fa';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
import type { PipelineRunStatus } from '../../../../interface/pipeline';
|
||||||
|
|
||||||
|
const RunStatusIcon = (props: { runStatus?: PipelineRunStatus }) => {
|
||||||
|
const { runStatus } = props;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'icon',
|
||||||
|
{ warning: runStatus?.status == 'failed' },
|
||||||
|
{ success: runStatus?.status == 'succeeded' },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<If condition={runStatus?.status == 'failed' || runStatus?.status == 'terminated'}>
|
||||||
|
<Icon type="wind-warning" />
|
||||||
|
</If>
|
||||||
|
<If condition={runStatus?.status == 'executing'}>
|
||||||
|
<Icon type="loading" />
|
||||||
|
</If>
|
||||||
|
<If condition={runStatus?.status == 'succeeded'}>
|
||||||
|
<Icon type="success-filling" />
|
||||||
|
</If>
|
||||||
|
<If condition={runStatus?.status == 'initializing'}>
|
||||||
|
<FaStopCircle />
|
||||||
|
</If>
|
||||||
|
<If condition={runStatus?.status == 'suspending'}>
|
||||||
|
<Icon type="clock-fill" />
|
||||||
|
</If>
|
||||||
|
<span className="status-text">{(runStatus?.status || 'pending').toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RunStatusIcon;
|
|
@ -0,0 +1,160 @@
|
||||||
|
.run-studio {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 230px);
|
||||||
|
background: linear-gradient(90deg, #f6fcff 9px, transparent 1%) center,
|
||||||
|
linear-gradient(#f6fcff 9px, transparent 1%) center, #bbc1c4;
|
||||||
|
background-size: 10px 10px;
|
||||||
|
.studio {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.detail {
|
||||||
|
width: 500px;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
transition: all 1s linear;
|
||||||
|
.next-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.next-tabs-tab-inner {
|
||||||
|
padding: 4px var(--padding-common) !important;
|
||||||
|
}
|
||||||
|
.next-tabs-nav {
|
||||||
|
padding: 4px 16px;
|
||||||
|
}
|
||||||
|
.detail-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 250px);
|
||||||
|
.step-info {
|
||||||
|
flex: auto;
|
||||||
|
overflow: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.step-log {
|
||||||
|
height: 600px;
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 16px;
|
||||||
|
color: var(--grey-300);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
background: #090909;
|
||||||
|
border-bottom: 1px solid var(--grey-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.step-info {
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--spacing-3);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
line-height: var(--font-size-small);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
width: 100px;
|
||||||
|
padding: var(--spacing-3);
|
||||||
|
color: var(--grey-500);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid var(--grey-200);
|
||||||
|
.name_metadata {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80% minmax(10px, 1fr);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
margin-bottom: var(--spacing-3);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
.label_key {
|
||||||
|
color: var(--grey-500);
|
||||||
|
}
|
||||||
|
.label_value {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: var(--grey-900);
|
||||||
|
}
|
||||||
|
.metadata {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-3);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
--typography-size: var(--font-size-small);
|
||||||
|
--intent-color: var(--grey-900);
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 90px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f7e19e;
|
||||||
|
--execution-status-color: var(--primary-color);
|
||||||
|
&.warning {
|
||||||
|
background-color: var(--red-100);
|
||||||
|
--execution-status-color: var(--failed-color);
|
||||||
|
}
|
||||||
|
&.success {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
--execution-status-color: #fff;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 16px;
|
||||||
|
color: var(--execution-status-color) !important;
|
||||||
|
svg:not([fill]) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
.status-text {
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
.summary {
|
||||||
|
margin-bottom: var(--spacing-1);
|
||||||
|
color: var(--red-600);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
line-height: 1.33;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-status-text-succeeded {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-status-text-failed {
|
||||||
|
color: var(--failed-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-status-text-running {
|
||||||
|
color: var(--running-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-status-text-skipped {
|
||||||
|
color: var(--grey-400);
|
||||||
|
}
|
|
@ -0,0 +1,480 @@
|
||||||
|
import { Button, Card, Icon, Loading, Tab } from '@b-design/ui';
|
||||||
|
import Ansi from 'ansi-to-react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'dva/router';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { If } from 'tsx-control-statements/components';
|
||||||
|
import {
|
||||||
|
loadPipeline,
|
||||||
|
loadPipelineRunBase,
|
||||||
|
loadPipelineRunStatus,
|
||||||
|
loadPipelineRunStepInputs,
|
||||||
|
loadPipelineRunStepLogs,
|
||||||
|
loadPipelineRunStepOutputs,
|
||||||
|
} from '../../api/pipeline';
|
||||||
|
import Empty from '../../components/Empty';
|
||||||
|
import PipelineGraph from '../../components/PipelineGraph';
|
||||||
|
import Translation from '../../components/Translation';
|
||||||
|
import type { WorkflowStepStatus } from '../../interface/application';
|
||||||
|
import type {
|
||||||
|
PipelineDetail,
|
||||||
|
PipelineRunBase,
|
||||||
|
PipelineRunStatus,
|
||||||
|
WorkflowStep,
|
||||||
|
WorkflowStepInputs,
|
||||||
|
WorkflowStepOutputs,
|
||||||
|
} from '../../interface/pipeline';
|
||||||
|
import { convertAny, timeDiff } from '../../utils/common';
|
||||||
|
import Header from './components/Header';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
match: {
|
||||||
|
params: {
|
||||||
|
pipelineName: string;
|
||||||
|
projectName: string;
|
||||||
|
runName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
location?: {
|
||||||
|
pathname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
loading: boolean;
|
||||||
|
runBase?: PipelineRunBase;
|
||||||
|
runStatus?: PipelineRunStatus;
|
||||||
|
showDetail: boolean;
|
||||||
|
stepName?: string;
|
||||||
|
logs?: string[];
|
||||||
|
logLoading?: boolean;
|
||||||
|
stepStatus?: WorkflowStepStatus;
|
||||||
|
activeKey: string | number;
|
||||||
|
pipelineDetail?: PipelineDetail;
|
||||||
|
outputLoading?: boolean;
|
||||||
|
inputLoading?: boolean;
|
||||||
|
outputs?: WorkflowStepOutputs;
|
||||||
|
inputs?: WorkflowStepInputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PipelineRunPage extends Component<Props, State> {
|
||||||
|
loop: boolean;
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
showDetail: false,
|
||||||
|
activeKey: 'detail',
|
||||||
|
};
|
||||||
|
this.loop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.onGetPipeline(this.props.match);
|
||||||
|
this.onGetRunBase(this.props.match);
|
||||||
|
this.onGetRunStatus(this.props.match);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
if (nextProps.location?.pathname != this.props.location?.pathname && nextProps.match) {
|
||||||
|
this.onGetPipeline(nextProps.match);
|
||||||
|
this.onGetRunBase(nextProps.match);
|
||||||
|
this.onGetRunStatus(nextProps.match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onGetPipeline = (match?: {
|
||||||
|
params: {
|
||||||
|
pipelineName: string;
|
||||||
|
projectName: string;
|
||||||
|
runName: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { projectName, pipelineName } = match ? match.params : this.props.match.params;
|
||||||
|
loadPipeline({ projectName: projectName, pipelineName: pipelineName }).then(
|
||||||
|
(res: PipelineDetail) => {
|
||||||
|
this.setState({ pipelineDetail: res });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onGetRunBase = (match?: {
|
||||||
|
params: {
|
||||||
|
pipelineName: string;
|
||||||
|
projectName: string;
|
||||||
|
runName: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { projectName, pipelineName, runName } = match ? match.params : this.props.match.params;
|
||||||
|
loadPipelineRunBase({ projectName, pipelineName, runName }).then((res) => {
|
||||||
|
this.setState({ runBase: res });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onGetStepLogs = () => {
|
||||||
|
const { projectName, pipelineName, runName } = this.props.match.params;
|
||||||
|
const { stepStatus } = this.state;
|
||||||
|
if (stepStatus?.name) {
|
||||||
|
this.setState({ logLoading: true });
|
||||||
|
loadPipelineRunStepLogs({
|
||||||
|
projectName,
|
||||||
|
pipelineName,
|
||||||
|
runName,
|
||||||
|
stepName: stepStatus?.name,
|
||||||
|
})
|
||||||
|
.then((res: { log: string }) => {
|
||||||
|
this.setState({ logs: res && res.log ? res.log.split('\n') : [] });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ logLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onGetRunStatus = (match?: {
|
||||||
|
params: {
|
||||||
|
pipelineName: string;
|
||||||
|
projectName: string;
|
||||||
|
runName: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { projectName, pipelineName, runName } =
|
||||||
|
match && match.params ? match.params : this.props.match.params;
|
||||||
|
this.setState({ loading: true });
|
||||||
|
loadPipelineRunStatus({ projectName, pipelineName, runName })
|
||||||
|
.then((res: PipelineRunStatus) => {
|
||||||
|
if (res) {
|
||||||
|
this.setState({ runStatus: res });
|
||||||
|
if (res.status === 'executing' && !this.loop) {
|
||||||
|
this.loop = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loop = false;
|
||||||
|
this.onGetRunStatus();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onGetStepOutput = () => {
|
||||||
|
const { projectName, pipelineName, runName } = this.props.match.params;
|
||||||
|
const { stepStatus } = this.state;
|
||||||
|
if (stepStatus) {
|
||||||
|
this.setState({ outputLoading: true });
|
||||||
|
loadPipelineRunStepOutputs({
|
||||||
|
projectName,
|
||||||
|
pipelineName,
|
||||||
|
runName,
|
||||||
|
stepName: stepStatus?.name,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const outputs: WorkflowStepOutputs | undefined =
|
||||||
|
res && Array.isArray(res.outputs) && res.outputs.length > 0
|
||||||
|
? res.outputs[0]
|
||||||
|
: undefined;
|
||||||
|
this.setState({
|
||||||
|
outputs: outputs,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ outputLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onGetStepInput = () => {
|
||||||
|
const { projectName, pipelineName, runName } = this.props.match.params;
|
||||||
|
const { stepStatus } = this.state;
|
||||||
|
if (stepStatus) {
|
||||||
|
this.setState({ inputLoading: true });
|
||||||
|
loadPipelineRunStepInputs({
|
||||||
|
projectName,
|
||||||
|
pipelineName,
|
||||||
|
runName,
|
||||||
|
stepName: stepStatus?.name,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const input: WorkflowStepInputs | undefined =
|
||||||
|
res && Array.isArray(res.inputs) && res.inputs.length > 0 ? res.inputs[0] : undefined;
|
||||||
|
this.setState({
|
||||||
|
inputs: input,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ inputLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onStepClick = (step: WorkflowStepStatus) => {
|
||||||
|
this.setState({ showDetail: true, stepStatus: step }, () => {
|
||||||
|
const { activeKey } = this.state;
|
||||||
|
this.onTabChange(activeKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onTabChange = (key: string | number) => {
|
||||||
|
const { stepStatus } = this.state;
|
||||||
|
this.setState({ activeKey: key });
|
||||||
|
if (key == 'outputs' && stepStatus) {
|
||||||
|
this.onGetStepOutput();
|
||||||
|
}
|
||||||
|
if (key == 'detail' && stepStatus) {
|
||||||
|
this.onGetStepLogs();
|
||||||
|
}
|
||||||
|
if (key == 'inputs' && stepStatus) {
|
||||||
|
this.onGetStepInput();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
runStatus,
|
||||||
|
showDetail,
|
||||||
|
logs,
|
||||||
|
runBase,
|
||||||
|
stepStatus,
|
||||||
|
logLoading,
|
||||||
|
pipelineDetail,
|
||||||
|
inputLoading,
|
||||||
|
outputLoading,
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
} = this.state;
|
||||||
|
let addonName: string | undefined = '';
|
||||||
|
let stepSpec: WorkflowStep | undefined;
|
||||||
|
runBase?.spec.workflowSpec.steps.map((step) => {
|
||||||
|
if (stepStatus && step.name == stepStatus.name) {
|
||||||
|
addonName = step.properties?.addonName;
|
||||||
|
stepSpec = step;
|
||||||
|
}
|
||||||
|
step.subSteps?.map((sub) => {
|
||||||
|
if (stepStatus && sub.name == stepStatus.name) {
|
||||||
|
addonName = sub.properties?.addonName;
|
||||||
|
stepSpec = sub;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="run-layout">
|
||||||
|
<Header
|
||||||
|
statusLoading={loading}
|
||||||
|
runBase={runBase}
|
||||||
|
runStatus={runStatus}
|
||||||
|
loadRunStatus={this.onGetRunStatus}
|
||||||
|
pipeline={pipelineDetail}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="run-studio"
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({ showDetail: false });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={classNames('studio')}>
|
||||||
|
{runStatus && (
|
||||||
|
<PipelineGraph pipeline={runStatus} zoom={1} onNodeClick={this.onStepClick} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<If condition={showDetail && stepStatus}>
|
||||||
|
<Card
|
||||||
|
title={'Step: ' + stepStatus?.name || stepStatus?.id}
|
||||||
|
className={classNames('detail')}
|
||||||
|
contentHeight="auto"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab animation={true} size="medium" onChange={this.onTabChange}>
|
||||||
|
<Tab.Item title={<Translation>Detail</Translation>} key={'detail'}>
|
||||||
|
<div className="detail-page">
|
||||||
|
<div className="step-info padding16">
|
||||||
|
{stepStatus && (
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Type:</th>
|
||||||
|
<td>{stepStatus.type}</td>
|
||||||
|
</tr>
|
||||||
|
<If condition={addonName}>
|
||||||
|
<tr>
|
||||||
|
<th>Addon:</th>
|
||||||
|
<td>
|
||||||
|
<Link to={`/addons/${addonName}`}>{addonName}</Link> (Click here to
|
||||||
|
check the addon detail)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</If>
|
||||||
|
<If condition={stepSpec?.if}>
|
||||||
|
<tr>
|
||||||
|
<th>Condition:</th>
|
||||||
|
<td>{stepSpec?.if}</td>
|
||||||
|
</tr>
|
||||||
|
</If>
|
||||||
|
<tr>
|
||||||
|
<th>First Execute Time:</th>
|
||||||
|
<td>{stepStatus.firstExecuteTime}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Last Execute Time:</th>
|
||||||
|
<td>{stepStatus.lastExecuteTime}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Duration:</th>
|
||||||
|
<td>
|
||||||
|
{timeDiff(stepStatus.firstExecuteTime, stepStatus.lastExecuteTime)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<If condition={stepSpec?.timeout}>
|
||||||
|
<tr>
|
||||||
|
<th>Timeout:</th>
|
||||||
|
<td>{stepSpec?.timeout}</td>
|
||||||
|
</tr>
|
||||||
|
</If>
|
||||||
|
<tr>
|
||||||
|
<th>Phase:</th>
|
||||||
|
<td className={'step-status-text-' + stepStatus.phase}>
|
||||||
|
{stepStatus.phase}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<If condition={stepStatus.message || stepStatus.reason}>
|
||||||
|
<tr>
|
||||||
|
<th>Message(Reason):</th>
|
||||||
|
<td>
|
||||||
|
{`${stepStatus.message || ''}`}
|
||||||
|
{stepStatus.reason && `(${stepStatus.reason})`}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</If>
|
||||||
|
</tbody>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="step-log">
|
||||||
|
<div className="header">
|
||||||
|
<div>Step Logs</div>
|
||||||
|
<Button
|
||||||
|
loading={logLoading}
|
||||||
|
onClick={() => {
|
||||||
|
this.onGetStepLogs();
|
||||||
|
}}
|
||||||
|
style={{ color: '#fff' }}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Icon type="refresh" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="logBox">
|
||||||
|
{logs?.map((line, i: number) => {
|
||||||
|
return (
|
||||||
|
<div key={`log-${i}`} className="logLine">
|
||||||
|
<span className="content">
|
||||||
|
<Ansi linkify={true}>{line}</Ansi>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab.Item>
|
||||||
|
<Tab.Item title="Properties" key={'properties'}>
|
||||||
|
<div className="step-info padding16">
|
||||||
|
<tbody>
|
||||||
|
{stepSpec &&
|
||||||
|
stepSpec.properties &&
|
||||||
|
Object.keys(stepSpec.properties).map((key: string) => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<th>{key}:</th>
|
||||||
|
<td>
|
||||||
|
{stepSpec &&
|
||||||
|
stepSpec.properties &&
|
||||||
|
convertAny(stepSpec.properties[key])}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</div>
|
||||||
|
</Tab.Item>
|
||||||
|
<Tab.Item title="Outputs" key={'outputs'}>
|
||||||
|
<If
|
||||||
|
condition={
|
||||||
|
!outputLoading && (!outputs || !outputs.values || outputs.values.length == 0)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Empty hideIcon message={'There are no outputs.'} />
|
||||||
|
</If>
|
||||||
|
<Loading visible={outputLoading} style={{ width: '100%' }}>
|
||||||
|
<div className="step-info padding16">
|
||||||
|
{outputs?.values?.map((value) => {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Name: </th>
|
||||||
|
<td>{value.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Value: </th>
|
||||||
|
<td>{value.value}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Value From</th>
|
||||||
|
<td>{value.valueFrom || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Loading>
|
||||||
|
</Tab.Item>
|
||||||
|
<Tab.Item title="Inputs" key={'inputs'}>
|
||||||
|
<If
|
||||||
|
condition={
|
||||||
|
!inputLoading && (!inputs || !inputs?.values || inputs.values.length == 0)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Empty hideIcon message={'There are no inputs.'} />
|
||||||
|
</If>
|
||||||
|
<Loading visible={inputLoading} style={{ width: '100%' }}>
|
||||||
|
<div className="step-info padding16">
|
||||||
|
<tbody>
|
||||||
|
{inputs?.values?.map((value) => {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>From Step: </th>
|
||||||
|
<td>{value.fromStep}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>From: </th>
|
||||||
|
<td>{value.from}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Value: </th>
|
||||||
|
<td>{value.value}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>ParameterKey</th>
|
||||||
|
<td>{value.parameterKey || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</div>
|
||||||
|
</Loading>
|
||||||
|
</Tab.Item>
|
||||||
|
</Tab>
|
||||||
|
</Card>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PipelineRunPage;
|
|
@ -1,12 +1,15 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { Table, Button, Tag, Balloon, Icon } from '@b-design/ui';
|
import { Table, Button, Tag, Balloon, Icon, Dialog, Message } from '@b-design/ui';
|
||||||
import type { ConfigDistribution } from '../../../../interface/configs';
|
import type { ConfigDistribution } from '../../../../interface/configs';
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
import Permission from '../../../../components/Permission';
|
import Permission from '../../../../components/Permission';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
import { momentDate } from '../../../../utils/common';
|
import { momentDate } from '../../../../utils/common';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { getProjectConfigDistributions } from '../../../../api/config';
|
import {
|
||||||
|
deleteProjectConfigDistribution,
|
||||||
|
getProjectConfigDistributions,
|
||||||
|
} from '../../../../api/config';
|
||||||
import type { WorkflowStepStatus } from '../../../../interface/application';
|
import type { WorkflowStepStatus } from '../../../../interface/application';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -47,7 +50,7 @@ class ConfigDistributionPage extends Component<Props, State> {
|
||||||
getProjectConfigDistributions({ projectName })
|
getProjectConfigDistributions({ projectName })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
distributions: Array.isArray(res.distributions) ? res.distributions : [],
|
distributions: res && Array.isArray(res.distributions) ? res.distributions : [],
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
@ -58,7 +61,20 @@ class ConfigDistributionPage extends Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
onDelete = (record: ConfigDistribution) => {
|
onDelete = (record: ConfigDistribution) => {
|
||||||
console.log(record);
|
const { projectName } = this.props;
|
||||||
|
Dialog.confirm({
|
||||||
|
type: 'confirm',
|
||||||
|
content: <Translation>Are you sure to delete?</Translation>,
|
||||||
|
onOk: () => {
|
||||||
|
deleteProjectConfigDistribution(projectName, record.name).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.success('Distribution deleted successfully');
|
||||||
|
this.listConfigDistributions(projectName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
locale: locale().Dialog,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -101,9 +117,11 @@ class ConfigDistributionPage extends Component<Props, State> {
|
||||||
) => {
|
) => {
|
||||||
const targetStatus = new Map<string, WorkflowStepStatus>();
|
const targetStatus = new Map<string, WorkflowStepStatus>();
|
||||||
record.status?.workflow?.steps?.map((step) => {
|
record.status?.workflow?.steps?.map((step) => {
|
||||||
const target = step.name.split('-');
|
if (step.name) {
|
||||||
if (target.length == 3 && target[0] == 'deploy') {
|
const target = step.name.split('-');
|
||||||
targetStatus.set(step.name.replace('deploy-', ''), step);
|
if (target.length == 3 && target[0] == 'deploy') {
|
||||||
|
targetStatus.set(step.name.replace('deploy-', ''), step);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
@ -204,14 +222,6 @@ class ConfigDistributionPage extends Component<Props, State> {
|
||||||
>
|
>
|
||||||
<Icon type="refresh" />
|
<Icon type="refresh" />
|
||||||
</Button>
|
</Button>
|
||||||
{/* <Permission
|
|
||||||
request={{ resource: `project/config:*`, action: 'create' }}
|
|
||||||
project={projectName}
|
|
||||||
>
|
|
||||||
<Button className="card-button-wrapper">
|
|
||||||
<Translation>Add</Translation>
|
|
||||||
</Button>
|
|
||||||
</Permission> */}
|
|
||||||
</section>
|
</section>
|
||||||
<section className="card-content-table">
|
<section className="card-content-table">
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -220,7 +220,7 @@ class Configs extends Component<Props, State> {
|
||||||
<Icon type="refresh" />
|
<Icon type="refresh" />
|
||||||
</Button>
|
</Button>
|
||||||
<Permission
|
<Permission
|
||||||
request={{ resource: `project/config:*`, action: 'create' }}
|
request={{ resource: `project:${projectName}/config:*`, action: 'create' }}
|
||||||
project={projectName}
|
project={projectName}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -56,9 +56,14 @@ class Summary extends Component<Props, State> {
|
||||||
<Targets projectName={params.projectName} />
|
<Targets projectName={params.projectName} />
|
||||||
<Permission
|
<Permission
|
||||||
project={params.projectName}
|
project={params.projectName}
|
||||||
request={{ resource: `project:${params.projectName}/configs:*`, action: 'list' }}
|
request={{ resource: `project:${params.projectName}/config:*`, action: 'list' }}
|
||||||
>
|
>
|
||||||
<Configs projectName={params.projectName} />
|
<Configs projectName={params.projectName} />
|
||||||
|
</Permission>
|
||||||
|
<Permission
|
||||||
|
project={params.projectName}
|
||||||
|
request={{ resource: `project:${params.projectName}/config:*`, action: 'distribute' }}
|
||||||
|
>
|
||||||
<ConfigDistributionPage projectName={params.projectName} />
|
<ConfigDistributionPage projectName={params.projectName} />
|
||||||
</Permission>
|
</Permission>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Table, Message, Dialog } from '@b-design/ui';
|
import { Table, Message, Dialog, Button } from '@b-design/ui';
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
import { deleteTarget } from '../../../../api/target';
|
import { deleteTarget } from '../../../../api/target';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
@ -8,6 +8,7 @@ import type { Project } from '../../../../interface/project';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
import { Link } from 'dva/router';
|
import { Link } from 'dva/router';
|
||||||
import Permission from '../../../../components/Permission';
|
import Permission from '../../../../components/Permission';
|
||||||
|
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
list?: [];
|
list?: [];
|
||||||
|
@ -84,14 +85,39 @@ class TableList extends Component<Props> {
|
||||||
key: 'operation',
|
key: 'operation',
|
||||||
title: <Translation>Actions</Translation>,
|
title: <Translation>Actions</Translation>,
|
||||||
dataIndex: 'operation',
|
dataIndex: 'operation',
|
||||||
|
width: '200px',
|
||||||
cell: (v: string, i: number, record: Target) => {
|
cell: (v: string, i: number, record: Target) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Permission
|
||||||
|
request={{ resource: `target:${record.name}`, action: 'update' }}
|
||||||
|
project={''}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
text={true}
|
||||||
|
component={'a'}
|
||||||
|
size={'medium'}
|
||||||
|
ghost={true}
|
||||||
|
className="margin-left-10"
|
||||||
|
onClick={() => {
|
||||||
|
this.onEdit(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AiFillSetting />
|
||||||
|
<Translation>Edit</Translation>
|
||||||
|
</Button>
|
||||||
|
<span className="line" />
|
||||||
|
</Permission>
|
||||||
<Permission
|
<Permission
|
||||||
request={{ resource: `target:${record.name}`, action: 'delete' }}
|
request={{ resource: `target:${record.name}`, action: 'delete' }}
|
||||||
project={''}
|
project={''}
|
||||||
>
|
>
|
||||||
<a
|
<Button
|
||||||
|
text={true}
|
||||||
|
component={'a'}
|
||||||
|
size={'medium'}
|
||||||
|
className="danger-btn"
|
||||||
|
ghost={true}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Dialog.confirm({
|
Dialog.confirm({
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
|
@ -107,21 +133,9 @@ class TableList extends Component<Props> {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<AiFillDelete />
|
||||||
<Translation>Remove</Translation>
|
<Translation>Remove</Translation>
|
||||||
</a>
|
</Button>
|
||||||
</Permission>
|
|
||||||
<Permission
|
|
||||||
request={{ resource: `target:${record.name}`, action: 'update' }}
|
|
||||||
project={''}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="margin-left-10"
|
|
||||||
onClick={() => {
|
|
||||||
this.onEdit(record);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Translation>Edit</Translation>
|
|
||||||
</a>
|
|
||||||
</Permission>
|
</Permission>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
.namespaceDialogWrapper {
|
|
||||||
width: 800px;
|
|
||||||
height: 300px;
|
|
||||||
.next-dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center !important;
|
|
||||||
.next-btn {
|
|
||||||
display: block;
|
|
||||||
flex: none !important;
|
|
||||||
width: 100px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.next-dialog-body {
|
|
||||||
width: 100% !important;
|
|
||||||
max-height: none !important;
|
|
||||||
}
|
|
||||||
.basic {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import { If } from 'tsx-control-statements/components';
|
||||||
import Translation from '../../../../components/Translation';
|
import Translation from '../../../../components/Translation';
|
||||||
import { createClusterNamespace } from '../../../../api/cluster';
|
import { createClusterNamespace } from '../../../../api/cluster';
|
||||||
import locale from '../../../../utils/locale';
|
import locale from '../../../../utils/locale';
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cluster?: string;
|
cluster?: string;
|
||||||
|
@ -123,7 +122,7 @@ class Namespace extends React.Component<Props, State> {
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
locale={locale().Dialog}
|
locale={locale().Dialog}
|
||||||
className={'namespaceDialogWrapper'}
|
className={'commonDialog'}
|
||||||
title={<Translation>Create Namespace</Translation>}
|
title={<Translation>Create Namespace</Translation>}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
isFullScreen={true}
|
isFullScreen={true}
|
||||||
|
|
|
@ -193,6 +193,7 @@ class UiSchema extends Component<Props, State> {
|
||||||
style={{ marginLeft: '8px' }}
|
style={{ marginLeft: '8px' }}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://kubevela.net/docs/reference/ui-schema"
|
href="https://kubevela.net/docs/reference/ui-schema"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Icon size="medium" type="external-link" />
|
<Icon size="medium" type="external-link" />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -52,10 +52,22 @@ export function isEnvPath(pathname: string) {
|
||||||
return (pathname || '').startsWith('/envs');
|
return (pathname || '').startsWith('/envs');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPipelinePath(pathname: string) {
|
||||||
|
if ((pathname || '').startsWith('/pipelines')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const re = new RegExp('^/projects/.*./pipelines.*');
|
||||||
|
return re.test(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
export function isUsersPath(pathname: string) {
|
export function isUsersPath(pathname: string) {
|
||||||
return (pathname || '').startsWith('/users');
|
return (pathname || '').startsWith('/users');
|
||||||
}
|
}
|
||||||
export function isProjectPath(pathname: string) {
|
export function isProjectPath(pathname: string) {
|
||||||
|
const re = new RegExp('^/projects/.*./pipelines.*');
|
||||||
|
if (re.test(pathname)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return (pathname || '').startsWith('/projects');
|
return (pathname || '').startsWith('/projects');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +122,21 @@ export function beautifyTime(time?: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function timeDiff(start?: string, end?: string): string {
|
||||||
|
if (!start) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
let endTime = moment(moment.now());
|
||||||
|
if (end) {
|
||||||
|
endTime = moment(end);
|
||||||
|
}
|
||||||
|
const seconds = endTime.diff(moment(start), 'seconds');
|
||||||
|
if (seconds > 60) {
|
||||||
|
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
||||||
|
}
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
export function beautifyBinarySize(value?: number) {
|
export function beautifyBinarySize(value?: number) {
|
||||||
if (null == value || value == 0) {
|
if (null == value || value == 0) {
|
||||||
return '0 Bytes';
|
return '0 Bytes';
|
||||||
|
@ -141,7 +168,7 @@ export const formItemLayout = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACKCLusterStatus = [
|
export const ACKClusterStatus = [
|
||||||
{
|
{
|
||||||
key: 'initial',
|
key: 'initial',
|
||||||
value: '集群创建中',
|
value: '集群创建中',
|
||||||
|
@ -249,3 +276,21 @@ export function checkEnabledAddon(addonName: string, enabledAddons?: AddonBaseSt
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertAny(data?: any): string {
|
||||||
|
if (!data) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
switch (typeof data) {
|
||||||
|
case 'string':
|
||||||
|
return data;
|
||||||
|
case 'boolean':
|
||||||
|
return data === true ? 'true' : 'false';
|
||||||
|
case 'object':
|
||||||
|
return JSON.stringify(data);
|
||||||
|
case 'number':
|
||||||
|
return data.toString();
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ const localeData = {
|
||||||
},
|
},
|
||||||
Dialog: {
|
Dialog: {
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
ok: 'Confirm',
|
ok: 'OK',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
},
|
},
|
||||||
Drawer: {
|
Drawer: {
|
||||||
|
@ -51,7 +51,7 @@ const localeData = {
|
||||||
},
|
},
|
||||||
Table: {
|
Table: {
|
||||||
empty: 'No Data',
|
empty: 'No Data',
|
||||||
ok: 'Confirm',
|
ok: 'OK',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
asc: 'Asc',
|
asc: 'Asc',
|
||||||
desc: 'Desc',
|
desc: 'Desc',
|
||||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -3276,6 +3276,11 @@ clone@^2.1.1, clone@^2.1.2:
|
||||||
resolved "https://registry.nlark.com/clone/download/clone-2.1.2.tgz"
|
resolved "https://registry.nlark.com/clone/download/clone-2.1.2.tgz"
|
||||||
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
|
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
|
||||||
|
|
||||||
|
clsx@^1.1.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||||
|
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||||
|
|
||||||
collection-visit@^1.0.0:
|
collection-visit@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz"
|
||||||
|
@ -9526,6 +9531,14 @@ react-dom@^16.3.0:
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.19.1"
|
scheduler "^0.19.1"
|
||||||
|
|
||||||
|
react-draggable@^4.4.5:
|
||||||
|
version "4.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c"
|
||||||
|
integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==
|
||||||
|
dependencies:
|
||||||
|
clsx "^1.1.1"
|
||||||
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
react-error-overlay@^6.0.9:
|
react-error-overlay@^6.0.9:
|
||||||
version "6.0.9"
|
version "6.0.9"
|
||||||
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz"
|
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz"
|
||||||
|
|
Loading…
Reference in New Issue