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 
			
		
		
		
	Proxy host modal basis, other improvements
This commit is contained in:
		| @@ -422,7 +422,6 @@ const internalProxyHost = { | |||||||
| 	 */ | 	 */ | ||||||
| 	getAll: async (access, expand, searchQuery) => { | 	getAll: async (access, expand, searchQuery) => { | ||||||
| 		const accessData = await access.can("proxy_hosts:list"); | 		const accessData = await access.can("proxy_hosts:list"); | ||||||
|  |  | ||||||
| 		const query = proxyHostModel | 		const query = proxyHostModel | ||||||
| 			.query() | 			.query() | ||||||
| 			.where("is_deleted", 0) | 			.where("is_deleted", 0) | ||||||
| @@ -446,11 +445,9 @@ const internalProxyHost = { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		const rows = await query.then(utils.omitRows(omissions())); | 		const rows = await query.then(utils.omitRows(omissions())); | ||||||
|  |  | ||||||
| 		if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { | 		if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { | ||||||
| 			return internalHost.cleanAllRowsCertificateMeta(rows); | 			return internalHost.cleanAllRowsCertificateMeta(rows); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return rows; | 		return rows; | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ export default function (tokenString) { | |||||||
| 						const rows = await query; | 						const rows = await query; | ||||||
| 						objects = []; | 						objects = []; | ||||||
| 						_.forEach(rows, (ruleRow) => { | 						_.forEach(rows, (ruleRow) => { | ||||||
| 							result.push(ruleRow.id); | 							objects.push(ruleRow.id); | ||||||
| 						}); | 						}); | ||||||
|  |  | ||||||
| 						// enum should not have less than 1 item | 						// enum should not have less than 1 item | ||||||
|   | |||||||
| @@ -70,3 +70,7 @@ | |||||||
| 	font-family: 'Courier New', Courier, monospace !important; | 	font-family: 'Courier New', Courier, monospace !important; | ||||||
| 	resize: vertical; | 	resize: vertical; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | label.row { | ||||||
|  | 	cursor: pointer; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -103,6 +103,7 @@ export interface ProxyHost { | |||||||
| 	modifiedOn: string; | 	modifiedOn: string; | ||||||
| 	ownerUserId: number; | 	ownerUserId: number; | ||||||
| 	domainNames: string[]; | 	domainNames: string[]; | ||||||
|  | 	forwardScheme: string; | ||||||
| 	forwardHost: string; | 	forwardHost: string; | ||||||
| 	forwardPort: number; | 	forwardPort: number; | ||||||
| 	accessListId: number; | 	accessListId: number; | ||||||
| @@ -114,9 +115,8 @@ export interface ProxyHost { | |||||||
| 	meta: Record<string, any>; | 	meta: Record<string, any>; | ||||||
| 	allowWebsocketUpgrade: boolean; | 	allowWebsocketUpgrade: boolean; | ||||||
| 	http2Support: boolean; | 	http2Support: boolean; | ||||||
| 	forwardScheme: string; |  | ||||||
| 	enabled: boolean; | 	enabled: boolean; | ||||||
| 	locations: string[]; // todo: string or object? | 	locations?: string[]; // todo: string or object? | ||||||
| 	hstsEnabled: boolean; | 	hstsEnabled: boolean; | ||||||
| 	hstsSubdomains: boolean; | 	hstsSubdomains: boolean; | ||||||
| 	// Expansions: | 	// Expansions: | ||||||
|   | |||||||
| @@ -7,8 +7,9 @@ interface Props { | |||||||
| 	forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields | 	forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields | ||||||
| 	forceDNSForNew?: boolean; | 	forceDNSForNew?: boolean; | ||||||
| 	requireDomainNames?: boolean; // used for streams | 	requireDomainNames?: boolean; // used for streams | ||||||
|  | 	color?: string; | ||||||
| } | } | ||||||
| export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomainNames }: Props) { | export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomainNames, color = "bg-cyan" }: Props) { | ||||||
| 	const { values, setFieldValue } = useFormikContext(); | 	const { values, setFieldValue } = useFormikContext(); | ||||||
| 	const v: any = values || {}; | 	const v: any = values || {}; | ||||||
|  |  | ||||||
| @@ -31,7 +32,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const toggleClasses = "form-check-input"; | 	const toggleClasses = "form-check-input"; | ||||||
| 	const toggleEnabled = cn(toggleClasses, "bg-cyan"); | 	const toggleEnabled = cn(toggleClasses, color); | ||||||
|  |  | ||||||
| 	const getHttpOptions = () => ( | 	const getHttpOptions = () => ( | ||||||
| 		<div> | 		<div> | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ export * from "./useDeadHosts"; | |||||||
| export * from "./useDnsProviders"; | export * from "./useDnsProviders"; | ||||||
| export * from "./useHealth"; | export * from "./useHealth"; | ||||||
| export * from "./useHostReport"; | export * from "./useHostReport"; | ||||||
|  | export * from "./useProxyHost"; | ||||||
| export * from "./useProxyHosts"; | export * from "./useProxyHosts"; | ||||||
| export * from "./useRedirectionHost"; | export * from "./useRedirectionHost"; | ||||||
| export * from "./useRedirectionHosts"; | export * from "./useRedirectionHosts"; | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								frontend/src/hooks/useProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								frontend/src/hooks/useProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||||||
|  | import { createProxyHost, getProxyHost, type ProxyHost, updateProxyHost } from "src/api/backend"; | ||||||
|  |  | ||||||
|  | const fetchProxyHost = (id: number | "new") => { | ||||||
|  | 	if (id === "new") { | ||||||
|  | 		return Promise.resolve({ | ||||||
|  | 			id: 0, | ||||||
|  | 			createdOn: "", | ||||||
|  | 			modifiedOn: "", | ||||||
|  | 			ownerUserId: 0, | ||||||
|  | 			domainNames: [], | ||||||
|  | 			forwardHost: "", | ||||||
|  | 			forwardPort: 0, | ||||||
|  | 			accessListId: 0, | ||||||
|  | 			certificateId: 0, | ||||||
|  | 			sslForced: false, | ||||||
|  | 			cachingEnabled: false, | ||||||
|  | 			blockExploits: false, | ||||||
|  | 			advancedConfig: "", | ||||||
|  | 			meta: {}, | ||||||
|  | 			allowWebsocketUpgrade: false, | ||||||
|  | 			http2Support: false, | ||||||
|  | 			forwardScheme: "", | ||||||
|  | 			enabled: true, | ||||||
|  | 			hstsEnabled: false, | ||||||
|  | 			hstsSubdomains: false, | ||||||
|  | 		} as ProxyHost); | ||||||
|  | 	} | ||||||
|  | 	return getProxyHost(id, ["owner"]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const useProxyHost = (id: number | "new", options = {}) => { | ||||||
|  | 	return useQuery<ProxyHost, Error>({ | ||||||
|  | 		queryKey: ["proxy-host", id], | ||||||
|  | 		queryFn: () => fetchProxyHost(id), | ||||||
|  | 		staleTime: 60 * 1000, // 1 minute | ||||||
|  | 		...options, | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const useSetProxyHost = () => { | ||||||
|  | 	const queryClient = useQueryClient(); | ||||||
|  | 	return useMutation({ | ||||||
|  | 		mutationFn: (values: ProxyHost) => (values.id ? updateProxyHost(values) : createProxyHost(values)), | ||||||
|  | 		onMutate: (values: ProxyHost) => { | ||||||
|  | 			if (!values.id) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			const previousObject = queryClient.getQueryData(["proxy-host", values.id]); | ||||||
|  | 			queryClient.setQueryData(["proxy-host", values.id], (old: ProxyHost) => ({ | ||||||
|  | 				...old, | ||||||
|  | 				...values, | ||||||
|  | 			})); | ||||||
|  | 			return () => queryClient.setQueryData(["proxy-host", values.id], previousObject); | ||||||
|  | 		}, | ||||||
|  | 		onError: (_, __, rollback: any) => rollback(), | ||||||
|  | 		onSuccess: async ({ id }: ProxyHost) => { | ||||||
|  | 			queryClient.invalidateQueries({ queryKey: ["proxy-host", id] }); | ||||||
|  | 			queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] }); | ||||||
|  | 			queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); | ||||||
|  | 		}, | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export { useProxyHost, useSetProxyHost }; | ||||||
| @@ -23,6 +23,7 @@ | |||||||
|   "close": "Close", |   "close": "Close", | ||||||
|   "column.access": "Access", |   "column.access": "Access", | ||||||
|   "column.authorization": "Authorization", |   "column.authorization": "Authorization", | ||||||
|  |   "column.custom-locations": "Custom Locations", | ||||||
|   "column.destination": "Destination", |   "column.destination": "Destination", | ||||||
|   "column.details": "Details", |   "column.details": "Details", | ||||||
|   "column.email": "Email", |   "column.email": "Email", | ||||||
| @@ -88,9 +89,13 @@ | |||||||
|   "event.updated-user": "Updated User", |   "event.updated-user": "Updated User", | ||||||
|   "footer.github-fork": "Fork me on Github", |   "footer.github-fork": "Fork me on Github", | ||||||
|   "host.flags.block-exploits": "Block Common Exploits", |   "host.flags.block-exploits": "Block Common Exploits", | ||||||
|  |   "host.flags.cache-assets": "Cache Assets", | ||||||
|   "host.flags.preserve-path": "Preserve Path", |   "host.flags.preserve-path": "Preserve Path", | ||||||
|   "host.flags.protocols": "Protocols", |   "host.flags.protocols": "Protocols", | ||||||
|   "host.flags.title": "Options", |   "host.flags.title": "Options", | ||||||
|  |   "host.flags.websockets-upgrade": "Websockets Support", | ||||||
|  |   "host.forward-port": "Forward Port", | ||||||
|  |   "host.forward-scheme": "Scheme", | ||||||
|   "hosts.title": "Hosts", |   "hosts.title": "Hosts", | ||||||
|   "http-only": "HTTP Only", |   "http-only": "HTTP Only", | ||||||
|   "lets-encrypt": "Let's Encrypt", |   "lets-encrypt": "Let's Encrypt", | ||||||
| @@ -128,13 +133,14 @@ | |||||||
|   "permissions.visibility.all": "All Items", |   "permissions.visibility.all": "All Items", | ||||||
|   "permissions.visibility.title": "Item Visibility", |   "permissions.visibility.title": "Item Visibility", | ||||||
|   "permissions.visibility.user": "Created Items Only", |   "permissions.visibility.user": "Created Items Only", | ||||||
|  |   "proxy-host.forward-host": "Forward Hostname / IP", | ||||||
|  |   "proxy-host.new": "New Proxy Host", | ||||||
|   "proxy-hosts.actions-title": "Proxy Host #{id}", |   "proxy-hosts.actions-title": "Proxy Host #{id}", | ||||||
|   "proxy-hosts.add": "Add Proxy Host", |   "proxy-hosts.add": "Add Proxy Host", | ||||||
|   "proxy-hosts.count": "{count} Proxy Hosts", |   "proxy-hosts.count": "{count} Proxy Hosts", | ||||||
|   "proxy-hosts.empty": "There are no Proxy Hosts", |   "proxy-hosts.empty": "There are no Proxy Hosts", | ||||||
|   "proxy-hosts.title": "Proxy Hosts", |   "proxy-hosts.title": "Proxy Hosts", | ||||||
|   "redirect-host.forward-domain": "Forward Domain", |   "redirection-host.forward-domain": "Forward Domain", | ||||||
|   "redirect-host.forward-scheme": "Scheme", |  | ||||||
|   "redirection-host.new": "New Redirection Host", |   "redirection-host.new": "New Redirection Host", | ||||||
|   "redirection-hosts.actions-title": "Redirection Host #{id}", |   "redirection-hosts.actions-title": "Redirection Host #{id}", | ||||||
|   "redirection-hosts.add": "Add Redirection Host", |   "redirection-hosts.add": "Add Redirection Host", | ||||||
| @@ -152,7 +158,6 @@ | |||||||
|   "stream.delete.content": "Are you sure you want to delete this Stream?", |   "stream.delete.content": "Are you sure you want to delete this Stream?", | ||||||
|   "stream.delete.title": "Delete Stream", |   "stream.delete.title": "Delete Stream", | ||||||
|   "stream.forward-host": "Forward Host", |   "stream.forward-host": "Forward Host", | ||||||
|   "stream.forward-port": "Forward Port", |  | ||||||
|   "stream.incoming-port": "Incoming Port", |   "stream.incoming-port": "Incoming Port", | ||||||
|   "stream.new": "New Stream", |   "stream.new": "New Stream", | ||||||
|   "streams.actions-title": "Stream #{id}", |   "streams.actions-title": "Stream #{id}", | ||||||
|   | |||||||
| @@ -71,6 +71,9 @@ | |||||||
| 	"column.authorization": { | 	"column.authorization": { | ||||||
| 		"defaultMessage": "Authorization" | 		"defaultMessage": "Authorization" | ||||||
| 	}, | 	}, | ||||||
|  | 	"column.custom-locations": { | ||||||
|  | 		"defaultMessage": "Custom Locations" | ||||||
|  | 	}, | ||||||
| 	"column.destination": { | 	"column.destination": { | ||||||
| 		"defaultMessage": "Destination" | 		"defaultMessage": "Destination" | ||||||
| 	}, | 	}, | ||||||
| @@ -266,6 +269,9 @@ | |||||||
| 	"host.flags.block-exploits": { | 	"host.flags.block-exploits": { | ||||||
| 		"defaultMessage": "Block Common Exploits" | 		"defaultMessage": "Block Common Exploits" | ||||||
| 	}, | 	}, | ||||||
|  | 	"host.flags.cache-assets": { | ||||||
|  | 		"defaultMessage": "Cache Assets" | ||||||
|  | 	}, | ||||||
| 	"host.flags.preserve-path": { | 	"host.flags.preserve-path": { | ||||||
| 		"defaultMessage": "Preserve Path" | 		"defaultMessage": "Preserve Path" | ||||||
| 	}, | 	}, | ||||||
| @@ -275,6 +281,15 @@ | |||||||
| 	"host.flags.title": { | 	"host.flags.title": { | ||||||
| 		"defaultMessage": "Options" | 		"defaultMessage": "Options" | ||||||
| 	}, | 	}, | ||||||
|  | 	"host.flags.websockets-upgrade": { | ||||||
|  | 		"defaultMessage": "Websockets Support" | ||||||
|  | 	}, | ||||||
|  | 	"host.forward-port": { | ||||||
|  | 		"defaultMessage": "Forward Port" | ||||||
|  | 	}, | ||||||
|  | 	"host.forward-scheme": { | ||||||
|  | 		"defaultMessage": "Scheme" | ||||||
|  | 	}, | ||||||
| 	"hosts.title": { | 	"hosts.title": { | ||||||
| 		"defaultMessage": "Hosts" | 		"defaultMessage": "Hosts" | ||||||
| 	}, | 	}, | ||||||
| @@ -386,6 +401,12 @@ | |||||||
| 	"permissions.visibility.user": { | 	"permissions.visibility.user": { | ||||||
| 		"defaultMessage": "Created Items Only" | 		"defaultMessage": "Created Items Only" | ||||||
| 	}, | 	}, | ||||||
|  | 	"proxy-host.forward-host": { | ||||||
|  | 		"defaultMessage": "Forward Hostname / IP" | ||||||
|  | 	}, | ||||||
|  | 	"proxy-host.new": { | ||||||
|  | 		"defaultMessage": "New Proxy Host" | ||||||
|  | 	}, | ||||||
| 	"proxy-hosts.actions-title": { | 	"proxy-hosts.actions-title": { | ||||||
| 		"defaultMessage": "Proxy Host #{id}" | 		"defaultMessage": "Proxy Host #{id}" | ||||||
| 	}, | 	}, | ||||||
| @@ -401,12 +422,9 @@ | |||||||
| 	"proxy-hosts.title": { | 	"proxy-hosts.title": { | ||||||
| 		"defaultMessage": "Proxy Hosts" | 		"defaultMessage": "Proxy Hosts" | ||||||
| 	}, | 	}, | ||||||
| 	"redirect-host.forward-domain": { | 	"redirection-host.forward-domain": { | ||||||
| 		"defaultMessage": "Forward Domain" | 		"defaultMessage": "Forward Domain" | ||||||
| 	}, | 	}, | ||||||
| 	"redirect-host.forward-scheme": { |  | ||||||
| 		"defaultMessage": "Scheme" |  | ||||||
| 	}, |  | ||||||
| 	"redirection-host.new": { | 	"redirection-host.new": { | ||||||
| 		"defaultMessage": "New Redirection Host" | 		"defaultMessage": "New Redirection Host" | ||||||
| 	}, | 	}, | ||||||
| @@ -458,9 +476,6 @@ | |||||||
| 	"stream.forward-host": { | 	"stream.forward-host": { | ||||||
| 		"defaultMessage": "Forward Host" | 		"defaultMessage": "Forward Host" | ||||||
| 	}, | 	}, | ||||||
| 	"stream.forward-port": { |  | ||||||
| 		"defaultMessage": "Forward Port" |  | ||||||
| 	}, |  | ||||||
| 	"stream.incoming-port": { | 	"stream.incoming-port": { | ||||||
| 		"defaultMessage": "Incoming Port" | 		"defaultMessage": "Incoming Port" | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -136,7 +136,7 @@ export function DeadHostModal({ id, onClose }: Props) { | |||||||
| 													label="ssl-certificate" | 													label="ssl-certificate" | ||||||
| 													allowNew | 													allowNew | ||||||
| 												/> | 												/> | ||||||
| 												<SSLOptionsFields /> | 												<SSLOptionsFields color="bg-red" /> | ||||||
| 											</div> | 											</div> | ||||||
| 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||||
| 												<NginxConfigField /> | 												<NginxConfigField /> | ||||||
| @@ -152,7 +152,7 @@ export function DeadHostModal({ id, onClose }: Props) { | |||||||
| 								<Button | 								<Button | ||||||
| 									type="submit" | 									type="submit" | ||||||
| 									actionType="primary" | 									actionType="primary" | ||||||
| 									className="ms-auto" | 									className="ms-auto bg-red" | ||||||
| 									data-bs-dismiss="modal" | 									data-bs-dismiss="modal" | ||||||
| 									isLoading={isSubmitting} | 									isLoading={isSubmitting} | ||||||
| 									disabled={isSubmitting} | 									disabled={isSubmitting} | ||||||
|   | |||||||
							
								
								
									
										377
									
								
								frontend/src/modals/ProxyHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										377
									
								
								frontend/src/modals/ProxyHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,377 @@ | |||||||
|  | import { IconSettings } from "@tabler/icons-react"; | ||||||
|  | import cn from "classnames"; | ||||||
|  | import { Field, Form, Formik } from "formik"; | ||||||
|  | import { useState } from "react"; | ||||||
|  | import { Alert } from "react-bootstrap"; | ||||||
|  | import Modal from "react-bootstrap/Modal"; | ||||||
|  | import { | ||||||
|  | 	Button, | ||||||
|  | 	DomainNamesField, | ||||||
|  | 	Loading, | ||||||
|  | 	NginxConfigField, | ||||||
|  | 	SSLCertificateField, | ||||||
|  | 	SSLOptionsFields, | ||||||
|  | } from "src/components"; | ||||||
|  | import { useProxyHost, useSetProxyHost } from "src/hooks"; | ||||||
|  | import { intl } from "src/locale"; | ||||||
|  | import { validateNumber, validateString } from "src/modules/Validations"; | ||||||
|  | import { showSuccess } from "src/notifications"; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  | 	id: number | "new"; | ||||||
|  | 	onClose: () => void; | ||||||
|  | } | ||||||
|  | export function ProxyHostModal({ id, onClose }: Props) { | ||||||
|  | 	const { data, isLoading, error } = useProxyHost(id); | ||||||
|  | 	const { mutate: setProxyHost } = useSetProxyHost(); | ||||||
|  | 	const [errorMsg, setErrorMsg] = useState<string | null>(null); | ||||||
|  | 	const [isSubmitting, setIsSubmitting] = useState(false); | ||||||
|  |  | ||||||
|  | 	const onSubmit = async (values: any, { setSubmitting }: any) => { | ||||||
|  | 		if (isSubmitting) return; | ||||||
|  | 		setIsSubmitting(true); | ||||||
|  | 		setErrorMsg(null); | ||||||
|  |  | ||||||
|  | 		const { ...payload } = { | ||||||
|  | 			id: id === "new" ? undefined : id, | ||||||
|  | 			...values, | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		setProxyHost(payload, { | ||||||
|  | 			onError: (err: any) => setErrorMsg(err.message), | ||||||
|  | 			onSuccess: () => { | ||||||
|  | 				showSuccess(intl.formatMessage({ id: "notification.proxy-host-saved" })); | ||||||
|  | 				onClose(); | ||||||
|  | 			}, | ||||||
|  | 			onSettled: () => { | ||||||
|  | 				setIsSubmitting(false); | ||||||
|  | 				setSubmitting(false); | ||||||
|  | 			}, | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	return ( | ||||||
|  | 		<Modal show onHide={onClose} animation={false}> | ||||||
|  | 			{!isLoading && error && ( | ||||||
|  | 				<Alert variant="danger" className="m-3"> | ||||||
|  | 					{error?.message || "Unknown error"} | ||||||
|  | 				</Alert> | ||||||
|  | 			)} | ||||||
|  | 			{isLoading && <Loading noLogo />} | ||||||
|  | 			{!isLoading && data && ( | ||||||
|  | 				<Formik | ||||||
|  | 					initialValues={ | ||||||
|  | 						{ | ||||||
|  | 							// Details tab | ||||||
|  | 							domainNames: data?.domainNames || [], | ||||||
|  | 							forwardScheme: data?.forwardScheme || "http", | ||||||
|  | 							forwardHost: data?.forwardHost || "", | ||||||
|  | 							forwardPort: data?.forwardPort || undefined, | ||||||
|  | 							accessListId: data?.accessListId || 0, | ||||||
|  | 							cachingEnabled: data?.cachingEnabled || false, | ||||||
|  | 							blockExploits: data?.blockExploits || false, | ||||||
|  | 							allowWebsocketUpgrade: data?.allowWebsocketUpgrade || false, | ||||||
|  | 							// Locations tab | ||||||
|  | 							locations: data?.locations || [], | ||||||
|  | 							// SSL tab | ||||||
|  | 							certificateId: data?.certificateId || 0, | ||||||
|  | 							sslForced: data?.sslForced || false, | ||||||
|  | 							http2Support: data?.http2Support || false, | ||||||
|  | 							hstsEnabled: data?.hstsEnabled || false, | ||||||
|  | 							hstsSubdomains: data?.hstsSubdomains || false, | ||||||
|  | 							// Advanced tab | ||||||
|  | 							advancedConfig: data?.advancedConfig || "", | ||||||
|  | 							meta: data?.meta || {}, | ||||||
|  | 						} as any | ||||||
|  | 					} | ||||||
|  | 					onSubmit={onSubmit} | ||||||
|  | 				> | ||||||
|  | 					{() => ( | ||||||
|  | 						<Form> | ||||||
|  | 							<Modal.Header closeButton> | ||||||
|  | 								<Modal.Title> | ||||||
|  | 									{intl.formatMessage({ | ||||||
|  | 										id: data?.id ? "proxy-host.edit" : "proxy-host.new", | ||||||
|  | 									})} | ||||||
|  | 								</Modal.Title> | ||||||
|  | 							</Modal.Header> | ||||||
|  | 							<Modal.Body className="p-0"> | ||||||
|  | 								<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible> | ||||||
|  | 									{errorMsg} | ||||||
|  | 								</Alert> | ||||||
|  |  | ||||||
|  | 								<div className="card m-0 border-0"> | ||||||
|  | 									<div className="card-header"> | ||||||
|  | 										<ul className="nav nav-tabs card-header-tabs" data-bs-toggle="tabs"> | ||||||
|  | 											<li className="nav-item" role="presentation"> | ||||||
|  | 												<a | ||||||
|  | 													href="#tab-details" | ||||||
|  | 													className="nav-link active" | ||||||
|  | 													data-bs-toggle="tab" | ||||||
|  | 													aria-selected="true" | ||||||
|  | 													role="tab" | ||||||
|  | 												> | ||||||
|  | 													{intl.formatMessage({ id: "column.details" })} | ||||||
|  | 												</a> | ||||||
|  | 											</li> | ||||||
|  | 											<li className="nav-item" role="presentation"> | ||||||
|  | 												<a | ||||||
|  | 													href="#tab-locations" | ||||||
|  | 													className="nav-link" | ||||||
|  | 													data-bs-toggle="tab" | ||||||
|  | 													aria-selected="false" | ||||||
|  | 													tabIndex={-1} | ||||||
|  | 													role="tab" | ||||||
|  | 												> | ||||||
|  | 													{intl.formatMessage({ id: "column.custom-locations" })} | ||||||
|  | 												</a> | ||||||
|  | 											</li> | ||||||
|  | 											<li className="nav-item" role="presentation"> | ||||||
|  | 												<a | ||||||
|  | 													href="#tab-ssl" | ||||||
|  | 													className="nav-link" | ||||||
|  | 													data-bs-toggle="tab" | ||||||
|  | 													aria-selected="false" | ||||||
|  | 													tabIndex={-1} | ||||||
|  | 													role="tab" | ||||||
|  | 												> | ||||||
|  | 													{intl.formatMessage({ id: "column.ssl" })} | ||||||
|  | 												</a> | ||||||
|  | 											</li> | ||||||
|  | 											<li className="nav-item ms-auto" role="presentation"> | ||||||
|  | 												<a | ||||||
|  | 													href="#tab-advanced" | ||||||
|  | 													className="nav-link" | ||||||
|  | 													title="Settings" | ||||||
|  | 													data-bs-toggle="tab" | ||||||
|  | 													aria-selected="false" | ||||||
|  | 													tabIndex={-1} | ||||||
|  | 													role="tab" | ||||||
|  | 												> | ||||||
|  | 													<IconSettings size={20} /> | ||||||
|  | 												</a> | ||||||
|  | 											</li> | ||||||
|  | 										</ul> | ||||||
|  | 									</div> | ||||||
|  | 									<div className="card-body"> | ||||||
|  | 										<div className="tab-content"> | ||||||
|  | 											<div className="tab-pane active show" id="tab-details" role="tabpanel"> | ||||||
|  | 												<DomainNamesField isWildcardPermitted /> | ||||||
|  | 												<div className="row"> | ||||||
|  | 													<div className="col-md-3"> | ||||||
|  | 														<Field name="forwardScheme"> | ||||||
|  | 															{({ field, form }: any) => ( | ||||||
|  | 																<div className="mb-3"> | ||||||
|  | 																	<label | ||||||
|  | 																		className="form-label" | ||||||
|  | 																		htmlFor="forwardScheme" | ||||||
|  | 																	> | ||||||
|  | 																		{intl.formatMessage({ | ||||||
|  | 																			id: "host.forward-scheme", | ||||||
|  | 																		})} | ||||||
|  | 																	</label> | ||||||
|  | 																	<select | ||||||
|  | 																		id="forwardScheme" | ||||||
|  | 																		className={`form-control ${form.errors.forwardScheme && form.touched.forwardScheme ? "is-invalid" : ""}`} | ||||||
|  | 																		required | ||||||
|  | 																		{...field} | ||||||
|  | 																	> | ||||||
|  | 																		<option value="http">http</option> | ||||||
|  | 																		<option value="https">https</option> | ||||||
|  | 																	</select> | ||||||
|  | 																	{form.errors.forwardScheme ? ( | ||||||
|  | 																		<div className="invalid-feedback"> | ||||||
|  | 																			{form.errors.forwardScheme && | ||||||
|  | 																			form.touched.forwardScheme | ||||||
|  | 																				? form.errors.forwardScheme | ||||||
|  | 																				: null} | ||||||
|  | 																		</div> | ||||||
|  | 																	) : null} | ||||||
|  | 																</div> | ||||||
|  | 															)} | ||||||
|  | 														</Field> | ||||||
|  | 													</div> | ||||||
|  | 													<div className="col-md-6"> | ||||||
|  | 														<Field name="forwardHost" validate={validateString(1, 255)}> | ||||||
|  | 															{({ field, form }: any) => ( | ||||||
|  | 																<div className="mb-3"> | ||||||
|  | 																	<label className="form-label" htmlFor="forwardHost"> | ||||||
|  | 																		{intl.formatMessage({ | ||||||
|  | 																			id: "proxy-host.forward-host", | ||||||
|  | 																		})} | ||||||
|  | 																	</label> | ||||||
|  | 																	<input | ||||||
|  | 																		id="forwardHost" | ||||||
|  | 																		type="text" | ||||||
|  | 																		className={`form-control ${form.errors.forwardHost && form.touched.forwardHost ? "is-invalid" : ""}`} | ||||||
|  | 																		required | ||||||
|  | 																		placeholder="example.com" | ||||||
|  | 																		{...field} | ||||||
|  | 																	/> | ||||||
|  | 																	{form.errors.forwardHost ? ( | ||||||
|  | 																		<div className="invalid-feedback"> | ||||||
|  | 																			{form.errors.forwardHost && | ||||||
|  | 																			form.touched.forwardHost | ||||||
|  | 																				? form.errors.forwardHost | ||||||
|  | 																				: null} | ||||||
|  | 																		</div> | ||||||
|  | 																	) : null} | ||||||
|  | 																</div> | ||||||
|  | 															)} | ||||||
|  | 														</Field> | ||||||
|  | 													</div> | ||||||
|  | 													<div className="col-md-3"> | ||||||
|  | 														<Field name="forwardPort" validate={validateNumber(1, 65535)}> | ||||||
|  | 															{({ field, form }: any) => ( | ||||||
|  | 																<div className="mb-3"> | ||||||
|  | 																	<label className="form-label" htmlFor="forwardPort"> | ||||||
|  | 																		{intl.formatMessage({ | ||||||
|  | 																			id: "host.forward-port", | ||||||
|  | 																		})} | ||||||
|  | 																	</label> | ||||||
|  | 																	<input | ||||||
|  | 																		id="forwardPort" | ||||||
|  | 																		type="number" | ||||||
|  | 																		min={1} | ||||||
|  | 																		max={65535} | ||||||
|  | 																		className={`form-control ${form.errors.forwardPort && form.touched.forwardPort ? "is-invalid" : ""}`} | ||||||
|  | 																		required | ||||||
|  | 																		placeholder="eg: 8081" | ||||||
|  | 																		{...field} | ||||||
|  | 																	/> | ||||||
|  | 																	{form.errors.forwardPort ? ( | ||||||
|  | 																		<div className="invalid-feedback"> | ||||||
|  | 																			{form.errors.forwardPort && | ||||||
|  | 																			form.touched.forwardPort | ||||||
|  | 																				? form.errors.forwardPort | ||||||
|  | 																				: null} | ||||||
|  | 																		</div> | ||||||
|  | 																	) : null} | ||||||
|  | 																</div> | ||||||
|  | 															)} | ||||||
|  | 														</Field> | ||||||
|  | 													</div> | ||||||
|  | 												</div> | ||||||
|  | 												<div className="my-3"> | ||||||
|  | 													<h4 className="py-2"> | ||||||
|  | 														{intl.formatMessage({ id: "host.flags.title" })} | ||||||
|  | 													</h4> | ||||||
|  | 													<div className="divide-y"> | ||||||
|  | 														<div> | ||||||
|  | 															<label className="row" htmlFor="cachingEnabled"> | ||||||
|  | 																<span className="col"> | ||||||
|  | 																	{intl.formatMessage({ | ||||||
|  | 																		id: "host.flags.cache-assets", | ||||||
|  | 																	})} | ||||||
|  | 																</span> | ||||||
|  | 																<span className="col-auto"> | ||||||
|  | 																	<Field name="cachingEnabled" type="checkbox"> | ||||||
|  | 																		{({ field }: any) => ( | ||||||
|  | 																			<label className="form-check form-check-single form-switch"> | ||||||
|  | 																				<input | ||||||
|  | 																					{...field} | ||||||
|  | 																					id="cachingEnabled" | ||||||
|  | 																					className={cn("form-check-input", { | ||||||
|  | 																						"bg-lime": field.checked, | ||||||
|  | 																					})} | ||||||
|  | 																					type="checkbox" | ||||||
|  | 																				/> | ||||||
|  | 																			</label> | ||||||
|  | 																		)} | ||||||
|  | 																	</Field> | ||||||
|  | 																</span> | ||||||
|  | 															</label> | ||||||
|  | 														</div> | ||||||
|  | 														<div> | ||||||
|  | 															<label className="row" htmlFor="blockExploits"> | ||||||
|  | 																<span className="col"> | ||||||
|  | 																	{intl.formatMessage({ | ||||||
|  | 																		id: "host.flags.block-exploits", | ||||||
|  | 																	})} | ||||||
|  | 																</span> | ||||||
|  | 																<span className="col-auto"> | ||||||
|  | 																	<Field name="blockExploits" type="checkbox"> | ||||||
|  | 																		{({ field }: any) => ( | ||||||
|  | 																			<label className="form-check form-check-single form-switch"> | ||||||
|  | 																				<input | ||||||
|  | 																					{...field} | ||||||
|  | 																					id="blockExploits" | ||||||
|  | 																					className={cn("form-check-input", { | ||||||
|  | 																						"bg-lime": field.checked, | ||||||
|  | 																					})} | ||||||
|  | 																					type="checkbox" | ||||||
|  | 																				/> | ||||||
|  | 																			</label> | ||||||
|  | 																		)} | ||||||
|  | 																	</Field> | ||||||
|  | 																</span> | ||||||
|  | 															</label> | ||||||
|  | 														</div> | ||||||
|  | 														<div> | ||||||
|  | 															<label className="row" htmlFor="allowWebsocketUpgrade"> | ||||||
|  | 																<span className="col"> | ||||||
|  | 																	{intl.formatMessage({ | ||||||
|  | 																		id: "host.flags.websockets-upgrade", | ||||||
|  | 																	})} | ||||||
|  | 																</span> | ||||||
|  | 																<span className="col-auto"> | ||||||
|  | 																	<Field name="allowWebsocketUpgrade" type="checkbox"> | ||||||
|  | 																		{({ field }: any) => ( | ||||||
|  | 																			<label className="form-check form-check-single form-switch"> | ||||||
|  | 																				<input | ||||||
|  | 																					{...field} | ||||||
|  | 																					id="allowWebsocketUpgrade" | ||||||
|  | 																					className={cn("form-check-input", { | ||||||
|  | 																						"bg-lime": field.checked, | ||||||
|  | 																					})} | ||||||
|  | 																					type="checkbox" | ||||||
|  | 																				/> | ||||||
|  | 																			</label> | ||||||
|  | 																		)} | ||||||
|  | 																	</Field> | ||||||
|  | 																</span> | ||||||
|  | 															</label> | ||||||
|  | 														</div> | ||||||
|  | 													</div> | ||||||
|  | 												</div> | ||||||
|  | 											</div> | ||||||
|  | 											<div className="tab-pane" id="tab-locations" role="tabpanel"> | ||||||
|  | 												locations | ||||||
|  | 											</div> | ||||||
|  | 											<div className="tab-pane" id="tab-ssl" role="tabpanel"> | ||||||
|  | 												<SSLCertificateField | ||||||
|  | 													name="certificateId" | ||||||
|  | 													label="ssl-certificate" | ||||||
|  | 													allowNew | ||||||
|  | 												/> | ||||||
|  | 												<SSLOptionsFields color="bg-lime" /> | ||||||
|  | 											</div> | ||||||
|  | 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||||
|  | 												<NginxConfigField /> | ||||||
|  | 											</div> | ||||||
|  | 										</div> | ||||||
|  | 									</div> | ||||||
|  | 								</div> | ||||||
|  | 							</Modal.Body> | ||||||
|  | 							<Modal.Footer> | ||||||
|  | 								<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> | ||||||
|  | 									{intl.formatMessage({ id: "cancel" })} | ||||||
|  | 								</Button> | ||||||
|  | 								<Button | ||||||
|  | 									type="submit" | ||||||
|  | 									actionType="primary" | ||||||
|  | 									className="ms-auto bg-lime" | ||||||
|  | 									data-bs-dismiss="modal" | ||||||
|  | 									isLoading={isSubmitting} | ||||||
|  | 									disabled={isSubmitting} | ||||||
|  | 								> | ||||||
|  | 									{intl.formatMessage({ id: "save" })} | ||||||
|  | 								</Button> | ||||||
|  | 							</Modal.Footer> | ||||||
|  | 						</Form> | ||||||
|  | 					)} | ||||||
|  | 				</Formik> | ||||||
|  | 			)} | ||||||
|  | 		</Modal> | ||||||
|  | 	); | ||||||
|  | } | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { IconSettings } from "@tabler/icons-react"; | import { IconSettings } from "@tabler/icons-react"; | ||||||
|  | import cn from "classnames"; | ||||||
| import { Field, Form, Formik } from "formik"; | import { Field, Form, Formik } from "formik"; | ||||||
| import { useState } from "react"; | import { useState } from "react"; | ||||||
| import { Alert } from "react-bootstrap"; | import { Alert } from "react-bootstrap"; | ||||||
| @@ -150,7 +151,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | |||||||
| 																		htmlFor="forwardScheme" | 																		htmlFor="forwardScheme" | ||||||
| 																	> | 																	> | ||||||
| 																		{intl.formatMessage({ | 																		{intl.formatMessage({ | ||||||
| 																			id: "redirect-host.forward-scheme", | 																			id: "host.forward-scheme", | ||||||
| 																		})} | 																		})} | ||||||
| 																	</label> | 																	</label> | ||||||
| 																	<select | 																	<select | ||||||
| @@ -187,7 +188,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | |||||||
| 																		htmlFor="forwardDomainName" | 																		htmlFor="forwardDomainName" | ||||||
| 																	> | 																	> | ||||||
| 																		{intl.formatMessage({ | 																		{intl.formatMessage({ | ||||||
| 																			id: "redirect-host.forward-domain", | 																			id: "redirection-host.forward-domain", | ||||||
| 																		})} | 																		})} | ||||||
| 																	</label> | 																	</label> | ||||||
| 																	<input | 																	<input | ||||||
| @@ -230,7 +231,9 @@ export function RedirectionHostModal({ id, onClose }: Props) { | |||||||
| 																				<input | 																				<input | ||||||
| 																					{...field} | 																					{...field} | ||||||
| 																					id="preservePath" | 																					id="preservePath" | ||||||
| 																					className="form-check-input" | 																					className={cn("form-check-input", { | ||||||
|  | 																						"bg-yellow": field.checked, | ||||||
|  | 																					})} | ||||||
| 																					type="checkbox" | 																					type="checkbox" | ||||||
| 																				/> | 																				/> | ||||||
| 																			</label> | 																			</label> | ||||||
| @@ -253,7 +256,9 @@ export function RedirectionHostModal({ id, onClose }: Props) { | |||||||
| 																				<input | 																				<input | ||||||
| 																					{...field} | 																					{...field} | ||||||
| 																					id="blockExploits" | 																					id="blockExploits" | ||||||
| 																					className="form-check-input" | 																					className={cn("form-check-input", { | ||||||
|  | 																						"bg-yellow": field.checked, | ||||||
|  | 																					})} | ||||||
| 																					type="checkbox" | 																					type="checkbox" | ||||||
| 																				/> | 																				/> | ||||||
| 																			</label> | 																			</label> | ||||||
| @@ -271,7 +276,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | |||||||
| 													label="ssl-certificate" | 													label="ssl-certificate" | ||||||
| 													allowNew | 													allowNew | ||||||
| 												/> | 												/> | ||||||
| 												<SSLOptionsFields /> | 												<SSLOptionsFields color="bg-yellow" /> | ||||||
| 											</div> | 											</div> | ||||||
| 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||||
| 												<NginxConfigField /> | 												<NginxConfigField /> | ||||||
| @@ -287,7 +292,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | |||||||
| 								<Button | 								<Button | ||||||
| 									type="submit" | 									type="submit" | ||||||
| 									actionType="primary" | 									actionType="primary" | ||||||
| 									className="ms-auto" | 									className="ms-auto bg-yellow" | ||||||
| 									data-bs-dismiss="modal" | 									data-bs-dismiss="modal" | ||||||
| 									isLoading={isSubmitting} | 									isLoading={isSubmitting} | ||||||
| 									disabled={isSubmitting} | 									disabled={isSubmitting} | ||||||
|   | |||||||
| @@ -179,7 +179,7 @@ export function StreamModal({ id, onClose }: Props) { | |||||||
| 																		htmlFor="forwardingPort" | 																		htmlFor="forwardingPort" | ||||||
| 																	> | 																	> | ||||||
| 																		{intl.formatMessage({ | 																		{intl.formatMessage({ | ||||||
| 																			id: "stream.forward-port", | 																			id: "host.forward-port", | ||||||
| 																		})} | 																		})} | ||||||
| 																	</label> | 																	</label> | ||||||
| 																	<input | 																	<input | ||||||
| @@ -292,7 +292,12 @@ export function StreamModal({ id, onClose }: Props) { | |||||||
| 													allowNew | 													allowNew | ||||||
| 													forHttp={false} | 													forHttp={false} | ||||||
| 												/> | 												/> | ||||||
| 												<SSLOptionsFields forHttp={false} forceDNSForNew requireDomainNames /> | 												<SSLOptionsFields | ||||||
|  | 													color="bg-blue" | ||||||
|  | 													forHttp={false} | ||||||
|  | 													forceDNSForNew | ||||||
|  | 													requireDomainNames | ||||||
|  | 												/> | ||||||
| 											</div> | 											</div> | ||||||
| 										</div> | 										</div> | ||||||
| 									</div> | 									</div> | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ export * from "./DeadHostModal"; | |||||||
| export * from "./DeleteConfirmModal"; | export * from "./DeleteConfirmModal"; | ||||||
| export * from "./EventDetailsModal"; | export * from "./EventDetailsModal"; | ||||||
| export * from "./PermissionsModal"; | export * from "./PermissionsModal"; | ||||||
|  | export * from "./ProxyHostModal"; | ||||||
| export * from "./RedirectionHostModal"; | export * from "./RedirectionHostModal"; | ||||||
| export * from "./SetPasswordModal"; | export * from "./SetPasswordModal"; | ||||||
| export * from "./StreamModal"; | export * from "./StreamModal"; | ||||||
|   | |||||||
| @@ -56,9 +56,9 @@ export default function TableWrapper() { | |||||||
| 						<div className="col"> | 						<div className="col"> | ||||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "dead-hosts.title" })}</h2> | 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "dead-hosts.title" })}</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"> | ||||||
| 								{data?.length ? ( | 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||||
| 									<div className="input-group input-group-flat w-auto"> | 									<div className="input-group input-group-flat w-auto"> | ||||||
| 										<span className="input-group-text input-group-text-sm"> | 										<span className="input-group-text input-group-text-sm"> | ||||||
| 											<IconSearch size={16} /> | 											<IconSearch size={16} /> | ||||||
| @@ -71,12 +71,13 @@ export default function TableWrapper() { | |||||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||||
| 										/> | 										/> | ||||||
| 									</div> | 									</div> | ||||||
| 								) : null} |  | ||||||
| 								<Button size="sm" className="btn-red" onClick={() => setEditId("new")}> | 									<Button size="sm" className="btn-red" onClick={() => setEditId("new")}> | ||||||
| 									{intl.formatMessage({ id: "dead-hosts.add" })} | 										{intl.formatMessage({ id: "dead-hosts.add" })} | ||||||
| 								</Button> | 									</Button> | ||||||
|  | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						) : null} | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<Table | 				<Table | ||||||
|   | |||||||
| @@ -2,22 +2,27 @@ import type { Table as ReactTable } from "@tanstack/react-table"; | |||||||
| import { Button } from "src/components"; | import { Button } from "src/components"; | ||||||
| import { intl } from "src/locale"; | 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 { | interface Props { | ||||||
| 	tableInstance: ReactTable<any>; | 	tableInstance: ReactTable<any>; | ||||||
|  | 	onNew?: () => void; | ||||||
|  | 	isFiltered?: boolean; | ||||||
| } | } | ||||||
| export default function Empty({ tableInstance }: Props) { | export default function Empty({ tableInstance, onNew, isFiltered }: Props) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<tr> | 		<tr> | ||||||
| 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | ||||||
| 				<div className="text-center my-4"> | 				<div className="text-center my-4"> | ||||||
| 					<h2>{intl.formatMessage({ id: "proxy-hosts.empty" })}</h2> | 					{isFiltered ? ( | ||||||
| 					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p> | 						<h2>{intl.formatMessage({ id: "empty-search" })}</h2> | ||||||
| 					<Button className="btn-lime my-3">{intl.formatMessage({ id: "proxy-hosts.add" })}</Button> | 					) : ( | ||||||
|  | 						<> | ||||||
|  | 							<h2>{intl.formatMessage({ id: "proxy-hosts.empty" })}</h2> | ||||||
|  | 							<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p> | ||||||
|  | 							<Button className="btn-lime my-3" onClick={onNew}> | ||||||
|  | 								{intl.formatMessage({ id: "proxy-hosts.add" })} | ||||||
|  | 							</Button> | ||||||
|  | 						</> | ||||||
|  | 					)} | ||||||
| 				</div> | 				</div> | ||||||
| 			</td> | 			</td> | ||||||
| 		</tr> | 		</tr> | ||||||
|   | |||||||
| @@ -9,9 +9,14 @@ import Empty from "./Empty"; | |||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
| 	data: ProxyHost[]; | 	data: ProxyHost[]; | ||||||
|  | 	isFiltered?: boolean; | ||||||
| 	isFetching?: boolean; | 	isFetching?: boolean; | ||||||
|  | 	onEdit?: (id: number) => void; | ||||||
|  | 	onDelete?: (id: number) => void; | ||||||
|  | 	onDisableToggle?: (id: number, enabled: boolean) => void; | ||||||
|  | 	onNew?: () => void; | ||||||
| } | } | ||||||
| export default function Table({ data, isFetching }: Props) { | export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) { | ||||||
| 	const columnHelper = createColumnHelper<ProxyHost>(); | 	const columnHelper = createColumnHelper<ProxyHost>(); | ||||||
| 	const columns = useMemo( | 	const columns = useMemo( | ||||||
| 		() => [ | 		() => [ | ||||||
| @@ -64,7 +69,7 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 			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"> | ||||||
| @@ -85,16 +90,39 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 										{ id: info.row.original.id }, | 										{ id: info.row.original.id }, | ||||||
| 									)} | 									)} | ||||||
| 								</span> | 								</span> | ||||||
| 								<a className="dropdown-item" href="#"> | 								<a | ||||||
|  | 									className="dropdown-item" | ||||||
|  | 									href="#" | ||||||
|  | 									onClick={(e) => { | ||||||
|  | 										e.preventDefault(); | ||||||
|  | 										onEdit?.(info.row.original.id); | ||||||
|  | 									}} | ||||||
|  | 								> | ||||||
| 									<IconEdit size={16} /> | 									<IconEdit size={16} /> | ||||||
| 									{intl.formatMessage({ id: "action.edit" })} | 									{intl.formatMessage({ id: "action.edit" })} | ||||||
| 								</a> | 								</a> | ||||||
| 								<a className="dropdown-item" href="#"> | 								<a | ||||||
|  | 									className="dropdown-item" | ||||||
|  | 									href="#" | ||||||
|  | 									onClick={(e) => { | ||||||
|  | 										e.preventDefault(); | ||||||
|  | 										onDisableToggle?.(info.row.original.id, !info.row.original.enabled); | ||||||
|  | 									}} | ||||||
|  | 								> | ||||||
| 									<IconPower size={16} /> | 									<IconPower size={16} /> | ||||||
| 									{intl.formatMessage({ id: "action.disable" })} | 									{intl.formatMessage({ | ||||||
|  | 										id: info.row.original.enabled ? "action.disable" : "action.enable", | ||||||
|  | 									})} | ||||||
| 								</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} /> | ||||||
| 									{intl.formatMessage({ id: "action.delete" })} | 									{intl.formatMessage({ id: "action.delete" })} | ||||||
| 								</a> | 								</a> | ||||||
| @@ -107,7 +135,7 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 		], | 		], | ||||||
| 		[columnHelper], | 		[columnHelper, onEdit, onDisableToggle, onDelete], | ||||||
| 	); | 	); | ||||||
|  |  | ||||||
| 	const tableInstance = useReactTable<ProxyHost>({ | 	const tableInstance = useReactTable<ProxyHost>({ | ||||||
| @@ -121,5 +149,10 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 		enableSortingRemoval: false, | 		enableSortingRemoval: false, | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	return <TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} />} />; | 	return ( | ||||||
|  | 		<TableLayout | ||||||
|  | 			tableInstance={tableInstance} | ||||||
|  | 			emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />} | ||||||
|  | 		/> | ||||||
|  | 	); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,20 @@ | |||||||
| import { IconSearch } from "@tabler/icons-react"; | import { IconSearch } from "@tabler/icons-react"; | ||||||
|  | import { useQueryClient } from "@tanstack/react-query"; | ||||||
|  | import { useState } from "react"; | ||||||
| import Alert from "react-bootstrap/Alert"; | import Alert from "react-bootstrap/Alert"; | ||||||
|  | import { deleteProxyHost, toggleProxyHost } from "src/api/backend"; | ||||||
| import { Button, LoadingPage } from "src/components"; | import { Button, LoadingPage } from "src/components"; | ||||||
| import { useProxyHosts } from "src/hooks"; | import { useProxyHosts } from "src/hooks"; | ||||||
| import { intl } from "src/locale"; | import { intl } from "src/locale"; | ||||||
|  | import { DeleteConfirmModal, ProxyHostModal } from "src/modals"; | ||||||
|  | import { showSuccess } from "src/notifications"; | ||||||
| import Table from "./Table"; | import Table from "./Table"; | ||||||
|  |  | ||||||
| export default function TableWrapper() { | export default function TableWrapper() { | ||||||
|  | 	const queryClient = useQueryClient(); | ||||||
|  | 	const [search, setSearch] = useState(""); | ||||||
|  | 	const [deleteId, setDeleteId] = useState(0); | ||||||
|  | 	const [editId, setEditId] = useState(0 as number | "new"); | ||||||
| 	const { isFetching, isLoading, isError, error, data } = useProxyHosts(["owner", "access_list", "certificate"]); | 	const { isFetching, isLoading, isError, error, data } = useProxyHosts(["owner", "access_list", "certificate"]); | ||||||
|  |  | ||||||
| 	if (isLoading) { | 	if (isLoading) { | ||||||
| @@ -16,6 +25,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 () => { | ||||||
|  | 		await deleteProxyHost(deleteId); | ||||||
|  | 		showSuccess(intl.formatMessage({ id: "notification.host-deleted" })); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const handleDisableToggle = async (id: number, enabled: boolean) => { | ||||||
|  | 		await toggleProxyHost(id, enabled); | ||||||
|  | 		queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] }); | ||||||
|  | 		queryClient.invalidateQueries({ queryKey: ["proxy-host", id] }); | ||||||
|  | 		showSuccess(intl.formatMessage({ id: enabled ? "notification.host-enabled" : "notification.host-disabled" })); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	let filtered = null; | ||||||
|  | 	if (search && data) { | ||||||
|  | 		filtered = data?.filter((_item) => { | ||||||
|  | 			return true; | ||||||
|  | 			// item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) || | ||||||
|  | 			// item.forwardDomainName.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-lime" /> | 			<div className="card-status-top bg-lime" /> | ||||||
| @@ -25,27 +59,48 @@ export default function TableWrapper() { | |||||||
| 						<div className="col"> | 						<div className="col"> | ||||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "proxy-hosts.title" })}</h2> | 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "proxy-hosts.title" })}</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> | ||||||
|  | 									<Button size="sm" className="btn-lime"> | ||||||
|  | 										{intl.formatMessage({ id: "proxy-hosts.add" })} | ||||||
|  | 									</Button> | ||||||
| 								</div> | 								</div> | ||||||
| 								<Button size="sm" className="btn-lime"> |  | ||||||
| 									{intl.formatMessage({ id: "proxy-hosts.add" })} |  | ||||||
| 								</Button> |  | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						) : null} | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<Table data={data ?? []} isFetching={isFetching} /> | 				<Table | ||||||
|  | 					data={filtered ?? data ?? []} | ||||||
|  | 					isFiltered={!!search} | ||||||
|  | 					isFetching={isFetching} | ||||||
|  | 					onEdit={(id: number) => setEditId(id)} | ||||||
|  | 					onDelete={(id: number) => setDeleteId(id)} | ||||||
|  | 					onDisableToggle={handleDisableToggle} | ||||||
|  | 					onNew={() => setEditId("new")} | ||||||
|  | 				/> | ||||||
|  | 				{editId ? <ProxyHostModal id={editId} onClose={() => setEditId(0)} /> : null} | ||||||
|  | 				{deleteId ? ( | ||||||
|  | 					<DeleteConfirmModal | ||||||
|  | 						title={intl.formatMessage({ id: "proxy-host.delete.title" })} | ||||||
|  | 						onConfirm={handleDelete} | ||||||
|  | 						onClose={() => setDeleteId(0)} | ||||||
|  | 						invalidations={[["proxy-hosts"], ["proxy-host", deleteId]]} | ||||||
|  | 					> | ||||||
|  | 						{intl.formatMessage({ id: "proxy-host.delete.content" })} | ||||||
|  | 					</DeleteConfirmModal> | ||||||
|  | 				) : null} | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	); | 	); | ||||||
|   | |||||||
| @@ -74,7 +74,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog | |||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 			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"> | ||||||
|   | |||||||
| @@ -59,9 +59,9 @@ export default function TableWrapper() { | |||||||
| 						<div className="col"> | 						<div className="col"> | ||||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "redirection-hosts.title" })}</h2> | 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "redirection-hosts.title" })}</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"> | ||||||
| 								{data?.length ? ( | 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||||
| 									<div className="input-group input-group-flat w-auto"> | 									<div className="input-group input-group-flat w-auto"> | ||||||
| 										<span className="input-group-text input-group-text-sm"> | 										<span className="input-group-text input-group-text-sm"> | ||||||
| 											<IconSearch size={16} /> | 											<IconSearch size={16} /> | ||||||
| @@ -74,12 +74,13 @@ export default function TableWrapper() { | |||||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||||
| 										/> | 										/> | ||||||
| 									</div> | 									</div> | ||||||
| 								) : null} |  | ||||||
| 								<Button size="sm" className="btn-yellow" onClick={() => setEditId("new")}> | 									<Button size="sm" className="btn-yellow" onClick={() => setEditId("new")}> | ||||||
| 									{intl.formatMessage({ id: "redirection-hosts.add" })} | 										{intl.formatMessage({ id: "redirection-hosts.add" })} | ||||||
| 								</Button> | 									</Button> | ||||||
|  | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						) : null} | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<Table | 				<Table | ||||||
|   | |||||||
| @@ -62,9 +62,9 @@ export default function TableWrapper() { | |||||||
| 						<div className="col"> | 						<div className="col"> | ||||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "streams.title" })}</h2> | 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "streams.title" })}</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"> | ||||||
| 								{data?.length ? ( | 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||||
| 									<div className="input-group input-group-flat w-auto"> | 									<div className="input-group input-group-flat w-auto"> | ||||||
| 										<span className="input-group-text input-group-text-sm"> | 										<span className="input-group-text input-group-text-sm"> | ||||||
| 											<IconSearch size={16} /> | 											<IconSearch size={16} /> | ||||||
| @@ -77,12 +77,12 @@ export default function TableWrapper() { | |||||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||||
| 										/> | 										/> | ||||||
| 									</div> | 									</div> | ||||||
| 								) : null} | 									<Button size="sm" className="btn-blue" onClick={() => setEditId("new")}> | ||||||
| 								<Button size="sm" className="btn-blue" onClick={() => setEditId("new")}> | 										{intl.formatMessage({ id: "streams.add" })} | ||||||
| 									{intl.formatMessage({ id: "streams.add" })} | 									</Button> | ||||||
| 								</Button> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						) : null} | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<Table | 				<Table | ||||||
|   | |||||||
| @@ -63,9 +63,9 @@ export default function TableWrapper() { | |||||||
| 						<div className="col"> | 						<div className="col"> | ||||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "users.title" })}</h2> | 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "users.title" })}</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"> | ||||||
| 								{data?.length ? ( | 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||||
| 									<div className="input-group input-group-flat w-auto"> | 									<div className="input-group input-group-flat w-auto"> | ||||||
| 										<span className="input-group-text input-group-text-sm"> | 										<span className="input-group-text input-group-text-sm"> | ||||||
| 											<IconSearch size={16} /> | 											<IconSearch size={16} /> | ||||||
| @@ -78,12 +78,13 @@ export default function TableWrapper() { | |||||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||||
| 										/> | 										/> | ||||||
| 									</div> | 									</div> | ||||||
| 								) : null} |  | ||||||
| 								<Button size="sm" className="btn-orange" onClick={() => setEditUserId("new")}> | 									<Button size="sm" className="btn-orange" onClick={() => setEditUserId("new")}> | ||||||
| 									{intl.formatMessage({ id: "users.add" })} | 										{intl.formatMessage({ id: "users.add" })} | ||||||
| 								</Button> | 									</Button> | ||||||
|  | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						) : null} | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<Table | 				<Table | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user