Compare commits

...

10 Commits
main ... v1.6.4

Author SHA1 Message Date
github-actions[bot] 2f68a03aa3
Feat: show the notification message after creating a config (#648)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit d72bc4fd22)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-12-03 21:32:29 +08:00
github-actions[bot] 67b8dc2afa
Fix: enable the addon with the registry name (#646)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit b7d962b458)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-11-24 10:55:10 +08:00
github-actions[bot] 4f45d87afb
Fix: the wrong environment list was loaded (#645)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 2c51cace5f)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-11-24 10:54:55 +08:00
github-actions[bot] 6548c3317c
Feat: support to set the registry for the ACR webhook (#641)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit b34f2f2cbb)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-11-22 10:07:29 +08:00
github-actions[bot] e4c6008c73
Docs: translate into chinese (#638)
Signed-off-by: wzjgo <wzjgo1023@gmail.com>
(cherry picked from commit 7e262cdd25)

Co-authored-by: wzjgo <wzjgo1023@gmail.com>
2022-11-09 13:47:23 +08:00
github-actions[bot] 1a08d6888f
Feat: add some translations and support to delete the distribution (#635)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 9999ff2d46)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-11-09 13:45:09 +08:00
github-actions[bot] bd3f768293
[Backport release-1.6] Fix: enhance the permissions (#632)
* Fix: disable the Input AutoComplete

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit de9853c763)

* Fix: enhance the permissions

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 26293bd92d)

* Fix: change the package version

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 31952533e7)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-11-01 21:24:52 +08:00
github-actions[bot] e4d73e1595
Feat: add the observability Pipeline template (#629)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 2a51b04e9d)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-10-31 11:14:40 +08:00
github-actions[bot] fa4cbc0c82
[Backport release-1.6] Fix: guides the user to enable the vale-workflow addon (#627)
* Fix: guides the user to enable the vale-workflow addon

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit c4245e68c5)

* Fix: component state property never read

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 35fccb0f39)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-10-29 20:29:27 +08:00
github-actions[bot] 2d49a97045
[Backport release-1.6] Feat: support to manage the Pipeline (#625)
* Feat: support to manage the Pipeline

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 041a8058ed)

* Fix: External links without noopener/noreferrer

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 04202f5172)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-10-29 16:07:55 +08:00
75 changed files with 4118 additions and 425 deletions

View File

@ -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",

View File

@ -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);
}

142
src/api/pipeline.ts Normal file
View File

@ -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);
}

View File

@ -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);
}

View File

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

View File

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

View File

@ -33,6 +33,7 @@ class DrawerWithFooter extends Component<Props, any> {
<Drawer
title={title}
closeMode="close"
className="customDrawer"
closeable="close"
onClose={onClose}
visible={true}

View File

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

View File

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

View File

@ -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 {

View File

@ -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>
);
}

View File

@ -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>
);
}
}

View File

@ -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>
);
};

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}

View File

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

View File

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

View File

@ -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),

View File

@ -79,4 +79,5 @@ export interface EnableAddonRequest {
version: string;
properties: Record<string, any>;
clusters?: string[];
registry?: string;
}

View File

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

158
src/interface/pipeline.ts Normal file
View File

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

View File

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

View File

@ -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" />

View File

@ -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} />

View File

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

View File

@ -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' },
},
],
},
{

View File

@ -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}>

View File

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

View File

@ -52,6 +52,7 @@
flex-wrap: nowrap;
height: 100%;
.item {
margin-right: var(--spacing-6);
a {
display: flex;
justify-content: center;

View File

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

View File

@ -59,7 +59,9 @@
.layout-shell {
display: flex;
}
.layout {
min-width: var(--min-layout-width);
}
.layout-navigation {
width: 240px;
min-width: 60px;

View File

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

View File

@ -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": "标签"
}

View File

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

View File

@ -1,4 +1,7 @@
.addon-search {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: #fff;
.tag-search {

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
});
}

View File

@ -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>
);
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}

View File

@ -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(() => {

View File

@ -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>
}

View File

@ -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>
);
},

View File

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

View File

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

View File

@ -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."
`,
},
];

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}

View File

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

View File

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

View File

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

View File

@ -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"