lk
3 months ago
1 changed files with 590 additions and 0 deletions
@ -0,0 +1,590 @@ |
|||||
|
import React, { useEffect, useMemo, useState } from "react"; |
||||
|
import { |
||||
|
Table, |
||||
|
Input, |
||||
|
Button, |
||||
|
Tag, |
||||
|
Progress, |
||||
|
Space, |
||||
|
Drawer, |
||||
|
Typography, |
||||
|
Form, |
||||
|
DatePicker, |
||||
|
Tooltip, |
||||
|
Modal, |
||||
|
ProgressProps, |
||||
|
} from "antd"; |
||||
|
import { CheckCircleFilled, CheckCircleOutlined, CopyOutlined, SearchOutlined } from "@ant-design/icons"; |
||||
|
import { t } from "@/i18n.ts"; |
||||
|
import ListPageLayout from "@/layout/ListPageLayout.tsx"; |
||||
|
import TextArea from "antd/es/input/TextArea"; |
||||
|
import { useNavigate } from "@tanstack/react-router"; |
||||
|
import { |
||||
|
certListAtom, |
||||
|
checkDomainAtom, |
||||
|
deletesCertificateAtom, |
||||
|
downloadCertificateAtom, |
||||
|
editCertificateAtom, |
||||
|
getCertificateLogsAtom, |
||||
|
Req_CertList, |
||||
|
req_CertLogs, |
||||
|
Req_DeletesCert, |
||||
|
Req_DownloadCert, |
||||
|
Req_UpdateCert, |
||||
|
saveOrUpdateCertAtom, |
||||
|
} from "@/store/websites/cert.ts"; |
||||
|
import { useAtomValue } from "jotai/index"; |
||||
|
import { format } from "date-fns"; |
||||
|
import websitesServ from "@/service/websites.ts"; |
||||
|
import { useAtom } from "jotai"; |
||||
|
import { getToken } from "@/store/system.ts"; |
||||
|
|
||||
|
const { confirm } = Modal; |
||||
|
const { Text, Link } = Typography; |
||||
|
const i18nPrefix = "cert.management"; |
||||
|
|
||||
|
const CertApplyingDrawer = (props: { id: number; onCloseApplyingDrawer; applyingDrawerVisible }) => { |
||||
|
const [dData, setDdata] = useState<req_CertLogs>({ |
||||
|
log_id: props.id, |
||||
|
log_type: "cert_apply", |
||||
|
websocket: false, |
||||
|
log_pos: 0, |
||||
|
// 根据实际数据结构添加其他属性的初始值
|
||||
|
}); |
||||
|
const [showTextInfo, setShowTextInfo] = useState(""); |
||||
|
const [onChangeDT, setOnChangeDT] = useState(0); |
||||
|
const { data: certLogsData, isFetching: certLogsFetching } = useAtomValue( |
||||
|
useMemo(() => getCertificateLogsAtom(dData), [onChangeDT]), |
||||
|
); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (certLogsData && (certLogsData as any)?.log) { |
||||
|
setShowTextInfo((prev) => prev + (certLogsData as any).log.toString()); |
||||
|
} |
||||
|
}, [certLogsData]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (props.applyingDrawerVisible) { |
||||
|
setShowTextInfo(""); |
||||
|
setOnChangeDT((prev) => prev + 1); |
||||
|
} |
||||
|
let timer: number | undefined; |
||||
|
if (props.applyingDrawerVisible) { |
||||
|
timer = window.setInterval(() => { |
||||
|
setDdata((prevState) => ({ |
||||
|
...prevState, |
||||
|
log_id: props.id, |
||||
|
log_pos: (certLogsData as any)?.log_pos || 0, |
||||
|
})); |
||||
|
// console.log("update drawer");
|
||||
|
setOnChangeDT((prev) => prev + 1); |
||||
|
}, 5000); |
||||
|
} else { |
||||
|
setDdata((prevState) => ({ |
||||
|
...prevState, |
||||
|
log_id: props.id, |
||||
|
log_pos: 0, |
||||
|
})); |
||||
|
window.clearInterval(timer); |
||||
|
} |
||||
|
|
||||
|
return () => window.clearInterval(timer); |
||||
|
}, [props.applyingDrawerVisible]); |
||||
|
|
||||
|
return ( |
||||
|
<Drawer |
||||
|
title="证书申请中..." |
||||
|
width={window.innerWidth / 2} |
||||
|
placement="right" |
||||
|
onClose={props.onCloseApplyingDrawer} |
||||
|
visible={props.applyingDrawerVisible} |
||||
|
bodyStyle={{ display: "flex", flexDirection: "column", height: "100%" }} |
||||
|
> |
||||
|
<TextArea value={showTextInfo} readOnly style={{ flex: 1 }} /> |
||||
|
</Drawer> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const CertificateManagement: React.FC = () => { |
||||
|
const [dlDrawerVisible, setDLDrawerVisible] = useState(false); |
||||
|
const [editDrawerVisible, setEditDrawerVisible] = useState(false); |
||||
|
const [detailsVisible, setDetailsVisible] = useState(false); |
||||
|
const navigate = useNavigate(); |
||||
|
const dData: Req_CertList = { order: "", prop: "", page: 1, pageSize: 10 }; |
||||
|
const [updataList, setUpdataList] = useState(0); |
||||
|
const { data: certListData, isFetching: certListFetching } = useAtomValue( |
||||
|
useMemo(() => certListAtom(dData), [updataList]), |
||||
|
); |
||||
|
|
||||
|
const [applyingDrawerVisible, setApplyingDrawerVisible] = useState(false); |
||||
|
const [certApplyingId, setCertApplyingId] = useState(0); |
||||
|
const [currentDLInfo, setCurrentDLInfo] = useState({}); |
||||
|
const { mutate: editCertUpdate } = useAtomValue(editCertificateAtom); |
||||
|
const [remakeTextInfo, setRemakeTextInfo] = useState(""); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
window.setInterval(() => { |
||||
|
// console.log("update drawer");
|
||||
|
setUpdataList((prev) => prev + 1); |
||||
|
}, 5000); |
||||
|
}, []); |
||||
|
|
||||
|
const showDLDrawer = (record) => { |
||||
|
setCurrentDLInfo(record); |
||||
|
setDLDrawerVisible(true); |
||||
|
}; |
||||
|
|
||||
|
const closeDLDrawer = () => { |
||||
|
setDLDrawerVisible(false); |
||||
|
}; |
||||
|
|
||||
|
const showEditDrawer = (record) => { |
||||
|
setCurrentDLInfo(record); |
||||
|
setEditDrawerVisible(true); |
||||
|
}; |
||||
|
const enterEditDrawer = (record) => { |
||||
|
const data: Req_UpdateCert = { |
||||
|
id: record.id, |
||||
|
remark: remakeTextInfo, |
||||
|
}; |
||||
|
editCertUpdate(data); |
||||
|
setEditDrawerVisible(false); |
||||
|
}; |
||||
|
const closeEditDrawer = () => { |
||||
|
setEditDrawerVisible(false); |
||||
|
}; |
||||
|
const openDetails = () => { |
||||
|
setDetailsVisible(true); |
||||
|
}; |
||||
|
|
||||
|
const closeDetails = () => { |
||||
|
setDetailsVisible(false); |
||||
|
}; |
||||
|
|
||||
|
const showApplyingDrawer = () => { |
||||
|
setApplyingDrawerVisible(true); |
||||
|
}; |
||||
|
|
||||
|
const onCloseApplyingDrawer = () => { |
||||
|
setApplyingDrawerVisible(false); |
||||
|
}; |
||||
|
|
||||
|
const applyCertClick = () => { |
||||
|
navigate({ to: `/client/cert/apply` }); // 确保路径正确
|
||||
|
}; |
||||
|
|
||||
|
const clickStateTag = (id: number) => { |
||||
|
setCertApplyingId(id); |
||||
|
showApplyingDrawer(); |
||||
|
}; |
||||
|
|
||||
|
const handleRemarkChange = (e) => { |
||||
|
setRemakeTextInfo(e.target.value); |
||||
|
}; |
||||
|
|
||||
|
const clickDownLoad = (record: any) => { |
||||
|
const cert_format: string = record.certificateFormat.toString().split("/")[0].replace(/\s+/g, ""); |
||||
|
// const data: Req_DownloadCert = {
|
||||
|
// id: (currentDLInfo as any)?.id,
|
||||
|
// cert_format: cert_format,
|
||||
|
// };
|
||||
|
// websitesServ.cert.downloadCertificate(data);
|
||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
|
const token = getToken(); |
||||
|
const certUrl = "http://127.0.0.1:8000/api/v1/cert"; |
||||
|
const url = `${certUrl}/apply/download?id=${(currentDLInfo as any)?.id}&cert_format=${cert_format}&token=${token}`; |
||||
|
window.open(url, "_blank"); |
||||
|
}; |
||||
|
|
||||
|
const clickDelete = (id: number) => { |
||||
|
confirm({ |
||||
|
title: "提示", |
||||
|
content: "是否要删除证书?", |
||||
|
okText: "确定", |
||||
|
okType: "danger", |
||||
|
cancelText: "取消", |
||||
|
onOk() { |
||||
|
const data: Req_DeletesCert = { ids: [id] }; |
||||
|
websitesServ.cert.deletesCertificate(data); |
||||
|
}, |
||||
|
onCancel() { |
||||
|
console.log("取消删除证书"); |
||||
|
}, |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const drawerColumns = [ |
||||
|
{ |
||||
|
title: "服务器类型", |
||||
|
dataIndex: "serverType", |
||||
|
key: "serverType", |
||||
|
}, |
||||
|
{ |
||||
|
title: "证书格式", |
||||
|
dataIndex: "certificateFormat", |
||||
|
key: "certificateFormat", |
||||
|
render: (text: any) => ( |
||||
|
<Space> |
||||
|
{text.split("/").map((format: string) => ( |
||||
|
<Button key={format} size="small" icon={<CopyOutlined />} iconPosition={"end"}> |
||||
|
{format.trim()} |
||||
|
</Button> |
||||
|
))} |
||||
|
</Space> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
title: "操作", |
||||
|
key: "canDownLoad", |
||||
|
render: (text: string, record: any) => ( |
||||
|
<Space size="middle"> |
||||
|
<Button type="link">帮助</Button> |
||||
|
<Button type="link" disabled={!record.canDownLoad} onClick={() => clickDownLoad(record)}> |
||||
|
下载 |
||||
|
</Button> |
||||
|
</Space> |
||||
|
), |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const drawerDataSource = [ |
||||
|
{ |
||||
|
key: "1", |
||||
|
serverType: "Nginx", |
||||
|
certificateFormat: "pem / key", |
||||
|
canDownLoad: true, |
||||
|
}, |
||||
|
{ |
||||
|
key: "2", |
||||
|
serverType: "Tomcat", |
||||
|
certificateFormat: "pfx", |
||||
|
canDownLoad: false, |
||||
|
}, |
||||
|
{ |
||||
|
key: "3", |
||||
|
serverType: "Apache", |
||||
|
certificateFormat: "crt / key", |
||||
|
canDownLoad: true, |
||||
|
}, |
||||
|
{ |
||||
|
key: "4", |
||||
|
serverType: "IIS", |
||||
|
certificateFormat: "pfx", |
||||
|
canDownLoad: false, |
||||
|
}, |
||||
|
{ |
||||
|
key: "5", |
||||
|
serverType: "JKS", |
||||
|
certificateFormat: "jks", |
||||
|
canDownLoad: true, |
||||
|
}, |
||||
|
{ |
||||
|
key: "6", |
||||
|
serverType: "宝塔", |
||||
|
certificateFormat: "pem / key", |
||||
|
canDownLoad: true, |
||||
|
}, |
||||
|
{ |
||||
|
key: "7", |
||||
|
serverType: "其他", |
||||
|
certificateFormat: "pem / key", |
||||
|
canDownLoad: true, |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const columns = [ |
||||
|
{ |
||||
|
title: "域名", |
||||
|
dataIndex: "primary_domain", |
||||
|
key: "primary_domain", |
||||
|
render: (text: string) => ( |
||||
|
<Tooltip title={`点击查看详情`} color={"orange"}> |
||||
|
<Button type="link" onClick={openDetails}> |
||||
|
{text} |
||||
|
</Button> |
||||
|
</Tooltip> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
title: "域名验证", |
||||
|
dataIndex: "domainVerification", |
||||
|
key: "domainVerification", |
||||
|
render: () => <CheckCircleFilled style={{ color: "green" }} />, |
||||
|
}, |
||||
|
{ |
||||
|
title: "证书品牌", |
||||
|
dataIndex: "acme_type", |
||||
|
key: "acme_type", |
||||
|
render: (text) => ( |
||||
|
<Tooltip title={`证书名字`}> |
||||
|
<span>{text}</span> |
||||
|
{/*<img src="https://placehold.co/20x20?text=Logo" alt="Let's Encrypt Logo" />{" "}*/} |
||||
|
</Tooltip> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
/* |
||||
|
const ( |
||||
|
SSLSuccess CertStatus = "success" // 成功
|
||||
|
SSLInit CertStatus = "init" // 初始化
|
||||
|
SSLError CertStatus = "error" // 错误
|
||||
|
SSLReady CertStatus = "ready" // 准备中
|
||||
|
SSLApply CertStatus = "applying" // 申请中
|
||||
|
SSLApplyError CertStatus = "applyError" // 申请失败
|
||||
|
) |
||||
|
*/ |
||||
|
title: "有效期(天)", |
||||
|
dataIndex: "validityDays", |
||||
|
key: "validityDays", |
||||
|
render: (text, record: { created_at: string; updated_at: string; expiredate: string }) => { |
||||
|
if (record) { |
||||
|
// const twoColors: ProgressProps["strokeColor"] = {
|
||||
|
// "0%": "#00ff59",
|
||||
|
// "100%": "#ff0000",
|
||||
|
// };
|
||||
|
record.created_at = !record?.created_at ? format(new Date(), "yyyy-MM-dd HH:mm:ss") : record.created_at; |
||||
|
record.expiredate = !record?.expiredate ? format(new Date(), "yyyy-MM-dd HH:mm:ss") : record.expiredate; |
||||
|
const createdDate = new Date(record.created_at.replace(/-/g, "/")); |
||||
|
const expireDateObj = new Date(record.expiredate.replace(/-/g, "/")); |
||||
|
const currentDate = new Date(); |
||||
|
const totalValidTime = expireDateObj.getTime() - createdDate.getTime(); |
||||
|
const remainingTime = expireDateObj.getTime() - currentDate.getTime(); |
||||
|
const percentage = (remainingTime / totalValidTime) * 100; |
||||
|
//const percentage = 60;
|
||||
|
let cColor = "#000000"; |
||||
|
|
||||
|
if (percentage < 50) { |
||||
|
cColor = "#00ff59"; |
||||
|
} else if (percentage >= 50 && percentage <= 75) { |
||||
|
cColor = "#00641a"; |
||||
|
} else if (percentage >= 75 && percentage <= 90) { |
||||
|
cColor = "#ffdd00"; |
||||
|
} else { |
||||
|
cColor = "#ff0000"; |
||||
|
} |
||||
|
return ( |
||||
|
<Space> |
||||
|
<Tooltip |
||||
|
title={ |
||||
|
<div> |
||||
|
创建时间:{record.created_at} |
||||
|
<br /> |
||||
|
到期时间:{record.expiredate} |
||||
|
</div> |
||||
|
} |
||||
|
> |
||||
|
<Progress |
||||
|
style={{ width: 200 }} |
||||
|
percent={percentage} |
||||
|
strokeColor={cColor} |
||||
|
size="small" |
||||
|
showInfo={false} |
||||
|
/> |
||||
|
<span>{Math.round(percentage)}/100</span> |
||||
|
</Tooltip> |
||||
|
</Space> |
||||
|
); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
title: "加密方式", |
||||
|
dataIndex: "keytype", |
||||
|
key: "keytype", |
||||
|
render: (encryptionMethod: string) => <Tag color="green">{encryptionMethod}</Tag>, |
||||
|
}, |
||||
|
{ |
||||
|
title: "状态", |
||||
|
dataIndex: "cert_status", |
||||
|
key: "cert_status", |
||||
|
render: (state, record: any) => { |
||||
|
const { id } = record; |
||||
|
if (state === "applying") { |
||||
|
return ( |
||||
|
<Tag color="blue" onClick={() => clickStateTag(id)} style={{ cursor: "pointer" }}> |
||||
|
{state} |
||||
|
</Tag> |
||||
|
); |
||||
|
} else { |
||||
|
return ( |
||||
|
<Tag color="green" onClick={() => clickStateTag(id)} style={{ cursor: "pointer" }}> |
||||
|
{state} |
||||
|
</Tag> |
||||
|
); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
title: "备注", |
||||
|
dataIndex: "remark", |
||||
|
key: "remark", |
||||
|
}, |
||||
|
{ |
||||
|
title: "操作", |
||||
|
key: "action", |
||||
|
render: (record: any) => ( |
||||
|
<Space size="middle"> |
||||
|
<a onClick={() => showEditDrawer(record)}>编辑</a> |
||||
|
<a onClick={() => showDLDrawer(record)}>下载</a> |
||||
|
<a onClick={() => clickDelete(record.id)}>删除</a> |
||||
|
</Space> |
||||
|
), |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const detailsColumns = [ |
||||
|
{ |
||||
|
title: "状态", |
||||
|
dataIndex: "status", |
||||
|
key: "status", |
||||
|
render: (text: string) => <span style={{ color: "#52c41a" }}>{text}</span>, |
||||
|
}, |
||||
|
{ |
||||
|
title: "域名", |
||||
|
dataIndex: "domain", |
||||
|
key: "domain", |
||||
|
}, |
||||
|
{ |
||||
|
title: "服务商", |
||||
|
dataIndex: "provider", |
||||
|
key: "provider", |
||||
|
render: (text: string) => <span style={{ color: "#1890ff" }}>{text}</span>, |
||||
|
}, |
||||
|
{ |
||||
|
title: "主机记录", |
||||
|
dataIndex: "hostRecord", |
||||
|
key: "hostRecord", |
||||
|
}, |
||||
|
{ |
||||
|
title: "记录类型", |
||||
|
dataIndex: "recordType", |
||||
|
key: "recordType", |
||||
|
render: (text: string) => <span style={{ color: "red" }}>{text}</span>, |
||||
|
}, |
||||
|
{ |
||||
|
title: "记录值", |
||||
|
dataIndex: "recordValue", |
||||
|
key: "recordValue", |
||||
|
}, |
||||
|
{ |
||||
|
title: "操作", |
||||
|
dataIndex: "action", |
||||
|
key: "action", |
||||
|
render: (text: string) => <Link href="#">{text}</Link>, |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const detailsData = [ |
||||
|
{ |
||||
|
key: "1", |
||||
|
status: "通过", |
||||
|
domain: "*.bidiy2.com", |
||||
|
provider: "Cloudflare", |
||||
|
hostRecord: "_acme-challenge", |
||||
|
recordType: "CNAME", |
||||
|
recordValue: "8cb0d498cecabd92.httpsok.com", |
||||
|
action: "检测命令", |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<ListPageLayout title={t(`${i18nPrefix}.apply.title`, "证书申请")}> |
||||
|
<Space style={{ marginBottom: 16 }}> |
||||
|
<Input addonBefore={<SearchOutlined />} placeholder="域名 | 备注" style={{ width: 200 }} /> |
||||
|
<Button>搜索</Button> |
||||
|
<Button type="primary" onClick={applyCertClick}> |
||||
|
免费申请证书 |
||||
|
</Button> |
||||
|
</Space> |
||||
|
<Table columns={columns} dataSource={(certListData as any)?.rows} pagination={false} /> |
||||
|
</ListPageLayout> |
||||
|
|
||||
|
<CertApplyingDrawer |
||||
|
id={certApplyingId} |
||||
|
onCloseApplyingDrawer={onCloseApplyingDrawer} |
||||
|
applyingDrawerVisible={applyingDrawerVisible} |
||||
|
/> |
||||
|
|
||||
|
<Drawer title="下载证书" placement="right" onClose={closeDLDrawer} visible={dlDrawerVisible} width={600}> |
||||
|
<Text>请根据您的服务器类型选择证书下载:</Text> |
||||
|
<Table dataSource={drawerDataSource} columns={drawerColumns} pagination={false} style={{ marginTop: 16 }} /> |
||||
|
<div style={{ backgroundColor: "#f6ffed", padding: 16, marginTop: 16 }}> |
||||
|
<Text>加入SSL证书交流群</Text> |
||||
|
<br /> |
||||
|
<Text>产品咨询 问题反馈</Text> |
||||
|
<br /> |
||||
|
<Link href="https://httpsok">请备注 httpsok</Link> |
||||
|
</div> |
||||
|
</Drawer> |
||||
|
|
||||
|
<Drawer |
||||
|
title="编辑" |
||||
|
placement="right" |
||||
|
closable={false} |
||||
|
onClose={closeEditDrawer} |
||||
|
visible={editDrawerVisible} |
||||
|
style={{ textAlign: "center" }} |
||||
|
bodyStyle={{ display: "flex", justifyContent: "center", alignItems: "center" }} |
||||
|
> |
||||
|
<Form layout="vertical" style={{ maxWidth: "400px", width: "100%" }}> |
||||
|
<Form.Item label="域名"> |
||||
|
<span>{(currentDLInfo as any)?.primary_domain}</span> |
||||
|
</Form.Item> |
||||
|
<Form.Item label="证书品牌"> |
||||
|
<span>{(currentDLInfo as any)?.acme_type}</span> |
||||
|
</Form.Item> |
||||
|
<Form.Item label="失效时间"> |
||||
|
<span>{(currentDLInfo as any)?.expiredate}</span> |
||||
|
</Form.Item> |
||||
|
<Form.Item label="创建时间"> |
||||
|
<span>{(currentDLInfo as any)?.created_at}</span> |
||||
|
</Form.Item> |
||||
|
<Form.Item label="备注"> |
||||
|
<TextArea |
||||
|
rows={4} |
||||
|
maxLength={100} |
||||
|
defaultValue={(currentDLInfo as any)?.remark} |
||||
|
onChange={handleRemarkChange} |
||||
|
/> |
||||
|
</Form.Item> |
||||
|
<Form.Item> |
||||
|
<Button onClick={closeEditDrawer} style={{ marginRight: 8 }}> |
||||
|
取消 |
||||
|
</Button> |
||||
|
<Button type="primary" onClick={() => enterEditDrawer(currentDLInfo)}> |
||||
|
确定 |
||||
|
</Button> |
||||
|
</Form.Item> |
||||
|
</Form> |
||||
|
</Drawer> |
||||
|
|
||||
|
<Modal |
||||
|
title="域名验证" |
||||
|
visible={detailsVisible} |
||||
|
onOk={openDetails} |
||||
|
onCancel={closeDetails} |
||||
|
width={1200} // 设置宽度为800px
|
||||
|
footer={[ |
||||
|
<Button key="ok" type="primary" onClick={openDetails}> |
||||
|
确定 |
||||
|
</Button>, |
||||
|
]} |
||||
|
> |
||||
|
<Text> |
||||
|
请您添加以下DNS解析记录 <Link href="#">参考文档</Link> |
||||
|
</Text> |
||||
|
<br /> |
||||
|
<Text>1. 只需要添加一次即可,添加后请勿删除记录。</Text> |
||||
|
<br /> |
||||
|
<Text> |
||||
|
2. 需等待1-2分钟。(<Text type="danger">如果长时间没通过,请联系客服解决</Text>) |
||||
|
</Text> |
||||
|
<Table columns={detailsColumns} dataSource={detailsData} pagination={false} /> |
||||
|
</Modal> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default CertificateManagement; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue