mirror of
https://github.com/whyour/qinglong.git
synced 2026-02-13 06:25:39 +08:00
Add frontend UI for global SSH keys management
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
43aaac4bcc
commit
715045cc3a
|
|
@ -42,6 +42,12 @@ export default {
|
||||||
icon: <IconFont type="ql-icon-env" />,
|
icon: <IconFont type="ql-icon-env" />,
|
||||||
component: '@/pages/env/index',
|
component: '@/pages/env/index',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/sshKey',
|
||||||
|
name: 'SSH密钥',
|
||||||
|
icon: <IconFont type="ql-icon-key" />,
|
||||||
|
component: '@/pages/sshKey/index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/config',
|
path: '/config',
|
||||||
name: intl.get('配置文件'),
|
name: intl.get('配置文件'),
|
||||||
|
|
|
||||||
300
src/pages/sshKey/index.tsx
Normal file
300
src/pages/sshKey/index.tsx
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
StopOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
import { useOutletContext } from '@umijs/max';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
message,
|
||||||
|
} from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { useVT } from 'virtualizedtableforantd4';
|
||||||
|
import Copy from '../../components/copy';
|
||||||
|
import SshKeyModal from './modal';
|
||||||
|
|
||||||
|
const { Paragraph, Text } = Typography;
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
'已启用',
|
||||||
|
'已禁用',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StatusColor {
|
||||||
|
'success',
|
||||||
|
'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperationName {
|
||||||
|
'启用',
|
||||||
|
'禁用',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperationPath {
|
||||||
|
'enable',
|
||||||
|
'disable',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SshKey = () => {
|
||||||
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
|
const columns: any = [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
width: 80,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '别名',
|
||||||
|
dataIndex: 'alias',
|
||||||
|
key: 'alias',
|
||||||
|
sorter: (a: any, b: any) => a.alias.localeCompare(b.alias),
|
||||||
|
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',
|
||||||
|
width: '35%',
|
||||||
|
sorter: (a: any, b: any) => (a.remarks || '').localeCompare(b.remarks || ''),
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={text} placement="topLeft">
|
||||||
|
<div className="text-ellipsis">{text}</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
width: 90,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
text: '已启用',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '已禁用',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onFilter: (value: number, record: any) => record.status === value,
|
||||||
|
render: (value: number, record: any) => {
|
||||||
|
return <Tag color={StatusColor[value]}>{Status[value]}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 185,
|
||||||
|
sorter: (a: any, b: any) => {
|
||||||
|
return (
|
||||||
|
dayjs(a.createdAt || 0).valueOf() - dayjs(b.createdAt || 0).valueOf()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
const d = dayjs(text);
|
||||||
|
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 120,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
return (
|
||||||
|
<Space size="middle">
|
||||||
|
<Tooltip title={OperationName[record.status]}>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
operateSSHKey(
|
||||||
|
[record.id],
|
||||||
|
record.status === 0 ? OperationPath[1] : OperationPath[0],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{OperationName[record.status]}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
editSSHKey(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
deleteSSHKey(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const [value, setValue] = useState<any>();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
const tableRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const scrollHeight = useTableScrollHeight(tableRef, data, [
|
||||||
|
headerStyle.marginTop,
|
||||||
|
]);
|
||||||
|
const [vt] = useVT(() => ({ scroll: { y: scrollHeight } }), [scrollHeight]);
|
||||||
|
|
||||||
|
const getSSHKeys = (needLoading = true) => {
|
||||||
|
setLoading(needLoading);
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}sshKeys?searchValue=${searchValue}`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setData(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const editSSHKey = (record: any) => {
|
||||||
|
setValue(record);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSSHKey = (record: any) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认删除SSH密钥
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{record.alias}
|
||||||
|
</Text>
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}sshKeys`, [record.id])
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('删除成功');
|
||||||
|
getSSHKeys();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const operateSSHKey = (ids: any[], operationPath: string) => {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}sshKeys/${operationPath}`, ids)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
message.success(`批量${OperationName[OperationPath[operationPath]]}成功`);
|
||||||
|
getSSHKeys(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = (value: string) => {
|
||||||
|
setSearchValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = (keys?: any[]) => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
if (keys) {
|
||||||
|
getSSHKeys();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSSHKey = () => {
|
||||||
|
setValue(null);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSSHKeys();
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
const rowSelection = {
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
|
||||||
|
setSelectedRowKeys(selectedRowKeys);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
className="ql-container-wrapper"
|
||||||
|
title="SSH密钥"
|
||||||
|
extra={[
|
||||||
|
<Button key="1" type="primary" onClick={addSSHKey}>
|
||||||
|
新建
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
header={{
|
||||||
|
style: headerStyle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={tableRef}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: 768 }}
|
||||||
|
sticky
|
||||||
|
components={vt}
|
||||||
|
size="middle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isModalVisible && (
|
||||||
|
<SshKeyModal sshKey={value} handleCancel={handleCancel} />
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SshKey;
|
||||||
97
src/pages/sshKey/modal.tsx
Normal file
97
src/pages/sshKey/modal.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, message, Input, Form } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
|
||||||
|
const SshKeyModal = ({
|
||||||
|
sshKey,
|
||||||
|
handleCancel,
|
||||||
|
}: {
|
||||||
|
sshKey?: any;
|
||||||
|
handleCancel: (keys?: any[]) => void;
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = async (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
const method = sshKey ? 'put' : 'post';
|
||||||
|
const payload = sshKey ? { ...values, id: sshKey.id } : [values];
|
||||||
|
try {
|
||||||
|
const { code, data } = await request[method](
|
||||||
|
`${config.apiPrefix}sshKeys`,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (code === 200) {
|
||||||
|
message.success(
|
||||||
|
sshKey ? '更新SSH密钥成功' : '创建SSH密钥成功',
|
||||||
|
);
|
||||||
|
handleCancel(data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={sshKey ? '编辑SSH密钥' : '创建SSH密钥'}
|
||||||
|
open={true}
|
||||||
|
forceRender
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
|
onOk={() => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
handleOk(values);
|
||||||
|
})
|
||||||
|
.catch((info) => {
|
||||||
|
console.log('Validate Failed:', info);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => handleCancel()}
|
||||||
|
confirmLoading={loading}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" name="ssh_key_modal" initialValues={sshKey}>
|
||||||
|
<Form.Item
|
||||||
|
name="alias"
|
||||||
|
label="别名"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入SSH密钥别名',
|
||||||
|
whitespace: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入SSH密钥别名" disabled={!!sshKey} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="private_key"
|
||||||
|
label="私钥"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入SSH私钥',
|
||||||
|
whitespace: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
autoSize={{ minRows: 4, maxRows: 12 }}
|
||||||
|
placeholder="请输入SSH私钥内容(以 -----BEGIN 开头)"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="remarks" label="备注">
|
||||||
|
<Input placeholder="请输入备注" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SshKeyModal;
|
||||||
Loading…
Reference in New Issue
Block a user