Compare commits

...

9 Commits
main ... v1.7.4

Author SHA1 Message Date
github-actions[bot] 51e71f1feb
Feat: support to guide the chart configuration from the OCI registry (#684)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 7b1455d9e9)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-02-15 17:16:52 +08:00
github-actions[bot] c894356e50
[Backport release-1.7] Feat: support for deleting the workflow (#682)
* Feat: support for deleting the workflow

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

* Fix: enhance the code style

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

---------

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-01-31 12:02:03 +08:00
github-actions[bot] df900036d4
Fix: Step repetition rendering (#681)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 8ac167b697)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-01-31 12:01:37 +08:00
github-actions[bot] d631fcee6f
Feat: support for updating the trigger (#679)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 097dedf7c6)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-01-31 10:58:57 +08:00
github-actions[bot] 65af805131
Feat: support to update, clone, and edit the pipeline context (#676)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit dfb346f406)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-01-31 10:42:04 +08:00
github-actions[bot] 02e51bb37e
Feat: optimize the style and support to delete the group step (#674)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 636ef7395f)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-01-30 20:49:01 +08:00
github-actions[bot] d18b2013c5
Fix: beautify (#671)
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
(cherry picked from commit edea9ae5e2)

Co-authored-by: Yin Da <yd219913@alibaba-inc.com>
2023-01-30 13:29:46 +08:00
github-actions[bot] bbe7b2165d
Fix: can not binding the environment (#670)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 0b5e16f0c1)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-01-30 13:29:21 +08:00
github-actions[bot] 5ef1a137c4
Feat: add the envbinding route page (#663)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit ca4301acd5)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2023-01-13 13:50:03 +08:00
52 changed files with 1127 additions and 231 deletions

View File

@ -5,13 +5,14 @@ import { getDomain } from '../utils/common';
import type { import type {
ApplicationDeployRequest, ApplicationDeployRequest,
Trait, Trait,
Trigger,
ApplicationComponentConfig, ApplicationComponentConfig,
ApplicationQuery, ApplicationQuery,
ApplicationCompareRequest, ApplicationCompareRequest,
ApplicationDryRunRequest, ApplicationDryRunRequest,
CreatePolicyRequest, CreatePolicyRequest,
UpdatePolicyRequest, UpdatePolicyRequest,
UpdateTriggerRequest,
CreateTriggerRequest,
} from '../interface/application'; } from '../interface/application';
interface TraitQuery { interface TraitQuery {
@ -102,12 +103,11 @@ export function createApplicationTemplate(params: any) {
return post(`${url}/${params.name}/template`, params).then((res) => res); return post(`${url}/${params.name}/template`, params).then((res) => res);
} }
export function createApplicationEnv(params: { appName?: string }) { export function createApplicationEnvbinding(params: { appName?: string }) {
delete params.appName;
return post(`${url}/${params.appName}/envs`, params).then((res) => res); return post(`${url}/${params.appName}/envs`, params).then((res) => res);
} }
export function updateApplicationEnv(params: { appName?: string; name: string }) { export function updateApplicationEnvbinding(params: { appName?: string; name: string }) {
return put(`${url}/${params.appName}/envs/${params.name}`, params).then((res) => res); return put(`${url}/${params.appName}/envs/${params.name}`, params).then((res) => res);
} }
@ -221,12 +221,20 @@ export function updateApplication(params: any) {
return put(`${url}/${params.name}`, params).then((res) => res); return put(`${url}/${params.name}`, params).then((res) => res);
} }
export function createTriggers(params: Trigger, query: { appName: string }) { export function createTrigger(params: CreateTriggerRequest, query: { appName: string }) {
const { appName } = query; const { appName } = query;
return post(`${url}/${appName}/triggers`, params).then((res) => res); return post(`${url}/${appName}/triggers`, params).then((res) => res);
} }
export function deleteTriggers(params: { appName: string; token: string }) { export function updateTrigger(
params: UpdateTriggerRequest,
query: { appName: string; token: string },
) {
const { appName, token } = query;
return put(`${url}/${appName}/triggers/${token}`, params).then((res) => res);
}
export function deleteTrigger(params: { appName: string; token: string }) {
const { appName, token } = params; const { appName, token } = params;
return rdelete(`${url}/${appName}/triggers/${token}`, {}).then((res) => res); return rdelete(`${url}/${appName}/triggers/${token}`, {}).then((res) => res);
} }

View File

@ -100,6 +100,29 @@ export function listPipelineContexts(projectName: string, pipelineName: string)
return get(url, {}).then((res) => res); return get(url, {}).then((res) => res);
} }
export function deletePipelineContext(
projectName: string,
pipelineName: string,
contextName: string,
) {
const url =
base + `/api/v1/projects/${projectName}/pipelines/${pipelineName}/contexts/${contextName}`;
return rdelete(url, {}).then((res) => res);
}
export function updatePipelineContext(
projectName: string,
pipelineName: string,
context: {
name: string;
values: { key: string; value: string }[];
},
) {
const url =
base + `/api/v1/projects/${projectName}/pipelines/${pipelineName}/contexts/${context.name}`;
return put(url, context).then((res) => res);
}
export function loadPipelineRunStepOutputs(params: { export function loadPipelineRunStepOutputs(params: {
projectName: string; projectName: string;
pipelineName: string; pipelineName: string;

View File

@ -19,6 +19,25 @@ export function getChartValues(params: {
}).then((res) => res); }).then((res) => res);
} }
export function getChartValueFiles(params: {
url: string;
repoType: string;
secretName?: string;
chart: string;
version: string;
}) {
const url = baseURL + repository + '/chart/values';
return get(url, {
params: {
repoUrl: params.url,
repoType: params.repoType,
secretName: params.secretName,
chart: params.chart,
version: params.version,
},
}).then((res) => res);
}
export function getCharts(params: { url: string; repoType: string; secretName?: string }) { export function getCharts(params: { url: string; repoType: string; secretName?: string }) {
const url = baseURL + repository + '/charts'; const url = baseURL + repository + '/charts';
return get(url, { return get(url, {
@ -32,9 +51,14 @@ export function getChartVersions(params: {
chart: string; chart: string;
secretName?: string; secretName?: string;
}) { }) {
const url = baseURL + repository + '/charts/' + params.chart + '/versions'; const url = baseURL + repository + '/chart/versions';
return get(url, { return get(url, {
params: { repoUrl: params.url, repoType: params.repoType, secretName: params.secretName }, params: {
repoUrl: params.url,
repoType: params.repoType,
secretName: params.secretName,
chart: params.chart,
},
}).then((res) => res); }).then((res) => res);
} }

View File

@ -391,7 +391,8 @@ a {
.next-btn { .next-btn {
display: block; display: block;
flex: none !important; flex: none !important;
width: 100px !important; width: auto !important;
min-width: 100px !important;
} }
} }
@ -502,10 +503,6 @@ a {
color: @dangerColor !important; color: @dangerColor !important;
} }
.line {
border: solid 0.2px rgba(0, 0, 0, 0.1);
}
.breadcrumb { .breadcrumb {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -516,6 +513,7 @@ a {
.next-breadcrumb-separator { .next-breadcrumb-separator {
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
font-size: 16px;
a { a {
color: @primarycolor; color: @primarycolor;
} }
@ -524,7 +522,6 @@ a {
.next-btn, .next-btn,
.inline-center { .inline-center {
display: inline-flex;
align-items: center; align-items: center;
} }

View File

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Dialog, Message } from '@b-design/ui'; import { Button, Dialog, Message } from '@b-design/ui';
import type { ApplicationCompareResponse } from '../../interface/application'; import type { ApplicationCompareResponse } from '../../interface/application';
import './index.less'; import './index.less';
import { DiffEditor } from '../DiffEditor'; import { DiffEditor } from '../DiffEditor';
@ -11,17 +11,26 @@ type ApplicationDiffProps = {
targetName: string; targetName: string;
compare: ApplicationCompareResponse; compare: ApplicationCompareResponse;
onClose: () => void; onClose: () => void;
rollback2Revision?: () => void;
id?: string; id?: string;
}; };
export const ApplicationDiff = (props: ApplicationDiffProps) => { export const ApplicationDiff = (props: ApplicationDiffProps) => {
const { baseName, targetName, compare } = props; const { baseName, targetName, compare, rollback2Revision } = props;
const container = 'revision' + baseName + targetName; const container = 'revision' + baseName + targetName;
return ( return (
<Dialog <Dialog
className={'commonDialog application-diff'} className={'commonDialog application-diff'}
isFullScreen={true} isFullScreen={true}
footer={<div />} footer={
<div>
<If condition={compare.isDiff && rollback2Revision}>
<Button type="primary" onClick={rollback2Revision}>
Rollback To Revision
</Button>
</If>
</div>
}
id={props.id} id={props.id}
visible={true} visible={true}
onClose={props.onClose} onClose={props.onClose}

View File

@ -0,0 +1,5 @@
.helmValueDialog {
.next-dialog-body {
padding: 0 !important;
}
}

View File

@ -0,0 +1,42 @@
import { Dialog, Select } from '@b-design/ui';
import * as React from 'react';
import i18n from '../../i18n';
import DefinitionCode from '../DefinitionCode';
import './index.less';
type Props = {
onClose: () => void;
valueFiles: Record<string, string>;
name: string;
};
const HelmValueShow: React.FC<Props> = (props: Props) => {
const [valueFile, setValueFile] = React.useState<string>('values.yaml');
return (
<Dialog
className={'commonDialog helmValueDialog'}
isFullScreen={true}
visible={true}
onClose={props.onClose}
footer={<div />}
title={i18n.t('Show Values File')}
>
<Select
onChange={(name) => {
setValueFile(name);
}}
defaultValue={valueFile}
dataSource={Object.keys(props.valueFiles)}
style={{ marginBottom: '8px' }}
/>
<div id={'chart-' + props.name} className="diff-box">
<DefinitionCode
language={'yaml'}
containerId={'chart-' + props.name}
readOnly
value={props.valueFiles[valueFile]}
/>
</div>
</Dialog>
);
};
export default HelmValueShow;

View File

@ -1,15 +1,23 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import './index.less'; import './index.less';
export type Props = { export type Props = {
title: string | React.ReactNode; title: string | React.ReactNode;
number?: number; number?: number;
to?: string;
}; };
const NumItem: React.FC<Props> = (props) => { const NumItem: React.FC<Props> = (props) => {
return ( return (
<div className="num-item"> <div className="num-item">
<div className="number">{props.number || 0}</div> <div className="number">
{props.to ? (
<Link to={props.to}>{props.number || 0}</Link>
) : (
<span>{props.number || 0}</span>
)}
</div>
<div className="title">{props.title}</div> <div className="title">{props.title}</div>
</div> </div>
); );

View File

@ -50,7 +50,6 @@ class PipelineGraph extends React.Component<PipelineGraphProps, State> {
<Draggable> <Draggable>
<div <div
className="workflow-graph" className="workflow-graph"
key={name}
style={{ style={{
transform: `scale(${zoom})`, transform: `scale(${zoom})`,
}} }}
@ -66,7 +65,7 @@ class PipelineGraph extends React.Component<PipelineGraphProps, State> {
steps.map((step, i: number) => { steps.map((step, i: number) => {
return ( return (
<Step <Step
key={name + step.id} key={name + step.name}
probeState={this.state} probeState={this.state}
step={step} step={step}
group={step.type == 'step-group'} group={step.type == 'step-group'}

View File

@ -14,17 +14,28 @@
width: 100%; width: 100%;
margin-top: var(--spacing-4); margin-top: var(--spacing-4);
padding: 16px; padding: 16px;
border: solid 1px var(--grey-200); border: dashed 1px var(--grey-200);
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
.context-name { .context-name {
flex-grow: 0;
width: 100px;
margin-right: var(--spacing-4); margin-right: var(--spacing-4);
font-weight: 600; font-weight: 600;
font-size: var(--font-size-normal); font-size: var(--font-size-normal);
line-height: 24px; line-height: 24px;
} }
.context-values {
flex: auto;
flex-grow: 1;
}
&.active { &.active {
border: solid 2px var(--primary-color); border: solid 2px var(--primary-color);
} }
.actions {
display: flex;
flex-grow: 0;
flex-wrap: nowrap;
}
} }
} }

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Checkbox, Dialog, Loading, Message, Tag } from '@b-design/ui'; import { Button, Checkbox, Dialog, Loading, Message, Tag } from '@b-design/ui';
import { listPipelineContexts, runPipeline } from '../../api/pipeline'; import { deletePipelineContext, listPipelineContexts, runPipeline } from '../../api/pipeline';
import i18n from '../../i18n'; import i18n from '../../i18n';
import type { KeyValue, PipelineListItem } from '../../interface/pipeline'; import type { KeyValue, PipelineListItem } from '../../interface/pipeline';
import './index.less'; import './index.less';
@ -10,6 +10,9 @@ import classNames from 'classnames';
import { If } from 'tsx-control-statements/components'; import { If } from 'tsx-control-statements/components';
import NewContext from './new-context'; import NewContext from './new-context';
import Translation from '../Translation'; import Translation from '../Translation';
import Permission from '../Permission';
import { BiCopyAlt } from 'react-icons/bi';
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
export interface PipelineProps { export interface PipelineProps {
pipeline: PipelineListItem; pipeline: PipelineListItem;
@ -20,11 +23,38 @@ export interface PipelineProps {
const RunPipeline = (props: PipelineProps) => { const RunPipeline = (props: PipelineProps) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [contexts, setContexts] = useState<Record<string, KeyValue[]>>({}); const [contexts, setContexts] = useState<Record<string, KeyValue[]>>({});
const [context, setContext] = useState<{ name: string; values: KeyValue[]; clone: boolean }>();
const [noContext, setNoContext] = useState<boolean>(); const [noContext, setNoContext] = useState<boolean>();
const [contextName, setSelectContextName] = useState(''); const [contextName, setSelectContextName] = useState('');
const [addContext, showAddContext] = useState(false); const [addContext, showAddContext] = useState(false);
const { pipeline } = props; const { pipeline } = props;
const onClonePipelineContext = (key: string) => {
showAddContext(true);
setContext({ name: key, values: contexts[key], clone: true });
};
const onEditPipelineContext = (key: string) => {
showAddContext(true);
setContext({ name: key, values: contexts[key], clone: false });
};
const onDeletePipelineContext = (key: string) => {
Dialog.confirm({
type: 'confirm',
content: <Translation>Unrecoverable after deletion, are you sure to delete it?</Translation>,
onOk: () => {
deletePipelineContext(pipeline.project.name, pipeline.name, key).then((res) => {
if (res) {
Message.success(i18n.t('The Pipeline context removed successfully'));
setLoading(true);
}
});
},
locale: locale().Dialog,
});
};
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
listPipelineContexts(pipeline.project.name, pipeline.name) listPipelineContexts(pipeline.project.name, pipeline.name)
@ -86,20 +116,98 @@ const RunPipeline = (props: PipelineProps) => {
setSelectContextName(''); setSelectContextName('');
} }
}} }}
title={
contextName === key ? i18n.t('Click and deselect') : i18n.t('Click and select')
}
> >
<div className="context-name">{key}</div> <div className="context-name">{key}</div>
<div className="context-values"> <div className="context-values">
{Array.isArray(contexts[key]) && {Array.isArray(contexts[key]) &&
contexts[key].map((item) => { contexts[key].map((item) => {
return <Tag>{`${item.key}=${item.value}`}</Tag>; return (
<Tag
style={{ marginBottom: '8px' }}
key={item.key}
>{`${item.key}=${item.value}`}</Tag>
);
})} })}
</div> </div>
<div className="actions">
<Permission
request={{
resource: `project:${pipeline.project.name}/pipeline:${pipeline.name}/context:${key}`,
action: 'update',
}}
project={pipeline.project.name}
>
<Button
className="margin-left-10"
text={true}
component={'a'}
size={'medium'}
ghost={true}
onClick={(event) => {
onEditPipelineContext(key);
event.stopPropagation();
}}
>
<AiFillSetting />
<Translation>Edit</Translation>
</Button>
<span className="line" />
</Permission>
<Permission
project={pipeline.project.name}
resource={{
resource: `project:${pipeline.project.name}/pipeline:${pipeline.name}/context:${key}`,
action: 'create',
}}
>
<Button
text
size={'medium'}
ghost={true}
component={'a'}
onClick={(event) => {
onClonePipelineContext(key);
event.stopPropagation();
}}
>
<BiCopyAlt /> <Translation>Clone</Translation>
</Button>
<span className="line" />
</Permission>
<Permission
project={pipeline.project.name}
resource={{
resource: `project:${pipeline.project.name}/pipeline:${pipeline.name}/context:${key}`,
action: 'delete',
}}
>
<Button
text
size={'medium'}
ghost={true}
className={'danger-btn'}
component={'a'}
onClick={(event) => {
onDeletePipelineContext(key);
event.stopPropagation();
}}
>
<AiFillDelete />
<Translation>Remove</Translation>
</Button>
</Permission>
</div>
</div> </div>
); );
})} })}
<If condition={addContext}> <If condition={addContext}>
<div className="context-item"> <div className="context-item">
<NewContext <NewContext
clone={context?.clone}
context={context}
pipeline={props.pipeline} pipeline={props.pipeline}
onCancel={() => showAddContext(false)} onCancel={() => showAddContext(false)}
onSuccess={() => { onSuccess={() => {

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Field } from '@b-design/ui'; import { Field, Loading } from '@b-design/ui';
import { Button, Form, Grid, Input } from '@b-design/ui'; import { Button, Form, Grid, Input } from '@b-design/ui';
import KV from '../../extends/KV'; import KV from '../../extends/KV';
import i18n from '../../i18n'; import i18n from '../../i18n';
import Translation from '../Translation'; import Translation from '../Translation';
import { createPipelineContext } from '../../api/pipeline'; import { createPipelineContext, updatePipelineContext } from '../../api/pipeline';
import type { KeyValue, PipelineListItem } from '../../interface/pipeline'; import type { KeyValue, PipelineListItem } from '../../interface/pipeline';
import { checkName } from '../../utils/common'; import { checkName } from '../../utils/common';
@ -14,6 +14,8 @@ export interface NewContextProps {
pipeline: PipelineListItem; pipeline: PipelineListItem;
onSuccess: () => void; onSuccess: () => void;
onCancel: () => void; onCancel: () => void;
context?: { name: string; values: KeyValue[] };
clone?: boolean;
} }
class NewContext extends React.Component<NewContextProps> { class NewContext extends React.Component<NewContextProps> {
@ -23,7 +25,29 @@ class NewContext extends React.Component<NewContextProps> {
super(props); super(props);
this.field = new Field(this); this.field = new Field(this);
} }
componentDidMount() {
const { clone, context } = this.props;
if (context) {
const editValues: Record<string, string> = {};
context.values.map((v) => {
editValues[v.key] = v.value;
});
if (clone) {
this.field.setValues({
name: context.name + '-clone',
values: editValues,
});
} else {
this.field.setValues({
name: context.name,
values: editValues,
});
}
}
}
submitContext = () => { submitContext = () => {
const { context, clone } = this.props;
const editMode = context && !clone;
this.field.validate((errs, values: any) => { this.field.validate((errs, values: any) => {
if (errs) { if (errs) {
return; return;
@ -33,17 +57,33 @@ class NewContext extends React.Component<NewContextProps> {
Object.keys(values.values).map((key) => { Object.keys(values.values).map((key) => {
keyValues.push({ key: key, value: values.values[key] }); keyValues.push({ key: key, value: values.values[key] });
}); });
createPipelineContext(project.name, name, { name: values.name, values: keyValues }).then( if (editMode) {
(res) => { updatePipelineContext(project.name, name, { name: values.name, values: keyValues }).then(
if (res) { (res) => {
this.props.onSuccess(); if (res) {
} this.props.onSuccess();
}, }
); },
);
} else {
createPipelineContext(project.name, name, { name: values.name, values: keyValues }).then(
(res) => {
if (res) {
this.props.onSuccess();
}
},
);
}
}); });
}; };
render() { render() {
const { init } = this.field; const { init } = this.field;
const { context, clone } = this.props;
const editMode = context && !clone;
// waiting init the values
if (context && Object.keys(this.field.getValues()).length === 0) {
return <Loading visible />;
}
return ( return (
<Form field={this.field} style={{ width: '100%' }}> <Form field={this.field} style={{ width: '100%' }}>
<Row style={{ width: '100%' }} wrap> <Row style={{ width: '100%' }} wrap>
@ -63,6 +103,7 @@ class NewContext extends React.Component<NewContextProps> {
], ],
})} })}
placeholder={i18n.t('Please input the context name')} placeholder={i18n.t('Please input the context name')}
disabled={editMode}
/> />
</Form.Item> </Form.Item>
</Col> </Col>

View File

@ -47,8 +47,19 @@ export const Step = (props: StepProps) => {
}} }}
> >
{showAlias(step.name, step.alias)} {showAlias(step.name, step.alias)}
<span className="step-delete">
<Icon
size={14}
type="ashbin"
onClick={(event) => {
onDelete(step.name);
event.stopPropagation();
}}
title="Delete this step group."
/>
</span>
</div> </div>
<div className="groups asd" style={{ width: stepWidth + 'px' }}> <div className="groups" style={{ width: stepWidth + 'px' }}>
{step.subSteps?.map((subStep) => { {step.subSteps?.map((subStep) => {
return ( return (
<div <div

View File

@ -10,19 +10,23 @@ import Translation from '../Translation';
import type { Rule } from '@alifd/meet-react/lib/field'; import type { Rule } from '@alifd/meet-react/lib/field';
import type { WorkflowStepBase } from '../../interface/pipeline'; import type { WorkflowStepBase } from '../../interface/pipeline';
import i18n from '../../i18n'; import i18n from '../../i18n';
import Item from '../Item';
const { Row, Col } = Grid; const { Row, Col } = Grid;
interface DefinitionCatalog { interface DefinitionCatalog {
title: string; title: string;
sort: number;
description?: string; description?: string;
definitions: DefinitionBase[]; definitions: DefinitionBase[];
} }
const defaultDefinitionCatalog: Record<string, string> = { const defaultDefinitionCatalog: Record<string, string> = {
'step-group': 'Group',
deploy: 'Delivery', deploy: 'Delivery',
'apply-app': 'Delivery', 'apply-app': 'Delivery',
'apply-deployment': 'Delivery', 'apply-deployment': 'Delivery',
'apply-component': 'Delivery',
'addon-operation': 'Delivery', 'addon-operation': 'Delivery',
'deploy-cloud-resource': 'Delivery', 'deploy-cloud-resource': 'Delivery',
'share-cloud-resource': 'Delivery', 'share-cloud-resource': 'Delivery',
@ -34,34 +38,47 @@ const defaultDefinitionCatalog: Record<string, string> = {
'list-config': 'Config Management', 'list-config': 'Config Management',
'read-config': 'Config Management', 'read-config': 'Config Management',
'export-data': 'Config Management', 'export-data': 'Config Management',
export2config: 'Config Management',
export2secret: 'Config Management',
suspend: 'Notification', suspend: 'Notification',
}; };
const defaultCatalog: Record<string, DefinitionCatalog> = { const defaultCatalog: Record<string, DefinitionCatalog> = {
Group: {
title: 'Group',
description: 'Merge a set of steps.',
definitions: [],
sort: 1,
},
Delivery: { Delivery: {
title: 'Delivery', title: 'Delivery',
description: 'Delivery the Application or workloads to the Targets', description: 'Delivery the Application or workloads to the Targets',
definitions: [], definitions: [],
sort: 2,
}, },
Approval: { Approval: {
title: 'Approval', title: 'Approval',
description: 'Approval or reject the changes during Workflow or Pipeline progress', description: 'Approval or reject the changes during Workflow or Pipeline progress',
definitions: [], definitions: [],
sort: 3,
}, },
'Config Management': { 'Config Management': {
title: 'Config Management', title: 'Config Management',
description: 'Create or read the config.', description: 'Create or read the config.',
definitions: [], definitions: [],
sort: 4,
}, },
Notification: { Notification: {
title: 'Approval', title: 'Notification',
description: 'Send messages to users or other applications.', description: 'Send messages to users or other applications.',
definitions: [], definitions: [],
sort: 5,
}, },
Custom: { Custom: {
title: 'Custom', title: 'Custom',
description: 'Custom Workflow or Pipeline steps', description: 'Custom Workflow or Pipeline steps',
definitions: [], definitions: [],
sort: 1000,
}, },
}; };
@ -93,10 +110,12 @@ const buildDefinitionCatalog = (defs: DefinitionBase[]) => {
if (catalogMap[catalog]) { if (catalogMap[catalog]) {
catalogMap[catalog].definitions.push(def); catalogMap[catalog].definitions.push(def);
} else { } else {
catalogMap[catalog] = { title: catalog, definitions: [def] }; catalogMap[catalog] = { title: catalog, definitions: [def], sort: 100 };
} }
}); });
return Object.values(catalogMap); return Object.values(catalogMap).sort((a, b) => {
return a.sort - b.sort;
});
}; };
type Props = { type Props = {
@ -107,7 +126,7 @@ type Props = {
addSub?: boolean; addSub?: boolean;
}; };
type State = { type State = {
selectType?: string; selectType?: DefinitionBase;
}; };
class TypeSelect extends React.Component<Props, State> { class TypeSelect extends React.Component<Props, State> {
@ -126,7 +145,12 @@ class TypeSelect extends React.Component<Props, State> {
return; return;
} }
const { name, alias, description } = values; const { name, alias, description } = values;
this.props.addStep({ type: selectType, name: name, alias: alias, description: description }); this.props.addStep({
type: selectType.name,
name: name,
alias: alias,
description: description,
});
}); });
}; };
@ -136,6 +160,7 @@ class TypeSelect extends React.Component<Props, State> {
const catalogs = buildDefinitionCatalog( const catalogs = buildDefinitionCatalog(
definitions?.filter((def) => !addSub || def.name != 'step-group') || [], definitions?.filter((def) => !addSub || def.name != 'step-group') || [],
); );
console.log(catalogs);
const { init } = this.field; const { init } = this.field;
const checkStepNameRule = (rule: Rule, value: any, callback: (error?: string) => void) => { const checkStepNameRule = (rule: Rule, value: any, callback: (error?: string) => void) => {
if (checkStepName(value)) { if (checkStepName(value)) {
@ -153,7 +178,7 @@ class TypeSelect extends React.Component<Props, State> {
onClose={onClose} onClose={onClose}
onCancel={onClose} onCancel={onClose}
onOk={this.onSubmit} onOk={this.onSubmit}
title={i18n.t('Select Step Type')} title={selectType ? i18n.t('Set Step Basic Info') : i18n.t('Select Step Type')}
visible visible
> >
<div> <div>
@ -175,7 +200,7 @@ class TypeSelect extends React.Component<Props, State> {
<div <div
className="icon" className="icon"
onClick={() => { onClick={() => {
this.setState({ selectType: def.name }); this.setState({ selectType: def });
}} }}
> >
<StepTypeIcon type={def.name} /> <StepTypeIcon type={def.name} />
@ -198,6 +223,17 @@ class TypeSelect extends React.Component<Props, State> {
})} })}
{selectType && ( {selectType && (
<Form field={this.field}> <Form field={this.field}>
<Row wrap>
<Col span={24}>
<Item
label={i18n.t('Select Type')}
value={showAlias(selectType.name, selectType.alias)}
/>
</Col>
<Col span={24}>
<Item label={i18n.t('Type Description')} value={selectType.description} />
</Col>
</Row>
<Row> <Row>
<Col span={12} style={{ padding: '0 8px' }}> <Col span={12} style={{ padding: '0 8px' }}>
<Form.Item <Form.Item

View File

@ -2,6 +2,8 @@ import React from 'react';
import type { WorkflowStep } from '../../interface/pipeline'; import type { WorkflowStep } from '../../interface/pipeline';
import DefinitionCode from '../DefinitionCode'; import DefinitionCode from '../DefinitionCode';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { Message } from '@b-design/ui';
import Translation from '../Translation';
type Props = { type Props = {
steps?: WorkflowStep[]; steps?: WorkflowStep[];
@ -12,21 +14,36 @@ export const WorkflowYAML = (props: Props) => {
const id = 'workflow:' + props.name; const id = 'workflow:' + props.name;
const content = yaml.dump(props.steps); const content = yaml.dump(props.steps);
return ( return (
<div id={id} style={{ height: 'calc(100vh - 340px)' }}> <div>
<DefinitionCode <Message type="help">
onChange={(value: string) => { <div>
try { <Translation>The workflow step spec reference document</Translation>:
const newSteps: any = yaml.load(value); <a
props.onChange(newSteps); href="http://kubevela.net/docs/end-user/workflow/built-in-workflow-defs"
} catch (err) { target="_blank"
console.log(err); rel="noopener noreferrer"
} style={{ marginLeft: '16px' }}
}} >
value={content} <Translation>Click here</Translation>
language="yaml" </a>
theme="hc-black" </div>
containerId={id} </Message>
/> <div id={id} style={{ height: 'calc(100vh - 340px)' }}>
<DefinitionCode
onChange={(value: string) => {
try {
const newSteps: any = yaml.load(value);
props.onChange(newSteps);
} catch (err) {
console.log(err);
}
}}
value={content}
language="yaml"
theme="hc-black"
containerId={id}
/>
</div>
</div> </div>
); );
}; };

View File

@ -37,27 +37,37 @@ class HelmRepoSelect extends Component<Props, State> {
} }
componentDidMount() { componentDidMount() {
if (!this.props.helm?.repoType || this.props.helm?.repoType == 'helm') { if (!this.props.helm?.repoType || this.props.helm?.repoType == 'helm') {
this.onLoadRepos(); this.onLoadRepos(this.props.helm?.repoType);
} }
} }
componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps: Props) {
const repoType = nextProps.helm?.repoType; const repoType = nextProps.helm?.repoType;
if (repoType !== this.props.helm?.repoType && repoType == 'helm') { if (repoType !== this.props.helm?.repoType && (repoType == 'helm' || repoType == 'oci')) {
this.onLoadRepos(); this.onLoadRepos(repoType);
} }
} }
onLoadRepos = () => { onLoadRepos = (repoType?: string) => {
const { project } = this.props; const { project } = this.props;
const defaultRepos = [{ url: 'https://charts.bitnami.com/bitnami', type: 'helm' }]; const defaultRepos = [{ url: 'https://charts.bitnami.com/bitnami', type: 'helm' }];
this.setState({ loading: true, repos: defaultRepos }); this.setState({ loading: true, repos: repoType === 'helm' ? defaultRepos : [] });
console.log(repoType);
getChartRepos({ project: project }).then((res) => { getChartRepos({ project: project }).then((res) => {
let repos: HelmRepo[] = []; let repos: HelmRepo[] = [];
if (res && res.repos) { if (res && res.repos) {
repos = repos.concat(res.repos); res.repos.map((repo: HelmRepo) => {
if (repoType == 'oci' && repo.url.startsWith('oci://')) {
repos.push(repo);
}
if (repoType == 'helm' && !repo.url.startsWith('oci://')) {
repos.push(repo);
}
});
}
if (repoType == 'helm') {
repos = repos.concat(defaultRepos);
} }
repos = repos.concat(defaultRepos);
this.setState({ this.setState({
repos: repos, repos: repos,
loading: false, loading: false,

View File

@ -4,9 +4,12 @@ import _ from 'lodash';
import type { UIParam } from '../../interface/application'; import type { UIParam } from '../../interface/application';
import type { HelmRepo } from '../../interface/repository'; import type { HelmRepo } from '../../interface/repository';
import KV from '../KV'; import KV from '../KV';
import { getChartValues } from '../../api/repository'; import { getChartValueFiles } from '../../api/repository';
import { Loading } from '@b-design/ui'; import { Button, Loading } from '@b-design/ui';
import { connect } from 'dva'; import { connect } from 'dva';
import YAML from 'js-yaml';
import Translation from '../../components/Translation';
import HelmValueShow from '../../components/HelmValueShow';
type Props = { type Props = {
value?: any; value?: any;
@ -62,6 +65,8 @@ type State = {
version: string; version: string;
}; };
loading: boolean; loading: boolean;
valueFiles?: Record<string, string>;
showValuesFile?: boolean;
}; };
@connect((store: any) => { @connect((store: any) => {
return { ...store.uischema }; return { ...store.uischema };
@ -81,11 +86,19 @@ class HelmValues extends Component<Props, State> {
loadChartValues = () => { loadChartValues = () => {
const { helm, repo } = this.props; const { helm, repo } = this.props;
if (helm?.chart && helm.version && helm.url) { if (helm?.chart && helm.version && helm.url) {
getChartValues({ ...helm, secretName: repo?.secretName }).then((re) => { getChartValueFiles({ ...helm, secretName: repo?.secretName }).then(
if (re) { (re: Record<string, string>) => {
this.setState({ values: re.BusinessCode ? undefined : re, helm: helm, loading: false }); if (re) {
} try {
}); const defaultValueFile = re['values.yaml'];
const defaultValue = YAML.load(defaultValueFile);
const newValues: any = {};
getValues(defaultValue, '', newValues);
this.setState({ values: newValues, valueFiles: re, helm: helm, loading: false });
} catch (err) {}
}
},
);
} }
}; };
@ -115,7 +128,7 @@ class HelmValues extends Component<Props, State> {
render() { render() {
const { helm } = this.props; const { helm } = this.props;
const { values, loading, helm: stateHelm } = this.state; const { values, loading, helm: stateHelm, showValuesFile, valueFiles } = this.state;
if (this.renderHelmKey(helm) != this.renderHelmKey(stateHelm)) { if (this.renderHelmKey(helm) != this.renderHelmKey(stateHelm)) {
this.loadChartValues(); this.loadChartValues();
} }
@ -131,6 +144,26 @@ class HelmValues extends Component<Props, State> {
subParameters={this.props.subParameters} subParameters={this.props.subParameters}
id={this.props.id} id={this.props.id}
/> />
{valueFiles && (
<Button
onClick={() => {
this.setState({ showValuesFile: true });
}}
size="small"
type="secondary"
>
<Translation>Show Values File</Translation>
</Button>
)}
{showValuesFile && valueFiles && (
<HelmValueShow
name={helm?.chart || 'default'}
valueFiles={valueFiles}
onClose={() => {
this.setState({ showValuesFile: false });
}}
/>
)}
</Loading> </Loading>
); );
} }

View File

@ -82,7 +82,6 @@ class KV extends Component<Props, State> {
this.form.setValue('envValue-' + key, value[label]); this.form.setValue('envValue-' + key, value[label]);
} }
} }
this.setState({ items: newItems }); this.setState({ items: newItems });
}; };

View File

@ -353,6 +353,26 @@ export interface Trigger {
componentName?: string; componentName?: string;
} }
export interface CreateTriggerRequest {
name: string;
alias?: string;
description?: string;
workflowName: string;
type: 'webhook';
payloadType?: 'custom' | 'dockerHub' | 'ACR' | 'harbor' | 'artifactory';
registry?: string;
componentName?: string;
}
export interface UpdateTriggerRequest {
alias?: string;
description?: string;
workflowName: string;
payloadType?: 'custom' | 'dockerHub' | 'ACR' | 'harbor' | 'artifactory';
registry?: string;
componentName?: string;
}
export interface ApplicationComponentConfig { export interface ApplicationComponentConfig {
name: string; name: string;
alias: string; alias: string;

View File

@ -1,6 +1,6 @@
export interface HelmRepo { export interface HelmRepo {
url: string; url: string;
type: string; type?: string;
secretName?: string; secretName?: string;
} }

View File

@ -3,7 +3,10 @@ import { withTranslation } from 'react-i18next';
import { connect } from 'dva'; import { connect } from 'dva';
import { Dialog, Field, Form, Grid, Message, Select, Button } from '@b-design/ui'; import { Dialog, Field, Form, Grid, Message, Select, Button } from '@b-design/ui';
import type { ApplicationDetail, EnvBinding } from '../../../../interface/application'; import type { ApplicationDetail, EnvBinding } from '../../../../interface/application';
import { createApplicationEnv, updateApplicationEnv } from '../../../../api/application'; import {
createApplicationEnvbinding,
updateApplicationEnvbinding,
} from '../../../../api/application';
import Translation from '../../../../components/Translation'; import Translation from '../../../../components/Translation';
import locale from '../../../../utils/locale'; import locale from '../../../../utils/locale';
import { getEnvs } from '../../../../api/env'; import { getEnvs } from '../../../../api/env';
@ -11,6 +14,8 @@ import type { Env } from '../../../../interface/env';
import { If } from 'tsx-control-statements/components'; import { If } from 'tsx-control-statements/components';
import EnvDialog from '../../../../pages/EnvPage/components/EnvDialog'; import EnvDialog from '../../../../pages/EnvPage/components/EnvDialog';
import type { LoginUserInfo } from '../../../../interface/user'; import type { LoginUserInfo } from '../../../../interface/user';
import { showAlias } from '../../../../utils/common';
import type { Dispatch } from 'redux';
interface Props { interface Props {
onClose: () => void; onClose: () => void;
@ -19,7 +24,7 @@ interface Props {
envbinding?: EnvBinding[]; envbinding?: EnvBinding[];
targets?: []; targets?: [];
userInfo?: LoginUserInfo; userInfo?: LoginUserInfo;
dispatch?: ({}) => {}; dispatch?: Dispatch<any>;
} }
interface State { interface State {
@ -53,9 +58,9 @@ class EnvBindPlanDialog extends Component<Props, State> {
loadEnvs = async (callback?: Callback) => { loadEnvs = async (callback?: Callback) => {
const { applicationDetail, envbinding } = this.props; const { applicationDetail, envbinding } = this.props;
if (applicationDetail) { if (applicationDetail && applicationDetail.project?.name) {
//Temporary logic //Temporary logic
getEnvs({ project: applicationDetail.project?.name || 'default', page: 0 }).then((re) => { getEnvs({ project: applicationDetail.project?.name, page: 0 }).then((re) => {
const existEnvs = const existEnvs =
envbinding?.map((eb) => { envbinding?.map((eb) => {
return eb.name; return eb.name;
@ -65,7 +70,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
this.setState({ envs: canAdd }); this.setState({ envs: canAdd });
const envOption = canAdd?.map((env: { name: string; alias: string }) => { const envOption = canAdd?.map((env: { name: string; alias: string }) => {
return { return {
label: env.alias ? `${env.alias}(${env.name})` : env.name, label: showAlias(env.name, env.alias),
value: env.name, value: env.name,
}; };
}); });
@ -78,19 +83,19 @@ class EnvBindPlanDialog extends Component<Props, State> {
onSubmit = () => { onSubmit = () => {
const { applicationDetail } = this.props; const { applicationDetail } = this.props;
if (!applicationDetail) {
return;
}
const { isEdit } = this.state; const { isEdit } = this.state;
this.field.validate((error: any, values: any) => { this.field.validate((error: any, values: any) => {
if (error) { if (error) {
return; return;
} }
this.setState({ loading: true }); this.setState({ loading: true });
const { name, alias, description, targetNames } = values; const { name } = values;
const params = { const params = {
appName: applicationDetail && applicationDetail.name, appName: applicationDetail && applicationDetail.name,
name, name,
alias,
targetNames,
description,
}; };
if (isEdit) { if (isEdit) {
this.onUpdateApplicationEnv(params); this.onUpdateApplicationEnv(params);
@ -101,7 +106,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
}; };
onCreateApplicationEnv(params: any) { onCreateApplicationEnv(params: any) {
createApplicationEnv(params) createApplicationEnvbinding(params)
.then((res) => { .then((res) => {
if (res) { if (res) {
Message.success(<Translation>Environment bound successfully</Translation>); Message.success(<Translation>Environment bound successfully</Translation>);
@ -114,7 +119,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
} }
onUpdateApplicationEnv(params: any) { onUpdateApplicationEnv(params: any) {
updateApplicationEnv(params) updateApplicationEnvbinding(params)
.then((res: any) => { .then((res: any) => {
if (res) { if (res) {
Message.success(<Translation>Environment bound successfully</Translation>); Message.success(<Translation>Environment bound successfully</Translation>);
@ -207,7 +212,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
this.changeEnvDialog(true); this.changeEnvDialog(true);
}} }}
> >
<Translation>New environment</Translation> <Translation>New Environment</Translation>
</a> </a>
} }
required={true} required={true}

View File

@ -26,6 +26,7 @@
font-size: var(--font-size-normal); font-size: var(--font-size-normal);
text-align: center; text-align: center;
background: #fff; background: #fff;
border: var(--grey-200) dashed 1px;
cursor: pointer; cursor: pointer;
&:first-child { &:first-child {
margin-left: 0px; margin-left: 0px;
@ -33,6 +34,7 @@
&.active { &.active {
color: #fff; color: #fff;
background: var(--primary-color); background: var(--primary-color);
border: 0;
} }
} }
} }

View File

@ -82,7 +82,9 @@ class TabsContent extends Component<Props, State> {
trigger={ trigger={
<Link <Link
key={item.name + 'link'} key={item.name + 'link'}
className={classNames('top-menu-item', { active: activeKey === item.name })} className={classNames('top-menu-item', 'item-env', {
active: activeKey === item.name,
})}
to={`/applications/${applicationDetail?.name}/envbinding/${item.name}/workflow`} to={`/applications/${applicationDetail?.name}/envbinding/${item.name}/workflow`}
> >
<span title={item.description}>{item.alias ? item.alias : item.name}</span> <span title={item.description}>{item.alias ? item.alias : item.name}</span>

View File

@ -155,15 +155,13 @@ class ApplicationHeader extends Component<Props, State> {
<If condition={applicationDetail?.readOnly}> <If condition={applicationDetail?.readOnly}>
<Message <Message
type="notice" type="notice"
title={i18n title={i18n.t('This application is managed by the addon, and it is readonly')}
.t('This application is managed by the addon, and it is readonly')
.toString()}
/> />
</If> </If>
<If condition={sourceOfTrust === 'from-k8s-resource'}> <If condition={sourceOfTrust === 'from-k8s-resource'}>
<Message <Message
type="warning" type="warning"
title={i18n.t('The application is synchronizing from the cluster.').toString()} title={i18n.t('The application is synchronizing from the cluster.')}
/> />
</If> </If>
<Permission <Permission

View File

@ -36,6 +36,11 @@ class Menu extends Component<Props, any> {
label: <Translation>Revisions</Translation>, label: <Translation>Revisions</Translation>,
to: `/applications/${appName}/revisions`, to: `/applications/${appName}/revisions`,
}, },
{
key: 'workflows',
label: <Translation>Workflows</Translation>,
to: `/applications/${appName}/workflows`,
},
], ],
envPage: [ envPage: [
{ {

View File

@ -32,6 +32,8 @@ import PipelineListPage from '../../pages/PipelineListPage';
import ApplicationWorkflowStudio from '../../pages/ApplicationWorkflowStudio'; import ApplicationWorkflowStudio from '../../pages/ApplicationWorkflowStudio';
import PipelineStudio from '../../pages/PipelineStudio'; import PipelineStudio from '../../pages/PipelineStudio';
import ProjectPipelines from '../../pages/ProjectPipelines'; import ProjectPipelines from '../../pages/ProjectPipelines';
import ApplicationEnvRoute from '../../pages/ApplicationEnvRoute';
import ApplicationWorkflowList from '../../pages/ApplicationWorkflowList';
export default function Content() { export default function Content() {
return ( return (
@ -73,6 +75,28 @@ export default function Content() {
); );
}} }}
/> />
<Route
exact
path="/applications/:appName/envbinding"
render={(props: any) => {
return (
<ApplicationLayout {...props}>
<ApplicationEnvRoute {...props} />;
</ApplicationLayout>
);
}}
/>
<Route
exact
path="/applications/:appName/workflows"
render={(props: any) => {
return (
<ApplicationLayout {...props}>
<ApplicationWorkflowList {...props} />
</ApplicationLayout>
);
}}
/>
<Route <Route
exact exact
path="/applications/:appName/envbinding/:envName" path="/applications/:appName/envbinding/:envName"

View File

@ -1,5 +1,5 @@
:root { :root {
--min-layout-width: 1400px; --min-layout-width: 1500px;
--primary-color: #1b58f4; --primary-color: #1b58f4;
--warning-color: var(--message-warning-color-icon-inline, #fac800); --warning-color: var(--message-warning-color-icon-inline, #fac800);
--hover-color: #f7bca9; --hover-color: #f7bca9;

View File

@ -195,6 +195,7 @@
"New Environment": "新增环境", "New Environment": "新增环境",
"New Trait": "新增运维特征", "New Trait": "新增运维特征",
"New Trigger": "新增触发器", "New Trigger": "新增触发器",
"Edit Trigger": "编辑触发器",
"New Workflow": "新增工作流", "New Workflow": "新增工作流",
"Next Step": "下一步", "Next Step": "下一步",
"No version record": "暂无版本记录", "No version record": "暂无版本记录",
@ -596,5 +597,25 @@
"Launch Workflow Studio": "工作流画板", "Launch Workflow Studio": "工作流画板",
"Available Targets": "可用的交付目标", "Available Targets": "可用的交付目标",
"Select Step Type": "选择步骤类型", "Select Step Type": "选择步骤类型",
"Unsaved changes": "变更未保存" "Unsaved changes": "变更未保存",
"DAG": "并行执行",
"StepByStep": "串行执行",
"Mode": "执行模式",
"Sub Mode": "子步骤",
"The workflow step spec reference document": "工作流部署规范参考文档",
"Click here": "点击这里",
"Select Contexts": "选择上下文参数",
"The context is the runtime inputs for the Pipeline": "上下文参数作为工作流的运行时输入",
"New Context": "新建上下文参数",
"No context is required for the execution of this Pipeline": "工作流执行无必需的上下文参数",
"Run Pipeline": "执行工作流",
"Click and deselect": "点击取消选中",
"Click and select": "点击选中",
"Edit Pipeline": "编辑流水线",
"Pipeline loaded successfully and is ready to clone.": "流水线配置加载完成,可以开始克隆。",
"Clone Pipeline": "克隆流水线",
"Includes": "包括",
"contexts": "上下文参数",
"Are you sure to rerun this Pipeline?": "确定要重新运行流水线吗?",
"Show Values File": "查看 Value 文件"
} }

View File

@ -58,6 +58,7 @@ type State = {
loading: boolean; loading: boolean;
status: 'disabled' | 'enabled' | 'enabling' | 'suspend' | 'disabling' | ''; status: 'disabled' | 'enabled' | 'enabling' | 'suspend' | 'disabling' | '';
statusLoading: boolean; statusLoading: boolean;
enableLoading?: boolean;
upgradeLoading: boolean; upgradeLoading: boolean;
args?: any; args?: any;
addonsStatus?: ApplicationStatus; addonsStatus?: ApplicationStatus;
@ -251,7 +252,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
Message.warning(i18n.t('Please firstly select at least one cluster.')); Message.warning(i18n.t('Please firstly select at least one cluster.'));
return; return;
} }
this.setState({ statusLoading: true }, () => { this.setState({ statusLoading: true, enableLoading: true }, () => {
if (this.state.version) { if (this.state.version) {
const params: EnableAddonRequest = { const params: EnableAddonRequest = {
name: this.props.addonName, name: this.props.addonName,
@ -262,9 +263,13 @@ class AddonDetailDialog extends React.Component<Props, State> {
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) { if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
params.clusters = this.state.clusters; params.clusters = this.state.clusters;
} }
enableAddon(params).then(() => { enableAddon(params)
this.loadAddonStatus(); .then(() => {
}); this.loadAddonStatus();
})
.finally(() => {
this.setState({ enableLoading: false });
});
} }
}); });
}; };
@ -311,6 +316,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
status, status,
statusLoading, statusLoading,
upgradeLoading, upgradeLoading,
enableLoading,
addonsStatus, addonsStatus,
showStatusVisible, showStatusVisible,
version, version,
@ -390,7 +396,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
project={''} project={''}
> >
<Button <Button
loading={status === 'enabling'} loading={status === 'enabling' || enableLoading}
type="primary" type="primary"
onClick={this.onEnable} onClick={this.onEnable}
style={{ marginLeft: '16px' }} style={{ marginLeft: '16px' }}
@ -462,7 +468,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
{`${i18n.t('Addon status is ')}${addonsStatus?.status || 'Init'}`} {`${i18n.t('Addon status is ')}${addonsStatus?.status || 'Init'}`}
<Link <Link
style={{ marginLeft: '16px' }} style={{ marginLeft: '16px' }}
to={`/applications/addon-${addonDetailInfo?.name}/envbinding/system/workflow`} to={`/applications/addon-${addonDetailInfo?.name}/envbinding`}
> >
<Translation>Check the details</Translation> <Translation>Check the details</Translation>
</Link> </Link>

View File

@ -8,7 +8,7 @@
.components-list-title { .components-list-title {
display: flex; display: flex;
height: 24px; height: 24px;
color: #1b58f4; color: var(--primary-color);
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
} }

View File

@ -15,6 +15,7 @@ import Permission from '../../../../components/Permission';
import terraform from '../../../../assets/terraform.svg'; import terraform from '../../../../assets/terraform.svg';
import kubernetes from '../../../../assets/kubernetes.svg'; import kubernetes from '../../../../assets/kubernetes.svg';
import helm from '../../../../assets/helm.svg'; import helm from '../../../../assets/helm.svg';
import { showAlias } from '../../../../utils/common';
type Props = { type Props = {
application?: ApplicationBase; application?: ApplicationBase;
@ -57,8 +58,8 @@ class ComponentsList extends Component<Props> {
<div className="list-warper"> <div className="list-warper">
<div className="box"> <div className="box">
{(components || []).map((item: ApplicationComponentBase) => ( {(components || []).map((item: ApplicationComponentBase) => (
<Row wrap={true} className="box-item"> <Row key={item.name} wrap={true} className="box-item">
<Col span={24} key={item.name}> <Col span={24}>
<Card locale={locale().Card} contentHeight="auto"> <Card locale={locale().Card} contentHeight="auto">
<div className="components-list-nav"> <div className="components-list-nav">
<div <div
@ -68,7 +69,7 @@ class ComponentsList extends Component<Props> {
}} }}
> >
{this.getComponentTypeIcon(item)} {this.getComponentTypeIcon(item)}
{item.alias ? `${item.alias}(${item.name})` : item.name} {showAlias(item)}
</div> </div>
<If condition={item.main != true}> <If condition={item.main != true}>
<div className="components-list-operation"> <div className="components-list-operation">
@ -98,7 +99,7 @@ class ComponentsList extends Component<Props> {
</If> </If>
<Row wrap={true}> <Row wrap={true}>
{item.traits?.map((trait) => { {item.traits?.map((trait) => {
const label = trait.alias ? trait.alias + '(' + trait.type + ')' : trait.type; const label = showAlias(trait.type, trait.alias);
return ( return (
<div <div
key={trait.type} key={trait.type}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Grid, Field, Form, Select, Message, Button, Input } from '@b-design/ui'; import { Grid, Field, Form, Select, Message, Button, Input } from '@b-design/ui';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { createTriggers } from '../../../../api/application'; import { createTrigger, updateTrigger } from '../../../../api/application';
import { getPayloadType } from '../../../../api/payload'; import { getPayloadType } from '../../../../api/payload';
import { detailComponentDefinition } from '../../../../api/definitions'; import { detailComponentDefinition } from '../../../../api/definitions';
import type { import type {
@ -9,6 +9,8 @@ import type {
Trigger, Trigger,
UIParam, UIParam,
ApplicationComponentBase, ApplicationComponentBase,
CreateTriggerRequest,
UpdateTriggerRequest,
} from '../../../../interface/application'; } from '../../../../interface/application';
import DrawerWithFooter from '../../../../components/Drawer'; import DrawerWithFooter from '../../../../components/Drawer';
import Translation from '../../../../components/Translation'; import Translation from '../../../../components/Translation';
@ -16,6 +18,7 @@ import { checkName } from '../../../../utils/common';
import locale from '../../../../utils/locale'; import locale from '../../../../utils/locale';
import { If } from 'tsx-control-statements/components'; import { If } from 'tsx-control-statements/components';
import i18n from '../../../../i18n'; import i18n from '../../../../i18n';
import type { Dispatch } from 'redux';
type Props = { type Props = {
visible: boolean; visible: boolean;
@ -23,8 +26,9 @@ type Props = {
workflows?: Workflow[]; workflows?: Workflow[];
onOK: (params: Trigger) => void; onOK: (params: Trigger) => void;
onClose: () => void; onClose: () => void;
dispatch?: ({}) => {}; dispatch?: Dispatch<any>;
components: ApplicationComponentBase[]; components: ApplicationComponentBase[];
trigger?: Trigger;
}; };
type State = { type State = {
@ -32,6 +36,7 @@ type State = {
payloadTypes: string[]; payloadTypes: string[];
hasImage: boolean; hasImage: boolean;
component?: ApplicationComponentBase; component?: ApplicationComponentBase;
submitLoading?: boolean;
}; };
class TriggerDialog extends React.Component<Props, State> { class TriggerDialog extends React.Component<Props, State> {
@ -47,6 +52,10 @@ class TriggerDialog extends React.Component<Props, State> {
} }
componentDidMount() { componentDidMount() {
const { trigger } = this.props;
if (trigger) {
this.field.setValues(trigger);
}
const type = this.field.getValue('type'); const type = this.field.getValue('type');
if (type === 'webhook') { if (type === 'webhook') {
this.onGetPayloadType(); this.onGetPayloadType();
@ -90,6 +99,8 @@ class TriggerDialog extends React.Component<Props, State> {
}; };
onSubmit = () => { onSubmit = () => {
const { trigger } = this.props;
const editMode = trigger != undefined;
this.field.validate((error: any, values: any) => { this.field.validate((error: any, values: any) => {
if (error) { if (error) {
return; return;
@ -106,38 +117,68 @@ class TriggerDialog extends React.Component<Props, State> {
registry = '', registry = '',
} = values; } = values;
const query = { appName }; const query = { appName };
const params: Trigger = { this.setState({ submitLoading: true });
name, if (editMode) {
alias, const params: UpdateTriggerRequest = {
description, alias,
type, description,
payloadType, payloadType,
workflowName, workflowName,
token: '', componentName,
componentName, registry,
registry, };
}; updateTrigger(params, { appName, token: trigger.token })
createTriggers(params, query).then((res: any) => { .then((res: any) => {
if (res) { if (res) {
Message.success({ Message.success({
duration: 4000, duration: 4000,
content: 'Trigger created successfully.', content: 'Trigger updated successfully.',
});
this.props.onOK(res);
}
})
.finally(() => {
this.setState({ submitLoading: false });
}); });
this.props.onOK(res); } else {
} const params: CreateTriggerRequest = {
}); name,
alias,
description,
type,
payloadType,
workflowName,
componentName,
registry,
};
createTrigger(params, query)
.then((res: any) => {
if (res) {
Message.success({
duration: 4000,
content: 'Trigger created successfully.',
});
this.props.onOK(res);
}
})
.finally(() => {
this.setState({ submitLoading: false });
});
}
}); });
}; };
extButtonList = () => { extButtonList = () => {
const { onClose } = this.props; const { onClose, trigger } = this.props;
const { submitLoading } = this.state;
const editMode = trigger != undefined;
return ( return (
<div> <div>
<Button type="secondary" onClick={onClose} className="margin-right-10"> <Button type="secondary" onClick={onClose} className="margin-right-10">
<Translation>Cancel</Translation> <Translation>Cancel</Translation>
</Button> </Button>
<Button type="primary" onClick={this.onSubmit}> <Button loading={submitLoading} type="primary" onClick={this.onSubmit}>
<Translation>Create</Translation> <Translation>{editMode ? 'Update' : 'Create'}</Translation>
</Button> </Button>
</div> </div>
); );
@ -180,7 +221,8 @@ class TriggerDialog extends React.Component<Props, State> {
const init = this.field.init; const init = this.field.init;
const FormItem = Form.Item; const FormItem = Form.Item;
const { Row, Col } = Grid; const { Row, Col } = Grid;
const { workflows, onClose, components } = this.props; const { workflows, onClose, components, trigger } = this.props;
const editMode = trigger != undefined;
const { payloadTypes } = this.state; const { payloadTypes } = this.state;
const workflowOption = workflows?.map((workflow) => { const workflowOption = workflows?.map((workflow) => {
return { return {
@ -205,7 +247,7 @@ class TriggerDialog extends React.Component<Props, State> {
return ( return (
<DrawerWithFooter <DrawerWithFooter
title={i18n.t('Add Trigger')} title={editMode ? i18n.t('Edit Trigger') : i18n.t('Add Trigger')}
placement="right" placement="right"
width={800} width={800}
onClose={onClose} onClose={onClose}
@ -217,6 +259,7 @@ class TriggerDialog extends React.Component<Props, State> {
<FormItem label={<Translation>Name</Translation>} required> <FormItem label={<Translation>Name</Translation>} required>
<Input <Input
name="name" name="name"
disabled={editMode}
placeholder={i18n.t('Please enter the name').toString()} placeholder={i18n.t('Please enter the name').toString()}
{...init('name', { {...init('name', {
rules: [ rules: [
@ -274,6 +317,7 @@ class TriggerDialog extends React.Component<Props, State> {
<Select <Select
name="type" name="type"
locale={locale().Select} locale={locale().Select}
disabled={editMode}
dataSource={[{ label: 'On Webhook Event', value: 'webhook' }]} dataSource={[{ label: 'On Webhook Event', value: 'webhook' }]}
{...init('type', { {...init('type', {
initValue: 'webhook', initValue: 'webhook',

View File

@ -15,6 +15,8 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.trigger-list-title { .trigger-list-title {
color: var(--primary-color);
font-size: 16px; font-size: 16px;
cursor: pointer;
} }
} }

View File

@ -7,7 +7,7 @@ import type {
ApplicationDetail, ApplicationDetail,
} from '../../../../interface/application'; } from '../../../../interface/application';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { momentDate } from '../../../../utils/common'; import { momentDate, showAlias } from '../../../../utils/common';
import './index.less'; import './index.less';
import { If } from 'tsx-control-statements/components'; import { If } from 'tsx-control-statements/components';
import Empty from '../../../../components/Empty'; import Empty from '../../../../components/Empty';
@ -24,6 +24,7 @@ type Props = {
components: ApplicationComponentBase[]; components: ApplicationComponentBase[];
applicationDetail?: ApplicationDetail; applicationDetail?: ApplicationDetail;
onDeleteTrigger: (token: string) => void; onDeleteTrigger: (token: string) => void;
onEditTrigger: (t: Trigger) => void;
}; };
type State = { type State = {
@ -135,12 +136,17 @@ class TriggerList extends Component<Props, State> {
<div className="list-warper"> <div className="list-warper">
<div className="box"> <div className="box">
{(triggers || []).map((item: Trigger) => ( {(triggers || []).map((item: Trigger) => (
<Row wrap={true} className="box-item"> <Row wrap={true} key={item.type} className="box-item">
<Col span={24} key={item.type}> <Col span={24}>
<Card free={true} style={{ padding: '16px' }} locale={locale().Card}> <Card free={true} style={{ padding: '16px' }} locale={locale().Card}>
<div className="trigger-list-nav"> <div className="trigger-list-nav">
<div className="trigger-list-title"> <div
{item.alias ? `${item.alias}(${item.name})` : item.name} onClick={() => {
this.props.onEditTrigger(item);
}}
className="trigger-list-title"
>
{showAlias(item)}
</div> </div>
<div className="trigger-list-operation"> <div className="trigger-list-operation">
<Permission <Permission

View File

@ -6,7 +6,7 @@ import { If } from 'tsx-control-statements/components';
import { import {
deleteTrait, deleteTrait,
getApplicationTriggers, getApplicationTriggers,
deleteTriggers, deleteTrigger,
deleteComponent, deleteComponent,
deleteApplication, deleteApplication,
deletePolicy, deletePolicy,
@ -76,6 +76,7 @@ type State = {
mainComponent?: ApplicationComponent; mainComponent?: ApplicationComponent;
traitItem: Trait; traitItem: Trait;
triggers: Trigger[]; triggers: Trigger[];
trigger?: Trigger;
visibleTrigger: boolean; visibleTrigger: boolean;
createTriggerInfo: Trigger; createTriggerInfo: Trigger;
showEditApplication: boolean; showEditApplication: boolean;
@ -211,6 +212,7 @@ class ApplicationConfig extends Component<Props, State> {
onTriggerClose = () => { onTriggerClose = () => {
this.setState({ this.setState({
visibleTrigger: false, visibleTrigger: false,
trigger: undefined,
}); });
this.onLoadApplicationComponents(); this.onLoadApplicationComponents();
}; };
@ -219,6 +221,7 @@ class ApplicationConfig extends Component<Props, State> {
this.onGetApplicationTrigger(); this.onGetApplicationTrigger();
this.setState({ this.setState({
visibleTrigger: false, visibleTrigger: false,
trigger: undefined,
createTriggerInfo: res, createTriggerInfo: res,
}); });
}; };
@ -229,8 +232,12 @@ class ApplicationConfig extends Component<Props, State> {
appName, appName,
token, token,
}; };
deleteTriggers(params).then((res: any) => { deleteTrigger(params).then((res: any) => {
if (res) { if (res) {
Message.success({
duration: 4000,
content: 'Trigger deleted successfully.',
});
this.onGetApplicationTrigger(); this.onGetApplicationTrigger();
} }
}); });
@ -462,6 +469,7 @@ class ApplicationConfig extends Component<Props, State> {
componentName = '', componentName = '',
traitItem, traitItem,
triggers, triggers,
trigger,
visibleTrigger, visibleTrigger,
createTriggerInfo, createTriggerInfo,
showEditApplication, showEditApplication,
@ -481,7 +489,7 @@ class ApplicationConfig extends Component<Props, State> {
return ( return (
<div> <div>
<Row className="flex-row" wrap={true}> <Row className="flex-row" wrap={true}>
<Col xl={16} m={24} s={24} style={{ padding: '0 8px' }}> <Col xl={16} l={24} s={24} style={{ padding: '0 8px' }}>
<Card <Card
locale={locale().Card} locale={locale().Card}
contentHeight="auto" contentHeight="auto"
@ -525,7 +533,7 @@ class ApplicationConfig extends Component<Props, State> {
</div> </div>
</Col> </Col>
<Col l={8} xxs={24}> <Col l={8} xs={24}>
<Item <Item
label={<Translation>Project</Translation>} label={<Translation>Project</Translation>}
value={ value={
@ -538,7 +546,7 @@ class ApplicationConfig extends Component<Props, State> {
/> />
</Col> </Col>
<Col l={8} xxs={24}> <Col l={8} xs={24}>
<Item <Item
label={<Translation>Create Time</Translation>} label={<Translation>Create Time</Translation>}
value={ value={
@ -555,7 +563,7 @@ class ApplicationConfig extends Component<Props, State> {
/> />
</Col> </Col>
<Col l={8} xxs={24}> <Col l={8} xs={24}>
<Item <Item
label={<Translation>Update Time</Translation>} label={<Translation>Update Time</Translation>}
value={ value={
@ -578,6 +586,7 @@ class ApplicationConfig extends Component<Props, State> {
if (applicationDetail?.labels) { if (applicationDetail?.labels) {
return ( return (
<Tag <Tag
key={key}
style={{ margin: '4px' }} style={{ margin: '4px' }}
color="blue" color="blue"
>{`${key}=${applicationDetail?.labels[key]}`}</Tag> >{`${key}=${applicationDetail?.labels[key]}`}</Tag>
@ -588,7 +597,7 @@ class ApplicationConfig extends Component<Props, State> {
</Row> </Row>
</Card> </Card>
</Col> </Col>
<Col xl={8} m={24} s={24} style={{ padding: '0 8px' }}> <Col xl={8} l={24} s={24} style={{ padding: '0 8px' }}>
<Card locale={locale().Card} contentHeight="auto" style={{ height: '100%' }}> <Card locale={locale().Card} contentHeight="auto" style={{ height: '100%' }}>
<Row> <Row>
<Col span={6} style={{ padding: '22px 0' }}> <Col span={6} style={{ padding: '22px 0' }}>
@ -606,12 +615,14 @@ class ApplicationConfig extends Component<Props, State> {
<Col span={6} style={{ padding: '22px 0' }}> <Col span={6} style={{ padding: '22px 0' }}>
<NumItem <NumItem
number={statistics?.revisionCount} number={statistics?.revisionCount}
to={`/applications/${applicationDetail.name}/revisions`}
title={i18n.t('Revision Count').toString()} title={i18n.t('Revision Count').toString()}
/> />
</Col> </Col>
<Col span={6} style={{ padding: '22px 0' }}> <Col span={6} style={{ padding: '22px 0' }}>
<NumItem <NumItem
number={statistics?.workflowCount} number={statistics?.workflowCount}
to={`/applications/${applicationDetail.name}/workflows`}
title={i18n.t('Workflow Count').toString()} title={i18n.t('Workflow Count').toString()}
/> />
</Col> </Col>
@ -621,7 +632,7 @@ class ApplicationConfig extends Component<Props, State> {
</Row> </Row>
<Row wrap={true} className="app-spec"> <Row wrap={true} className="app-spec">
<Col xl={8} xs={24} className="app-spec-item"> <Col xl={8} xxs={24} className="app-spec-item">
<Row> <Row>
<Col span={24} className="padding16"> <Col span={24} className="padding16">
<Title <Title
@ -672,7 +683,7 @@ class ApplicationConfig extends Component<Props, State> {
changeTraitStats={this.changeTraitStats} changeTraitStats={this.changeTraitStats}
/> />
</Col> </Col>
<Col xl={8} xs={24} className="app-spec-item"> <Col xl={8} xxs={24} className="app-spec-item">
<Row> <Row>
<Col span={24} className="padding16"> <Col span={24} className="padding16">
<Title <Title
@ -713,7 +724,7 @@ class ApplicationConfig extends Component<Props, State> {
}} }}
/> />
</Col> </Col>
<Col xl={8} xs={24} className="app-spec-item"> <Col xl={8} xxs={24} className="app-spec-item">
<Row> <Row>
<Col span={24} className="padding16"> <Col span={24} className="padding16">
<Title <Title
@ -751,6 +762,9 @@ class ApplicationConfig extends Component<Props, State> {
}} }}
createTriggerInfo={createTriggerInfo} createTriggerInfo={createTriggerInfo}
applicationDetail={applicationDetail} applicationDetail={applicationDetail}
onEditTrigger={(t: Trigger) => {
this.setState({ visibleTrigger: true, trigger: t });
}}
/> />
</Col> </Col>
</Row> </Row>
@ -780,6 +794,7 @@ class ApplicationConfig extends Component<Props, State> {
<TriggerDialog <TriggerDialog
visible={visibleTrigger} visible={visibleTrigger}
appName={appName} appName={appName}
trigger={trigger}
workflows={workflows} workflows={workflows}
components={components || []} components={components || []}
onClose={this.onTriggerClose} onClose={this.onTriggerClose}

View File

@ -0,0 +1,25 @@
import { Loading } from '@b-design/ui';
import { connect } from 'dva';
import { routerRedux } from 'dva/router';
import React, { useEffect } from 'react';
import type { Dispatch } from 'redux';
import { getApplicationEnvbinding } from '../../api/application';
const EnvRoute = (props: { match: { params: { appName: string } }; dispatch: Dispatch<any> }) => {
useEffect(() => {
getApplicationEnvbinding({ appName: props.match.params.appName }).then((res) => {
if (res && Array.isArray(res.envBindings) && res.envBindings.length > 0) {
props.dispatch(
routerRedux.push(
`/applications/${props.match.params.appName}/envbinding/${res.envBindings[0].name}/workflow`,
),
);
} else {
props.dispatch(routerRedux.push(`/applications/${props.match.params.appName}`));
}
});
});
return <Loading visible={true} />;
};
export default connect()(EnvRoute);

View File

@ -434,7 +434,7 @@ class AppDialog extends React.Component<Props, State> {
this.changeEnvDialog(true); this.changeEnvDialog(true);
}} }}
> >
<Translation>New environment</Translation> <Translation>New Environment</Translation>
</a> </a>
} }
required={true} required={true}

View File

@ -32,6 +32,7 @@ type Props = {
type State = { type State = {
compare?: ApplicationCompareResponse; compare?: ApplicationCompareResponse;
revision?: ApplicationRevision;
visibleApplicationDiff: boolean; visibleApplicationDiff: boolean;
diffMode: 'latest' | 'cluster'; diffMode: 'latest' | 'cluster';
}; };
@ -80,22 +81,27 @@ class TableList extends Component<Props, State> {
} }
}; };
loadChanges = (revision: string, mode: 'latest' | 'cluster') => { loadChanges = (revision: ApplicationRevision, mode: 'latest' | 'cluster') => {
const { applicationDetail } = this.props; const { applicationDetail } = this.props;
if (!revision || !applicationDetail) { if (!revision || !applicationDetail) {
this.setState({ compare: undefined }); this.setState({ compare: undefined });
return; return;
} }
let params: ApplicationCompareRequest = { let params: ApplicationCompareRequest = {
compareRevisionWithLatest: { revision: revision }, compareRevisionWithLatest: { revision: revision.version },
}; };
if (mode === 'cluster') { if (mode === 'cluster') {
params = { params = {
compareRevisionWithRunning: { revision: revision }, compareRevisionWithRunning: { revision: revision.version },
}; };
} }
compareApplication(applicationDetail?.name, params).then((res: ApplicationCompareResponse) => { compareApplication(applicationDetail?.name, params).then((res: ApplicationCompareResponse) => {
this.setState({ compare: res, visibleApplicationDiff: true, diffMode: mode }); this.setState({
revision: revision,
compare: res,
visibleApplicationDiff: true,
diffMode: mode,
});
}); });
}; };
@ -223,14 +229,14 @@ class TableList extends Component<Props, State> {
<Menu> <Menu>
<Menu.Item <Menu.Item
onClick={() => { onClick={() => {
this.loadChanges(record.version, 'cluster'); this.loadChanges(record, 'cluster');
}} }}
> >
<Translation>Diff With Deployed Application</Translation> <Translation>Diff With Deployed Application</Translation>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
onClick={() => { onClick={() => {
this.loadChanges(record.version, 'latest'); this.loadChanges(record, 'latest');
}} }}
> >
<Translation>Diff With Latest Configuration</Translation> <Translation>Diff With Latest Configuration</Translation>
@ -271,7 +277,7 @@ class TableList extends Component<Props, State> {
const { Column } = Table; const { Column } = Table;
const columns = this.getColumns(); const columns = this.getColumns();
const { list } = this.props; const { list } = this.props;
const { visibleApplicationDiff, compare, diffMode } = this.state; const { visibleApplicationDiff, compare, diffMode, revision } = this.state;
const baseName = 'Current Revision'; const baseName = 'Current Revision';
let targetName = 'Latest Application Configuration'; let targetName = 'Latest Application Configuration';
if (diffMode == 'cluster') { if (diffMode == 'cluster') {
@ -298,8 +304,19 @@ class TableList extends Component<Props, State> {
{compare && ( {compare && (
<ApplicationDiff <ApplicationDiff
onClose={() => { onClose={() => {
this.setState({ visibleApplicationDiff: false, compare: undefined }); this.setState({
visibleApplicationDiff: false,
compare: undefined,
revision: undefined,
});
}} }}
rollback2Revision={
diffMode === 'cluster' && revision
? () => {
this.onRollback(revision);
}
: undefined
}
baseName={baseName} baseName={baseName}
targetName={targetName} targetName={targetName}
compare={compare} compare={compare}

View File

@ -76,8 +76,8 @@ class ApplicationRevisionList extends React.Component<Props, State> {
listRevisions(params).then((res) => { listRevisions(params).then((res) => {
if (res) { if (res) {
this.setState({ this.setState({
revisionsList: res && res.revisions, revisionsList: res.revisions || [],
revisionsListTotal: res && res.total, revisionsListTotal: res.total || 0,
}); });
} }
}); });

View File

@ -0,0 +1,173 @@
import React from 'react';
import { connect } from 'dva';
import { Button, Dialog, Message, Table } from '@b-design/ui';
import type { ApplicationDetail, EnvBinding, Workflow } from '../../interface/application';
import type { Dispatch } from 'redux';
import { deleteWorkflow, listWorkflow } from '../../api/workflows';
import i18n from '../../i18n';
import { momentDate } from '../../utils/common';
import Permission from '../../components/Permission';
import { AiFillDelete } from 'react-icons/ai';
import Translation from '../../components/Translation';
import { Link } from 'dva/router';
import { If } from 'tsx-control-statements/components';
import locale from '../../utils/locale';
type Props = {
revisions: [];
applicationDetail?: ApplicationDetail;
envbinding?: EnvBinding[];
dispatch: Dispatch<any>;
match: {
params: {
appName: string;
};
};
};
type State = {
workflowList: Workflow[];
};
@connect((store: any) => {
return { ...store.application };
})
class ApplicationWorkflowList extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
workflowList: [],
};
}
componentDidMount() {
this.getWorkflowList();
}
getWorkflowList = async () => {
const { applicationDetail } = this.props;
if (applicationDetail) {
const params = {
appName: applicationDetail?.name,
};
listWorkflow(params).then((res) => {
if (res) {
this.setState({
workflowList: res.workflows || [],
});
}
});
}
};
onDeleteWorkflow = (name: string) => {
const { applicationDetail } = this.props;
if (applicationDetail) {
Dialog.confirm({
type: 'confirm',
content: (
<Translation>Unrecoverable after deletion, are you sure to delete it?</Translation>
),
onOk: () => {
deleteWorkflow({
appName: applicationDetail.name,
name: name,
}).then((res) => {
if (res) {
Message.success(i18n.t('The Workflow removed successfully'));
this.getWorkflowList();
}
});
},
locale: locale().Dialog,
});
}
};
render() {
const { workflowList } = this.state;
const { applicationDetail } = this.props;
const projectName = applicationDetail?.project?.name;
return (
<div>
<Message type="notice" style={{ marginBottom: '8px' }}>
<Translation>One environment corresponds to one workflow</Translation>
</Message>
<Table dataSource={workflowList}>
<Table.Column dataIndex="name" title={i18n.t('Name')} />
<Table.Column
dataIndex="envName"
title={i18n.t('Environment')}
cell={(v: string) => {
return (
<Link
to={`/applications/${applicationDetail?.name}/envbinding/${v}/workflow/studio`}
>
{v}
</Link>
);
}}
/>
<Table.Column dataIndex="mode" title={i18n.t('Mode')} />
<Table.Column
dataIndex="steps"
title={i18n.t('Step Number')}
cell={(steps: []) => {
return steps ? steps.length : 0;
}}
/>
<Table.Column
dataIndex="createTime"
title={i18n.t('Create Time')}
cell={(v: string) => {
return momentDate(v);
}}
/>
<Table.Column
dataIndex="updateTime"
title={i18n.t('Update Time')}
cell={(v: string) => {
return momentDate(v);
}}
/>
<Table.Column
dataIndex="name"
title={i18n.t('Action')}
cell={(v: string, w: Workflow) => {
return (
<div>
<If condition={v === w.envName + '-workflow'}>
<Permission
project={projectName}
resource={{
resource: `project:${projectName}/application:${applicationDetail?.name}/workflow:${v}`,
action: 'delete',
}}
>
<Button
text
size={'medium'}
ghost={true}
className={'danger-btn'}
component={'a'}
onClick={() => {
this.onDeleteWorkflow(v);
}}
>
<AiFillDelete />
<Translation>Remove</Translation>
</Button>
</Permission>
</If>
</div>
);
}}
/>
</Table>
</div>
);
}
}
export default ApplicationWorkflowList;

View File

@ -445,7 +445,7 @@ class ApplicationWorkflowRecord extends React.Component<Props, State> {
<div className={classNames('studio')}> <div className={classNames('studio')}>
{showRecord && ( {showRecord && (
<PipelineGraph <PipelineGraph
name={`${showRecord?.name}/${showRecord?.status}`} name={`${showRecord?.name}`}
zoom={zoom} zoom={zoom}
onNodeClick={this.onStepClick} onNodeClick={this.onStepClick}
steps={showRecord?.steps} steps={showRecord?.steps}
@ -534,7 +534,7 @@ class ApplicationWorkflowRecord extends React.Component<Props, State> {
<Icon type="refresh" /> <Icon type="refresh" />
</Button> </Button>
</div> </div>
<div className="logBox"> <div className="log-content">
{logs?.map((line, i: number) => { {logs?.map((line, i: number) => {
return ( return (
<div key={`log-${i}`} className="logLine"> <div key={`log-${i}`} className="logLine">

View File

@ -47,6 +47,19 @@ type State = {
editMode: 'visual' | 'yaml'; editMode: 'visual' | 'yaml';
}; };
export const WorkflowModeOptions = [
{
value: 'DAG',
label: i18n.t('DAG'),
description: 'Workflows will be executed in parallel in DAG mode based on dependencies.',
},
{
value: 'StepByStep',
label: i18n.t('StepByStep'),
description: 'The workflow will be executed serially step by step .',
},
];
@connect((store: any) => { @connect((store: any) => {
return { ...store.application }; return { ...store.application };
}) })
@ -214,18 +227,22 @@ class ApplicationWorkflowStudio extends React.Component<Props, State> {
<Translation>Unsaved changes</Translation> <Translation>Unsaved changes</Translation>
</div> </div>
)} )}
<Form.Item label="Mode" labelAlign="inset" style={{ marginRight: '8px' }}> <Form.Item label={i18n.t('Mode')} labelAlign="inset" style={{ marginRight: '8px' }}>
<Select <Select
locale={locale().Select} locale={locale().Select}
defaultValue="StepByStep" defaultValue="StepByStep"
value={mode} value={mode}
dataSource={[{ value: 'StepByStep' }, { value: 'DAG' }]} dataSource={WorkflowModeOptions}
onChange={(value) => { onChange={(value) => {
this.setState({ mode: value, changed: this.state.mode !== value }); this.setState({ mode: value, changed: this.state.mode !== value });
}} }}
/> />
</Form.Item> </Form.Item>
<Form.Item label="Sub Mode" labelAlign="inset" style={{ marginRight: '8px' }}> <Form.Item
label={i18n.t('Sub Mode')}
labelAlign="inset"
style={{ marginRight: '8px' }}
>
<Select <Select
locale={locale().Select} locale={locale().Select}
defaultValue="DAG" defaultValue="DAG"
@ -233,7 +250,7 @@ class ApplicationWorkflowStudio extends React.Component<Props, State> {
onChange={(value) => { onChange={(value) => {
this.setState({ subMode: value, changed: this.state.subMode !== value }); this.setState({ subMode: value, changed: this.state.subMode !== value });
}} }}
dataSource={[{ value: 'DAG' }, { value: 'StepByStep' }]} dataSource={WorkflowModeOptions}
/> />
</Form.Item> </Form.Item>
<Button <Button

View File

@ -44,6 +44,7 @@ class TableList extends Component<Props> {
key: 'name', key: 'name',
title: <Translation>Name(Alias)</Translation>, title: <Translation>Name(Alias)</Translation>,
dataIndex: 'name', dataIndex: 'name',
width: '150px',
cell: (v: string, i: number, env: Env) => { cell: (v: string, i: number, env: Env) => {
return ( return (
<a <a
@ -60,6 +61,7 @@ class TableList extends Component<Props> {
key: 'project', key: 'project',
title: <Translation>Project</Translation>, title: <Translation>Project</Translation>,
dataIndex: 'project', dataIndex: 'project',
width: '100px',
cell: (v: Project) => { cell: (v: Project) => {
if (v && v.name) { if (v && v.name) {
return <Link to={`/projects/${v.name}/summary`}>{v.alias || v.name}</Link>; return <Link to={`/projects/${v.name}/summary`}>{v.alias || v.name}</Link>;
@ -72,6 +74,7 @@ class TableList extends Component<Props> {
key: 'namespace', key: 'namespace',
title: <Translation>Namespace</Translation>, title: <Translation>Namespace</Translation>,
dataIndex: 'namespace', dataIndex: 'namespace',
width: '100px',
cell: (v: string) => { cell: (v: string) => {
return <span>{v}</span>; return <span>{v}</span>;
}, },

View File

@ -8,7 +8,12 @@ import * as yaml from 'js-yaml';
import { connect } from 'dva'; import { connect } from 'dva';
import type { LoginUserInfo } from '../../../../interface/user'; import type { LoginUserInfo } from '../../../../interface/user';
import { checkPermission } from '../../../../utils/permission'; import { checkPermission } from '../../../../utils/permission';
import { createPipeline, loadPipeline, updatePipeline } from '../../../../api/pipeline'; import {
createPipeline,
createPipelineContext,
loadPipeline,
updatePipeline,
} from '../../../../api/pipeline';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { import type {
PipelineBase, PipelineBase,
@ -18,6 +23,7 @@ import type {
import { checkName } from '../../../../utils/common'; import { checkName } from '../../../../utils/common';
import locale from '../../../../utils/locale'; import locale from '../../../../utils/locale';
import { templates } from './pipeline-template'; import { templates } from './pipeline-template';
import { If } from 'tsx-control-statements/components';
const FormItem = Form.Item; const FormItem = Form.Item;
@ -33,6 +39,8 @@ export interface PipelineProps {
type State = { type State = {
configError?: string[]; configError?: string[];
containerId: string; containerId: string;
defaultContext?: Record<string, string>;
loading?: boolean;
}; };
@connect((store: any) => { @connect((store: any) => {
@ -97,24 +105,45 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
}, },
}, },
}; };
this.setState({ loading: true });
if (pipeline) { if (pipeline) {
updatePipeline(request).then((res) => { updatePipeline(request)
if (res) { .then((res) => {
Message.success(i18n.t('Pipeline updated successfully')); if (res) {
if (this.props.onSuccess) { Message.success(i18n.t('Pipeline updated successfully'));
this.props.onSuccess(res); if (this.props.onSuccess) {
this.props.onSuccess(res);
}
} }
} })
}); .finally(() => {
this.setState({ loading: false });
});
} else { } else {
createPipeline(request).then((res) => { createPipeline(request)
if (res) { .then((res) => {
Message.success(i18n.t('Pipeline created successfully')); if (res) {
if (this.props.onSuccess) { // Create the default context
this.props.onSuccess(res); const { defaultContext } = this.state;
if (defaultContext) {
const contextValues: {
key: string;
value: string;
}[] = [];
Object.keys(defaultContext).map((key) => {
contextValues.push({ key: key, value: defaultContext[key] });
});
createPipelineContext(project, name, { name: 'default', values: contextValues });
}
Message.success(i18n.t('Pipeline created successfully'));
if (this.props.onSuccess) {
this.props.onSuccess(res);
}
} }
} })
}); .finally(() => {
this.setState({ loading: false });
});
} }
}); });
}; };
@ -161,6 +190,7 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
const { init } = this.field; const { init } = this.field;
const { userInfo, pipeline } = this.props; const { userInfo, pipeline } = this.props;
let defaultProject = ''; let defaultProject = '';
const editMode = pipeline != undefined;
const projectOptions: { label: string; value: string }[] = []; const projectOptions: { label: string; value: string }[] = [];
(userInfo?.projects || []).map((project) => { (userInfo?.projects || []).map((project) => {
if ( if (
@ -180,12 +210,13 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
} }
}); });
const modeOptions = [{ value: 'StepByStep' }, { value: 'DAG' }]; const modeOptions = [{ value: 'StepByStep' }, { value: 'DAG' }];
const { loading } = this.state;
return ( return (
<DrawerWithFooter <DrawerWithFooter
title={i18n.t(pipeline == undefined ? 'New Pipeline' : 'Edit Pipeline')} title={i18n.t(!editMode ? 'New Pipeline' : 'Edit Pipeline')}
onClose={this.props.onClose} onClose={this.props.onClose}
onOk={this.onSubmit} onOk={this.onSubmit}
onOkButtonLoading={loading}
> >
<Form field={this.field}> <Form field={this.field}>
<Row wrap> <Row wrap>
@ -193,7 +224,7 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
<FormItem required label={<Translation>Name</Translation>}> <FormItem required label={<Translation>Name</Translation>}>
<Input <Input
name="name" name="name"
disabled={pipeline != undefined} disabled={editMode}
{...init('name', { {...init('name', {
initValue: '', initValue: '',
rules: [ rules: [
@ -235,6 +266,7 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
dataSource={projectOptions} dataSource={projectOptions}
filterLocal={true} filterLocal={true}
hasClear={true} hasClear={true}
disabled={editMode}
style={{ width: '100%' }} style={{ width: '100%' }}
{...init('project', { {...init('project', {
initValue: defaultProject, initValue: defaultProject,
@ -287,20 +319,24 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
/> />
</FormItem> </FormItem>
</Col> </Col>
<Col span={24} style={{ padding: '0 8px' }}> <If condition={!editMode}>
<FormItem label={<Translation>Template</Translation>}> <Col span={24} style={{ padding: '0 8px' }}>
<Select <FormItem label={<Translation>Template</Translation>}>
locale={locale().Select} <Select
name="template" locale={locale().Select}
dataSource={templates} name="template"
hasClear dataSource={templates}
placeholder="Select a template" hasClear
onChange={(value) => { placeholder="Select a template"
this.field.setValue('steps', value); onChange={(value) => {
}} this.field.setValue('steps', value);
/> const template = templates.find((t) => t.value == value);
</FormItem> this.setState({ defaultContext: template?.defaultContext });
</Col> }}
/>
</FormItem>
</Col>
</If>
</Row> </Row>
</Form> </Form>
</DrawerWithFooter> </DrawerWithFooter>

View File

@ -1,6 +1,9 @@
export const templates = [ export const templates = [
{ {
label: 'Observability Template', label: 'Observability Template',
defaultContext: {
readConfig: 'false',
},
value: ` value: `
- name: Enable Prism - name: Enable Prism
type: addon-operation type: addon-operation

View File

@ -4,8 +4,13 @@ import i18n from '../../../../i18n';
import { connect } from 'dva'; import { connect } from 'dva';
import type { LoginUserInfo } from '../../../../interface/user'; import type { LoginUserInfo } from '../../../../interface/user';
import { checkPermission } from '../../../../utils/permission'; import { checkPermission } from '../../../../utils/permission';
import { createPipeline, loadPipeline } from '../../../../api/pipeline'; import {
import type { PipelineListItem, PipelineDetail } from '../../../../interface/pipeline'; createPipeline,
createPipelineContext,
listPipelineContexts,
loadPipeline,
} from '../../../../api/pipeline';
import type { PipelineListItem, PipelineDetail, KeyValue } from '../../../../interface/pipeline';
import Translation from '../../../../components/Translation'; import Translation from '../../../../components/Translation';
import { checkName } from '../../../../utils/common'; import { checkName } from '../../../../utils/common';
import { If } from 'tsx-control-statements/components'; import { If } from 'tsx-control-statements/components';
@ -25,7 +30,10 @@ export interface PipelineProps {
type State = { type State = {
loading: boolean; loading: boolean;
loadingContext: boolean;
pipelineDetail?: PipelineDetail; pipelineDetail?: PipelineDetail;
contexts?: Record<string, KeyValue[]>;
cloneLoading?: boolean;
}; };
@connect((store: any) => { @connect((store: any) => {
@ -37,6 +45,7 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
super(props); super(props);
this.state = { this.state = {
loading: true, loading: true,
loadingContext: true,
}; };
this.field = new Field(this); this.field = new Field(this);
} }
@ -44,19 +53,34 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
componentDidMount() { componentDidMount() {
const { pipeline } = this.props; const { pipeline } = this.props;
if (pipeline) { if (pipeline) {
loadPipeline({ projectName: pipeline.project.name, pipelineName: pipeline.name }) this.onLoadingPipeline(pipeline);
.then((res: PipelineDetail) => { this.onLoadingPipelineContexts(pipeline);
this.setState({ pipelineDetail: res });
})
.finally(() => {
this.setState({ loading: false });
});
} }
} }
onLoadingPipeline = async (pipeline: PipelineListItem) => {
loadPipeline({ projectName: pipeline.project.name, pipelineName: pipeline.name })
.then((res: PipelineDetail) => {
this.setState({ pipelineDetail: res });
})
.finally(() => {
this.setState({ loading: false });
});
};
onLoadingPipelineContexts = async (pipeline: PipelineListItem) => {
listPipelineContexts(pipeline.project.name, pipeline.name)
.then((res) => {
this.setState({ contexts: res && res.contexts ? res.contexts : {} });
})
.finally(() => {
this.setState({ loadingContext: false });
});
};
onSubmit = () => { onSubmit = () => {
const { pipelineDetail } = this.state; const { pipelineDetail, contexts } = this.state;
if (pipelineDetail) { if (pipelineDetail) {
this.setState({ cloneLoading: true });
this.field.validate((errs: any, values: any) => { this.field.validate((errs: any, values: any) => {
if (errs) { if (errs) {
return; return;
@ -69,20 +93,29 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
name: name, name: name,
spec: pipelineDetail?.spec, spec: pipelineDetail?.spec,
}; };
createPipeline(request).then((res) => { createPipeline(request)
if (res) { .then((res) => {
Message.success(i18n.t('Pipeline cloned successfully')); if (res) {
if (this.props.onSuccess) { if (contexts) {
this.props.onSuccess(); Object.keys(contexts).map((key) => {
createPipelineContext(project, name, { name: key, values: contexts[key] });
});
}
Message.success(i18n.t('Pipeline cloned successfully'));
if (this.props.onSuccess) {
this.props.onSuccess();
}
} }
} })
}); .catch(() => {
this.setState({ cloneLoading: false });
});
}); });
} }
}; };
render() { render() {
const { loading, pipelineDetail } = this.state; const { loading, pipelineDetail, loadingContext, contexts, cloneLoading } = this.state;
const { userInfo } = this.props; const { userInfo } = this.props;
const { init } = this.field; const { init } = this.field;
const projectOptions: { label: string; value: string }[] = []; const projectOptions: { label: string; value: string }[] = [];
@ -100,9 +133,15 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
}); });
} }
}); });
const message = contexts
? i18n.t('Includes') + ` ${Object.keys(contexts).length} ` + i18n.t('contexts')
: '';
return ( return (
<Dialog <Dialog
onOk={this.onSubmit} onOk={this.onSubmit}
okProps={{
loading: cloneLoading,
}}
onClose={this.props.onClose} onClose={this.props.onClose}
onCancel={this.props.onClose} onCancel={this.props.onClose}
locale={locale().Dialog} locale={locale().Dialog}
@ -110,11 +149,11 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
className="commonDialog" className="commonDialog"
title="Clone Pipeline" title="Clone Pipeline"
> >
<Loading visible={loading}> <Loading visible={loading || loadingContext}>
<If condition={pipelineDetail}> <If condition={pipelineDetail && contexts}>
<Message <Message
type="success" type="success"
title={i18n.t('Pipeline loaded successfully and is ready to clone.')} title={i18n.t('Pipeline loaded successfully and is ready to clone.') + message}
/> />
<Form field={this.field}> <Form field={this.field}>
<Row wrap> <Row wrap>

View File

@ -17,7 +17,7 @@ import locale from '../../utils/locale';
import { Link, routerRedux } from 'dva/router'; import { Link, routerRedux } from 'dva/router';
import type { NameAlias } from '../../interface/env'; import type { NameAlias } from '../../interface/env';
import { deletePipeline, listPipelines } from '../../api/pipeline'; import { deletePipeline, listPipelines } from '../../api/pipeline';
import { AiFillCaretRight, AiFillDelete } from 'react-icons/ai'; import { AiFillCaretRight, AiFillDelete, AiFillSetting } from 'react-icons/ai';
import { BiCopyAlt } from 'react-icons/bi'; import { BiCopyAlt } from 'react-icons/bi';
import { HiViewList } from 'react-icons/hi'; import { HiViewList } from 'react-icons/hi';
import './index.less'; import './index.less';
@ -111,6 +111,10 @@ class PipelineListPage extends Component<Props, State> {
}); });
}; };
onEditPipeline = (pipeline: PipelineListItem) => {
this.setState({ showNewPipeline: true, pipeline: pipeline });
};
renderPipelineTable = () => { renderPipelineTable = () => {
const { pipelines } = this.state; const { pipelines } = this.state;
return ( return (
@ -236,7 +240,7 @@ class PipelineListPage extends Component<Props, State> {
key={'actions'} key={'actions'}
title={i18n.t('Actions')} title={i18n.t('Actions')}
dataIndex="name" dataIndex="name"
width={'360px'} width={'420px'}
cell={(name: string, i: number, pipeline: PipelineListItem) => { cell={(name: string, i: number, pipeline: PipelineListItem) => {
return ( return (
<div> <div>
@ -301,6 +305,26 @@ class PipelineListPage extends Component<Props, State> {
</Button> </Button>
<span className="line" /> <span className="line" />
</Permission> </Permission>
<Permission
project={pipeline.project.name}
resource={{
resource: `project:${pipeline.project.name}/pipeline:${pipeline.name}`,
action: 'update',
}}
>
<Button
text
size={'medium'}
ghost={true}
component={'a'}
onClick={() => {
this.onEditPipeline(pipeline);
}}
>
<AiFillSetting /> <Translation>Edit</Translation>
</Button>
<span className="line" />
</Permission>
<Permission <Permission
project={pipeline.project.name} project={pipeline.project.name}
resource={{ resource={{

View File

@ -53,7 +53,7 @@ class Header extends Component<Props, State> {
onRerun = () => { onRerun = () => {
Dialog.confirm({ Dialog.confirm({
type: 'alert', type: 'alert',
content: 'Are you sure to rerun this Pipeline?', content: i18n.t('Are you sure to rerun this Pipeline?'),
locale: locale().Dialog, locale: locale().Dialog,
onOk: () => { onOk: () => {
const { pipeline, runBase } = this.props; const { pipeline, runBase } = this.props;

View File

@ -29,24 +29,34 @@
.detail-page { .detail-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 250px); justify-content: space-between;
height: calc(100vh - 300px);
.step-info { .step-info {
flex: auto; height: 300px;
overflow: auto; overflow: auto;
border-collapse: collapse; border-collapse: collapse;
} }
.step-log { .step-log {
height: 600px; flex: 1;
overflow: auto;
background: #000;
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%;
padding: 4px 16px; padding: 4px 16px;
color: var(--grey-300); color: var(--grey-300);
font-size: var(--font-size-small); font-size: var(--font-size-small);
background: #090909; background: #090909;
border-bottom: 1px solid var(--grey-800); border-bottom: 1px solid var(--grey-800);
} }
.log-content {
margin-bottom: 32px;
padding: 16px;
color: #fff;
}
} }
} }
.step-info { .step-info {

View File

@ -128,7 +128,8 @@ class PipelineRunPage extends Component<Props, State> {
stepName: stepStatus?.name, stepName: stepStatus?.name,
}) })
.then((res: { log: string }) => { .then((res: { log: string }) => {
this.setState({ logs: res && res.log ? res.log.split('\n') : [] }); const logLines = res && res.log ? res.log.split('\n') : [];
this.setState({ logs: logLines });
}) })
.finally(() => { .finally(() => {
this.setState({ logLoading: false }); this.setState({ logLoading: false });
@ -287,7 +288,7 @@ class PipelineRunPage extends Component<Props, State> {
<div className={classNames('studio')}> <div className={classNames('studio')}>
{runStatus && ( {runStatus && (
<PipelineGraph <PipelineGraph
name={`${runBase?.pipelineRunName}+${runStatus.status}`} name={`${runBase?.pipelineRunName}`}
steps={runStatus.steps} steps={runStatus.steps}
zoom={1} zoom={1}
onNodeClick={this.onStepClick} onNodeClick={this.onStepClick}
@ -380,7 +381,7 @@ class PipelineRunPage extends Component<Props, State> {
<Icon type="refresh" /> <Icon type="refresh" />
</Button> </Button>
</div> </div>
<div className="logBox"> <div className="log-content">
{logs?.map((line, i: number) => { {logs?.map((line, i: number) => {
return ( return (
<div key={`log-${i}`} className="logLine"> <div key={`log-${i}`} className="logLine">

View File

@ -21,6 +21,7 @@ import i18n from '../../i18n';
import { If } from 'tsx-control-statements/components'; import { If } from 'tsx-control-statements/components';
import RunPipeline from '../../components/RunPipeline'; import RunPipeline from '../../components/RunPipeline';
import { routerRedux } from 'dva/router'; import { routerRedux } from 'dva/router';
import { WorkflowModeOptions } from '../ApplicationWorkflowStudio';
const { Row, Col } = Grid; const { Row, Col } = Grid;
@ -234,18 +235,22 @@ class PipelineStudio extends React.Component<Props, State> {
<Translation>Unsaved changes</Translation> <Translation>Unsaved changes</Translation>
</div> </div>
)} )}
<Form.Item label="Mode" labelAlign="inset" style={{ marginRight: '8px' }}> <Form.Item label={i18n.t('Mode')} labelAlign="inset" style={{ marginRight: '8px' }}>
<Select <Select
locale={locale().Select} locale={locale().Select}
defaultValue="StepByStep" defaultValue="StepByStep"
value={mode} value={mode}
dataSource={[{ value: 'StepByStep' }, { value: 'DAG' }]} dataSource={WorkflowModeOptions}
onChange={(value) => { onChange={(value) => {
this.setState({ mode: value, changed: this.state.mode !== value }); this.setState({ mode: value, changed: this.state.mode !== value });
}} }}
/> />
</Form.Item> </Form.Item>
<Form.Item label="Sub Mode" labelAlign="inset" style={{ marginRight: '8px' }}> <Form.Item
label={i18n.t('Sub Mode')}
labelAlign="inset"
style={{ marginRight: '8px' }}
>
<Select <Select
locale={locale().Select} locale={locale().Select}
defaultValue="DAG" defaultValue="DAG"
@ -253,7 +258,7 @@ class PipelineStudio extends React.Component<Props, State> {
onChange={(value) => { onChange={(value) => {
this.setState({ subMode: value, changed: this.state.subMode !== value }); this.setState({ subMode: value, changed: this.state.subMode !== value });
}} }}
dataSource={[{ value: 'DAG' }, { value: 'StepByStep' }]} dataSource={WorkflowModeOptions}
/> />
</Form.Item> </Form.Item>
<Button <Button

View File

@ -295,9 +295,20 @@ export function convertAny(data?: any): string {
} }
} }
export function showAlias(name: string, alias?: string) { export function showAlias(name: string, alias?: string): string;
if (alias) { export function showAlias(item: { name: string; alias?: string }): string;
return `${name}(${alias})`; export function showAlias(item: { name: string; alias?: string } | string, alias?: string) {
if (typeof item == 'string') {
if (alias) {
return `${item}(${alias})`;
}
return item;
} }
return name; if (!item) {
return '';
}
if (item.alias) {
return `${item.name}(${item.alias})`;
}
return item.name;
} }