You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 18:05:34 +03:00 
			
		
		
		
	Certificates react table basis
This commit is contained in:
		| @@ -1,7 +1,9 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function getCertificates(expand?: string[], params = {}): Promise<Certificate[]> { | ||||
| export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts"; | ||||
|  | ||||
| export async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise<Certificate[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/certificates", | ||||
| 		params: { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| export * from "./useAccessLists"; | ||||
| export * from "./useAuditLog"; | ||||
| export * from "./useAuditLogs"; | ||||
| export * from "./useCertificates"; | ||||
| export * from "./useDeadHosts"; | ||||
| export * from "./useHealth"; | ||||
| export * from "./useHostReport"; | ||||
|   | ||||
							
								
								
									
										17
									
								
								frontend/src/hooks/useCertificates.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/hooks/useCertificates.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type Certificate, type CertificateExpansion, getCertificates } from "src/api/backend"; | ||||
|  | ||||
| const fetchCertificates = (expand?: CertificateExpansion[]) => { | ||||
| 	return getCertificates(expand); | ||||
| }; | ||||
|  | ||||
| const useCertificates = (expand?: CertificateExpansion[], options = {}) => { | ||||
| 	return useQuery<Certificate[], Error>({ | ||||
| 		queryKey: ["certificates", { expand }], | ||||
| 		queryFn: () => fetchCertificates(expand), | ||||
| 		staleTime: 60 * 1000, | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { fetchCertificates, useCertificates }; | ||||
| @@ -15,6 +15,10 @@ | ||||
|   "action.view-details": "View Details", | ||||
|   "auditlog.title": "Audit Log", | ||||
|   "cancel": "Cancel", | ||||
|   "certificates.actions-title": "Certificate #{id}", | ||||
|   "certificates.add": "Add Certificate", | ||||
|   "certificates.custom": "Custom Certificate", | ||||
|   "certificates.empty": "There are no Certificates", | ||||
|   "certificates.title": "SSL Certificates", | ||||
|   "close": "Close", | ||||
|   "column.access": "Access", | ||||
| @@ -22,10 +26,12 @@ | ||||
|   "column.destination": "Destination", | ||||
|   "column.email": "Email", | ||||
|   "column.event": "Event", | ||||
|   "column.expires": "Expires", | ||||
|   "column.http-code": "Access", | ||||
|   "column.incoming-port": "Incoming Port", | ||||
|   "column.name": "Name", | ||||
|   "column.protocol": "Protocol", | ||||
|   "column.provider": "Provider", | ||||
|   "column.roles": "Roles", | ||||
|   "column.satisfy": "Satisfy", | ||||
|   "column.scheme": "Scheme", | ||||
|   | ||||
| @@ -47,6 +47,18 @@ | ||||
| 	"cancel": { | ||||
| 		"defaultMessage": "Cancel" | ||||
| 	}, | ||||
| 	"certificates.actions-title": { | ||||
| 		"defaultMessage": "Certificate #{id}" | ||||
| 	}, | ||||
| 	"certificates.add": { | ||||
| 		"defaultMessage": "Add Certificate" | ||||
| 	}, | ||||
| 	"certificates.custom": { | ||||
| 		"defaultMessage": "Custom Certificate" | ||||
| 	}, | ||||
| 	"certificates.empty": { | ||||
| 		"defaultMessage": "There are no Certificates" | ||||
| 	}, | ||||
| 	"certificates.title": { | ||||
| 		"defaultMessage": "SSL Certificates" | ||||
| 	}, | ||||
| @@ -71,6 +83,9 @@ | ||||
| 	"column.event": { | ||||
| 		"defaultMessage": "Event" | ||||
| 	}, | ||||
| 	"column.expires": { | ||||
| 		"defaultMessage": "Expires" | ||||
| 	}, | ||||
| 	"column.http-code": { | ||||
| 		"defaultMessage": "Access" | ||||
| 	}, | ||||
| @@ -83,6 +98,9 @@ | ||||
| 	"column.protocol": { | ||||
| 		"defaultMessage": "Protocol" | ||||
| 	}, | ||||
| 	"column.provider": { | ||||
| 		"defaultMessage": "Provider" | ||||
| 	}, | ||||
| 	"column.roles": { | ||||
| 		"defaultMessage": "Roles" | ||||
| 	}, | ||||
|   | ||||
							
								
								
									
										36
									
								
								frontend/src/pages/Certificates/Empty.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/src/pages/Certificates/Empty.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import type { Table as ReactTable } from "@tanstack/react-table"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| /** | ||||
|  * This component should never render as there should always be 1 user minimum, | ||||
|  * but I'm keeping it for consistency. | ||||
|  */ | ||||
|  | ||||
| interface Props { | ||||
| 	tableInstance: ReactTable<any>; | ||||
| } | ||||
| export default function Empty({ tableInstance }: Props) { | ||||
| 	return ( | ||||
| 		<tr> | ||||
| 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | ||||
| 				<div className="text-center my-4"> | ||||
| 					<h2>{intl.formatMessage({ id: "certificates.empty" })}</h2> | ||||
| 					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p> | ||||
| 					<div className="dropdown"> | ||||
| 						<button type="button" className="btn dropdown-toggle btn-pink my-3" data-bs-toggle="dropdown"> | ||||
| 							{intl.formatMessage({ id: "certificates.add" })} | ||||
| 						</button> | ||||
| 						<div className="dropdown-menu"> | ||||
| 							<a className="dropdown-item" href="#"> | ||||
| 								{intl.formatMessage({ id: "lets-encrypt" })} | ||||
| 							</a> | ||||
| 							<a className="dropdown-item" href="#"> | ||||
| 								{intl.formatMessage({ id: "certificates.custom" })} | ||||
| 							</a> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</td> | ||||
| 		</tr> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										116
									
								
								frontend/src/pages/Certificates/Table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								frontend/src/pages/Certificates/Table.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| import { IconDotsVertical, IconEdit, IconPower, 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, GravatarFormatter } from "src/components"; | ||||
| import { TableLayout } from "src/components/Table/TableLayout"; | ||||
| import { intl } from "src/locale"; | ||||
| import Empty from "./Empty"; | ||||
|  | ||||
| interface Props { | ||||
| 	data: Certificate[]; | ||||
| 	isFetching?: boolean; | ||||
| } | ||||
| export default function Table({ data, isFetching }: Props) { | ||||
| 	const columnHelper = createColumnHelper<Certificate>(); | ||||
| 	const columns = useMemo( | ||||
| 		() => [ | ||||
| 			columnHelper.accessor((row: any) => row.owner, { | ||||
| 				id: "owner", | ||||
| 				cell: (info: any) => { | ||||
| 					const value = info.getValue(); | ||||
| 					return <GravatarFormatter url={value.avatar} name={value.name} />; | ||||
| 				}, | ||||
| 				meta: { | ||||
| 					className: "w-1", | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: any) => row, { | ||||
| 				id: "domainNames", | ||||
| 				header: intl.formatMessage({ id: "column.name" }), | ||||
| 				cell: (info: any) => { | ||||
| 					const value = info.getValue(); | ||||
| 					return <DomainsFormatter domains={value.domainNames} createdOn={value.createdOn} />; | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: any) => row.provider, { | ||||
| 				id: "provider", | ||||
| 				header: intl.formatMessage({ id: "column.provider" }), | ||||
| 				cell: (info: any) => { | ||||
| 					return info.getValue(); | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: any) => row.expires_on, { | ||||
| 				id: "expires_on", | ||||
| 				header: intl.formatMessage({ id: "column.expires" }), | ||||
| 				cell: (info: any) => { | ||||
| 					return info.getValue(); | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: any) => row, { | ||||
| 				id: "id", | ||||
| 				header: intl.formatMessage({ id: "column.status" }), | ||||
| 				cell: (info: any) => { | ||||
| 					return info.getValue(); | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.display({ | ||||
| 				id: "id", // todo: not needed for a display? | ||||
| 				cell: (info: any) => { | ||||
| 					return ( | ||||
| 						<span className="dropdown"> | ||||
| 							<button | ||||
| 								type="button" | ||||
| 								className="btn dropdown-toggle btn-action btn-sm px-1" | ||||
| 								data-bs-boundary="viewport" | ||||
| 								data-bs-toggle="dropdown" | ||||
| 							> | ||||
| 								<IconDotsVertical /> | ||||
| 							</button> | ||||
| 							<div className="dropdown-menu dropdown-menu-end"> | ||||
| 								<span className="dropdown-header"> | ||||
| 									{intl.formatMessage( | ||||
| 										{ | ||||
| 											id: "certificates.actions-title", | ||||
| 										}, | ||||
| 										{ id: info.row.original.id }, | ||||
| 									)} | ||||
| 								</span> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 									<IconEdit size={16} /> | ||||
| 									{intl.formatMessage({ id: "action.edit" })} | ||||
| 								</a> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 									<IconPower size={16} /> | ||||
| 									{intl.formatMessage({ id: "action.disable" })} | ||||
| 								</a> | ||||
| 								<div className="dropdown-divider" /> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 									<IconTrash size={16} /> | ||||
| 									{intl.formatMessage({ id: "action.delete" })} | ||||
| 								</a> | ||||
| 							</div> | ||||
| 						</span> | ||||
| 					); | ||||
| 				}, | ||||
| 				meta: { | ||||
| 					className: "text-end w-1", | ||||
| 				}, | ||||
| 			}), | ||||
| 		], | ||||
| 		[columnHelper], | ||||
| 	); | ||||
|  | ||||
| 	const tableInstance = useReactTable<Certificate>({ | ||||
| 		columns, | ||||
| 		data, | ||||
| 		getCoreRowModel: getCoreRowModel(), | ||||
| 		rowCount: data.length, | ||||
| 		meta: { | ||||
| 			isFetching, | ||||
| 		}, | ||||
| 		enableSortingRemoval: false, | ||||
| 	}); | ||||
|  | ||||
| 	return <TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} />} />; | ||||
| } | ||||
							
								
								
									
										71
									
								
								frontend/src/pages/Certificates/TableWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								frontend/src/pages/Certificates/TableWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import { IconSearch } from "@tabler/icons-react"; | ||||
| import Alert from "react-bootstrap/Alert"; | ||||
| import { LoadingPage } from "src/components"; | ||||
| import { useCertificates } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
| import Table from "./Table"; | ||||
|  | ||||
| export default function TableWrapper() { | ||||
| 	const { isFetching, isLoading, isError, error, data } = useCertificates([ | ||||
| 		"owner", | ||||
| 		"dead_hosts", | ||||
| 		"proxy_hosts", | ||||
| 		"redirection_hosts", | ||||
| 	]); | ||||
|  | ||||
| 	if (isLoading) { | ||||
| 		return <LoadingPage />; | ||||
| 	} | ||||
|  | ||||
| 	if (isError) { | ||||
| 		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-pink" /> | ||||
| 			<div className="card-table"> | ||||
| 				<div className="card-header"> | ||||
| 					<div className="row w-full"> | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "certificates.title" })}</h2> | ||||
| 						</div> | ||||
| 						<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"> | ||||
| 									<span className="input-group-text input-group-text-sm"> | ||||
| 										<IconSearch size={16} /> | ||||
| 									</span> | ||||
| 									<input | ||||
| 										id="advanced-table-search" | ||||
| 										type="text" | ||||
| 										className="form-control form-control-sm" | ||||
| 										autoComplete="off" | ||||
| 									/> | ||||
| 								</div> | ||||
| 								<div className="dropdown"> | ||||
| 									<button | ||||
| 										type="button" | ||||
| 										className="btn btn-sm dropdown-toggle btn-pink mt-1" | ||||
| 										data-bs-toggle="dropdown" | ||||
| 									> | ||||
| 										{intl.formatMessage({ id: "certificates.add" })} | ||||
| 									</button> | ||||
| 									<div className="dropdown-menu"> | ||||
| 										<a className="dropdown-item" href="#"> | ||||
| 											{intl.formatMessage({ id: "lets-encrypt" })} | ||||
| 										</a> | ||||
| 										<a className="dropdown-item" href="#"> | ||||
| 											{intl.formatMessage({ id: "certificates.custom" })} | ||||
| 										</a> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table data={data ?? []} isFetching={isFetching} /> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { HasPermission } from "src/components"; | ||||
| import CertificateTable from "./CertificateTable"; | ||||
| import TableWrapper from "./TableWrapper"; | ||||
|  | ||||
| const Certificates = () => { | ||||
| 	return ( | ||||
| 		<HasPermission permission="certificates" type="view" pageLoading loadingNoLogo> | ||||
| 			<CertificateTable /> | ||||
| 			<TableWrapper /> | ||||
| 		</HasPermission> | ||||
| 	); | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user