This commit is contained in:
dengqichen 2024-12-26 16:39:23 +08:00
parent ab3330fe95
commit 2d41b2793f
8 changed files with 330 additions and 180 deletions

View File

@ -240,36 +240,11 @@ const ApplicationList: React.FC = () => {
];
return (
<PageContainer
header={{
title: '应用管理',
subTitle: (
<Space>
<span></span>
<Select
value={selectedProjectGroupId}
onChange={handleProjectChange}
style={{width: 200}}
options={projectGroups.map(project => ({
label: (
<Space>
{project.projectGroupName}
<Tag color={getProjectTypeInfo(project.type).color}>
{getProjectTypeInfo(project.type).label}
</Tag>
</Space>
),
value: project.id,
}))}
/>
</Space>
),
}}
>
<>
<ProTable<Application>
columns={columns}
actionRef={actionRef}
scroll={{x: 1300}}
scroll={{x: 'max-content'}}
cardBordered
request={async (params) => {
if (!selectedProjectGroupId) {
@ -343,7 +318,7 @@ const ApplicationList: React.FC = () => {
projectGroupId={selectedProjectGroupId}
/>
)}
</PageContainer>
</>
);
};

View File

@ -1,8 +1,9 @@
import React, {useEffect, useState} from 'react';
import {Modal, Form, Input, Select, message} from 'antd';
import React, {useEffect} from 'react';
import {Modal, Form, Input, InputNumber, Select, message} from 'antd';
import type {Environment} from '../types';
import {BuildTypeEnum, DeployTypeEnum} from '../types';
import {createEnvironment, updateEnvironment} from '../service';
import {getBuildTypeInfo, getDeployTypeInfo} from '../utils';
interface EnvironmentModalProps {
visible: boolean;
@ -12,105 +13,133 @@ interface EnvironmentModalProps {
}
const EnvironmentModal: React.FC<EnvironmentModalProps> = ({
visible,
onCancel,
onSuccess,
initialValues,
}) => {
visible,
onCancel,
onSuccess,
initialValues,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const isEdit = !!initialValues?.id;
useEffect(() => {
if (visible) {
form.setFieldsValue(initialValues || {});
if (visible && initialValues) {
form.setFieldsValue(initialValues);
}
}, [visible, initialValues]);
}, [visible, initialValues, form]);
const handleOk = async () => {
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
if (initialValues?.id) {
if (isEdit) {
await updateEnvironment({
...values,
id: initialValues.id,
});
message.success('更新成功');
} else {
await createEnvironment(values);
message.success('创建成功');
}
message.success(`${isEdit ? '更新' : '创建'}成功`);
form.resetFields();
onSuccess();
onCancel();
} catch (error) {
console.error('Submit failed:', error);
} finally {
setLoading(false);
message.error(`${isEdit ? '更新' : '创建'}失败`);
}
};
return (
<Modal
title={initialValues ? '编辑环境' : '新建环境'}
title={`${isEdit ? '编辑' : '新建'}环境`}
open={visible}
onOk={handleOk}
onCancel={onCancel}
confirmLoading={loading}
destroyOnClose
onCancel={() => {
form.resetFields();
onCancel();
}}
onOk={handleSubmit}
width={600}
>
<Form
form={form}
layout="vertical"
initialValues={initialValues || {sort: 0}}
initialValues={{
sort: 0,
}}
>
<Form.Item
name="envName"
label="环境名称"
rules={[
{required: true, message: '请输入环境名称'},
{max: 50, message: '环境名称最多50个字符'}
]}
>
<Input placeholder="请输入环境名称"/>
</Form.Item>
<Form.Item
name="envCode"
label="环境编码"
rules={[
{required: true, message: '请输入环境编码'},
{max: 50, message: '环境编码最多50个字符'},
{
pattern: /^[A-Za-z0-9_-]+$/,
message: '环境编码只能包含字母、数字、下划线和连字符'
}
{max: 50, message: '环境编码长度不能超过50个字符'},
]}
>
<Input placeholder="请输入环境编码"/>
</Form.Item>
<Form.Item
name="envName"
label="环境名称"
rules={[
{required: true, message: '请输入环境名称'},
{max: 50, message: '环境名称长度不能超过50个字符'},
]}
>
<Input placeholder="请输入环境名称"/>
</Form.Item>
<Form.Item
name="envDesc"
label="环境描述"
rules={[{max: 200, message: '环境描述最多200个字符'}]}
rules={[{max: 200, message: '环境描述长度不能超过200个字符'}]}
>
<Input.TextArea
placeholder="请输入环境描述"
rows={4}
showCount
maxLength={200}
/>
<Input.TextArea rows={4} placeholder="请输入环境描述"/>
</Form.Item>
<Form.Item
name="buildType"
label="构建方式"
rules={[{required: true, message: '请选择构建方式'}]}
>
<Select placeholder="请选择构建方式">
{Object.values(BuildTypeEnum).map((type) => {
const typeInfo = getBuildTypeInfo(type);
return (
<Select.Option key={type} value={type}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', color: typeInfo.color }}>
{typeInfo.icon}
{typeInfo.label}
</div>
</Select.Option>
);
})}
</Select>
</Form.Item>
<Form.Item
name="deployType"
label="部署方式"
rules={[{required: true, message: '请选择部署方式'}]}
>
<Select placeholder="请选择部署方式">
{Object.values(DeployTypeEnum).map((type) => {
const typeInfo = getDeployTypeInfo(type);
return (
<Select.Option key={type} value={type}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', color: typeInfo.color }}>
{typeInfo.icon}
{typeInfo.label}
</div>
</Select.Option>
);
})}
</Select>
</Form.Item>
<Form.Item
name="sort"
label="排序"
tooltip="数字越小越靠前"
initialValue={0}
rules={[{required: true, message: '请输入排序号'}]}
>
<Input type="number" placeholder="请输入排序号"/>
<InputNumber min={0} max={999} style={{width: '100%'}}/>
</Form.Item>
</Form>
</Modal>

View File

@ -1,18 +1,19 @@
import React, {useState} from 'react';
import {PageContainer} from '@ant-design/pro-layout';
import {Button, message, Popconfirm, Space, Tag, Form, Input, Row, Col} from 'antd';
import {PlusOutlined, DeleteOutlined, EditOutlined, SearchOutlined, ReloadOutlined} from '@ant-design/icons';
import {getEnvironmentList, deleteEnvironment, getEnvironmentPage} from './service';
import type {Environment} from './types';
import {Button, message, Popconfirm, Space, Tag} from 'antd';
import {PlusOutlined, EditOutlined, DeleteOutlined} from '@ant-design/icons';
import {getEnvironmentPage, deleteEnvironment} from './service';
import type {Environment, EnvironmentQueryParams} from './types';
import {BuildTypeEnum, DeployTypeEnum} from './types';
import {getBuildTypeInfo, getDeployTypeInfo} from './utils';
import EnvironmentModal from './components/EnvironmentModal';
import {ProTable} from '@ant-design/pro-components';
import type {ProColumns} from '@ant-design/pro-components';
import type {ProColumns, ActionType} from '@ant-design/pro-components';
const EnvironmentList: React.FC = () => {
const [modalVisible, setModalVisible] = useState(false);
const [currentEnvironment, setCurrentEnvironment] = useState<Environment>();
const actionRef = React.useRef();
const [form] = Form.useForm();
const actionRef = React.useRef<ActionType>();
const handleDelete = async (id: number) => {
try {
@ -53,21 +54,57 @@ const EnvironmentList: React.FC = () => {
title: '环境描述',
dataIndex: 'envDesc',
ellipsis: true,
width: '30%',
},
{
title: '构建类型',
title: '构建方式',
dataIndex: 'buildType',
width: 100,
render: (buildType) => (
<Tag color={buildType === 'JENKINS' ? 'blue' : 'green'}>
{buildType}
</Tag>
),
width: 180,
render: (buildType) => {
if (!buildType) return '-';
const typeInfo = getBuildTypeInfo(buildType as BuildTypeEnum);
return (
<Tag color={typeInfo.color} style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
{typeInfo.icon}
{typeInfo.label}
</Tag>
);
},
filters: [
{text: 'Jenkins构建', value: BuildTypeEnum.JENKINS},
{text: 'GitLab Runner构建', value: BuildTypeEnum.GITLAB_RUNNER},
{text: 'GitHub Action构建', value: BuildTypeEnum.GITHUB_ACTION},
],
filterMode: 'menu',
filtered: false,
},
{
title: '部署方式',
dataIndex: 'deployType',
width: 180,
render: (deployType) => {
if (!deployType) return '-';
const typeInfo = getDeployTypeInfo(deployType as DeployTypeEnum);
return (
<Tag color={typeInfo.color} style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
{typeInfo.icon}
{typeInfo.label}
</Tag>
);
},
filters: [
{text: 'Kubernetes集群部署', value: DeployTypeEnum.K8S},
{text: 'Docker容器部署', value: DeployTypeEnum.DOCKER},
{text: '虚拟机部署', value: DeployTypeEnum.VM},
],
filterMode: 'menu',
filtered: false,
},
{
title: '排序',
dataIndex: 'sort',
width: 80,
align: 'center',
sorter: true,
},
{
@ -76,30 +113,31 @@ const EnvironmentList: React.FC = () => {
key: 'action',
valueType: 'option',
fixed: 'right',
render: (_, record) => (
<Space>
align: 'center',
render: (_, record) => [
<Button
key="edit"
type="link"
icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该环境吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
danger
icon={<DeleteOutlined/>}
>
</Button>
<Popconfirm
title="确定要删除该环境吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
danger
icon={<DeleteOutlined/>}
>
</Button>
</Popconfirm>
</Space>
),
</Popconfirm>
],
},
];
@ -108,13 +146,18 @@ const EnvironmentList: React.FC = () => {
<ProTable<Environment>
columns={columns}
actionRef={actionRef}
scroll={{x: 'max-content'}}
cardBordered
request={async (params) => {
const {current, pageSize} = params;
const data = await getEnvironmentPage({
current,
pageSize,
});
const queryParams: EnvironmentQueryParams = {
pageSize: params.pageSize,
pageNum: params.current,
envCode: params.envCode as string,
envName: params.envName as string,
buildType: params.buildType as BuildTypeEnum,
deployType: params.deployType as DeployTypeEnum,
};
const data = await getEnvironmentPage(queryParams);
return {
data: data.content || [],
success: true,

View File

@ -1,33 +1,33 @@
import request from '@/utils/request';
import type { CreateEnvironmentRequest, UpdateEnvironmentRequest, Environment, EnvironmentQuery } from './types';
import type { Page } from '@/types/base';
import type {Environment, CreateEnvironmentRequest, UpdateEnvironmentRequest, EnvironmentQueryParams} from './types';
import type {Page} from '@/types/base';
import request from "@/utils/request";
const BASE_URL = '/api/v1/environments';
// 获取环境分页列表
export const getEnvironmentPage = (params: EnvironmentQueryParams) =>
request.get<Page<Environment>>(`${BASE_URL}/page`, {params});
// 创建环境
export const createEnvironment = (data: CreateEnvironmentRequest) =>
request.post<void>(BASE_URL, data);
export const createEnvironment = (data: CreateEnvironmentRequest) =>
request.post<Environment>(BASE_URL, data);
// 更新环境
export const updateEnvironment = (data: UpdateEnvironmentRequest) =>
request.put<void>(`${BASE_URL}/${data.id}`, data);
export const updateEnvironment = (data: UpdateEnvironmentRequest) =>
request.put<Environment>(`${BASE_URL}/${data.id}`, data);
// 删除环境
export const deleteEnvironment = (id: number) =>
request.delete<void>(`${BASE_URL}/${id}`);
export const deleteEnvironment = (id: number) =>
request.delete(`${BASE_URL}/${id}`);
// 获取环境详情
export const getEnvironment = (id: number) =>
request.get<Environment>(`${BASE_URL}/${id}`);
// 分页查询环境列表
export const getEnvironmentPage = (params?: EnvironmentQuery) =>
request.get<Page<Environment>>(`${BASE_URL}/page`, { params });
// 获取所有环境列表
export const getEnvironmentList = () =>
request.get<Environment[]>(BASE_URL);
// 条件查询环境列表
export const getEnvironmentListByCondition = (params?: EnvironmentQuery) =>
export const getEnvironmentListByCondition = (params?: EnvironmentQueryParams) =>
request.get<Environment[]>(`${BASE_URL}/list`, { params });

View File

@ -1,32 +1,50 @@
import type { BaseQuery } from '@/types/base';
import {BaseResponse, BaseRequest, BaseQuery} from '@/types/base';
export interface Environment {
id: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
// 构建方式枚举
export enum BuildTypeEnum {
JENKINS = 'JENKINS',
GITLAB_RUNNER = 'GITLAB_RUNNER',
GITHUB_ACTION = 'GITHUB_ACTION'
}
export interface CreateEnvironmentRequest {
projectId: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
// 部署方式枚举
export enum DeployTypeEnum {
K8S = 'K8S',
DOCKER = 'DOCKER',
VM = 'VM'
}
export interface UpdateEnvironmentRequest {
id: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
// 环境基础信息
export interface Environment extends BaseResponse {
tenantCode: string;
envCode: string;
envName: string;
envDesc?: string;
buildType: BuildTypeEnum;
deployType: DeployTypeEnum;
sort: number;
}
export interface EnvironmentQuery extends BaseQuery {
projectId?: number;
envCode?: string;
envName?: string;
// 创建环境请求参数
export interface CreateEnvironmentRequest extends BaseRequest {
tenantCode: string;
envCode: string;
envName: string;
envDesc?: string;
buildType: BuildTypeEnum;
deployType: DeployTypeEnum;
sort: number;
}
// 更新环境请求参数
export interface UpdateEnvironmentRequest extends CreateEnvironmentRequest {
id: number;
}
// 分页查询参数
export interface EnvironmentQueryParams extends BaseQuery {
envCode?: string;
envName?: string;
buildType?: BuildTypeEnum;
deployType?: DeployTypeEnum;
}

View File

@ -0,0 +1,76 @@
import React from 'react';
import {
GithubOutlined,
GitlabOutlined,
CloudServerOutlined,
CloudOutlined,
DesktopOutlined,
ApiOutlined,
} from '@ant-design/icons';
import {BuildTypeEnum, DeployTypeEnum} from './types';
interface TypeInfo {
label: string;
color: string;
icon: React.ReactNode;
}
// 获取构建方式信息
export const getBuildTypeInfo = (type: BuildTypeEnum): TypeInfo => {
switch (type) {
case BuildTypeEnum.JENKINS:
return {
label: 'Jenkins构建',
color: '#D24939',
icon: <ApiOutlined/>,
};
case BuildTypeEnum.GITLAB_RUNNER:
return {
label: 'GitLab Runner构建',
color: '#FC6D26',
icon: <GitlabOutlined/>,
};
case BuildTypeEnum.GITHUB_ACTION:
return {
label: 'GitHub Action构建',
color: '#2088FF',
icon: <GithubOutlined/>,
};
default:
return {
label: type || '未知',
color: '#666666',
icon: <ApiOutlined/>,
};
}
};
// 获取部署方式信息
export const getDeployTypeInfo = (type: DeployTypeEnum): TypeInfo => {
switch (type) {
case DeployTypeEnum.K8S:
return {
label: 'Kubernetes集群部署',
color: '#326CE5',
icon: <CloudServerOutlined/>,
};
case DeployTypeEnum.DOCKER:
return {
label: 'Docker容器部署',
color: '#2496ED',
icon: <CloudOutlined/>,
};
case DeployTypeEnum.VM:
return {
label: '虚拟机部署',
color: '#F7B93E',
icon: <DesktopOutlined/>,
};
default:
return {
label: type || '未知',
color: '#666666',
icon: <CloudServerOutlined/>,
};
}
};

View File

@ -100,7 +100,7 @@ const ProjectGroupList: React.FC = () => {
<Tooltip title="环境数量">
<Space>
<EnvironmentOutlined/>
{record.environments?.length || 0}
{record?.totalEnvironments || 0}
</Space>
</Tooltip>
),
@ -126,35 +126,42 @@ const ProjectGroupList: React.FC = () => {
},
{
title: '操作',
width: 180,
width: 280,
key: 'action',
valueType: 'option',
fixed: 'right',
align: 'center',
render: (_, record) => (
<Space>
render: (_, record) => [
<Button
key="edit"
type="link"
icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
>
</Button>,
<Button
key="bind"
type="link"
icon={<EnvironmentOutlined/>}
>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
danger
icon={<DeleteOutlined/>}
>
</Button>
<Popconfirm
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
danger
icon={<DeleteOutlined/>}
>
</Button>
</Popconfirm>
</Space>
),
</Popconfirm>
],
},
];
@ -163,6 +170,7 @@ const ProjectGroupList: React.FC = () => {
<ProTable<ProjectGroup>
columns={columns}
actionRef={actionRef}
scroll={{ x: 'max-content' }}
cardBordered
request={async (params) => {
const queryParams: ProjectGroupQueryParams = {

View File

@ -13,6 +13,7 @@ export interface ProjectGroup extends BaseResponse {
projectGroupName: string;
projectGroupDesc?: string;
enabled: boolean;
totalEnvironments: number;
totalApplications: number;
sort: number;
}