qinglong/src/pages/env/index.tsx
2022-03-08 20:12:33 +08:00

776 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useRef, useState, useEffect } from 'react';
import {
Button,
message,
Modal,
Table,
Tag,
Space,
Typography,
Tooltip,
Input,
Form,
notification,
} from 'antd';
import {
EditOutlined,
DeleteOutlined,
SyncOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
CheckCircleOutlined,
StopOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import EnvModal from './modal';
import EditNameModal from './editNameModal';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { getTableScroll } from '@/utils/index';
import { doc } from 'prettier';
const { Text } = Typography;
const { Search, TextArea } = Input;
enum Status {
'已启用',
'已禁用',
}
enum StatusColor {
'success',
'error',
}
enum OperationName {
'启用',
'禁用',
}
enum OperationPath {
'enable',
'disable',
}
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const Env = ({ headerStyle, isPhone, theme }: any) => {
const columns: any = [
{
title: '序号',
align: 'center' as const,
width: 60,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
sorter: (a: any, b: any) => a.name.localeCompare(b.name),
},
{
title: '值',
dataIndex: 'value',
key: 'value',
align: 'center' as const,
width: '35%',
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => {
return (
<Tooltip
placement="topLeft"
title={text}
trigger={['hover', 'click']}
>
<span>{text}</span>
</Tooltip>
);
},
},
{
title: '备注',
dataIndex: 'remarks',
key: 'remarks',
align: 'center' as const,
},
{
title: '更新时间',
dataIndex: 'timestamp',
key: 'timestamp',
align: 'center' as const,
width: 165,
ellipsis: {
showTitle: false,
},
sorter: {
compare: (a: any, b: any) => {
const updatedAtA = new Date(a.updatedAt || a.timestamp).getTime();
const updatedAtB = new Date(b.updatedAt || b.timestamp).getTime();
return updatedAtA - updatedAtB;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
const time = record.updatedAt || record.timestamp;
const date = new Date(time)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:');
return (
<Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span>
</Tooltip>
);
},
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: 70,
filters: [
{
text: '已启用',
value: 0,
},
{
text: '已禁用',
value: 1,
},
],
onFilter: (value: number, record: any) => record.status === value,
render: (text: string, record: any, index: number) => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag color={StatusColor[record.status]} style={{ marginRight: 0 }}>
{Status[record.status]}
</Tag>
</Space>
);
},
},
{
title: '操作',
key: 'action',
width: 120,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
return (
<Space size="middle">
<Tooltip title={isPc ? '编辑' : ''}>
<a onClick={() => editEnv(record, index)}>
<EditOutlined />
</a>
</Tooltip>
<Tooltip
title={
isPc ? (record.status === Status. ? '启用' : '禁用') : ''
}
>
<a onClick={() => enabledOrDisabledEnv(record, index)}>
{record.status === Status. ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)}
</a>
</Tooltip>
<Tooltip title={isPc ? '删除' : ''}>
<a onClick={() => deleteEnv(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
</Space>
);
},
},
];
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isEditNameModalVisible, setIsEditNameModalVisible] = useState(false);
const [editedEnv, setEditedEnv] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const getEnvs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then((data: any) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const enabledOrDisabledEnv = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.status === Status. ? '启用' : '禁用'}`,
content: (
<>
{record.status === Status. ? '启用' : '禁用'}
Env{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value}
</Text>{' '}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}envs/${
record.status === Status. ? 'enable' : 'disable'
}`,
{
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
message.success(
`${record.status === Status. ? '启用' : '禁用'}成功`,
);
const newStatus =
record.status === Status. ? Status.已启用 : Status.已禁用;
const result = [...value];
result.splice(index, 1, {
...record,
status: newStatus,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const addEnv = () => {
setEditedEnv(null as any);
setIsModalVisible(true);
};
const editEnv = (record: any, index: number) => {
setEditedEnv(record);
setIsModalVisible(true);
};
const deleteEnv = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}: {record.value}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
message.success('删除成功');
const result = [...value];
result.splice(index, 1);
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handleCancel = (env?: any[]) => {
setIsModalVisible(false);
env && handleEnv(env);
};
const handleEditNameCancel = (env?: any[]) => {
setIsEditNameModalVisible(false);
getEnvs();
};
const handleEnv = (env: any) => {
const result = [...value];
const index = value.findIndex((x) => x.id === env.id);
if (index === -1) {
env = Array.isArray(env) ? env : [env];
result.push(...env);
} else {
result.splice(index, 1, {
...env,
});
}
setValue(result);
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = value[dragIndex];
request
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
if (data.code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]);
} else {
message.error(data);
}
});
},
[value],
);
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
};
const rowSelection = {
selectedRowIds,
onChange: onSelectChange,
};
const delEnvs = () => {
Modal.confirm({
title: '确认删除',
content: <></>,
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getEnvs();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const operateEnvs = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
onOk() {
request
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
getEnvs();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const [importForm] = Form.useForm();
const operateImport = () => {
const importList: Array<any> = [
// {
// index: 1,
// name: '',
// icon: '',
// color: ''
// }
];
let importRes: any;
const insertOneEnv = async (values: any) => {
const { value, split, name, remarks } = values;
const method = 'post';
let payload;
if (split === '1') {
const symbol = value.includes('&') ? '&' : '\n';
payload = value.split(symbol).map((x: any) => {
return {
name: name,
value: x,
remarks: remarks,
};
});
} else {
payload = [{ value, name, remarks }];
}
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload,
});
return { code, data };
};
const submitImport = async (value: string) => {
let now_at: any = '';
try {
setLoading(true);
const dataList = JSON.parse(value); // 是一个数组
for (const [index, item] of dataList.entries()) {
importList.push({
index: index,
name: item.name,
icon: <SyncOutlined />,
color: 'default',
text: '等待中',
});
}
for (const [index, item] of dataList.entries()) {
Object.assign(importList[index], {
icon: <ClockCircleOutlined />,
color: 'processing',
text: '进行中',
});
now_at = importList[index];
const { code = 0 } = await insertOneEnv(item);
if (code === 200) {
Object.assign(importList[index], {
icon: <CheckCircleOutlined />,
color: 'success',
text: '成功',
});
} else {
Object.assign(importList[index], {
icon: <CloseCircleOutlined />,
color: 'error',
text: '失败',
});
}
}
importRes = importList.map((one) => {
return (
<div>
${one.index + 1} - ${one.name}{' '}
<Tag icon={one.icon} color={one.color}>
</Tag>
</div>
);
});
const count_success = importList.filter(
(one) => one.color === 'success',
).length;
const count_failed = importList.filter(
(one) => one.color === 'error',
).length;
notification.success({
message: '导入完成',
description: `成功导入${count_success}个,失败${count_failed}`,
});
return true;
} catch (e) {
if (e.message.includes('JSON')) {
message.error('数据格式有问题请检查并更正json格式');
} else {
message.error(
`导入第 ${now_at.index + 1} 个数据 ${
now_at.name
} 失败,请检查是否存在相同变量`,
);
console.log(e);
}
} finally {
setLoading(false);
}
return false;
};
Modal.confirm({
title: `环境变量配置数据-导入-请手动粘贴`,
okText: '导入',
content: (
<div>
<Form form={importForm} layout="vertical" name="form_in_modal">
<Form.Item name="envs" label="环境变量">
<TextArea
placeholder="请粘贴导入的环境变量"
style={{ height: 120, marginTop: '20px' }}
autoFocus
></TextArea>
</Form.Item>
</Form>
</div>
),
async onOk() {
const values = await importForm.validateFields();
const { envs }: { envs: string } = values;
const isOk = await submitImport(envs);
if (isOk) {
setTimeout(() => {
location.reload();
}, 1000);
return Promise.resolve();
}
return Promise.reject('');
},
onCancel() {
importForm.resetFields();
},
});
};
const operateExport = () => {
const result = [...value];
const selectItems = result
.filter((one) => selectedRowIds.includes(one.id))
.map((one) => {
return {
name: one.name,
value: one.value,
split: one.split,
remarks: one.remarks,
};
});
const resJson = JSON.stringify(selectItems, function (key: any, val: any) {
console.log(val);
if (val) return val;
});
Modal.confirm({
title: `环境变量配置数据-导出-请手动复制`,
content: (
<TextArea
style={{ height: 120, marginTop: '20px' }}
value={resJson}
autoFocus
></TextArea>
),
onOk() {
console.log('Ok');
},
onCancel() {
console.log('Cancel');
},
});
};
const modifyName = () => {
setIsEditNameModalVisible(true);
};
const onSearch = (value: string) => {
setSearchText(value.trim());
};
useEffect(() => {
getEnvs();
}, [searchText]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
return (
<PageContainer
className="ql-container-wrapper env-wrapper"
title="环境变量"
extra={[
<Search
placeholder="请输入名称/值/备注"
style={{ width: 'auto' }}
enterButton
loading={loading}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addEnv()}>
</Button>,
]}
header={{
style: headerStyle,
}}
>
<div style={{ marginBottom: 16, display: 'flex' }}>
<Button
type="primary"
style={{ marginBottom: 5 }}
onClick={operateImport}
>
</Button>
{selectedRowIds.length > 0 && (
<div>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={operateExport}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={modifyName}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delEnvs}
>
</Button>
<Button
type="primary"
onClick={() => operateEnvs(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateEnvs(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
</div>
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
components={components}
loading={loading}
onRow={(record: any, index: number) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<EnvModal
visible={isModalVisible}
handleCancel={handleCancel}
env={editedEnv}
/>
<EditNameModal
visible={isEditNameModalVisible}
handleCancel={handleEditNameCancel}
ids={selectedRowIds}
/>
</PageContainer>
);
};
export default Env;