Compare commits

...

8 Commits
main ... v1.7.2

Author SHA1 Message Date
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
45 changed files with 985 additions and 204 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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?": "确定要重新运行流水线吗?"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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);
}}
>
<Translation>New environment</Translation>
<Translation>New Environment</Translation>
</a>
}
required={true}

View File

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

View File

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

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')}>
{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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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