mirror of https://github.com/kubevela/velaux.git
Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
|
c894356e50 | |
|
df900036d4 | |
|
d631fcee6f | |
|
65af805131 | |
|
02e51bb37e | |
|
d18b2013c5 | |
|
bbe7b2165d | |
|
5ef1a137c4 |
|
@ -5,13 +5,14 @@ import { getDomain } from '../utils/common';
|
|||
import type {
|
||||
ApplicationDeployRequest,
|
||||
Trait,
|
||||
Trigger,
|
||||
ApplicationComponentConfig,
|
||||
ApplicationQuery,
|
||||
ApplicationCompareRequest,
|
||||
ApplicationDryRunRequest,
|
||||
CreatePolicyRequest,
|
||||
UpdatePolicyRequest,
|
||||
UpdateTriggerRequest,
|
||||
CreateTriggerRequest,
|
||||
} from '../interface/application';
|
||||
|
||||
interface TraitQuery {
|
||||
|
@ -102,12 +103,11 @@ export function createApplicationTemplate(params: any) {
|
|||
return post(`${url}/${params.name}/template`, params).then((res) => res);
|
||||
}
|
||||
|
||||
export function createApplicationEnv(params: { appName?: string }) {
|
||||
delete params.appName;
|
||||
export function createApplicationEnvbinding(params: { appName?: string }) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -221,12 +221,20 @@ export function updateApplication(params: any) {
|
|||
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;
|
||||
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;
|
||||
return rdelete(`${url}/${appName}/triggers/${token}`, {}).then((res) => res);
|
||||
}
|
||||
|
|
|
@ -100,6 +100,29 @@ export function listPipelineContexts(projectName: string, pipelineName: string)
|
|||
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: {
|
||||
projectName: string;
|
||||
pipelineName: string;
|
||||
|
|
|
@ -391,7 +391,8 @@ a {
|
|||
.next-btn {
|
||||
display: block;
|
||||
flex: none !important;
|
||||
width: 100px !important;
|
||||
width: auto !important;
|
||||
min-width: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -502,10 +503,6 @@ a {
|
|||
color: @dangerColor !important;
|
||||
}
|
||||
|
||||
.line {
|
||||
border: solid 0.2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
@ -516,6 +513,7 @@ a {
|
|||
.next-breadcrumb-separator {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 16px;
|
||||
a {
|
||||
color: @primarycolor;
|
||||
}
|
||||
|
@ -524,7 +522,6 @@ a {
|
|||
|
||||
.next-btn,
|
||||
.inline-center {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 './index.less';
|
||||
import { DiffEditor } from '../DiffEditor';
|
||||
|
@ -11,17 +11,26 @@ type ApplicationDiffProps = {
|
|||
targetName: string;
|
||||
compare: ApplicationCompareResponse;
|
||||
onClose: () => void;
|
||||
rollback2Revision?: () => void;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const ApplicationDiff = (props: ApplicationDiffProps) => {
|
||||
const { baseName, targetName, compare } = props;
|
||||
const { baseName, targetName, compare, rollback2Revision } = props;
|
||||
const container = 'revision' + baseName + targetName;
|
||||
return (
|
||||
<Dialog
|
||||
className={'commonDialog application-diff'}
|
||||
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}
|
||||
visible={true}
|
||||
onClose={props.onClose}
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './index.less';
|
||||
|
||||
export type Props = {
|
||||
title: string | React.ReactNode;
|
||||
number?: number;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
const NumItem: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -50,7 +50,6 @@ class PipelineGraph extends React.Component<PipelineGraphProps, State> {
|
|||
<Draggable>
|
||||
<div
|
||||
className="workflow-graph"
|
||||
key={name}
|
||||
style={{
|
||||
transform: `scale(${zoom})`,
|
||||
}}
|
||||
|
@ -66,7 +65,7 @@ class PipelineGraph extends React.Component<PipelineGraphProps, State> {
|
|||
steps.map((step, i: number) => {
|
||||
return (
|
||||
<Step
|
||||
key={name + step.id}
|
||||
key={name + step.name}
|
||||
probeState={this.state}
|
||||
step={step}
|
||||
group={step.type == 'step-group'}
|
||||
|
|
|
@ -14,17 +14,28 @@
|
|||
width: 100%;
|
||||
margin-top: var(--spacing-4);
|
||||
padding: 16px;
|
||||
border: solid 1px var(--grey-200);
|
||||
border: dashed 1px var(--grey-200);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
.context-name {
|
||||
flex-grow: 0;
|
||||
width: 100px;
|
||||
margin-right: var(--spacing-4);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-normal);
|
||||
line-height: 24px;
|
||||
}
|
||||
.context-values {
|
||||
flex: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
&.active {
|
||||
border: solid 2px var(--primary-color);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Checkbox, Dialog, Loading, Message, Tag } from '@b-design/ui';
|
||||
import { listPipelineContexts, runPipeline } from '../../api/pipeline';
|
||||
import { Button, Checkbox, Dialog, Loading, Message, Tag } from '@b-design/ui';
|
||||
import { deletePipelineContext, listPipelineContexts, runPipeline } from '../../api/pipeline';
|
||||
import i18n from '../../i18n';
|
||||
import type { KeyValue, PipelineListItem } from '../../interface/pipeline';
|
||||
import './index.less';
|
||||
|
@ -10,6 +10,9 @@ import classNames from 'classnames';
|
|||
import { If } from 'tsx-control-statements/components';
|
||||
import NewContext from './new-context';
|
||||
import Translation from '../Translation';
|
||||
import Permission from '../Permission';
|
||||
import { BiCopyAlt } from 'react-icons/bi';
|
||||
import { AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||
|
||||
export interface PipelineProps {
|
||||
pipeline: PipelineListItem;
|
||||
|
@ -20,11 +23,38 @@ export interface PipelineProps {
|
|||
const RunPipeline = (props: PipelineProps) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [contexts, setContexts] = useState<Record<string, KeyValue[]>>({});
|
||||
const [context, setContext] = useState<{ name: string; values: KeyValue[]; clone: boolean }>();
|
||||
const [noContext, setNoContext] = useState<boolean>();
|
||||
const [contextName, setSelectContextName] = useState('');
|
||||
const [addContext, showAddContext] = useState(false);
|
||||
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(() => {
|
||||
if (loading) {
|
||||
listPipelineContexts(pipeline.project.name, pipeline.name)
|
||||
|
@ -86,20 +116,98 @@ const RunPipeline = (props: PipelineProps) => {
|
|||
setSelectContextName('');
|
||||
}
|
||||
}}
|
||||
title={
|
||||
contextName === key ? i18n.t('Click and deselect') : i18n.t('Click and select')
|
||||
}
|
||||
>
|
||||
<div className="context-name">{key}</div>
|
||||
<div className="context-values">
|
||||
{Array.isArray(contexts[key]) &&
|
||||
contexts[key].map((item) => {
|
||||
return <Tag>{`${item.key}=${item.value}`}</Tag>;
|
||||
return (
|
||||
<Tag
|
||||
style={{ marginBottom: '8px' }}
|
||||
key={item.key}
|
||||
>{`${item.key}=${item.value}`}</Tag>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
<If condition={addContext}>
|
||||
<div className="context-item">
|
||||
<NewContext
|
||||
clone={context?.clone}
|
||||
context={context}
|
||||
pipeline={props.pipeline}
|
||||
onCancel={() => showAddContext(false)}
|
||||
onSuccess={() => {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
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 KV from '../../extends/KV';
|
||||
import i18n from '../../i18n';
|
||||
import Translation from '../Translation';
|
||||
import { createPipelineContext } from '../../api/pipeline';
|
||||
import { createPipelineContext, updatePipelineContext } from '../../api/pipeline';
|
||||
import type { KeyValue, PipelineListItem } from '../../interface/pipeline';
|
||||
import { checkName } from '../../utils/common';
|
||||
|
||||
|
@ -14,6 +14,8 @@ export interface NewContextProps {
|
|||
pipeline: PipelineListItem;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
context?: { name: string; values: KeyValue[] };
|
||||
clone?: boolean;
|
||||
}
|
||||
|
||||
class NewContext extends React.Component<NewContextProps> {
|
||||
|
@ -23,7 +25,29 @@ class NewContext extends React.Component<NewContextProps> {
|
|||
super(props);
|
||||
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 = () => {
|
||||
const { context, clone } = this.props;
|
||||
const editMode = context && !clone;
|
||||
this.field.validate((errs, values: any) => {
|
||||
if (errs) {
|
||||
return;
|
||||
|
@ -33,17 +57,33 @@ class NewContext extends React.Component<NewContextProps> {
|
|||
Object.keys(values.values).map((key) => {
|
||||
keyValues.push({ key: key, value: values.values[key] });
|
||||
});
|
||||
createPipelineContext(project.name, name, { name: values.name, values: keyValues }).then(
|
||||
(res) => {
|
||||
if (res) {
|
||||
this.props.onSuccess();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (editMode) {
|
||||
updatePipelineContext(project.name, name, { name: values.name, values: keyValues }).then(
|
||||
(res) => {
|
||||
if (res) {
|
||||
this.props.onSuccess();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
createPipelineContext(project.name, name, { name: values.name, values: keyValues }).then(
|
||||
(res) => {
|
||||
if (res) {
|
||||
this.props.onSuccess();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
render() {
|
||||
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 (
|
||||
<Form field={this.field} style={{ width: '100%' }}>
|
||||
<Row style={{ width: '100%' }} wrap>
|
||||
|
@ -63,6 +103,7 @@ class NewContext extends React.Component<NewContextProps> {
|
|||
],
|
||||
})}
|
||||
placeholder={i18n.t('Please input the context name')}
|
||||
disabled={editMode}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
|
@ -47,8 +47,19 @@ export const Step = (props: StepProps) => {
|
|||
}}
|
||||
>
|
||||
{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 className="groups asd" style={{ width: stepWidth + 'px' }}>
|
||||
<div className="groups" style={{ width: stepWidth + 'px' }}>
|
||||
{step.subSteps?.map((subStep) => {
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -10,19 +10,23 @@ import Translation from '../Translation';
|
|||
import type { Rule } from '@alifd/meet-react/lib/field';
|
||||
import type { WorkflowStepBase } from '../../interface/pipeline';
|
||||
import i18n from '../../i18n';
|
||||
import Item from '../Item';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
interface DefinitionCatalog {
|
||||
title: string;
|
||||
sort: number;
|
||||
description?: string;
|
||||
definitions: DefinitionBase[];
|
||||
}
|
||||
|
||||
const defaultDefinitionCatalog: Record<string, string> = {
|
||||
'step-group': 'Group',
|
||||
deploy: 'Delivery',
|
||||
'apply-app': 'Delivery',
|
||||
'apply-deployment': 'Delivery',
|
||||
'apply-component': 'Delivery',
|
||||
'addon-operation': 'Delivery',
|
||||
'deploy-cloud-resource': 'Delivery',
|
||||
'share-cloud-resource': 'Delivery',
|
||||
|
@ -34,34 +38,47 @@ const defaultDefinitionCatalog: Record<string, string> = {
|
|||
'list-config': 'Config Management',
|
||||
'read-config': 'Config Management',
|
||||
'export-data': 'Config Management',
|
||||
export2config: 'Config Management',
|
||||
export2secret: 'Config Management',
|
||||
suspend: 'Notification',
|
||||
};
|
||||
|
||||
const defaultCatalog: Record<string, DefinitionCatalog> = {
|
||||
Group: {
|
||||
title: 'Group',
|
||||
description: 'Merge a set of steps.',
|
||||
definitions: [],
|
||||
sort: 1,
|
||||
},
|
||||
Delivery: {
|
||||
title: 'Delivery',
|
||||
description: 'Delivery the Application or workloads to the Targets',
|
||||
definitions: [],
|
||||
sort: 2,
|
||||
},
|
||||
Approval: {
|
||||
title: 'Approval',
|
||||
description: 'Approval or reject the changes during Workflow or Pipeline progress',
|
||||
definitions: [],
|
||||
sort: 3,
|
||||
},
|
||||
'Config Management': {
|
||||
title: 'Config Management',
|
||||
description: 'Create or read the config.',
|
||||
definitions: [],
|
||||
sort: 4,
|
||||
},
|
||||
Notification: {
|
||||
title: 'Approval',
|
||||
title: 'Notification',
|
||||
description: 'Send messages to users or other applications.',
|
||||
definitions: [],
|
||||
sort: 5,
|
||||
},
|
||||
Custom: {
|
||||
title: 'Custom',
|
||||
description: 'Custom Workflow or Pipeline steps',
|
||||
definitions: [],
|
||||
sort: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -93,10 +110,12 @@ const buildDefinitionCatalog = (defs: DefinitionBase[]) => {
|
|||
if (catalogMap[catalog]) {
|
||||
catalogMap[catalog].definitions.push(def);
|
||||
} 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 = {
|
||||
|
@ -107,7 +126,7 @@ type Props = {
|
|||
addSub?: boolean;
|
||||
};
|
||||
type State = {
|
||||
selectType?: string;
|
||||
selectType?: DefinitionBase;
|
||||
};
|
||||
|
||||
class TypeSelect extends React.Component<Props, State> {
|
||||
|
@ -126,7 +145,12 @@ class TypeSelect extends React.Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
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(
|
||||
definitions?.filter((def) => !addSub || def.name != 'step-group') || [],
|
||||
);
|
||||
console.log(catalogs);
|
||||
const { init } = this.field;
|
||||
const checkStepNameRule = (rule: Rule, value: any, callback: (error?: string) => void) => {
|
||||
if (checkStepName(value)) {
|
||||
|
@ -153,7 +178,7 @@ class TypeSelect extends React.Component<Props, State> {
|
|||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onOk={this.onSubmit}
|
||||
title={i18n.t('Select Step Type')}
|
||||
title={selectType ? i18n.t('Set Step Basic Info') : i18n.t('Select Step Type')}
|
||||
visible
|
||||
>
|
||||
<div>
|
||||
|
@ -175,7 +200,7 @@ class TypeSelect extends React.Component<Props, State> {
|
|||
<div
|
||||
className="icon"
|
||||
onClick={() => {
|
||||
this.setState({ selectType: def.name });
|
||||
this.setState({ selectType: def });
|
||||
}}
|
||||
>
|
||||
<StepTypeIcon type={def.name} />
|
||||
|
@ -198,6 +223,17 @@ class TypeSelect extends React.Component<Props, State> {
|
|||
})}
|
||||
{selectType && (
|
||||
<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>
|
||||
<Col span={12} style={{ padding: '0 8px' }}>
|
||||
<Form.Item
|
||||
|
|
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||
import type { WorkflowStep } from '../../interface/pipeline';
|
||||
import DefinitionCode from '../DefinitionCode';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { Message } from '@b-design/ui';
|
||||
import Translation from '../Translation';
|
||||
|
||||
type Props = {
|
||||
steps?: WorkflowStep[];
|
||||
|
@ -12,21 +14,36 @@ export const WorkflowYAML = (props: Props) => {
|
|||
const id = 'workflow:' + props.name;
|
||||
const content = yaml.dump(props.steps);
|
||||
return (
|
||||
<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>
|
||||
<Message type="help">
|
||||
<div>
|
||||
<Translation>The workflow step spec reference document</Translation>:
|
||||
<a
|
||||
href="http://kubevela.net/docs/end-user/workflow/built-in-workflow-defs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ marginLeft: '16px' }}
|
||||
>
|
||||
<Translation>Click here</Translation>
|
||||
</a>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -82,7 +82,6 @@ class KV extends Component<Props, State> {
|
|||
this.form.setValue('envValue-' + key, value[label]);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ items: newItems });
|
||||
};
|
||||
|
||||
|
|
|
@ -353,6 +353,26 @@ export interface Trigger {
|
|||
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 {
|
||||
name: string;
|
||||
alias: string;
|
||||
|
|
|
@ -3,7 +3,10 @@ import { withTranslation } from 'react-i18next';
|
|||
import { connect } from 'dva';
|
||||
import { Dialog, Field, Form, Grid, Message, Select, Button } from '@b-design/ui';
|
||||
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 locale from '../../../../utils/locale';
|
||||
import { getEnvs } from '../../../../api/env';
|
||||
|
@ -11,6 +14,8 @@ import type { Env } from '../../../../interface/env';
|
|||
import { If } from 'tsx-control-statements/components';
|
||||
import EnvDialog from '../../../../pages/EnvPage/components/EnvDialog';
|
||||
import type { LoginUserInfo } from '../../../../interface/user';
|
||||
import { showAlias } from '../../../../utils/common';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
@ -19,7 +24,7 @@ interface Props {
|
|||
envbinding?: EnvBinding[];
|
||||
targets?: [];
|
||||
userInfo?: LoginUserInfo;
|
||||
dispatch?: ({}) => {};
|
||||
dispatch?: Dispatch<any>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -53,9 +58,9 @@ class EnvBindPlanDialog extends Component<Props, State> {
|
|||
|
||||
loadEnvs = async (callback?: Callback) => {
|
||||
const { applicationDetail, envbinding } = this.props;
|
||||
if (applicationDetail) {
|
||||
if (applicationDetail && applicationDetail.project?.name) {
|
||||
//Temporary logic
|
||||
getEnvs({ project: applicationDetail.project?.name || 'default', page: 0 }).then((re) => {
|
||||
getEnvs({ project: applicationDetail.project?.name, page: 0 }).then((re) => {
|
||||
const existEnvs =
|
||||
envbinding?.map((eb) => {
|
||||
return eb.name;
|
||||
|
@ -65,7 +70,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
|
|||
this.setState({ envs: canAdd });
|
||||
const envOption = canAdd?.map((env: { name: string; alias: string }) => {
|
||||
return {
|
||||
label: env.alias ? `${env.alias}(${env.name})` : env.name,
|
||||
label: showAlias(env.name, env.alias),
|
||||
value: env.name,
|
||||
};
|
||||
});
|
||||
|
@ -78,19 +83,19 @@ class EnvBindPlanDialog extends Component<Props, State> {
|
|||
|
||||
onSubmit = () => {
|
||||
const { applicationDetail } = this.props;
|
||||
if (!applicationDetail) {
|
||||
return;
|
||||
}
|
||||
const { isEdit } = this.state;
|
||||
this.field.validate((error: any, values: any) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
this.setState({ loading: true });
|
||||
const { name, alias, description, targetNames } = values;
|
||||
const { name } = values;
|
||||
const params = {
|
||||
appName: applicationDetail && applicationDetail.name,
|
||||
name,
|
||||
alias,
|
||||
targetNames,
|
||||
description,
|
||||
};
|
||||
if (isEdit) {
|
||||
this.onUpdateApplicationEnv(params);
|
||||
|
@ -101,7 +106,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
|
|||
};
|
||||
|
||||
onCreateApplicationEnv(params: any) {
|
||||
createApplicationEnv(params)
|
||||
createApplicationEnvbinding(params)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
Message.success(<Translation>Environment bound successfully</Translation>);
|
||||
|
@ -114,7 +119,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
|
|||
}
|
||||
|
||||
onUpdateApplicationEnv(params: any) {
|
||||
updateApplicationEnv(params)
|
||||
updateApplicationEnvbinding(params)
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
Message.success(<Translation>Environment bound successfully</Translation>);
|
||||
|
@ -207,7 +212,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
|
|||
this.changeEnvDialog(true);
|
||||
}}
|
||||
>
|
||||
<Translation>New environment</Translation>
|
||||
<Translation>New Environment</Translation>
|
||||
</a>
|
||||
}
|
||||
required={true}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
font-size: var(--font-size-normal);
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: var(--grey-200) dashed 1px;
|
||||
cursor: pointer;
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
|
@ -33,6 +34,7 @@
|
|||
&.active {
|
||||
color: #fff;
|
||||
background: var(--primary-color);
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,9 @@ class TabsContent extends Component<Props, State> {
|
|||
trigger={
|
||||
<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`}
|
||||
>
|
||||
<span title={item.description}>{item.alias ? item.alias : item.name}</span>
|
||||
|
|
|
@ -155,15 +155,13 @@ class ApplicationHeader extends Component<Props, State> {
|
|||
<If condition={applicationDetail?.readOnly}>
|
||||
<Message
|
||||
type="notice"
|
||||
title={i18n
|
||||
.t('This application is managed by the addon, and it is readonly')
|
||||
.toString()}
|
||||
title={i18n.t('This application is managed by the addon, and it is readonly')}
|
||||
/>
|
||||
</If>
|
||||
<If condition={sourceOfTrust === 'from-k8s-resource'}>
|
||||
<Message
|
||||
type="warning"
|
||||
title={i18n.t('The application is synchronizing from the cluster.').toString()}
|
||||
title={i18n.t('The application is synchronizing from the cluster.')}
|
||||
/>
|
||||
</If>
|
||||
<Permission
|
||||
|
|
|
@ -36,6 +36,11 @@ class Menu extends Component<Props, any> {
|
|||
label: <Translation>Revisions</Translation>,
|
||||
to: `/applications/${appName}/revisions`,
|
||||
},
|
||||
{
|
||||
key: 'workflows',
|
||||
label: <Translation>Workflows</Translation>,
|
||||
to: `/applications/${appName}/workflows`,
|
||||
},
|
||||
],
|
||||
envPage: [
|
||||
{
|
||||
|
|
|
@ -32,6 +32,8 @@ import PipelineListPage from '../../pages/PipelineListPage';
|
|||
import ApplicationWorkflowStudio from '../../pages/ApplicationWorkflowStudio';
|
||||
import PipelineStudio from '../../pages/PipelineStudio';
|
||||
import ProjectPipelines from '../../pages/ProjectPipelines';
|
||||
import ApplicationEnvRoute from '../../pages/ApplicationEnvRoute';
|
||||
import ApplicationWorkflowList from '../../pages/ApplicationWorkflowList';
|
||||
|
||||
export default function Content() {
|
||||
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
|
||||
exact
|
||||
path="/applications/:appName/envbinding/:envName"
|
||||
|
|
|
@ -195,6 +195,7 @@
|
|||
"New Environment": "新增环境",
|
||||
"New Trait": "新增运维特征",
|
||||
"New Trigger": "新增触发器",
|
||||
"Edit Trigger": "编辑触发器",
|
||||
"New Workflow": "新增工作流",
|
||||
"Next Step": "下一步",
|
||||
"No version record": "暂无版本记录",
|
||||
|
@ -596,5 +597,24 @@
|
|||
"Launch Workflow Studio": "工作流画板",
|
||||
"Available Targets": "可用的交付目标",
|
||||
"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?": "确定要重新运行流水线吗?"
|
||||
}
|
|
@ -58,6 +58,7 @@ type State = {
|
|||
loading: boolean;
|
||||
status: 'disabled' | 'enabled' | 'enabling' | 'suspend' | 'disabling' | '';
|
||||
statusLoading: boolean;
|
||||
enableLoading?: boolean;
|
||||
upgradeLoading: boolean;
|
||||
args?: any;
|
||||
addonsStatus?: ApplicationStatus;
|
||||
|
@ -251,7 +252,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
|||
Message.warning(i18n.t('Please firstly select at least one cluster.'));
|
||||
return;
|
||||
}
|
||||
this.setState({ statusLoading: true }, () => {
|
||||
this.setState({ statusLoading: true, enableLoading: true }, () => {
|
||||
if (this.state.version) {
|
||||
const params: EnableAddonRequest = {
|
||||
name: this.props.addonName,
|
||||
|
@ -262,9 +263,13 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
|||
if (this.state.addonDetailInfo?.deployTo?.runtimeCluster) {
|
||||
params.clusters = this.state.clusters;
|
||||
}
|
||||
enableAddon(params).then(() => {
|
||||
this.loadAddonStatus();
|
||||
});
|
||||
enableAddon(params)
|
||||
.then(() => {
|
||||
this.loadAddonStatus();
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ enableLoading: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -311,6 +316,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
|||
status,
|
||||
statusLoading,
|
||||
upgradeLoading,
|
||||
enableLoading,
|
||||
addonsStatus,
|
||||
showStatusVisible,
|
||||
version,
|
||||
|
@ -390,7 +396,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
|||
project={''}
|
||||
>
|
||||
<Button
|
||||
loading={status === 'enabling'}
|
||||
loading={status === 'enabling' || enableLoading}
|
||||
type="primary"
|
||||
onClick={this.onEnable}
|
||||
style={{ marginLeft: '16px' }}
|
||||
|
@ -462,7 +468,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
|
|||
{`${i18n.t('Addon status is ')}${addonsStatus?.status || 'Init'}`}
|
||||
<Link
|
||||
style={{ marginLeft: '16px' }}
|
||||
to={`/applications/addon-${addonDetailInfo?.name}/envbinding/system/workflow`}
|
||||
to={`/applications/addon-${addonDetailInfo?.name}/envbinding`}
|
||||
>
|
||||
<Translation>Check the details</Translation>
|
||||
</Link>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
.components-list-title {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
color: #1b58f4;
|
||||
color: var(--primary-color);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import Permission from '../../../../components/Permission';
|
|||
import terraform from '../../../../assets/terraform.svg';
|
||||
import kubernetes from '../../../../assets/kubernetes.svg';
|
||||
import helm from '../../../../assets/helm.svg';
|
||||
import { showAlias } from '../../../../utils/common';
|
||||
|
||||
type Props = {
|
||||
application?: ApplicationBase;
|
||||
|
@ -57,8 +58,8 @@ class ComponentsList extends Component<Props> {
|
|||
<div className="list-warper">
|
||||
<div className="box">
|
||||
{(components || []).map((item: ApplicationComponentBase) => (
|
||||
<Row wrap={true} className="box-item">
|
||||
<Col span={24} key={item.name}>
|
||||
<Row key={item.name} wrap={true} className="box-item">
|
||||
<Col span={24}>
|
||||
<Card locale={locale().Card} contentHeight="auto">
|
||||
<div className="components-list-nav">
|
||||
<div
|
||||
|
@ -68,7 +69,7 @@ class ComponentsList extends Component<Props> {
|
|||
}}
|
||||
>
|
||||
{this.getComponentTypeIcon(item)}
|
||||
{item.alias ? `${item.alias}(${item.name})` : item.name}
|
||||
{showAlias(item)}
|
||||
</div>
|
||||
<If condition={item.main != true}>
|
||||
<div className="components-list-operation">
|
||||
|
@ -98,7 +99,7 @@ class ComponentsList extends Component<Props> {
|
|||
</If>
|
||||
<Row wrap={true}>
|
||||
{item.traits?.map((trait) => {
|
||||
const label = trait.alias ? trait.alias + '(' + trait.type + ')' : trait.type;
|
||||
const label = showAlias(trait.type, trait.alias);
|
||||
return (
|
||||
<div
|
||||
key={trait.type}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Grid, Field, Form, Select, Message, Button, Input } from '@b-design/ui';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { createTriggers } from '../../../../api/application';
|
||||
import { createTrigger, updateTrigger } from '../../../../api/application';
|
||||
import { getPayloadType } from '../../../../api/payload';
|
||||
import { detailComponentDefinition } from '../../../../api/definitions';
|
||||
import type {
|
||||
|
@ -9,6 +9,8 @@ import type {
|
|||
Trigger,
|
||||
UIParam,
|
||||
ApplicationComponentBase,
|
||||
CreateTriggerRequest,
|
||||
UpdateTriggerRequest,
|
||||
} from '../../../../interface/application';
|
||||
import DrawerWithFooter from '../../../../components/Drawer';
|
||||
import Translation from '../../../../components/Translation';
|
||||
|
@ -16,6 +18,7 @@ import { checkName } from '../../../../utils/common';
|
|||
import locale from '../../../../utils/locale';
|
||||
import { If } from 'tsx-control-statements/components';
|
||||
import i18n from '../../../../i18n';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
|
@ -23,8 +26,9 @@ type Props = {
|
|||
workflows?: Workflow[];
|
||||
onOK: (params: Trigger) => void;
|
||||
onClose: () => void;
|
||||
dispatch?: ({}) => {};
|
||||
dispatch?: Dispatch<any>;
|
||||
components: ApplicationComponentBase[];
|
||||
trigger?: Trigger;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -32,6 +36,7 @@ type State = {
|
|||
payloadTypes: string[];
|
||||
hasImage: boolean;
|
||||
component?: ApplicationComponentBase;
|
||||
submitLoading?: boolean;
|
||||
};
|
||||
|
||||
class TriggerDialog extends React.Component<Props, State> {
|
||||
|
@ -47,6 +52,10 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { trigger } = this.props;
|
||||
if (trigger) {
|
||||
this.field.setValues(trigger);
|
||||
}
|
||||
const type = this.field.getValue('type');
|
||||
if (type === 'webhook') {
|
||||
this.onGetPayloadType();
|
||||
|
@ -90,6 +99,8 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { trigger } = this.props;
|
||||
const editMode = trigger != undefined;
|
||||
this.field.validate((error: any, values: any) => {
|
||||
if (error) {
|
||||
return;
|
||||
|
@ -106,38 +117,68 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
registry = '',
|
||||
} = values;
|
||||
const query = { appName };
|
||||
const params: Trigger = {
|
||||
name,
|
||||
alias,
|
||||
description,
|
||||
type,
|
||||
payloadType,
|
||||
workflowName,
|
||||
token: '',
|
||||
componentName,
|
||||
registry,
|
||||
};
|
||||
createTriggers(params, query).then((res: any) => {
|
||||
if (res) {
|
||||
Message.success({
|
||||
duration: 4000,
|
||||
content: 'Trigger created successfully.',
|
||||
this.setState({ submitLoading: true });
|
||||
if (editMode) {
|
||||
const params: UpdateTriggerRequest = {
|
||||
alias,
|
||||
description,
|
||||
payloadType,
|
||||
workflowName,
|
||||
componentName,
|
||||
registry,
|
||||
};
|
||||
updateTrigger(params, { appName, token: trigger.token })
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
Message.success({
|
||||
duration: 4000,
|
||||
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 = () => {
|
||||
const { onClose } = this.props;
|
||||
const { onClose, trigger } = this.props;
|
||||
const { submitLoading } = this.state;
|
||||
const editMode = trigger != undefined;
|
||||
return (
|
||||
<div>
|
||||
<Button type="secondary" onClick={onClose} className="margin-right-10">
|
||||
<Translation>Cancel</Translation>
|
||||
</Button>
|
||||
<Button type="primary" onClick={this.onSubmit}>
|
||||
<Translation>Create</Translation>
|
||||
<Button loading={submitLoading} type="primary" onClick={this.onSubmit}>
|
||||
<Translation>{editMode ? 'Update' : 'Create'}</Translation>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
@ -180,7 +221,8 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
const init = this.field.init;
|
||||
const FormItem = Form.Item;
|
||||
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 workflowOption = workflows?.map((workflow) => {
|
||||
return {
|
||||
|
@ -205,7 +247,7 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<DrawerWithFooter
|
||||
title={i18n.t('Add Trigger')}
|
||||
title={editMode ? i18n.t('Edit Trigger') : i18n.t('Add Trigger')}
|
||||
placement="right"
|
||||
width={800}
|
||||
onClose={onClose}
|
||||
|
@ -217,6 +259,7 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
<FormItem label={<Translation>Name</Translation>} required>
|
||||
<Input
|
||||
name="name"
|
||||
disabled={editMode}
|
||||
placeholder={i18n.t('Please enter the name').toString()}
|
||||
{...init('name', {
|
||||
rules: [
|
||||
|
@ -274,6 +317,7 @@ class TriggerDialog extends React.Component<Props, State> {
|
|||
<Select
|
||||
name="type"
|
||||
locale={locale().Select}
|
||||
disabled={editMode}
|
||||
dataSource={[{ label: 'On Webhook Event', value: 'webhook' }]}
|
||||
{...init('type', {
|
||||
initValue: 'webhook',
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
.trigger-list-title {
|
||||
color: var(--primary-color);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
|||
ApplicationDetail,
|
||||
} from '../../../../interface/application';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { momentDate } from '../../../../utils/common';
|
||||
import { momentDate, showAlias } from '../../../../utils/common';
|
||||
import './index.less';
|
||||
import { If } from 'tsx-control-statements/components';
|
||||
import Empty from '../../../../components/Empty';
|
||||
|
@ -24,6 +24,7 @@ type Props = {
|
|||
components: ApplicationComponentBase[];
|
||||
applicationDetail?: ApplicationDetail;
|
||||
onDeleteTrigger: (token: string) => void;
|
||||
onEditTrigger: (t: Trigger) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -135,12 +136,17 @@ class TriggerList extends Component<Props, State> {
|
|||
<div className="list-warper">
|
||||
<div className="box">
|
||||
{(triggers || []).map((item: Trigger) => (
|
||||
<Row wrap={true} className="box-item">
|
||||
<Col span={24} key={item.type}>
|
||||
<Row wrap={true} key={item.type} className="box-item">
|
||||
<Col span={24}>
|
||||
<Card free={true} style={{ padding: '16px' }} locale={locale().Card}>
|
||||
<div className="trigger-list-nav">
|
||||
<div className="trigger-list-title">
|
||||
{item.alias ? `${item.alias}(${item.name})` : item.name}
|
||||
<div
|
||||
onClick={() => {
|
||||
this.props.onEditTrigger(item);
|
||||
}}
|
||||
className="trigger-list-title"
|
||||
>
|
||||
{showAlias(item)}
|
||||
</div>
|
||||
<div className="trigger-list-operation">
|
||||
<Permission
|
||||
|
|
|
@ -6,7 +6,7 @@ import { If } from 'tsx-control-statements/components';
|
|||
import {
|
||||
deleteTrait,
|
||||
getApplicationTriggers,
|
||||
deleteTriggers,
|
||||
deleteTrigger,
|
||||
deleteComponent,
|
||||
deleteApplication,
|
||||
deletePolicy,
|
||||
|
@ -76,6 +76,7 @@ type State = {
|
|||
mainComponent?: ApplicationComponent;
|
||||
traitItem: Trait;
|
||||
triggers: Trigger[];
|
||||
trigger?: Trigger;
|
||||
visibleTrigger: boolean;
|
||||
createTriggerInfo: Trigger;
|
||||
showEditApplication: boolean;
|
||||
|
@ -211,6 +212,7 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
onTriggerClose = () => {
|
||||
this.setState({
|
||||
visibleTrigger: false,
|
||||
trigger: undefined,
|
||||
});
|
||||
this.onLoadApplicationComponents();
|
||||
};
|
||||
|
@ -219,6 +221,7 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
this.onGetApplicationTrigger();
|
||||
this.setState({
|
||||
visibleTrigger: false,
|
||||
trigger: undefined,
|
||||
createTriggerInfo: res,
|
||||
});
|
||||
};
|
||||
|
@ -229,8 +232,12 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
appName,
|
||||
token,
|
||||
};
|
||||
deleteTriggers(params).then((res: any) => {
|
||||
deleteTrigger(params).then((res: any) => {
|
||||
if (res) {
|
||||
Message.success({
|
||||
duration: 4000,
|
||||
content: 'Trigger deleted successfully.',
|
||||
});
|
||||
this.onGetApplicationTrigger();
|
||||
}
|
||||
});
|
||||
|
@ -462,6 +469,7 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
componentName = '',
|
||||
traitItem,
|
||||
triggers,
|
||||
trigger,
|
||||
visibleTrigger,
|
||||
createTriggerInfo,
|
||||
showEditApplication,
|
||||
|
@ -578,6 +586,7 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
if (applicationDetail?.labels) {
|
||||
return (
|
||||
<Tag
|
||||
key={key}
|
||||
style={{ margin: '4px' }}
|
||||
color="blue"
|
||||
>{`${key}=${applicationDetail?.labels[key]}`}</Tag>
|
||||
|
@ -606,12 +615,14 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
<Col span={6} style={{ padding: '22px 0' }}>
|
||||
<NumItem
|
||||
number={statistics?.revisionCount}
|
||||
to={`/applications/${applicationDetail.name}/revisions`}
|
||||
title={i18n.t('Revision Count').toString()}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6} style={{ padding: '22px 0' }}>
|
||||
<NumItem
|
||||
number={statistics?.workflowCount}
|
||||
to={`/applications/${applicationDetail.name}/workflows`}
|
||||
title={i18n.t('Workflow Count').toString()}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -751,6 +762,9 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
}}
|
||||
createTriggerInfo={createTriggerInfo}
|
||||
applicationDetail={applicationDetail}
|
||||
onEditTrigger={(t: Trigger) => {
|
||||
this.setState({ visibleTrigger: true, trigger: t });
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -780,6 +794,7 @@ class ApplicationConfig extends Component<Props, State> {
|
|||
<TriggerDialog
|
||||
visible={visibleTrigger}
|
||||
appName={appName}
|
||||
trigger={trigger}
|
||||
workflows={workflows}
|
||||
components={components || []}
|
||||
onClose={this.onTriggerClose}
|
||||
|
|
|
@ -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);
|
|
@ -434,7 +434,7 @@ class AppDialog extends React.Component<Props, State> {
|
|||
this.changeEnvDialog(true);
|
||||
}}
|
||||
>
|
||||
<Translation>New environment</Translation>
|
||||
<Translation>New Environment</Translation>
|
||||
</a>
|
||||
}
|
||||
required={true}
|
||||
|
|
|
@ -32,6 +32,7 @@ type Props = {
|
|||
|
||||
type State = {
|
||||
compare?: ApplicationCompareResponse;
|
||||
revision?: ApplicationRevision;
|
||||
visibleApplicationDiff: boolean;
|
||||
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;
|
||||
if (!revision || !applicationDetail) {
|
||||
this.setState({ compare: undefined });
|
||||
return;
|
||||
}
|
||||
let params: ApplicationCompareRequest = {
|
||||
compareRevisionWithLatest: { revision: revision },
|
||||
compareRevisionWithLatest: { revision: revision.version },
|
||||
};
|
||||
if (mode === 'cluster') {
|
||||
params = {
|
||||
compareRevisionWithRunning: { revision: revision },
|
||||
compareRevisionWithRunning: { revision: revision.version },
|
||||
};
|
||||
}
|
||||
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.Item
|
||||
onClick={() => {
|
||||
this.loadChanges(record.version, 'cluster');
|
||||
this.loadChanges(record, 'cluster');
|
||||
}}
|
||||
>
|
||||
<Translation>Diff With Deployed Application</Translation>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
this.loadChanges(record.version, 'latest');
|
||||
this.loadChanges(record, 'latest');
|
||||
}}
|
||||
>
|
||||
<Translation>Diff With Latest Configuration</Translation>
|
||||
|
@ -271,7 +277,7 @@ class TableList extends Component<Props, State> {
|
|||
const { Column } = Table;
|
||||
const columns = this.getColumns();
|
||||
const { list } = this.props;
|
||||
const { visibleApplicationDiff, compare, diffMode } = this.state;
|
||||
const { visibleApplicationDiff, compare, diffMode, revision } = this.state;
|
||||
const baseName = 'Current Revision';
|
||||
let targetName = 'Latest Application Configuration';
|
||||
if (diffMode == 'cluster') {
|
||||
|
@ -298,8 +304,19 @@ class TableList extends Component<Props, State> {
|
|||
{compare && (
|
||||
<ApplicationDiff
|
||||
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}
|
||||
targetName={targetName}
|
||||
compare={compare}
|
||||
|
|
|
@ -76,8 +76,8 @@ class ApplicationRevisionList extends React.Component<Props, State> {
|
|||
listRevisions(params).then((res) => {
|
||||
if (res) {
|
||||
this.setState({
|
||||
revisionsList: res && res.revisions,
|
||||
revisionsListTotal: res && res.total,
|
||||
revisionsList: res.revisions || [],
|
||||
revisionsListTotal: res.total || 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -445,7 +445,7 @@ class ApplicationWorkflowRecord extends React.Component<Props, State> {
|
|||
<div className={classNames('studio')}>
|
||||
{showRecord && (
|
||||
<PipelineGraph
|
||||
name={`${showRecord?.name}/${showRecord?.status}`}
|
||||
name={`${showRecord?.name}`}
|
||||
zoom={zoom}
|
||||
onNodeClick={this.onStepClick}
|
||||
steps={showRecord?.steps}
|
||||
|
@ -534,7 +534,7 @@ class ApplicationWorkflowRecord extends React.Component<Props, State> {
|
|||
<Icon type="refresh" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="logBox">
|
||||
<div className="log-content">
|
||||
{logs?.map((line, i: number) => {
|
||||
return (
|
||||
<div key={`log-${i}`} className="logLine">
|
||||
|
|
|
@ -47,6 +47,19 @@ type State = {
|
|||
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) => {
|
||||
return { ...store.application };
|
||||
})
|
||||
|
@ -214,18 +227,22 @@ class ApplicationWorkflowStudio extends React.Component<Props, State> {
|
|||
<Translation>Unsaved changes</Translation>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item label="Mode" labelAlign="inset" style={{ marginRight: '8px' }}>
|
||||
<Form.Item label={i18n.t('Mode')} labelAlign="inset" style={{ marginRight: '8px' }}>
|
||||
<Select
|
||||
locale={locale().Select}
|
||||
defaultValue="StepByStep"
|
||||
value={mode}
|
||||
dataSource={[{ value: 'StepByStep' }, { value: 'DAG' }]}
|
||||
dataSource={WorkflowModeOptions}
|
||||
onChange={(value) => {
|
||||
this.setState({ mode: value, changed: this.state.mode !== value });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
locale={locale().Select}
|
||||
defaultValue="DAG"
|
||||
|
@ -233,7 +250,7 @@ class ApplicationWorkflowStudio extends React.Component<Props, State> {
|
|||
onChange={(value) => {
|
||||
this.setState({ subMode: value, changed: this.state.subMode !== value });
|
||||
}}
|
||||
dataSource={[{ value: 'DAG' }, { value: 'StepByStep' }]}
|
||||
dataSource={WorkflowModeOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
|
|
|
@ -44,6 +44,7 @@ class TableList extends Component<Props> {
|
|||
key: 'name',
|
||||
title: <Translation>Name(Alias)</Translation>,
|
||||
dataIndex: 'name',
|
||||
width: '150px',
|
||||
cell: (v: string, i: number, env: Env) => {
|
||||
return (
|
||||
<a
|
||||
|
@ -60,6 +61,7 @@ class TableList extends Component<Props> {
|
|||
key: 'project',
|
||||
title: <Translation>Project</Translation>,
|
||||
dataIndex: 'project',
|
||||
width: '100px',
|
||||
cell: (v: Project) => {
|
||||
if (v && v.name) {
|
||||
return <Link to={`/projects/${v.name}/summary`}>{v.alias || v.name}</Link>;
|
||||
|
@ -72,6 +74,7 @@ class TableList extends Component<Props> {
|
|||
key: 'namespace',
|
||||
title: <Translation>Namespace</Translation>,
|
||||
dataIndex: 'namespace',
|
||||
width: '100px',
|
||||
cell: (v: string) => {
|
||||
return <span>{v}</span>;
|
||||
},
|
||||
|
|
|
@ -8,7 +8,12 @@ import * as yaml from 'js-yaml';
|
|||
import { connect } from 'dva';
|
||||
import type { LoginUserInfo } from '../../../../interface/user';
|
||||
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 type {
|
||||
PipelineBase,
|
||||
|
@ -18,6 +23,7 @@ import type {
|
|||
import { checkName } from '../../../../utils/common';
|
||||
import locale from '../../../../utils/locale';
|
||||
import { templates } from './pipeline-template';
|
||||
import { If } from 'tsx-control-statements/components';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
|
@ -33,6 +39,8 @@ export interface PipelineProps {
|
|||
type State = {
|
||||
configError?: string[];
|
||||
containerId: string;
|
||||
defaultContext?: Record<string, string>;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
@connect((store: any) => {
|
||||
|
@ -97,24 +105,45 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
|
|||
},
|
||||
},
|
||||
};
|
||||
this.setState({ loading: true });
|
||||
if (pipeline) {
|
||||
updatePipeline(request).then((res) => {
|
||||
if (res) {
|
||||
Message.success(i18n.t('Pipeline updated successfully'));
|
||||
if (this.props.onSuccess) {
|
||||
this.props.onSuccess(res);
|
||||
updatePipeline(request)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
Message.success(i18n.t('Pipeline updated successfully'));
|
||||
if (this.props.onSuccess) {
|
||||
this.props.onSuccess(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
} else {
|
||||
createPipeline(request).then((res) => {
|
||||
if (res) {
|
||||
Message.success(i18n.t('Pipeline created successfully'));
|
||||
if (this.props.onSuccess) {
|
||||
this.props.onSuccess(res);
|
||||
createPipeline(request)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
// Create the default context
|
||||
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 { userInfo, pipeline } = this.props;
|
||||
let defaultProject = '';
|
||||
const editMode = pipeline != undefined;
|
||||
const projectOptions: { label: string; value: string }[] = [];
|
||||
(userInfo?.projects || []).map((project) => {
|
||||
if (
|
||||
|
@ -180,12 +210,13 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
|
|||
}
|
||||
});
|
||||
const modeOptions = [{ value: 'StepByStep' }, { value: 'DAG' }];
|
||||
|
||||
const { loading } = this.state;
|
||||
return (
|
||||
<DrawerWithFooter
|
||||
title={i18n.t(pipeline == undefined ? 'New Pipeline' : 'Edit Pipeline')}
|
||||
title={i18n.t(!editMode ? 'New Pipeline' : 'Edit Pipeline')}
|
||||
onClose={this.props.onClose}
|
||||
onOk={this.onSubmit}
|
||||
onOkButtonLoading={loading}
|
||||
>
|
||||
<Form field={this.field}>
|
||||
<Row wrap>
|
||||
|
@ -193,7 +224,7 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
|
|||
<FormItem required label={<Translation>Name</Translation>}>
|
||||
<Input
|
||||
name="name"
|
||||
disabled={pipeline != undefined}
|
||||
disabled={editMode}
|
||||
{...init('name', {
|
||||
initValue: '',
|
||||
rules: [
|
||||
|
@ -235,6 +266,7 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
|
|||
dataSource={projectOptions}
|
||||
filterLocal={true}
|
||||
hasClear={true}
|
||||
disabled={editMode}
|
||||
style={{ width: '100%' }}
|
||||
{...init('project', {
|
||||
initValue: defaultProject,
|
||||
|
@ -287,20 +319,24 @@ class CreatePipeline extends React.Component<PipelineProps, State> {
|
|||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span={24} style={{ padding: '0 8px' }}>
|
||||
<FormItem label={<Translation>Template</Translation>}>
|
||||
<Select
|
||||
locale={locale().Select}
|
||||
name="template"
|
||||
dataSource={templates}
|
||||
hasClear
|
||||
placeholder="Select a template"
|
||||
onChange={(value) => {
|
||||
this.field.setValue('steps', value);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<If condition={!editMode}>
|
||||
<Col span={24} style={{ padding: '0 8px' }}>
|
||||
<FormItem label={<Translation>Template</Translation>}>
|
||||
<Select
|
||||
locale={locale().Select}
|
||||
name="template"
|
||||
dataSource={templates}
|
||||
hasClear
|
||||
placeholder="Select a template"
|
||||
onChange={(value) => {
|
||||
this.field.setValue('steps', value);
|
||||
const template = templates.find((t) => t.value == value);
|
||||
this.setState({ defaultContext: template?.defaultContext });
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</If>
|
||||
</Row>
|
||||
</Form>
|
||||
</DrawerWithFooter>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
export const templates = [
|
||||
{
|
||||
label: 'Observability Template',
|
||||
defaultContext: {
|
||||
readConfig: 'false',
|
||||
},
|
||||
value: `
|
||||
- name: Enable Prism
|
||||
type: addon-operation
|
||||
|
|
|
@ -4,8 +4,13 @@ import i18n from '../../../../i18n';
|
|||
import { connect } from 'dva';
|
||||
import type { LoginUserInfo } from '../../../../interface/user';
|
||||
import { checkPermission } from '../../../../utils/permission';
|
||||
import { createPipeline, loadPipeline } from '../../../../api/pipeline';
|
||||
import type { PipelineListItem, PipelineDetail } from '../../../../interface/pipeline';
|
||||
import {
|
||||
createPipeline,
|
||||
createPipelineContext,
|
||||
listPipelineContexts,
|
||||
loadPipeline,
|
||||
} from '../../../../api/pipeline';
|
||||
import type { PipelineListItem, PipelineDetail, KeyValue } from '../../../../interface/pipeline';
|
||||
import Translation from '../../../../components/Translation';
|
||||
import { checkName } from '../../../../utils/common';
|
||||
import { If } from 'tsx-control-statements/components';
|
||||
|
@ -25,7 +30,10 @@ export interface PipelineProps {
|
|||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
loadingContext: boolean;
|
||||
pipelineDetail?: PipelineDetail;
|
||||
contexts?: Record<string, KeyValue[]>;
|
||||
cloneLoading?: boolean;
|
||||
};
|
||||
|
||||
@connect((store: any) => {
|
||||
|
@ -37,6 +45,7 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
|
|||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
loadingContext: true,
|
||||
};
|
||||
this.field = new Field(this);
|
||||
}
|
||||
|
@ -44,19 +53,34 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
|
|||
componentDidMount() {
|
||||
const { pipeline } = this.props;
|
||||
if (pipeline) {
|
||||
loadPipeline({ projectName: pipeline.project.name, pipelineName: pipeline.name })
|
||||
.then((res: PipelineDetail) => {
|
||||
this.setState({ pipelineDetail: res });
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
this.onLoadingPipeline(pipeline);
|
||||
this.onLoadingPipelineContexts(pipeline);
|
||||
}
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
const { pipelineDetail } = this.state;
|
||||
const { pipelineDetail, contexts } = this.state;
|
||||
if (pipelineDetail) {
|
||||
this.setState({ cloneLoading: true });
|
||||
this.field.validate((errs: any, values: any) => {
|
||||
if (errs) {
|
||||
return;
|
||||
|
@ -69,20 +93,29 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
|
|||
name: name,
|
||||
spec: pipelineDetail?.spec,
|
||||
};
|
||||
createPipeline(request).then((res) => {
|
||||
if (res) {
|
||||
Message.success(i18n.t('Pipeline cloned successfully'));
|
||||
if (this.props.onSuccess) {
|
||||
this.props.onSuccess();
|
||||
createPipeline(request)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
if (contexts) {
|
||||
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() {
|
||||
const { loading, pipelineDetail } = this.state;
|
||||
const { loading, pipelineDetail, loadingContext, contexts, cloneLoading } = this.state;
|
||||
const { userInfo } = this.props;
|
||||
const { init } = this.field;
|
||||
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 (
|
||||
<Dialog
|
||||
onOk={this.onSubmit}
|
||||
okProps={{
|
||||
loading: cloneLoading,
|
||||
}}
|
||||
onClose={this.props.onClose}
|
||||
onCancel={this.props.onClose}
|
||||
locale={locale().Dialog}
|
||||
|
@ -110,11 +149,11 @@ class ClonePipeline extends React.Component<PipelineProps, State> {
|
|||
className="commonDialog"
|
||||
title="Clone Pipeline"
|
||||
>
|
||||
<Loading visible={loading}>
|
||||
<If condition={pipelineDetail}>
|
||||
<Loading visible={loading || loadingContext}>
|
||||
<If condition={pipelineDetail && contexts}>
|
||||
<Message
|
||||
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}>
|
||||
<Row wrap>
|
||||
|
|
|
@ -17,7 +17,7 @@ import locale from '../../utils/locale';
|
|||
import { Link, routerRedux } from 'dva/router';
|
||||
import type { NameAlias } from '../../interface/env';
|
||||
import { deletePipeline, listPipelines } from '../../api/pipeline';
|
||||
import { AiFillCaretRight, AiFillDelete } from 'react-icons/ai';
|
||||
import { AiFillCaretRight, AiFillDelete, AiFillSetting } from 'react-icons/ai';
|
||||
import { BiCopyAlt } from 'react-icons/bi';
|
||||
import { HiViewList } from 'react-icons/hi';
|
||||
import './index.less';
|
||||
|
@ -111,6 +111,10 @@ class PipelineListPage extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
onEditPipeline = (pipeline: PipelineListItem) => {
|
||||
this.setState({ showNewPipeline: true, pipeline: pipeline });
|
||||
};
|
||||
|
||||
renderPipelineTable = () => {
|
||||
const { pipelines } = this.state;
|
||||
return (
|
||||
|
@ -236,7 +240,7 @@ class PipelineListPage extends Component<Props, State> {
|
|||
key={'actions'}
|
||||
title={i18n.t('Actions')}
|
||||
dataIndex="name"
|
||||
width={'360px'}
|
||||
width={'420px'}
|
||||
cell={(name: string, i: number, pipeline: PipelineListItem) => {
|
||||
return (
|
||||
<div>
|
||||
|
@ -301,6 +305,26 @@ class PipelineListPage extends Component<Props, State> {
|
|||
</Button>
|
||||
<span className="line" />
|
||||
</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
|
||||
project={pipeline.project.name}
|
||||
resource={{
|
||||
|
|
|
@ -53,7 +53,7 @@ class Header extends Component<Props, State> {
|
|||
onRerun = () => {
|
||||
Dialog.confirm({
|
||||
type: 'alert',
|
||||
content: 'Are you sure to rerun this Pipeline?',
|
||||
content: i18n.t('Are you sure to rerun this Pipeline?'),
|
||||
locale: locale().Dialog,
|
||||
onOk: () => {
|
||||
const { pipeline, runBase } = this.props;
|
||||
|
|
|
@ -29,24 +29,34 @@
|
|||
.detail-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 250px);
|
||||
justify-content: space-between;
|
||||
height: calc(100vh - 300px);
|
||||
.step-info {
|
||||
flex: auto;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.step-log {
|
||||
height: 600px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
background: #000;
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 4px 16px;
|
||||
color: var(--grey-300);
|
||||
font-size: var(--font-size-small);
|
||||
background: #090909;
|
||||
border-bottom: 1px solid var(--grey-800);
|
||||
}
|
||||
.log-content {
|
||||
margin-bottom: 32px;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.step-info {
|
||||
|
|
|
@ -128,7 +128,8 @@ class PipelineRunPage extends Component<Props, State> {
|
|||
stepName: stepStatus?.name,
|
||||
})
|
||||
.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(() => {
|
||||
this.setState({ logLoading: false });
|
||||
|
@ -287,7 +288,7 @@ class PipelineRunPage extends Component<Props, State> {
|
|||
<div className={classNames('studio')}>
|
||||
{runStatus && (
|
||||
<PipelineGraph
|
||||
name={`${runBase?.pipelineRunName}+${runStatus.status}`}
|
||||
name={`${runBase?.pipelineRunName}`}
|
||||
steps={runStatus.steps}
|
||||
zoom={1}
|
||||
onNodeClick={this.onStepClick}
|
||||
|
@ -380,7 +381,7 @@ class PipelineRunPage extends Component<Props, State> {
|
|||
<Icon type="refresh" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="logBox">
|
||||
<div className="log-content">
|
||||
{logs?.map((line, i: number) => {
|
||||
return (
|
||||
<div key={`log-${i}`} className="logLine">
|
||||
|
|
|
@ -21,6 +21,7 @@ import i18n from '../../i18n';
|
|||
import { If } from 'tsx-control-statements/components';
|
||||
import RunPipeline from '../../components/RunPipeline';
|
||||
import { routerRedux } from 'dva/router';
|
||||
import { WorkflowModeOptions } from '../ApplicationWorkflowStudio';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
|
@ -234,18 +235,22 @@ class PipelineStudio extends React.Component<Props, State> {
|
|||
<Translation>Unsaved changes</Translation>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item label="Mode" labelAlign="inset" style={{ marginRight: '8px' }}>
|
||||
<Form.Item label={i18n.t('Mode')} labelAlign="inset" style={{ marginRight: '8px' }}>
|
||||
<Select
|
||||
locale={locale().Select}
|
||||
defaultValue="StepByStep"
|
||||
value={mode}
|
||||
dataSource={[{ value: 'StepByStep' }, { value: 'DAG' }]}
|
||||
dataSource={WorkflowModeOptions}
|
||||
onChange={(value) => {
|
||||
this.setState({ mode: value, changed: this.state.mode !== value });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
locale={locale().Select}
|
||||
defaultValue="DAG"
|
||||
|
@ -253,7 +258,7 @@ class PipelineStudio extends React.Component<Props, State> {
|
|||
onChange={(value) => {
|
||||
this.setState({ subMode: value, changed: this.state.subMode !== value });
|
||||
}}
|
||||
dataSource={[{ value: 'DAG' }, { value: 'StepByStep' }]}
|
||||
dataSource={WorkflowModeOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
|
|
|
@ -295,9 +295,20 @@ export function convertAny(data?: any): string {
|
|||
}
|
||||
}
|
||||
|
||||
export function showAlias(name: string, alias?: string) {
|
||||
if (alias) {
|
||||
return `${name}(${alias})`;
|
||||
export function showAlias(name: string, alias?: string): string;
|
||||
export function showAlias(item: { name: string; alias?: string }): string;
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue