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)); | 	return processResponse(await baseGet(args, abortController)); | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function download(args: GetArgs, abortController?: AbortController) { | export async function download({ url, params }: GetArgs, filename = "download.file") { | ||||||
| 	return (await baseGet(args, abortController)).text(); | 	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 { | interface PostArgs { | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Binary } from "./responseTypes"; |  | ||||||
|  |  | ||||||
| export async function downloadCertificate(id: number): Promise<Binary> { | export async function downloadCertificate(id: number): Promise<void> { | ||||||
| 	return await api.get({ | 	await api.download( | ||||||
| 		url: `/nginx/certificates/${id}/download`, | 		{ | ||||||
| 	}); | 			url: `/nginx/certificates/${id}/download`, | ||||||
|  | 		}, | ||||||
|  | 		`certificate-${id}.zip`, | ||||||
|  | 	); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,5 +15,3 @@ export interface ValidatedCertificateResponse { | |||||||
| 	certificate: Record<string, any>; | 	certificate: Record<string, any>; | ||||||
| 	certificateKey: boolean; | 	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 "./AccessListformatter"; | ||||||
| export * from "./CertificateFormatter"; | export * from "./CertificateFormatter"; | ||||||
|  | export * from "./CertificateInUseFormatter"; | ||||||
|  | export * from "./DateFormatter"; | ||||||
| export * from "./DomainsFormatter"; | export * from "./DomainsFormatter"; | ||||||
| export * from "./EmailFormatter"; | export * from "./EmailFormatter"; | ||||||
| export * from "./EnabledFormatter"; | export * from "./EnabledFormatter"; | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ export * from "./useAccessList"; | |||||||
| export * from "./useAccessLists"; | export * from "./useAccessLists"; | ||||||
| export * from "./useAuditLog"; | export * from "./useAuditLog"; | ||||||
| export * from "./useAuditLogs"; | export * from "./useAuditLogs"; | ||||||
|  | export * from "./useCertificate"; | ||||||
| export * from "./useCertificates"; | export * from "./useCertificates"; | ||||||
| export * from "./useDeadHost"; | export * from "./useDeadHost"; | ||||||
| export * from "./useDeadHosts"; | 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.close": "Close", | ||||||
|   "action.delete": "Delete", |   "action.delete": "Delete", | ||||||
|   "action.disable": "Disable", |   "action.disable": "Disable", | ||||||
|  |   "action.download": "Download", | ||||||
|   "action.edit": "Edit", |   "action.edit": "Edit", | ||||||
|   "action.enable": "Enable", |   "action.enable": "Enable", | ||||||
|   "action.permissions": "Permissions", |   "action.permissions": "Permissions", | ||||||
|  |   "action.renew": "Renew", | ||||||
|   "action.view-details": "View Details", |   "action.view-details": "View Details", | ||||||
|   "auditlogs": "Audit Logs", |   "auditlogs": "Audit Logs", | ||||||
|   "cancel": "Cancel", |   "cancel": "Cancel", | ||||||
|   "certificate": "Certificate", |   "certificate": "Certificate", | ||||||
|  |   "certificate.in-use": "In Use", | ||||||
|   "certificate.none.subtitle": "No certificate assigned", |   "certificate.none.subtitle": "No certificate assigned", | ||||||
|   "certificate.none.subtitle.for-http": "This host will not use HTTPS", |   "certificate.none.subtitle.for-http": "This host will not use HTTPS", | ||||||
|   "certificate.none.title": "None", |   "certificate.none.title": "None", | ||||||
|  |   "certificate.not-in-use": "Not Used", | ||||||
|   "certificates": "Certificates", |   "certificates": "Certificates", | ||||||
|   "certificates.custom": "Custom Certificate", |   "certificates.custom": "Custom Certificate", | ||||||
|   "certificates.dns.credentials": "Credentials File Content", |   "certificates.dns.credentials": "Credentials File Content", | ||||||
| @@ -121,6 +125,7 @@ | |||||||
|   "notification.object-deleted": "{object} has been deleted", |   "notification.object-deleted": "{object} has been deleted", | ||||||
|   "notification.object-disabled": "{object} has been disabled", |   "notification.object-disabled": "{object} has been disabled", | ||||||
|   "notification.object-enabled": "{object} has been enabled", |   "notification.object-enabled": "{object} has been enabled", | ||||||
|  |   "notification.object-renewed": "{object} has been renewed", | ||||||
|   "notification.object-saved": "{object} has been saved", |   "notification.object-saved": "{object} has been saved", | ||||||
|   "notification.success": "Success", |   "notification.success": "Success", | ||||||
|   "object.actions-title": "{object} #{id}", |   "object.actions-title": "{object} #{id}", | ||||||
|   | |||||||
| @@ -47,6 +47,9 @@ | |||||||
| 	"action.disable": { | 	"action.disable": { | ||||||
| 		"defaultMessage": "Disable" | 		"defaultMessage": "Disable" | ||||||
| 	}, | 	}, | ||||||
|  | 	"action.download": { | ||||||
|  | 		"defaultMessage": "Download" | ||||||
|  | 	}, | ||||||
| 	"action.edit": { | 	"action.edit": { | ||||||
| 		"defaultMessage": "Edit" | 		"defaultMessage": "Edit" | ||||||
| 	}, | 	}, | ||||||
| @@ -56,6 +59,9 @@ | |||||||
| 	"action.permissions": { | 	"action.permissions": { | ||||||
| 		"defaultMessage": "Permissions" | 		"defaultMessage": "Permissions" | ||||||
| 	}, | 	}, | ||||||
|  | 	"action.renew": { | ||||||
|  | 		"defaultMessage": "Renew" | ||||||
|  | 	}, | ||||||
| 	"action.view-details": { | 	"action.view-details": { | ||||||
| 		"defaultMessage": "View Details" | 		"defaultMessage": "View Details" | ||||||
| 	}, | 	}, | ||||||
| @@ -68,6 +74,9 @@ | |||||||
| 	"certificate": { | 	"certificate": { | ||||||
| 		"defaultMessage": "Certificate" | 		"defaultMessage": "Certificate" | ||||||
| 	}, | 	}, | ||||||
|  | 	"certificate.in-use": { | ||||||
|  | 		"defaultMessage": "In Use" | ||||||
|  | 	}, | ||||||
| 	"certificate.none.subtitle": { | 	"certificate.none.subtitle": { | ||||||
| 		"defaultMessage": "No certificate assigned" | 		"defaultMessage": "No certificate assigned" | ||||||
| 	}, | 	}, | ||||||
| @@ -77,6 +86,9 @@ | |||||||
| 	"certificate.none.title": { | 	"certificate.none.title": { | ||||||
| 		"defaultMessage": "None" | 		"defaultMessage": "None" | ||||||
| 	}, | 	}, | ||||||
|  | 	"certificate.not-in-use": { | ||||||
|  | 		"defaultMessage": "Not Used" | ||||||
|  | 	}, | ||||||
| 	"certificates": { | 	"certificates": { | ||||||
| 		"defaultMessage": "Certificates" | 		"defaultMessage": "Certificates" | ||||||
| 	}, | 	}, | ||||||
| @@ -365,6 +377,9 @@ | |||||||
| 	"notification.object-enabled": { | 	"notification.object-enabled": { | ||||||
| 		"defaultMessage": "{object} has been enabled" | 		"defaultMessage": "{object} has been enabled" | ||||||
| 	}, | 	}, | ||||||
|  | 	"notification.object-renewed": { | ||||||
|  | 		"defaultMessage": "{object} has been renewed" | ||||||
|  | 	}, | ||||||
| 	"notification.object-saved": { | 	"notification.object-saved": { | ||||||
| 		"defaultMessage": "{object} has been saved" | 		"defaultMessage": "{object} has been saved" | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { IconAlertTriangle } from "@tabler/icons-react"; | import { IconAlertTriangle } from "@tabler/icons-react"; | ||||||
|  | import { useQueryClient } from "@tanstack/react-query"; | ||||||
| import EasyModal, { type InnerModalProps } from "ez-modal-react"; | import EasyModal, { type InnerModalProps } from "ez-modal-react"; | ||||||
| import { Form, Formik } from "formik"; | import { Form, Formik } from "formik"; | ||||||
| import { type ReactNode, useState } from "react"; | import { type ReactNode, useState } from "react"; | ||||||
| @@ -14,6 +15,7 @@ const showHTTPCertificateModal = () => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => { | const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => { | ||||||
|  | 	const queryClient = useQueryClient(); | ||||||
| 	const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); | 	const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); | ||||||
| 	const [isSubmitting, setIsSubmitting] = useState(false); | 	const [isSubmitting, setIsSubmitting] = useState(false); | ||||||
| 	const [domains, setDomains] = useState([] as string[]); | 	const [domains, setDomains] = useState([] as string[]); | ||||||
| @@ -32,6 +34,7 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPr | |||||||
| 		} catch (err: any) { | 		} catch (err: any) { | ||||||
| 			setErrorMsg(<T id={err.message} />); | 			setErrorMsg(<T id={err.message} />); | ||||||
| 		} | 		} | ||||||
|  | 		queryClient.invalidateQueries({ queryKey: ["certificates"] }); | ||||||
| 		setIsSubmitting(false); | 		setIsSubmitting(false); | ||||||
| 		setSubmitting(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 "./PermissionsModal"; | ||||||
| export * from "./ProxyHostModal"; | export * from "./ProxyHostModal"; | ||||||
| export * from "./RedirectionHostModal"; | export * from "./RedirectionHostModal"; | ||||||
|  | export * from "./RenewCertificateModal"; | ||||||
| export * from "./SetPasswordModal"; | export * from "./SetPasswordModal"; | ||||||
| export * from "./StreamModal"; | export * from "./StreamModal"; | ||||||
| export * from "./UserModal"; | 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 { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; | ||||||
| import { useMemo } from "react"; | import { useMemo } from "react"; | ||||||
| import type { Certificate } from "src/api/backend"; | 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 { TableLayout } from "src/components/Table/TableLayout"; | ||||||
| import { intl, T } from "src/locale"; | import { intl, T } from "src/locale"; | ||||||
| import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals"; | import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals"; | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
| 	data: Certificate[]; | 	data: Certificate[]; | ||||||
|  | 	isFiltered?: boolean; | ||||||
| 	isFetching?: 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 columnHelper = createColumnHelper<Certificate>(); | ||||||
| 	const columns = useMemo( | 	const columns = useMemo( | ||||||
| 		() => [ | 		() => [ | ||||||
| @@ -37,25 +47,35 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 				id: "provider", | 				id: "provider", | ||||||
| 				header: intl.formatMessage({ id: "column.provider" }), | 				header: intl.formatMessage({ id: "column.provider" }), | ||||||
| 				cell: (info: any) => { | 				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, { | 			columnHelper.accessor((row: any) => row.expiresOn, { | ||||||
| 				id: "expires_on", | 				id: "expiresOn", | ||||||
| 				header: intl.formatMessage({ id: "column.expires" }), | 				header: intl.formatMessage({ id: "column.expires" }), | ||||||
| 				cell: (info: any) => { | 				cell: (info: any) => { | ||||||
| 					return info.getValue(); | 					return <DateFormatter value={info.getValue()} highlightPast />; | ||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 			columnHelper.accessor((row: any) => row, { | 			columnHelper.accessor((row: any) => row, { | ||||||
| 				id: "id", | 				id: "proxyHosts", | ||||||
| 				header: intl.formatMessage({ id: "column.status" }), | 				header: intl.formatMessage({ id: "column.status" }), | ||||||
| 				cell: (info: any) => { | 				cell: (info: any) => { | ||||||
| 					return info.getValue(); | 					const r = info.getValue(); | ||||||
|  | 					return ( | ||||||
|  | 						<CertificateInUseFormatter | ||||||
|  | 							proxyHosts={r.proxyHosts} | ||||||
|  | 							redirectionHosts={r.redirectionHosts} | ||||||
|  | 							deadHosts={r.deadHosts} | ||||||
|  | 						/> | ||||||
|  | 					); | ||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 			columnHelper.display({ | 			columnHelper.display({ | ||||||
| 				id: "id", // todo: not needed for a display? | 				id: "id", | ||||||
| 				cell: (info: any) => { | 				cell: (info: any) => { | ||||||
| 					return ( | 					return ( | ||||||
| 						<span className="dropdown"> | 						<span className="dropdown"> | ||||||
| @@ -75,16 +95,37 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 										data={{ id: info.row.original.id }} | 										data={{ id: info.row.original.id }} | ||||||
| 									/> | 									/> | ||||||
| 								</span> | 								</span> | ||||||
| 								<a className="dropdown-item" href="#"> | 								<a | ||||||
| 									<IconEdit size={16} /> | 									className="dropdown-item" | ||||||
| 									<T id="action.edit" /> | 									href="#" | ||||||
|  | 									onClick={(e) => { | ||||||
|  | 										e.preventDefault(); | ||||||
|  | 										onRenew?.(info.row.original.id); | ||||||
|  | 									}} | ||||||
|  | 								> | ||||||
|  | 									<IconRefresh size={16} /> | ||||||
|  | 									<T id="action.renew" /> | ||||||
| 								</a> | 								</a> | ||||||
| 								<a className="dropdown-item" href="#"> | 								<a | ||||||
| 									<IconPower size={16} /> | 									className="dropdown-item" | ||||||
| 									<T id="action.disable" /> | 									href="#" | ||||||
|  | 									onClick={(e) => { | ||||||
|  | 										e.preventDefault(); | ||||||
|  | 										onDownload?.(info.row.original.id); | ||||||
|  | 									}} | ||||||
|  | 								> | ||||||
|  | 									<IconDownload size={16} /> | ||||||
|  | 									<T id="action.download" /> | ||||||
| 								</a> | 								</a> | ||||||
| 								<div className="dropdown-divider" /> | 								<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} /> | 									<IconTrash size={16} /> | ||||||
| 									<T id="action.delete" /> | 									<T id="action.delete" /> | ||||||
| 								</a> | 								</a> | ||||||
| @@ -97,7 +138,7 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 		], | 		], | ||||||
| 		[columnHelper], | 		[columnHelper, onDelete, onRenew, onDownload], | ||||||
| 	); | 	); | ||||||
|  |  | ||||||
| 	const tableInstance = useReactTable<Certificate>({ | 	const tableInstance = useReactTable<Certificate>({ | ||||||
| @@ -160,8 +201,7 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 					object="certificate" | 					object="certificate" | ||||||
| 					objects="certificates" | 					objects="certificates" | ||||||
| 					tableInstance={tableInstance} | 					tableInstance={tableInstance} | ||||||
| 					// onNew={onNew} | 					isFiltered={isFiltered} | ||||||
| 					// isFiltered={isFiltered} |  | ||||||
| 					color="pink" | 					color="pink" | ||||||
| 					customAddBtn={customAddBtn} | 					customAddBtn={customAddBtn} | ||||||
| 				/> | 				/> | ||||||
|   | |||||||
| @@ -1,12 +1,22 @@ | |||||||
| import { IconSearch } from "@tabler/icons-react"; | import { IconSearch } from "@tabler/icons-react"; | ||||||
|  | import { useState } from "react"; | ||||||
| import Alert from "react-bootstrap/Alert"; | import Alert from "react-bootstrap/Alert"; | ||||||
|  | import { deleteCertificate, downloadCertificate } from "src/api/backend"; | ||||||
| import { LoadingPage } from "src/components"; | import { LoadingPage } from "src/components"; | ||||||
| import { useCertificates } from "src/hooks"; | import { useCertificates } from "src/hooks"; | ||||||
| import { T } from "src/locale"; | 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"; | import Table from "./Table"; | ||||||
|  |  | ||||||
| export default function TableWrapper() { | export default function TableWrapper() { | ||||||
|  | 	const [search, setSearch] = useState(""); | ||||||
| 	const { isFetching, isLoading, isError, error, data } = useCertificates([ | 	const { isFetching, isLoading, isError, error, data } = useCertificates([ | ||||||
| 		"owner", | 		"owner", | ||||||
| 		"dead_hosts", | 		"dead_hosts", | ||||||
| @@ -22,6 +32,31 @@ export default function TableWrapper() { | |||||||
| 		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; | 		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 ( | 	return ( | ||||||
| 		<div className="card mt-4"> | 		<div className="card mt-4"> | ||||||
| 			<div className="card-status-top bg-pink" /> | 			<div className="card-status-top bg-pink" /> | ||||||
| @@ -33,66 +68,83 @@ export default function TableWrapper() { | |||||||
| 								<T id="certificates" /> | 								<T id="certificates" /> | ||||||
| 							</h2> | 							</h2> | ||||||
| 						</div> | 						</div> | ||||||
| 						<div className="col-md-auto col-sm-12"> | 						{data?.length ? ( | ||||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> | 							<div className="col-md-auto col-sm-12"> | ||||||
| 								<div className="input-group input-group-flat w-auto"> | 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||||
| 									<span className="input-group-text input-group-text-sm"> | 									<div className="input-group input-group-flat w-auto"> | ||||||
| 										<IconSearch size={16} /> | 										<span className="input-group-text input-group-text-sm"> | ||||||
| 									</span> | 											<IconSearch size={16} /> | ||||||
| 									<input | 										</span> | ||||||
| 										id="advanced-table-search" | 										<input | ||||||
| 										type="text" | 											id="advanced-table-search" | ||||||
| 										className="form-control form-control-sm" | 											type="text" | ||||||
| 										autoComplete="off" | 											className="form-control form-control-sm" | ||||||
| 									/> | 											autoComplete="off" | ||||||
| 								</div> | 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||||
| 								<div className="dropdown"> | 										/> | ||||||
| 									<button | 									</div> | ||||||
| 										type="button" | 									<div className="dropdown"> | ||||||
| 										className="btn btn-sm dropdown-toggle btn-pink mt-1" | 										<button | ||||||
| 										data-bs-toggle="dropdown" | 											type="button" | ||||||
| 									> | 											className="btn btn-sm dropdown-toggle btn-pink mt-1" | ||||||
| 										<T id="object.add" tData={{ object: "certificate" }} /> | 											data-bs-toggle="dropdown" | ||||||
| 									</button> |  | ||||||
| 									<div className="dropdown-menu"> |  | ||||||
| 										<a |  | ||||||
| 											className="dropdown-item" |  | ||||||
| 											href="#" |  | ||||||
| 											onClick={(e) => { |  | ||||||
| 												e.preventDefault(); |  | ||||||
| 												showHTTPCertificateModal(); |  | ||||||
| 											}} |  | ||||||
| 										> | 										> | ||||||
| 											<T id="lets-encrypt-via-http" /> | 											<T id="object.add" tData={{ object: "certificate" }} /> | ||||||
| 										</a> | 										</button> | ||||||
| 										<a | 										<div className="dropdown-menu"> | ||||||
| 											className="dropdown-item" | 											<a | ||||||
| 											href="#" | 												className="dropdown-item" | ||||||
| 											onClick={(e) => { | 												href="#" | ||||||
| 												e.preventDefault(); | 												onClick={(e) => { | ||||||
| 												showDNSCertificateModal(); | 													e.preventDefault(); | ||||||
| 											}} | 													showHTTPCertificateModal(); | ||||||
| 										> | 												}} | ||||||
| 											<T id="lets-encrypt-via-dns" /> | 											> | ||||||
| 										</a> | 												<T id="lets-encrypt-via-http" /> | ||||||
| 										<div className="dropdown-divider" /> | 											</a> | ||||||
| 										<a | 											<a | ||||||
| 											className="dropdown-item" | 												className="dropdown-item" | ||||||
| 											href="#" | 												href="#" | ||||||
| 											onClick={(e) => { | 												onClick={(e) => { | ||||||
| 												e.preventDefault(); | 													e.preventDefault(); | ||||||
| 												showCustomCertificateModal(); | 													showDNSCertificateModal(); | ||||||
| 											}} | 												}} | ||||||
| 										> | 											> | ||||||
| 											<T id="certificates.custom" /> | 												<T id="lets-encrypt-via-dns" /> | ||||||
| 										</a> | 											</a> | ||||||
|  | 											<div className="dropdown-divider" /> | ||||||
|  | 											<a | ||||||
|  | 												className="dropdown-item" | ||||||
|  | 												href="#" | ||||||
|  | 												onClick={(e) => { | ||||||
|  | 													e.preventDefault(); | ||||||
|  | 													showCustomCertificateModal(); | ||||||
|  | 												}} | ||||||
|  | 											> | ||||||
|  | 												<T id="certificates.custom" /> | ||||||
|  | 											</a> | ||||||
|  | 										</div> | ||||||
| 									</div> | 									</div> | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						) : null} | ||||||
| 					</div> | 					</div> | ||||||
| 				</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> | ||||||
| 		</div> | 		</div> | ||||||
| 	); | 	); | ||||||
|   | |||||||
| @@ -89,10 +89,10 @@ export default function TableWrapper() { | |||||||
| 					onEdit={(id: number) => showProxyHostModal(id)} | 					onEdit={(id: number) => showProxyHostModal(id)} | ||||||
| 					onDelete={(id: number) => | 					onDelete={(id: number) => | ||||||
| 						showDeleteConfirmModal({ | 						showDeleteConfirmModal({ | ||||||
| 							title: "proxy-host.delete.title", | 							title: <T id="object.delete" tData={{ object: "proxy-host" }} />, | ||||||
| 							onConfirm: () => handleDelete(id), | 							onConfirm: () => handleDelete(id), | ||||||
| 							invalidations: [["proxy-hosts"], ["proxy-host", 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} | 					onDisableToggle={handleDisableToggle} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user