mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00:00 
			
		
		
		
	Certificates react work
- renewal and download - table columns rendering - searching - deleting
This commit is contained in:
		| @@ -80,8 +80,16 @@ export async function get(args: GetArgs, abortController?: AbortController) { | ||||
| 	return processResponse(await baseGet(args, abortController)); | ||||
| } | ||||
|  | ||||
| export async function download(args: GetArgs, abortController?: AbortController) { | ||||
| 	return (await baseGet(args, abortController)).text(); | ||||
| export async function download({ url, params }: GetArgs, filename = "download.file") { | ||||
| 	const headers = buildAuthHeader(); | ||||
| 	const res = await fetch(buildUrl({ url, params }), { headers }); | ||||
| 	const bl = await res.blob(); | ||||
| 	const u = window.URL.createObjectURL(bl); | ||||
| 	const a = document.createElement("a"); | ||||
| 	a.href = u; | ||||
| 	a.download = filename; | ||||
| 	a.click(); | ||||
| 	window.URL.revokeObjectURL(url); | ||||
| } | ||||
|  | ||||
| interface PostArgs { | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Binary } from "./responseTypes"; | ||||
|  | ||||
| export async function downloadCertificate(id: number): Promise<Binary> { | ||||
| 	return await api.get({ | ||||
| export async function downloadCertificate(id: number): Promise<void> { | ||||
| 	await api.download( | ||||
| 		{ | ||||
| 			url: `/nginx/certificates/${id}/download`, | ||||
| 	}); | ||||
| 		}, | ||||
| 		`certificate-${id}.zip`, | ||||
| 	); | ||||
| } | ||||
|   | ||||
| @@ -15,5 +15,3 @@ export interface ValidatedCertificateResponse { | ||||
| 	certificate: Record<string, any>; | ||||
| 	certificateKey: boolean; | ||||
| } | ||||
|  | ||||
| export type Binary = number & { readonly __brand: unique symbol }; | ||||
|   | ||||
| @@ -0,0 +1,62 @@ | ||||
| import OverlayTrigger from "react-bootstrap/OverlayTrigger"; | ||||
| import Popover from "react-bootstrap/Popover"; | ||||
| import type { DeadHost, ProxyHost, RedirectionHost } from "src/api/backend"; | ||||
| import { T } from "src/locale"; | ||||
|  | ||||
| const getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | DeadHost[]) => { | ||||
| 	if (items.length === 0) { | ||||
| 		return null; | ||||
| 	} | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<div> | ||||
| 				<strong> | ||||
| 					<T id={title} /> | ||||
| 				</strong> | ||||
| 			</div> | ||||
| 			{items.map((host) => ( | ||||
| 				<div key={host.id} className="ms-1"> | ||||
| 					{host.domainNames.join(", ")} | ||||
| 				</div> | ||||
| 			))} | ||||
| 		</> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| interface Props { | ||||
| 	proxyHosts: ProxyHost[]; | ||||
| 	redirectionHosts: RedirectionHost[]; | ||||
| 	deadHosts: DeadHost[]; | ||||
| } | ||||
| export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts }: Props) { | ||||
| 	const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length; | ||||
| 	if (totalCount === 0) { | ||||
| 		return ( | ||||
| 			<span className="badge bg-red-lt"> | ||||
| 				<T id="certificate.not-in-use" /> | ||||
| 			</span> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	proxyHosts.sort(); | ||||
| 	redirectionHosts.sort(); | ||||
| 	deadHosts.sort(); | ||||
|  | ||||
| 	const popover = ( | ||||
| 		<Popover id="popover-basic"> | ||||
| 			<Popover.Body> | ||||
| 				{getSection("proxy-hosts", proxyHosts)} | ||||
| 				{getSection("redirection-hosts", redirectionHosts)} | ||||
| 				{getSection("dead-hosts", deadHosts)} | ||||
| 			</Popover.Body> | ||||
| 		</Popover> | ||||
| 	); | ||||
|  | ||||
| 	return ( | ||||
| 		<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}> | ||||
| 			<span className="badge bg-lime-lt"> | ||||
| 				<T id="certificate.in-use" /> | ||||
| 			</span> | ||||
| 		</OverlayTrigger> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/components/Table/Formatter/DateFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/components/Table/Formatter/DateFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import cn from "classnames"; | ||||
| import { isPast, parseISO } from "date-fns"; | ||||
| import { DateTimeFormat } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	value: string; | ||||
| 	highlightPast?: boolean; | ||||
| } | ||||
| export function DateFormatter({ value, highlightPast }: Props) { | ||||
| 	const dateIsPast = isPast(parseISO(value)); | ||||
| 	const cl = cn({ | ||||
| 		"text-danger": highlightPast && dateIsPast, | ||||
| 	}); | ||||
| 	return <span className={cl}>{DateTimeFormat(value)}</span>; | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| export * from "./AccessListformatter"; | ||||
| export * from "./CertificateFormatter"; | ||||
| export * from "./CertificateInUseFormatter"; | ||||
| export * from "./DateFormatter"; | ||||
| export * from "./DomainsFormatter"; | ||||
| export * from "./EmailFormatter"; | ||||
| export * from "./EnabledFormatter"; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ export * from "./useAccessList"; | ||||
| export * from "./useAccessLists"; | ||||
| export * from "./useAuditLog"; | ||||
| export * from "./useAuditLogs"; | ||||
| export * from "./useCertificate"; | ||||
| export * from "./useCertificates"; | ||||
| export * from "./useDeadHost"; | ||||
| export * from "./useDeadHosts"; | ||||
|   | ||||
							
								
								
									
										17
									
								
								frontend/src/hooks/useCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/hooks/useCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type Certificate, getCertificate } from "src/api/backend"; | ||||
|  | ||||
| const fetchCertificate = (id: number) => { | ||||
| 	return getCertificate(id, ["owner"]); | ||||
| }; | ||||
|  | ||||
| const useCertificate = (id: number, options = {}) => { | ||||
| 	return useQuery<Certificate, Error>({ | ||||
| 		queryKey: ["certificate", id], | ||||
| 		queryFn: () => fetchCertificate(id), | ||||
| 		staleTime: 60 * 1000, // 1 minute | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { useCertificate }; | ||||
| @@ -15,16 +15,20 @@ | ||||
|   "action.close": "Close", | ||||
|   "action.delete": "Delete", | ||||
|   "action.disable": "Disable", | ||||
|   "action.download": "Download", | ||||
|   "action.edit": "Edit", | ||||
|   "action.enable": "Enable", | ||||
|   "action.permissions": "Permissions", | ||||
|   "action.renew": "Renew", | ||||
|   "action.view-details": "View Details", | ||||
|   "auditlogs": "Audit Logs", | ||||
|   "cancel": "Cancel", | ||||
|   "certificate": "Certificate", | ||||
|   "certificate.in-use": "In Use", | ||||
|   "certificate.none.subtitle": "No certificate assigned", | ||||
|   "certificate.none.subtitle.for-http": "This host will not use HTTPS", | ||||
|   "certificate.none.title": "None", | ||||
|   "certificate.not-in-use": "Not Used", | ||||
|   "certificates": "Certificates", | ||||
|   "certificates.custom": "Custom Certificate", | ||||
|   "certificates.dns.credentials": "Credentials File Content", | ||||
| @@ -121,6 +125,7 @@ | ||||
|   "notification.object-deleted": "{object} has been deleted", | ||||
|   "notification.object-disabled": "{object} has been disabled", | ||||
|   "notification.object-enabled": "{object} has been enabled", | ||||
|   "notification.object-renewed": "{object} has been renewed", | ||||
|   "notification.object-saved": "{object} has been saved", | ||||
|   "notification.success": "Success", | ||||
|   "object.actions-title": "{object} #{id}", | ||||
|   | ||||
| @@ -47,6 +47,9 @@ | ||||
| 	"action.disable": { | ||||
| 		"defaultMessage": "Disable" | ||||
| 	}, | ||||
| 	"action.download": { | ||||
| 		"defaultMessage": "Download" | ||||
| 	}, | ||||
| 	"action.edit": { | ||||
| 		"defaultMessage": "Edit" | ||||
| 	}, | ||||
| @@ -56,6 +59,9 @@ | ||||
| 	"action.permissions": { | ||||
| 		"defaultMessage": "Permissions" | ||||
| 	}, | ||||
| 	"action.renew": { | ||||
| 		"defaultMessage": "Renew" | ||||
| 	}, | ||||
| 	"action.view-details": { | ||||
| 		"defaultMessage": "View Details" | ||||
| 	}, | ||||
| @@ -68,6 +74,9 @@ | ||||
| 	"certificate": { | ||||
| 		"defaultMessage": "Certificate" | ||||
| 	}, | ||||
| 	"certificate.in-use": { | ||||
| 		"defaultMessage": "In Use" | ||||
| 	}, | ||||
| 	"certificate.none.subtitle": { | ||||
| 		"defaultMessage": "No certificate assigned" | ||||
| 	}, | ||||
| @@ -77,6 +86,9 @@ | ||||
| 	"certificate.none.title": { | ||||
| 		"defaultMessage": "None" | ||||
| 	}, | ||||
| 	"certificate.not-in-use": { | ||||
| 		"defaultMessage": "Not Used" | ||||
| 	}, | ||||
| 	"certificates": { | ||||
| 		"defaultMessage": "Certificates" | ||||
| 	}, | ||||
| @@ -365,6 +377,9 @@ | ||||
| 	"notification.object-enabled": { | ||||
| 		"defaultMessage": "{object} has been enabled" | ||||
| 	}, | ||||
| 	"notification.object-renewed": { | ||||
| 		"defaultMessage": "{object} has been renewed" | ||||
| 	}, | ||||
| 	"notification.object-saved": { | ||||
| 		"defaultMessage": "{object} has been saved" | ||||
| 	}, | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { IconAlertTriangle } from "@tabler/icons-react"; | ||||
| import { useQueryClient } from "@tanstack/react-query"; | ||||
| import EasyModal, { type InnerModalProps } from "ez-modal-react"; | ||||
| import { Form, Formik } from "formik"; | ||||
| import { type ReactNode, useState } from "react"; | ||||
| @@ -14,6 +15,7 @@ const showHTTPCertificateModal = () => { | ||||
| }; | ||||
|  | ||||
| const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => { | ||||
| 	const queryClient = useQueryClient(); | ||||
| 	const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); | ||||
| 	const [isSubmitting, setIsSubmitting] = useState(false); | ||||
| 	const [domains, setDomains] = useState([] as string[]); | ||||
| @@ -32,6 +34,7 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPr | ||||
| 		} catch (err: any) { | ||||
| 			setErrorMsg(<T id={err.message} />); | ||||
| 		} | ||||
| 		queryClient.invalidateQueries({ queryKey: ["certificates"] }); | ||||
| 		setIsSubmitting(false); | ||||
| 		setSubmitting(false); | ||||
| 	}; | ||||
|   | ||||
							
								
								
									
										74
									
								
								frontend/src/modals/RenewCertificateModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/src/modals/RenewCertificateModal.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import { useQueryClient } from "@tanstack/react-query"; | ||||
| import EasyModal, { type InnerModalProps } from "ez-modal-react"; | ||||
| import { type ReactNode, useEffect, useState } from "react"; | ||||
| import { Alert } from "react-bootstrap"; | ||||
| import Modal from "react-bootstrap/Modal"; | ||||
| import { renewCertificate } from "src/api/backend"; | ||||
| import { Button, Loading } from "src/components"; | ||||
| import { useCertificate } from "src/hooks"; | ||||
| import { T } from "src/locale"; | ||||
| import { showObjectSuccess } from "src/notifications"; | ||||
|  | ||||
| interface Props extends InnerModalProps { | ||||
| 	id: number; | ||||
| } | ||||
|  | ||||
| const showRenewCertificateModal = (id: number) => { | ||||
| 	EasyModal.show(RenewCertificateModal, { id }); | ||||
| }; | ||||
|  | ||||
| const RenewCertificateModal = EasyModal.create(({ id, visible, remove }: Props) => { | ||||
| 	const queryClient = useQueryClient(); | ||||
| 	const { data, isLoading, error } = useCertificate(id); | ||||
| 	const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); | ||||
| 	const [isFresh, setIsFresh] = useState(true); | ||||
| 	const [isSubmitting, setIsSubmitting] = useState(false); | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		if (!data || !isFresh || isSubmitting) return; | ||||
| 		setIsFresh(false); | ||||
| 		setIsSubmitting(true); | ||||
|  | ||||
| 		renewCertificate(id) | ||||
| 			.then(() => { | ||||
| 				showObjectSuccess("certificate", "renewed"); | ||||
| 				queryClient.invalidateQueries({ queryKey: ["certificates"] }); | ||||
| 				remove(); | ||||
| 			}) | ||||
| 			.catch((err: any) => { | ||||
| 				setErrorMsg(<T id={err.message} />); | ||||
| 			}) | ||||
| 			.finally(() => { | ||||
| 				setIsSubmitting(false); | ||||
| 			}); | ||||
| 	}, [id, data, isFresh, isSubmitting, remove, queryClient.invalidateQueries]); | ||||
|  | ||||
| 	return ( | ||||
| 		<Modal show={visible} onHide={isSubmitting ? undefined : remove}> | ||||
| 			<Modal.Header closeButton={!isSubmitting}> | ||||
| 				<Modal.Title> | ||||
| 					<T id="renew-certificate" /> | ||||
| 				</Modal.Title> | ||||
| 			</Modal.Header> | ||||
| 			<Modal.Body> | ||||
| 				<Alert variant="danger" show={!!errorMsg}> | ||||
| 					{errorMsg} | ||||
| 				</Alert> | ||||
| 				{isLoading && <Loading noLogo />} | ||||
| 				{!isLoading && error && ( | ||||
| 					<Alert variant="danger" className="m-3"> | ||||
| 						{error?.message || "Unknown error"} | ||||
| 					</Alert> | ||||
| 				)} | ||||
| 				{data && isSubmitting && !errorMsg ? <p className="text-center mt-3">Please wait ...</p> : null} | ||||
| 			</Modal.Body> | ||||
| 			<Modal.Footer> | ||||
| 				<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}> | ||||
| 					<T id="action.close" /> | ||||
| 				</Button> | ||||
| 			</Modal.Footer> | ||||
| 		</Modal> | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| export { showRenewCertificateModal }; | ||||
| @@ -9,6 +9,7 @@ export * from "./HTTPCertificateModal"; | ||||
| export * from "./PermissionsModal"; | ||||
| export * from "./ProxyHostModal"; | ||||
| export * from "./RedirectionHostModal"; | ||||
| export * from "./RenewCertificateModal"; | ||||
| export * from "./SetPasswordModal"; | ||||
| export * from "./StreamModal"; | ||||
| export * from "./UserModal"; | ||||
|   | ||||
| @@ -1,17 +1,27 @@ | ||||
| import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react"; | ||||
| import { IconDotsVertical, IconDownload, IconRefresh, IconTrash } from "@tabler/icons-react"; | ||||
| import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; | ||||
| import { useMemo } from "react"; | ||||
| import type { Certificate } from "src/api/backend"; | ||||
| import { DomainsFormatter, EmptyData, GravatarFormatter } from "src/components"; | ||||
| import { | ||||
| 	CertificateInUseFormatter, | ||||
| 	DateFormatter, | ||||
| 	DomainsFormatter, | ||||
| 	EmptyData, | ||||
| 	GravatarFormatter, | ||||
| } from "src/components"; | ||||
| import { TableLayout } from "src/components/Table/TableLayout"; | ||||
| import { intl, T } from "src/locale"; | ||||
| import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals"; | ||||
|  | ||||
| interface Props { | ||||
| 	data: Certificate[]; | ||||
| 	isFiltered?: boolean; | ||||
| 	isFetching?: boolean; | ||||
| 	onDelete?: (id: number) => void; | ||||
| 	onRenew?: (id: number) => void; | ||||
| 	onDownload?: (id: number) => void; | ||||
| } | ||||
| export default function Table({ data, isFetching }: Props) { | ||||
| export default function Table({ data, isFetching, onDelete, onRenew, onDownload, isFiltered }: Props) { | ||||
| 	const columnHelper = createColumnHelper<Certificate>(); | ||||
| 	const columns = useMemo( | ||||
| 		() => [ | ||||
| @@ -37,25 +47,35 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 				id: "provider", | ||||
| 				header: intl.formatMessage({ id: "column.provider" }), | ||||
| 				cell: (info: any) => { | ||||
| 					return info.getValue(); | ||||
| 					if (info.getValue() === "letsencrypt") { | ||||
| 						return <T id="lets-encrypt" />; | ||||
| 					} | ||||
| 					return <T id={info.getValue()} />; | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: any) => row.expires_on, { | ||||
| 				id: "expires_on", | ||||
| 			columnHelper.accessor((row: any) => row.expiresOn, { | ||||
| 				id: "expiresOn", | ||||
| 				header: intl.formatMessage({ id: "column.expires" }), | ||||
| 				cell: (info: any) => { | ||||
| 					return info.getValue(); | ||||
| 					return <DateFormatter value={info.getValue()} highlightPast />; | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: any) => row, { | ||||
| 				id: "id", | ||||
| 				id: "proxyHosts", | ||||
| 				header: intl.formatMessage({ id: "column.status" }), | ||||
| 				cell: (info: any) => { | ||||
| 					return info.getValue(); | ||||
| 					const r = info.getValue(); | ||||
| 					return ( | ||||
| 						<CertificateInUseFormatter | ||||
| 							proxyHosts={r.proxyHosts} | ||||
| 							redirectionHosts={r.redirectionHosts} | ||||
| 							deadHosts={r.deadHosts} | ||||
| 						/> | ||||
| 					); | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.display({ | ||||
| 				id: "id", // todo: not needed for a display? | ||||
| 				id: "id", | ||||
| 				cell: (info: any) => { | ||||
| 					return ( | ||||
| 						<span className="dropdown"> | ||||
| @@ -75,16 +95,37 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 										data={{ id: info.row.original.id }} | ||||
| 									/> | ||||
| 								</span> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 									<IconEdit size={16} /> | ||||
| 									<T id="action.edit" /> | ||||
| 								<a | ||||
| 									className="dropdown-item" | ||||
| 									href="#" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										onRenew?.(info.row.original.id); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconRefresh size={16} /> | ||||
| 									<T id="action.renew" /> | ||||
| 								</a> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 									<IconPower size={16} /> | ||||
| 									<T id="action.disable" /> | ||||
| 								<a | ||||
| 									className="dropdown-item" | ||||
| 									href="#" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										onDownload?.(info.row.original.id); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconDownload size={16} /> | ||||
| 									<T id="action.download" /> | ||||
| 								</a> | ||||
| 								<div className="dropdown-divider" /> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 								<a | ||||
| 									className="dropdown-item" | ||||
| 									href="#" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										onDelete?.(info.row.original.id); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconTrash size={16} /> | ||||
| 									<T id="action.delete" /> | ||||
| 								</a> | ||||
| @@ -97,7 +138,7 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 				}, | ||||
| 			}), | ||||
| 		], | ||||
| 		[columnHelper], | ||||
| 		[columnHelper, onDelete, onRenew, onDownload], | ||||
| 	); | ||||
|  | ||||
| 	const tableInstance = useReactTable<Certificate>({ | ||||
| @@ -160,8 +201,7 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 					object="certificate" | ||||
| 					objects="certificates" | ||||
| 					tableInstance={tableInstance} | ||||
| 					// onNew={onNew} | ||||
| 					// isFiltered={isFiltered} | ||||
| 					isFiltered={isFiltered} | ||||
| 					color="pink" | ||||
| 					customAddBtn={customAddBtn} | ||||
| 				/> | ||||
|   | ||||
| @@ -1,12 +1,22 @@ | ||||
| import { IconSearch } from "@tabler/icons-react"; | ||||
| import { useState } from "react"; | ||||
| import Alert from "react-bootstrap/Alert"; | ||||
| import { deleteCertificate, downloadCertificate } from "src/api/backend"; | ||||
| import { LoadingPage } from "src/components"; | ||||
| import { useCertificates } from "src/hooks"; | ||||
| import { T } from "src/locale"; | ||||
| import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals"; | ||||
| import { | ||||
| 	showCustomCertificateModal, | ||||
| 	showDeleteConfirmModal, | ||||
| 	showDNSCertificateModal, | ||||
| 	showHTTPCertificateModal, | ||||
| 	showRenewCertificateModal, | ||||
| } from "src/modals"; | ||||
| import { showError, showObjectSuccess } from "src/notifications"; | ||||
| import Table from "./Table"; | ||||
|  | ||||
| export default function TableWrapper() { | ||||
| 	const [search, setSearch] = useState(""); | ||||
| 	const { isFetching, isLoading, isError, error, data } = useCertificates([ | ||||
| 		"owner", | ||||
| 		"dead_hosts", | ||||
| @@ -22,6 +32,31 @@ export default function TableWrapper() { | ||||
| 		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; | ||||
| 	} | ||||
|  | ||||
| 	const handleDelete = async (id: number) => { | ||||
| 		await deleteCertificate(id); | ||||
| 		showObjectSuccess("certificate", "deleted"); | ||||
| 	}; | ||||
|  | ||||
| 	const handleDownload = async (id: number) => { | ||||
| 		try { | ||||
| 			await downloadCertificate(id); | ||||
| 		} catch (err: any) { | ||||
| 			showError(err.message); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	let filtered = null; | ||||
| 	if (search && data) { | ||||
| 		filtered = data?.filter( | ||||
| 			(item) => | ||||
| 				item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) || | ||||
| 				item.niceName.toLowerCase().includes(search), | ||||
| 		); | ||||
| 	} else if (search !== "") { | ||||
| 		// this can happen if someone deletes the last item while searching | ||||
| 		setSearch(""); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-pink" /> | ||||
| @@ -33,6 +68,7 @@ export default function TableWrapper() { | ||||
| 								<T id="certificates" /> | ||||
| 							</h2> | ||||
| 						</div> | ||||
| 						{data?.length ? ( | ||||
| 							<div className="col-md-auto col-sm-12"> | ||||
| 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 									<div className="input-group input-group-flat w-auto"> | ||||
| @@ -44,6 +80,7 @@ export default function TableWrapper() { | ||||
| 											type="text" | ||||
| 											className="form-control form-control-sm" | ||||
| 											autoComplete="off" | ||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||
| 										/> | ||||
| 									</div> | ||||
| 									<div className="dropdown"> | ||||
| @@ -90,9 +127,24 @@ export default function TableWrapper() { | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						) : null} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table data={data ?? []} isFetching={isFetching} /> | ||||
| 				<Table | ||||
| 					data={filtered ?? data ?? []} | ||||
| 					isFiltered={!!search} | ||||
| 					isFetching={isFetching} | ||||
| 					onRenew={showRenewCertificateModal} | ||||
| 					onDownload={handleDownload} | ||||
| 					onDelete={(id: number) => | ||||
| 						showDeleteConfirmModal({ | ||||
| 							title: <T id="object.delete" tData={{ object: "certificate" }} />, | ||||
| 							onConfirm: () => handleDelete(id), | ||||
| 							invalidations: [["certificates"], ["certificate", id]], | ||||
| 							children: <T id="object.delete.content" tData={{ object: "certificate" }} />, | ||||
| 						}) | ||||
| 					} | ||||
| 				/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
|   | ||||
| @@ -89,10 +89,10 @@ export default function TableWrapper() { | ||||
| 					onEdit={(id: number) => showProxyHostModal(id)} | ||||
| 					onDelete={(id: number) => | ||||
| 						showDeleteConfirmModal({ | ||||
| 							title: "proxy-host.delete.title", | ||||
| 							title: <T id="object.delete" tData={{ object: "proxy-host" }} />, | ||||
| 							onConfirm: () => handleDelete(id), | ||||
| 							invalidations: [["proxy-hosts"], ["proxy-host", id]], | ||||
| 							children: <T id="proxy-host.delete.content" />, | ||||
| 							children: <T id="object.delete.content" tData={{ object: "proxy-host" }} />, | ||||
| 						}) | ||||
| 					} | ||||
| 					onDisableToggle={handleDisableToggle} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user