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",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node scripts/start.js",
|
||||
|
@ -69,6 +69,7 @@
|
|||
"react-dnd": "^7.3.2",
|
||||
"react-dnd-html5-backend": "^7.2.0",
|
||||
"react-dom": "^16.3.0",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-i18next": "11.13.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "7.1.0",
|
||||
|
|
|
@ -88,3 +88,8 @@ export function applyProjectConfigDistribution(
|
|||
const urlPath = project + `/${projectName}/distributions`;
|
||||
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 {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 800px;
|
||||
height: auto;
|
||||
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 {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
justify-content: center !important;
|
||||
|
||||
width: 100%;
|
||||
.next-btn {
|
||||
display: block;
|
||||
flex: none !important;
|
||||
|
@ -380,8 +396,10 @@ a {
|
|||
}
|
||||
|
||||
.next-dialog-body {
|
||||
flex: 1;
|
||||
width: 100% !important;
|
||||
max-height: none !important;
|
||||
margin-bottom: 48px !important;
|
||||
}
|
||||
|
||||
.basic {
|
||||
|
@ -423,10 +441,6 @@ a {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.next-dialog-body {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -474,10 +488,16 @@ a {
|
|||
color: @dangerColor !important;
|
||||
border: @dangerColor 1px solid !important;
|
||||
}
|
||||
|
||||
.next-btn.next-btn-text.danger-btn {
|
||||
border: @dangerColor 0px solid !important;
|
||||
}
|
||||
|
||||
.danger-btn:hover {
|
||||
color: #fff !important;
|
||||
background: @dangerColor !important;
|
||||
}
|
||||
|
||||
.danger-icon:hover {
|
||||
color: @dangerColor !important;
|
||||
}
|
||||
|
@ -485,3 +505,32 @@ a {
|
|||
.line {
|
||||
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;
|
||||
onChange?: (params: any) => void;
|
||||
onBlurEditor?: (value: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
class DefinitionCode extends React.Component<Props> {
|
||||
|
@ -88,8 +87,7 @@ class DefinitionCode extends React.Component<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { id, style } = this.props;
|
||||
return <div style={style} id={id} />;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,4 @@
|
|||
.drawer-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: calc(~'100% - 40px');
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.next-drawer {
|
||||
.customDrawer.next-drawer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
@ -27,6 +16,19 @@
|
|||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
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 {
|
||||
flex: 1;
|
||||
|
@ -35,19 +37,15 @@
|
|||
margin-top: 71px;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: calc(~'100% - 40px');
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ class DrawerWithFooter extends Component<Props, any> {
|
|||
<Drawer
|
||||
title={title}
|
||||
closeMode="close"
|
||||
className="customDrawer"
|
||||
closeable="close"
|
||||
onClose={onClose}
|
||||
visible={true}
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
padding: 32px 16px;
|
||||
svg {
|
||||
width: 80px;
|
||||
}
|
||||
.message {
|
||||
padding: 8px;
|
||||
color: var(--grey-500);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,60 @@
|
|||
import React from 'react';
|
||||
import { If } from 'tsx-control-statements/components';
|
||||
import Translation from '../Translation';
|
||||
|
||||
import './index.less';
|
||||
|
||||
type Props = {
|
||||
message?: string | React.ReactNode;
|
||||
style?: {};
|
||||
style?: React.CSSProperties;
|
||||
iconWidth?: string;
|
||||
hideIcon?: boolean;
|
||||
};
|
||||
|
||||
const Empty = (props: Props) => {
|
||||
return (
|
||||
<div className="empty" style={props.style}>
|
||||
<div>
|
||||
<svg
|
||||
style={{ width: props.iconWidth }}
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
p-id="1896"
|
||||
fill="#bfbfbf"
|
||||
/>
|
||||
</svg>
|
||||
<If condition={!props.hideIcon}>
|
||||
<svg
|
||||
style={{ width: props.iconWidth }}
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
p-id="1896"
|
||||
fill="#bfbfbf"
|
||||
/>
|
||||
</svg>
|
||||
</If>
|
||||
</div>
|
||||
<div className="message">{props.message || <Translation>Empty Data</Translation>}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
.title-wrapper {
|
||||
height: 45px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
line-height: 36px;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
|
@ -9,10 +13,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@media (max-width: 719px) and (min-width: 480px) {
|
||||
.title-wrapper {
|
||||
.subTitle {
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
import React from 'react';
|
||||
import { Button, Grid } from '@b-design/ui';
|
||||
import { Button } from '@b-design/ui';
|
||||
import Translation from '../Translation';
|
||||
import './index.less';
|
||||
import { If } from 'tsx-control-statements/components';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
subTitle?: string;
|
||||
extButtons?: [React.ReactNode];
|
||||
addButtonTitle?: string;
|
||||
addButtonClick?: () => void;
|
||||
buttonSize?: 'small' | 'medium' | 'large';
|
||||
};
|
||||
export default function (props: Props) {
|
||||
const { Row, Col } = Grid;
|
||||
const { title, subTitle, extButtons, addButtonTitle, addButtonClick } = props;
|
||||
const { title, subTitle, extButtons, addButtonTitle, addButtonClick, buttonSize } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row className="title-wrapper" wrap={true}>
|
||||
<Col xl={15} xs={24}>
|
||||
<span className="title font-size-20">
|
||||
<Translation>{title}</Translation>
|
||||
</span>
|
||||
<div className="title-wrapper">
|
||||
<div>
|
||||
<span className="title font-size-20">
|
||||
<Translation>{title}</Translation>
|
||||
</span>
|
||||
|
||||
{subTitle && (
|
||||
<span className="subTitle font-size-14">
|
||||
<Translation>{subTitle}</Translation>
|
||||
</span>
|
||||
</Col>
|
||||
<Col xl={9} xs={24}>
|
||||
<div className="float-right">
|
||||
{extButtons &&
|
||||
extButtons.map((item) => {
|
||||
return item;
|
||||
})}
|
||||
<If condition={addButtonTitle}>
|
||||
<Button type="primary" onClick={addButtonClick}>
|
||||
<Translation>{addButtonTitle ? addButtonTitle : ''}</Translation>
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="float-right">
|
||||
{extButtons &&
|
||||
extButtons.map((item) => {
|
||||
return item;
|
||||
})}
|
||||
<If condition={addButtonTitle}>
|
||||
<Button size={buttonSize ? buttonSize : 'medium'} type="primary" onClick={addButtonClick}>
|
||||
<Translation>{addButtonTitle ? addButtonTitle : ''}</Translation>
|
||||
</Button>
|
||||
</If>
|
||||
</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>
|
||||
</Form.Item>
|
||||
<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>
|
||||
</a>
|
||||
</If>
|
||||
|
@ -416,7 +416,11 @@ class PlatformSetting extends React.Component<Props, State> {
|
|||
title={
|
||||
<span>
|
||||
<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" />
|
||||
</a>
|
||||
</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}>
|
||||
<p>
|
||||
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
|
||||
</a>
|
||||
</p>
|
||||
|
@ -419,6 +424,7 @@ class UISchema extends Component<Props, State> {
|
|||
>
|
||||
<Input
|
||||
disabled={disableEdit}
|
||||
autoComplete="off"
|
||||
{...init(param.jsonKey, {
|
||||
initValue: initValue,
|
||||
rules: convertRule(param.validate),
|
||||
|
@ -438,6 +444,7 @@ class UISchema extends Component<Props, State> {
|
|||
<Input
|
||||
disabled={disableEdit}
|
||||
htmlType="password"
|
||||
autoComplete="off"
|
||||
{...init(param.jsonKey, {
|
||||
initValue: initValue,
|
||||
rules: convertRule(param.validate),
|
||||
|
|
|
@ -73,7 +73,7 @@ class ImageSecretSelect extends Component<Props, State> {
|
|||
render() {
|
||||
const { disabled, onChange, value } = this.props;
|
||||
const { registries, loading, inputRepo } = this.state;
|
||||
const dataSource = registries;
|
||||
const dataSource = registries || [];
|
||||
if (inputRepo) {
|
||||
dataSource.unshift({ secretName: inputRepo, name: inputRepo });
|
||||
}
|
||||
|
|
|
@ -79,4 +79,5 @@ export interface EnableAddonRequest {
|
|||
version: string;
|
||||
properties: Record<string, any>;
|
||||
clusters?: string[];
|
||||
registry?: string;
|
||||
}
|
||||
|
|
|
@ -142,17 +142,21 @@ export interface WorkflowStatus {
|
|||
startTime?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowStepStatus {
|
||||
interface StepStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
phase: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
phase: 'succeeded' | 'failed' | 'skipped' | 'stopped' | 'running' | 'pending';
|
||||
message?: string;
|
||||
reason?: string;
|
||||
firstExecuteTime?: string;
|
||||
lastExecuteTime?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowStepStatus extends StepStatus {
|
||||
subSteps?: StepStatus[];
|
||||
}
|
||||
|
||||
export interface Trait {
|
||||
alias?: string;
|
||||
description?: string;
|
||||
|
@ -309,6 +313,7 @@ export interface Trigger {
|
|||
workflowName: string;
|
||||
type: 'webhook';
|
||||
payloadType?: 'custom' | 'dockerHub' | 'ACR' | 'harbor' | 'artifactory';
|
||||
registry?: string;
|
||||
token: string;
|
||||
createTime?: 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 React, { Component } from 'react';
|
||||
import Translation from '../../../../components/Translation';
|
||||
|
@ -25,6 +25,7 @@ import locale from '../../../../utils/locale';
|
|||
import DeployConfig from '../DeployConfig';
|
||||
import i18n from 'i18next';
|
||||
import Permission from '../../../../components/Permission';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
|
@ -160,11 +161,11 @@ class ApplicationHeader extends Component<Props, State> {
|
|||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col span={6} className="padding16">
|
||||
<Col span={6} className={classNames('padding16', 'breadcrumb')}>
|
||||
<Link to={'/'}>
|
||||
<Icon type="home" />
|
||||
</Link>
|
||||
<Breadcrumb separator="/">
|
||||
<Breadcrumb.Item>
|
||||
<Translation>Projects</Translation>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<Link to={'/projects/' + projectName}>{projectName}</Link>
|
||||
</Breadcrumb.Item>
|
||||
|
|
|
@ -119,6 +119,7 @@ class CloudShell extends Component<Props, State> {
|
|||
className="full-screen"
|
||||
target="_blank"
|
||||
href={this.loadFullScreenAddress()}
|
||||
rel="noopener noreferrer"
|
||||
title="Full Screen"
|
||||
>
|
||||
<AiOutlineFullscreen color="#fff" />
|
||||
|
|
|
@ -27,6 +27,8 @@ import DefinitionsLayout from '../Definitions';
|
|||
import Definitions from '../../pages/Definitions';
|
||||
import DefinitionDetails from '../DefinitionDetails';
|
||||
import UiSchema from '../../pages/UiSchema';
|
||||
import PipelineRunPage from '../../pages/PipelineRunPage';
|
||||
import PipelineListPage from '../../pages/PipelineListPage';
|
||||
|
||||
export default function Content() {
|
||||
return (
|
||||
|
@ -130,6 +132,12 @@ export default function Content() {
|
|||
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="/clusters" component={Clusters} />
|
||||
<Route path="/addons/:addonName" component={Addons} />
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
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 Translation from '../../components/Translation';
|
||||
import type { LoginUserInfo } from '../../interface/user';
|
||||
import type { DefinitionMenuType } from '../../interface/definitions';
|
||||
import _ from 'lodash';
|
||||
import './index.less';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
|
@ -66,7 +67,10 @@ class DefinitionDetailsLayout extends Component<Props> {
|
|||
return (
|
||||
<Fragment>
|
||||
<Row>
|
||||
<Col span={6} className="padding16">
|
||||
<Col span={6} className={classNames('padding16', 'breadcrumb')}>
|
||||
<Link to={'/'}>
|
||||
<Icon type="home" />
|
||||
</Link>
|
||||
<Breadcrumb separator="/">
|
||||
<Breadcrumb.Item>
|
||||
<Translation>Definitions</Translation>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
isProjectPath,
|
||||
isRolesPath,
|
||||
isConfigPath,
|
||||
isPipelinePath,
|
||||
isDefinitionsPath,
|
||||
} from '../../utils/common';
|
||||
export function getLeftSlider(pathname) {
|
||||
|
@ -16,6 +17,7 @@ export function getLeftSlider(pathname) {
|
|||
const isAddons = isAddonsPath(pathname);
|
||||
const isTarget = isTargetURL(pathname);
|
||||
const isEnv = isEnvPath(pathname);
|
||||
const isPipeline = isPipelinePath(pathname);
|
||||
const isUser = isUsersPath(pathname);
|
||||
const isProject = isProjectPath(pathname);
|
||||
const isRole = isRolesPath(pathname);
|
||||
|
@ -35,10 +37,17 @@ export function getLeftSlider(pathname) {
|
|||
{
|
||||
className: isEnv,
|
||||
link: '/envs',
|
||||
iconType: 'Directory-tree',
|
||||
iconType: 'layer-group',
|
||||
navName: 'Environments',
|
||||
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 { connect } from 'dva';
|
||||
import { Grid, Breadcrumb } from '@b-design/ui';
|
||||
import { Grid, Breadcrumb, Icon } from '@b-design/ui';
|
||||
import { Link } from 'dva/router';
|
||||
import Translation from '../../components/Translation';
|
||||
import './index.less';
|
||||
import type { ProjectDetail } from '../../interface/project';
|
||||
import { checkPermission } from '../../utils/permission';
|
||||
import type { LoginUserInfo } from '../../interface/user';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
|
@ -109,10 +110,13 @@ class ProjectLayout extends Component<Props> {
|
|||
return (
|
||||
<Fragment>
|
||||
<Row>
|
||||
<Col span={6} className="padding16">
|
||||
<Col span={6} className={classNames('padding16', 'breadcrumb')}>
|
||||
<Link to={'/'}>
|
||||
<Icon type="home" />
|
||||
</Link>
|
||||
<Breadcrumb separator="/">
|
||||
<Breadcrumb.Item>
|
||||
<Translation>Projects</Translation>
|
||||
<Link to={'/projects'}>Projects</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<Link to={'/projects/' + projectDetails?.name}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 type { LoginUserInfo } from '../../../../interface/user';
|
||||
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');
|
||||
}
|
||||
|
||||
showClickButtons = () => {
|
||||
const { isLoading } = this.state;
|
||||
return [
|
||||
<Button type="primary" onClick={this.onUpdateUser} loading={isLoading}>
|
||||
{i18n.t('Update')}
|
||||
</Button>,
|
||||
];
|
||||
};
|
||||
|
||||
handleClickLook = () => {
|
||||
this.setState({
|
||||
isLookPassword: !this.state.isLookPassword,
|
||||
|
@ -80,6 +71,7 @@ class EditPlatFormUserDialog extends Component<Props, State> {
|
|||
|
||||
render() {
|
||||
const init = this.field.init;
|
||||
const { isLoading } = this.state;
|
||||
const { Row, Col } = Grid;
|
||||
const FormItem = Form.Item;
|
||||
const formItemLayout = {
|
||||
|
@ -95,12 +87,13 @@ class EditPlatFormUserDialog extends Component<Props, State> {
|
|||
<Dialog
|
||||
visible={true}
|
||||
title={this.showTitle()}
|
||||
className={'commonDialog'}
|
||||
style={{ width: '600px' }}
|
||||
onOk={this.onUpdateUser}
|
||||
locale={locale().Dialog}
|
||||
footerActions={['ok']}
|
||||
>
|
||||
<Form {...formItemLayout} field={this.field}>
|
||||
<Form loading={isLoading} {...formItemLayout} field={this.field}>
|
||||
<Row>
|
||||
<Col span={24} style={{ padding: '0 8px' }}>
|
||||
<FormItem label={<Translation>Password</Translation>} required>
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
.item {
|
||||
margin-right: var(--spacing-6);
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
@ -266,7 +266,12 @@ class TopBar extends Component<Props, State> {
|
|||
</Permission>
|
||||
|
||||
<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" />
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -59,7 +59,9 @@
|
|||
.layout-shell {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.layout {
|
||||
min-width: var(--min-layout-width);
|
||||
}
|
||||
.layout-navigation {
|
||||
width: 240px;
|
||||
min-width: 60px;
|
||||
|
|
43
src/lib.less
43
src/lib.less
|
@ -1,8 +1,49 @@
|
|||
:root {
|
||||
--min-layout-width: 1400px;
|
||||
--primary-color: #1b58f4;
|
||||
--warning-color: var(--message-warning-color-icon-inline, #fac800);
|
||||
--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;
|
||||
@F7F7F7: #f7f7f7;
|
||||
@F9F8FF: #f9f8ff;
|
||||
|
@ -11,7 +52,7 @@
|
|||
@dangerColor: rgb(248, 81, 73);
|
||||
@warningColor: var(--message-warning-color-icon-inline, #fac800);
|
||||
@colorRED: red;
|
||||
@colorGREEN: green;
|
||||
@colorGREEN: #3fb950;
|
||||
@colorBLUE: blue;
|
||||
@colorFFF: #fff;
|
||||
@color333: #333;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"App Name-": "应用名称-",
|
||||
"Cluster Bind": "集群绑定",
|
||||
"App Describe": "应用备注",
|
||||
"Confirm": "确认",
|
||||
"OK": "确认",
|
||||
"Visit": "访问",
|
||||
"Workflow": "工作流",
|
||||
"Enter template name": "输入模版名称",
|
||||
|
@ -447,7 +447,8 @@
|
|||
"Are you sure you want to delete the project": "你确定想删除这个项目吗",
|
||||
"Please enter a project name": "请输入符合规范的项目名称",
|
||||
"Name(Alias)": "名称(别名)",
|
||||
"Configs": "集成配置",
|
||||
"Configs": "配置",
|
||||
"Config Distributions": "配置分发",
|
||||
"Summary": "概览",
|
||||
"Roles": "角色",
|
||||
"Members": "成员",
|
||||
|
@ -580,5 +581,16 @@
|
|||
"This policy is being used by workflow, do you want to force delete it?": "此策略正被工作流引用,是否强制删除它?",
|
||||
"The application is synchronizing 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,
|
||||
version: this.state.version,
|
||||
properties: values.properties,
|
||||
registry: this.state.addonDetailInfo?.registryName,
|
||||
};
|
||||
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
|
||||
params.clusters = this.state.clusters;
|
||||
|
@ -248,6 +249,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
|||
name: this.props.addonName,
|
||||
version: this.state.version,
|
||||
properties: properties,
|
||||
registry: this.state.addonDetailInfo?.registryName,
|
||||
};
|
||||
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
|
||||
params.clusters = this.state.clusters;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
.addon-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
.tag-search {
|
||||
|
|
|
@ -71,58 +71,60 @@ class SelectSearch extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div className="border-radius-8 addon-search">
|
||||
<Row 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.handleChangRegistry}
|
||||
className="item"
|
||||
value={registryValue}
|
||||
>
|
||||
<Option value="">
|
||||
<Translation>All</Translation>
|
||||
</Option>
|
||||
{registries?.map((item: any) => {
|
||||
return (
|
||||
<Option key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Col>
|
||||
<div>
|
||||
<Row 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.handleChangRegistry}
|
||||
className="item"
|
||||
value={registryValue}
|
||||
>
|
||||
<Option value="">
|
||||
<Translation>All</Translation>
|
||||
</Option>
|
||||
{registries?.map((item: any) => {
|
||||
return (
|
||||
<Option key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</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 }}
|
||||
/>
|
||||
}
|
||||
placeholder={queryPlaceholder}
|
||||
onChange={this.handleChangName}
|
||||
onPressEnter={this.handleClickSearch}
|
||||
value={inputValue}
|
||||
className="item"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="tag-search">
|
||||
<div className="tag-name">
|
||||
<Translation>Tags</Translation>
|
||||
</div>
|
||||
<div className="tag-list">
|
||||
<Checkbox.Group
|
||||
dataSource={this.generateTagList()}
|
||||
onChange={(tags) => {
|
||||
this.props.onTagChange(tags);
|
||||
}}
|
||||
/>
|
||||
<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 }}
|
||||
/>
|
||||
}
|
||||
placeholder={queryPlaceholder}
|
||||
onChange={this.handleChangName}
|
||||
onPressEnter={this.handleClickSearch}
|
||||
value={inputValue}
|
||||
className="item"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="tag-search">
|
||||
<div className="tag-name">
|
||||
<Translation>Tags</Translation>
|
||||
</div>
|
||||
<div className="tag-list">
|
||||
<Checkbox.Group
|
||||
dataSource={this.generateTagList()}
|
||||
onChange={(tags) => {
|
||||
this.props.onTagChange(tags);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -102,6 +102,7 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
payloadType = '',
|
||||
workflowName = '',
|
||||
componentName = '',
|
||||
registry = '',
|
||||
} = values;
|
||||
const query = { appName };
|
||||
const params: Trigger = {
|
||||
|
@ -113,13 +114,13 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
workflowName,
|
||||
token: '',
|
||||
componentName,
|
||||
registry,
|
||||
};
|
||||
createTriggers(params, query).then((res: any) => {
|
||||
if (res) {
|
||||
Message.success({
|
||||
duration: 4000,
|
||||
title: 'Trigger create success.',
|
||||
content: 'Trigger create success.',
|
||||
content: 'Trigger created successfully.',
|
||||
});
|
||||
this.props.onOK(res);
|
||||
}
|
||||
|
@ -266,7 +267,7 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Row wrap>
|
||||
<Col span={12} style={{ padding: '0 8px' }}>
|
||||
<FormItem label={<Translation>Type</Translation>} required>
|
||||
<Select
|
||||
|
@ -307,6 +308,34 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
</FormItem>
|
||||
</If>
|
||||
</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>
|
||||
|
|
|
@ -236,6 +236,9 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
|||
(c) => c.workloadType?.type == 'configurations.terraform.core.oam.dev',
|
||||
);
|
||||
const showCloudInstance = cloudComponents?.length && cloudComponents?.length > 0;
|
||||
const queryPod =
|
||||
cloudComponents?.length == undefined ||
|
||||
(components?.length && components.length > cloudComponents?.length);
|
||||
const { target, componentName } = this.state;
|
||||
const envs = envbinding.filter((item) => item.name == envName);
|
||||
if (applicationDetail && applicationDetail.name && envs.length > 0) {
|
||||
|
@ -251,20 +254,22 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
|||
param.clusterNs = target.cluster?.namespace || '';
|
||||
}
|
||||
this.setState({ loading: true });
|
||||
listApplicationPods(param)
|
||||
.then((re) => {
|
||||
if (re && re.podList) {
|
||||
re.podList.map((item: any) => {
|
||||
item.primaryKey = item.metadata.name;
|
||||
});
|
||||
this.setState({ podList: re.podList });
|
||||
} else {
|
||||
this.setState({ podList: [] });
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
if (queryPod) {
|
||||
listApplicationPods(param)
|
||||
.then((re) => {
|
||||
if (re && re.podList) {
|
||||
re.podList.map((item: any) => {
|
||||
item.primaryKey = item.metadata.name;
|
||||
});
|
||||
this.setState({ podList: re.podList });
|
||||
} else {
|
||||
this.setState({ podList: [] });
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
if (showCloudInstance) {
|
||||
this.setState({ loading: true });
|
||||
listCloudResources(param)
|
||||
|
@ -587,7 +592,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
|||
cell={(value: string, index: number, record: CloudInstance) => {
|
||||
if (record.url) {
|
||||
return (
|
||||
<a target="_blank" href={record.url}>
|
||||
<a target="_blank" href={record.url} rel="noopener noreferrer">
|
||||
{value}
|
||||
</a>
|
||||
);
|
||||
|
@ -625,7 +630,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
|
|||
cell={(value: string, index: number, record: CloudInstance) => {
|
||||
if (record.instanceName) {
|
||||
return (
|
||||
<a target="_blank" href={value}>
|
||||
<a target="_blank" href={value} rel="noopener noreferrer">
|
||||
<Translation>Console</Translation>
|
||||
</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 DrawerWithFooter from '../../../../components/Drawer';
|
||||
import Translation from '../../../../components/Translation';
|
||||
import './index.less';
|
||||
import type { Project } from '../../../../interface/project';
|
||||
import { connect } from 'dva';
|
||||
import locale from '../../../../utils/locale';
|
||||
|
@ -96,8 +95,8 @@ class AppDialog extends React.Component<Props, State> {
|
|||
}
|
||||
return;
|
||||
});
|
||||
if (defaultProject != '') {
|
||||
this.setState({ project: defaultProject }, () => {
|
||||
if (projectName || defaultProject) {
|
||||
this.setState({ project: projectName ? projectName : defaultProject }, () => {
|
||||
this.loadEnvs();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { checkPermission } from '../../../../utils/permission';
|
|||
import Permission from '../../../../components/Permission';
|
||||
import type { ShowMode } from '../..';
|
||||
import type { Project } from '../../../../interface/project';
|
||||
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||
const { Column } = Table;
|
||||
|
||||
type State = {
|
||||
|
@ -79,6 +80,7 @@ class CardContent extends React.Component<Props, State> {
|
|||
this.onEditAppPlan(item);
|
||||
}}
|
||||
>
|
||||
<AiFillSetting />
|
||||
<Translation>Edit</Translation>
|
||||
</Button>
|
||||
);
|
||||
|
@ -89,7 +91,10 @@ class CardContent extends React.Component<Props, State> {
|
|||
this.onEditAppPlan(item);
|
||||
}}
|
||||
>
|
||||
<Translation>Edit</Translation>
|
||||
<div className="dropdown-menu-item inline-center">
|
||||
<AiFillSetting />
|
||||
<Translation>Edit</Translation>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
} else {
|
||||
|
@ -116,14 +121,23 @@ class CardContent extends React.Component<Props, State> {
|
|||
if (checkPermission(request, project, userInfo)) {
|
||||
if (button) {
|
||||
return (
|
||||
<Button text size={'medium'} ghost={true} component={'a'} onClick={onClick}>
|
||||
<Translation>Remove</Translation>
|
||||
<Button
|
||||
text
|
||||
size={'medium'}
|
||||
className="danger-btn"
|
||||
ghost={true}
|
||||
component={'a'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<AiFillDelete /> <Translation>Remove</Translation>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Menu.Item onClick={onClick}>
|
||||
<Translation>Remove</Translation>
|
||||
<div className="dropdown-menu-item inline-center">
|
||||
<AiFillDelete /> <Translation>Remove</Translation>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
} else {
|
||||
|
@ -171,9 +185,9 @@ class CardContent extends React.Component<Props, State> {
|
|||
cell: (v: string, i: number, record: ApplicationBase) => {
|
||||
return (
|
||||
<div>
|
||||
{this.isDeletePermission(record, true)}
|
||||
<span className="line" />
|
||||
{this.isEditPermission(record, true)}
|
||||
<span className="line" />
|
||||
{this.isDeletePermission(record, true)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,16 +1,5 @@
|
|||
.application-logs-wrapper {
|
||||
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 {
|
||||
text-decoration: underline dotted;
|
||||
cursor: pointer;
|
||||
|
@ -23,3 +12,15 @@
|
|||
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 { checkName } from '../../../../utils/common';
|
||||
import { getClusterDetails, updateCluster } from '../../../../api/cluster';
|
||||
import './index.less';
|
||||
import Translation from '../../../../components/Translation';
|
||||
import type { Cluster } from '../../../../interface/cluster';
|
||||
import locale from '../../../../utils/locale';
|
||||
|
|
|
@ -108,7 +108,12 @@ class CardContent extends React.Component<Props, State> {
|
|||
<Row className="content-title">
|
||||
<Col span={16} className="font-size-16 color1A1A1A">
|
||||
<If condition={dashboardURL}>
|
||||
<a title={name} target="_blank" href={dashboardURL}>
|
||||
<a
|
||||
title={name}
|
||||
target="_blank"
|
||||
href={dashboardURL}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{showName}
|
||||
</a>
|
||||
</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';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { If } from 'tsx-control-statements/components';
|
||||
import { ACKCLusterStatus } from '../../../../utils/common';
|
||||
import { ACKClusterStatus } from '../../../../utils/common';
|
||||
import { getCloudClustersList } from '../../../../api/cluster';
|
||||
import './index.less';
|
||||
import { handleError } from '../../../../utils/errors';
|
||||
import Translation from '../../../../components/Translation';
|
||||
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 { accessKeyID, accessKeySecret, provider } = this.field.getValues();
|
||||
const params = {
|
||||
|
@ -212,7 +211,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
|
|||
title: <Translation>Cluster Status</Translation>,
|
||||
dataIndex: 'status',
|
||||
cell: (v: string) => {
|
||||
const findArr = ACKCLusterStatus.filter((item) => {
|
||||
const findArr = ACKClusterStatus.filter((item) => {
|
||||
return item.key == v;
|
||||
});
|
||||
return <span style={{ color: findArr[0].color || '' }}> {v} </span>;
|
||||
|
@ -254,7 +253,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
|
|||
ghost={true}
|
||||
component={'a'}
|
||||
onClick={() => {
|
||||
this.connectcloudCluster(record);
|
||||
this.connectCloudCluster(record);
|
||||
}}
|
||||
>
|
||||
<Translation>Connect</Translation>
|
||||
|
@ -271,7 +270,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
|
|||
<React.Fragment>
|
||||
<Dialog
|
||||
locale={locale().Dialog}
|
||||
className="dialog-cloudService-wrapper"
|
||||
className="commonDialog"
|
||||
title={<Translation>Connect Kubernetes Cluster From Cloud</Translation>}
|
||||
autoFocus={true}
|
||||
visible={visible}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Icon,
|
||||
Loading,
|
||||
Select,
|
||||
Dialog,
|
||||
} from '@b-design/ui';
|
||||
import DrawerWithFooter from '../../../../components/Drawer';
|
||||
import UISchema from '../../../../components/UISchema';
|
||||
|
@ -33,6 +34,7 @@ import type {
|
|||
} from '../../../../interface/configs';
|
||||
import Permission from '../../../../components/Permission';
|
||||
import locale from '../../../../utils/locale';
|
||||
import { connect } from 'dva';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
|
@ -52,6 +54,7 @@ type State = {
|
|||
templates?: ConfigTemplate[];
|
||||
};
|
||||
|
||||
@connect()
|
||||
class CreateConfigDialog extends React.Component<Props, State> {
|
||||
field: Field;
|
||||
uiSchemaRef: React.RefObject<UISchema>;
|
||||
|
@ -170,7 +173,23 @@ class CreateConfigDialog extends React.Component<Props, State> {
|
|||
.then((res) => {
|
||||
if (res) {
|
||||
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(() => {
|
||||
|
|
|
@ -265,7 +265,7 @@ class EnvDialog extends React.Component<Props, State> {
|
|||
footer={
|
||||
<div>
|
||||
<Button onClick={this.onOk} type="primary" loading={submitLoading}>
|
||||
<Translation>Confirm</Translation>
|
||||
<Translation>OK</Translation>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 './index.less';
|
||||
import locale from '../../../../utils/locale';
|
||||
|
@ -11,6 +11,7 @@ import type { Project } from '../../../../interface/project';
|
|||
import Permission from '../../../../components/Permission';
|
||||
import { checkPermission } from '../../../../utils/permission';
|
||||
import type { LoginUserInfo } from '../../../../interface/user';
|
||||
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||
const { Group: TagGroup } = Tag;
|
||||
|
||||
type Props = {
|
||||
|
@ -124,48 +125,58 @@ class TableList extends Component<Props> {
|
|||
key: 'operation',
|
||||
title: <Translation>Actions</Translation>,
|
||||
dataIndex: 'operation',
|
||||
width: '120px',
|
||||
width: '200px',
|
||||
cell: (v: string, i: number, record: Env) => {
|
||||
return (
|
||||
<div>
|
||||
<If condition={record.targets?.length}>
|
||||
<Permission
|
||||
request={{ resource: `environment:${record.name}`, action: 'delete' }}
|
||||
project={record.project.name}
|
||||
<Permission
|
||||
request={{ resource: `environment:${record.name}`, action: 'update' }}
|
||||
project={record.project.name}
|
||||
>
|
||||
<Button
|
||||
className="margin-left-10"
|
||||
text={true}
|
||||
component={'a'}
|
||||
size={'medium'}
|
||||
ghost={true}
|
||||
onClick={() => {
|
||||
this.onEdit(record);
|
||||
}}
|
||||
>
|
||||
<a
|
||||
onClick={() => {
|
||||
Dialog.confirm({
|
||||
type: 'confirm',
|
||||
content: (
|
||||
<Translation>
|
||||
Unrecoverable after deletion, are you sure to delete it?
|
||||
</Translation>
|
||||
),
|
||||
onOk: () => {
|
||||
this.onDelete(record);
|
||||
},
|
||||
locale: locale().Dialog,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Translation>Remove</Translation>
|
||||
</a>
|
||||
</Permission>
|
||||
<Permission
|
||||
request={{ resource: `environment:${record.name}`, action: 'update' }}
|
||||
project={record.project.name}
|
||||
<AiFillSetting />
|
||||
<Translation>Edit</Translation>
|
||||
</Button>
|
||||
<span className="line" />
|
||||
</Permission>
|
||||
<Permission
|
||||
request={{ resource: `environment:${record.name}`, action: 'delete' }}
|
||||
project={record.project.name}
|
||||
>
|
||||
<Button
|
||||
text={true}
|
||||
component={'a'}
|
||||
size={'medium'}
|
||||
ghost={true}
|
||||
className={'danger-btn'}
|
||||
onClick={() => {
|
||||
Dialog.confirm({
|
||||
type: 'confirm',
|
||||
content: (
|
||||
<Translation>
|
||||
Unrecoverable after deletion, are you sure to delete it?
|
||||
</Translation>
|
||||
),
|
||||
onOk: () => {
|
||||
this.onDelete(record);
|
||||
},
|
||||
locale: locale().Dialog,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<a
|
||||
className="margin-left-10"
|
||||
onClick={() => {
|
||||
this.onEdit(record);
|
||||
}}
|
||||
>
|
||||
<Translation>Edit</Translation>
|
||||
</a>
|
||||
</Permission>
|
||||
</If>
|
||||
<AiFillDelete />
|
||||
<Translation>Remove</Translation>
|
||||
</Button>
|
||||
</Permission>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -144,7 +144,12 @@ export default class LoginPage extends Component<Props, State> {
|
|||
<div style={{ flex: '1 1 0%' }} />
|
||||
<div className="right">
|
||||
<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" />
|
||||
</a>
|
||||
</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 { 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 Translation from '../../../../components/Translation';
|
||||
import Permission from '../../../../components/Permission';
|
||||
import locale from '../../../../utils/locale';
|
||||
import { momentDate } from '../../../../utils/common';
|
||||
import './index.less';
|
||||
import { getProjectConfigDistributions } from '../../../../api/config';
|
||||
import {
|
||||
deleteProjectConfigDistribution,
|
||||
getProjectConfigDistributions,
|
||||
} from '../../../../api/config';
|
||||
import type { WorkflowStepStatus } from '../../../../interface/application';
|
||||
|
||||
type Props = {
|
||||
|
@ -47,7 +50,7 @@ class ConfigDistributionPage extends Component<Props, State> {
|
|||
getProjectConfigDistributions({ projectName })
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
distributions: Array.isArray(res.distributions) ? res.distributions : [],
|
||||
distributions: res && Array.isArray(res.distributions) ? res.distributions : [],
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
|
@ -58,7 +61,20 @@ class ConfigDistributionPage extends Component<Props, State> {
|
|||
};
|
||||
|
||||
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() {
|
||||
|
@ -101,9 +117,11 @@ class ConfigDistributionPage extends Component<Props, State> {
|
|||
) => {
|
||||
const targetStatus = new Map<string, WorkflowStepStatus>();
|
||||
record.status?.workflow?.steps?.map((step) => {
|
||||
const target = step.name.split('-');
|
||||
if (target.length == 3 && target[0] == 'deploy') {
|
||||
targetStatus.set(step.name.replace('deploy-', ''), step);
|
||||
if (step.name) {
|
||||
const target = step.name.split('-');
|
||||
if (target.length == 3 && target[0] == 'deploy') {
|
||||
targetStatus.set(step.name.replace('deploy-', ''), step);
|
||||
}
|
||||
}
|
||||
});
|
||||
return (
|
||||
|
@ -204,14 +222,6 @@ class ConfigDistributionPage extends Component<Props, State> {
|
|||
>
|
||||
<Icon type="refresh" />
|
||||
</Button>
|
||||
{/* <Permission
|
||||
request={{ resource: `project/config:*`, action: 'create' }}
|
||||
project={projectName}
|
||||
>
|
||||
<Button className="card-button-wrapper">
|
||||
<Translation>Add</Translation>
|
||||
</Button>
|
||||
</Permission> */}
|
||||
</section>
|
||||
<section className="card-content-table">
|
||||
<Table
|
||||
|
|
|
@ -220,7 +220,7 @@ class Configs extends Component<Props, State> {
|
|||
<Icon type="refresh" />
|
||||
</Button>
|
||||
<Permission
|
||||
request={{ resource: `project/config:*`, action: 'create' }}
|
||||
request={{ resource: `project:${projectName}/config:*`, action: 'create' }}
|
||||
project={projectName}
|
||||
>
|
||||
<Button
|
||||
|
|
|
@ -56,9 +56,14 @@ class Summary extends Component<Props, State> {
|
|||
<Targets projectName={params.projectName} />
|
||||
<Permission
|
||||
project={params.projectName}
|
||||
request={{ resource: `project:${params.projectName}/configs:*`, action: 'list' }}
|
||||
request={{ resource: `project:${params.projectName}/config:*`, action: 'list' }}
|
||||
>
|
||||
<Configs projectName={params.projectName} />
|
||||
</Permission>
|
||||
<Permission
|
||||
project={params.projectName}
|
||||
request={{ resource: `project:${params.projectName}/config:*`, action: 'distribute' }}
|
||||
>
|
||||
<ConfigDistributionPage projectName={params.projectName} />
|
||||
</Permission>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { deleteTarget } from '../../../../api/target';
|
||||
import './index.less';
|
||||
|
@ -8,6 +8,7 @@ import type { Project } from '../../../../interface/project';
|
|||
import locale from '../../../../utils/locale';
|
||||
import { Link } from 'dva/router';
|
||||
import Permission from '../../../../components/Permission';
|
||||
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||
|
||||
type Props = {
|
||||
list?: [];
|
||||
|
@ -84,14 +85,39 @@ class TableList extends Component<Props> {
|
|||
key: 'operation',
|
||||
title: <Translation>Actions</Translation>,
|
||||
dataIndex: 'operation',
|
||||
width: '200px',
|
||||
cell: (v: string, i: number, record: Target) => {
|
||||
return (
|
||||
<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
|
||||
request={{ resource: `target:${record.name}`, action: 'delete' }}
|
||||
project={''}
|
||||
>
|
||||
<a
|
||||
<Button
|
||||
text={true}
|
||||
component={'a'}
|
||||
size={'medium'}
|
||||
className="danger-btn"
|
||||
ghost={true}
|
||||
onClick={() => {
|
||||
Dialog.confirm({
|
||||
type: 'confirm',
|
||||
|
@ -107,21 +133,9 @@ class TableList extends Component<Props> {
|
|||
});
|
||||
}}
|
||||
>
|
||||
<AiFillDelete />
|
||||
<Translation>Remove</Translation>
|
||||
</a>
|
||||
</Permission>
|
||||
<Permission
|
||||
request={{ resource: `target:${record.name}`, action: 'update' }}
|
||||
project={''}
|
||||
>
|
||||
<a
|
||||
className="margin-left-10"
|
||||
onClick={() => {
|
||||
this.onEdit(record);
|
||||
}}
|
||||
>
|
||||
<Translation>Edit</Translation>
|
||||
</a>
|
||||
</Button>
|
||||
</Permission>
|
||||
</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 { createClusterNamespace } from '../../../../api/cluster';
|
||||
import locale from '../../../../utils/locale';
|
||||
import './index.less';
|
||||
|
||||
type Props = {
|
||||
cluster?: string;
|
||||
|
@ -123,7 +122,7 @@ class Namespace extends React.Component<Props, State> {
|
|||
|
||||
<Dialog
|
||||
locale={locale().Dialog}
|
||||
className={'namespaceDialogWrapper'}
|
||||
className={'commonDialog'}
|
||||
title={<Translation>Create Namespace</Translation>}
|
||||
autoFocus={true}
|
||||
isFullScreen={true}
|
||||
|
|
|
@ -193,6 +193,7 @@ class UiSchema extends Component<Props, State> {
|
|||
style={{ marginLeft: '8px' }}
|
||||
target="_blank"
|
||||
href="https://kubevela.net/docs/reference/ui-schema"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon size="medium" type="external-link" />
|
||||
</a>
|
||||
|
|
|
@ -52,10 +52,22 @@ export function isEnvPath(pathname: string) {
|
|||
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) {
|
||||
return (pathname || '').startsWith('/users');
|
||||
}
|
||||
export function isProjectPath(pathname: string) {
|
||||
const re = new RegExp('^/projects/.*./pipelines.*');
|
||||
if (re.test(pathname)) {
|
||||
return false;
|
||||
}
|
||||
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) {
|
||||
if (null == value || value == 0) {
|
||||
return '0 Bytes';
|
||||
|
@ -141,7 +168,7 @@ export const formItemLayout = {
|
|||
},
|
||||
};
|
||||
|
||||
export const ACKCLusterStatus = [
|
||||
export const ACKClusterStatus = [
|
||||
{
|
||||
key: 'initial',
|
||||
value: '集群创建中',
|
||||
|
@ -249,3 +276,21 @@ export function checkEnabledAddon(addonName: string, enabledAddons?: AddonBaseSt
|
|||
}
|
||||
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: {
|
||||
close: 'Close',
|
||||
ok: 'Confirm',
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
Drawer: {
|
||||
|
@ -51,7 +51,7 @@ const localeData = {
|
|||
},
|
||||
Table: {
|
||||
empty: 'No Data',
|
||||
ok: 'Confirm',
|
||||
ok: 'OK',
|
||||
reset: 'Reset',
|
||||
asc: 'Asc',
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "6.0.9"
|
||||
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz"
|
||||
|
|
Loading…
Reference in New Issue