qinglong/src/pages/env/index.tsx
2023-07-17 23:13:06 +08:00

608 lines
15 KiB
TypeScript

import React, {
useCallback,
useRef,
useState,
useEffect,
useMemo,
} from 'react';
import {
Button,
message,
Modal,
Table,
Tag,
Space,
Typography,
Tooltip,
Input,
UploadProps,
Upload,
} from 'antd';
import {
EditOutlined,
DeleteOutlined,
SyncOutlined,
CheckCircleOutlined,
StopOutlined,
UploadOutlined,
} 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 { exportJson } from '@/utils/index';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { useVT } from 'virtualizedtableforantd4';
const { Text } = Typography;
const { Search } = Input;
enum Status {
'已启用',
'已禁用',
}
enum StatusColor {
'success',
'error',
}
enum OperationName {
'启用',
'禁用',
}
enum OperationPath {
'enable',
'disable',
}
const type = 'DragableBodyRow';
const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [
{
title: '序号',
width: 60,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
sorter: (a: any, b: any) => a.name.localeCompare(b.name),
},
{
title: '值',
dataIndex: 'value',
key: 'value',
width: '35%',
render: (text: string, record: any) => {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
<Copy text={text} />
</div>
);
},
},
{
title: '备注',
dataIndex: 'remarks',
key: 'remarks',
render: (text: string, record: any) => {
return (
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
);
},
},
{
title: '更新时间',
dataIndex: 'timestamp',
key: 'timestamp',
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',
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,
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 [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getEnvs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then(({ code, data }) => {
if (code === 200) {
setValue(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'
}`,
[record.id],
)
.then(({ code, data }) => {
if (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);
}
});
},
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(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...value];
result.splice(index, 1);
setValue(result);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handleCancel = (env?: any[]) => {
setIsModalVisible(false);
getEnvs();
};
const handleEditNameCancel = (env?: any[]) => {
setIsEditNameModalVisible(false);
getEnvs();
};
const [vt, setVT] = useVT(
() => ({ scroll: { y: tableScrollHeight } }),
[tableScrollHeight],
);
const DragableBodyRow = React.forwardRef((props: any, ref) => {
const { index, moveRow, className, style, ...restProps } = props;
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(),
}),
});
useEffect(() => {
drop(drag(ref));
}, [ref]);
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
});
useEffect(
() =>
setVT({
body: {
row: DragableBodyRow,
},
}),
[],
);
const moveRow = useCallback(
(dragIndex: number, hoverIndex: number) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = value[dragIndex];
request
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
fromIndex: dragIndex,
toIndex: hoverIndex,
})
.then(({ code, data }) => {
if (code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setValue([...newData]);
}
});
},
[value],
);
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
};
const rowSelection = {
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
};
const delEnvs = () => {
Modal.confirm({
title: '确认删除',
content: <></>,
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then(({ code, data }) => {
if (code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getEnvs();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const operateEnvs = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
onOk() {
request
.put(
`${config.apiPrefix}envs/${OperationPath[operationStatus]}`,
selectedRowIds,
)
.then(({ code, data }) => {
if (code === 200) {
getEnvs();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const exportEnvs = () => {
const envs = value
.filter((x) => selectedRowIds.includes(x.id))
.map((x) => ({ value: x.value, name: x.name, remarks: x.remarks }));
exportJson('env.json', JSON.stringify(envs));
};
const modifyName = () => {
setIsEditNameModalVisible(true);
};
const onSearch = (value: string) => {
setSearchText(value.trim());
};
const uploadProps: UploadProps = {
accept: 'application/json',
beforeUpload: async (file) => {
const formData = new FormData();
formData.append('env', file);
setImportLoading(true);
try {
const { code, data } = await request.post(
`${config.apiPrefix}envs/upload`,
formData,
);
if (code === 200) {
message.success(`成功上传${data.length}个环境变量`);
getEnvs();
}
setImportLoading(false);
} catch (error: any) {
setImportLoading(false);
}
return false;
},
fileList: [],
};
useEffect(() => {
getEnvs();
}, [searchText]);
return (
<PageContainer
className="ql-container-wrapper env-wrapper"
title="环境变量"
extra={[
<Search
placeholder="请输入名称/值/备注"
style={{ width: 'auto' }}
enterButton
loading={loading}
onSearch={onSearch}
/>,
<Upload {...uploadProps}>
<Button
type="primary"
icon={<UploadOutlined />}
loading={importLoading}
>
</Button>
</Upload>,
<Button key="2" type="primary" onClick={() => addEnv()}>
</Button>,
]}
header={{
style: headerStyle,
}}
>
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5 }}
onClick={modifyName}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delEnvs}
>
</Button>
<Button
type="primary"
onClick={() => exportEnvs()}
style={{ marginLeft: 8, marginRight: 8 }}
>
</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>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
components={vt}
loading={loading}
onRow={(record: any, index: number | undefined) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
</div>
<EnvModal
visible={isModalVisible}
handleCancel={handleCancel}
env={editedEnv}
/>
<EditNameModal
visible={isEditNameModalVisible}
handleCancel={handleEditNameCancel}
ids={selectedRowIds}
/>
</PageContainer>
);
};
export default Env;