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) => { | ||||
| 		const accessData = await access.can("proxy_hosts:list"); | ||||
|  | ||||
| 		const query = proxyHostModel | ||||
| 			.query() | ||||
| 			.where("is_deleted", 0) | ||||
| @@ -446,11 +445,9 @@ const internalProxyHost = { | ||||
| 		} | ||||
|  | ||||
| 		const rows = await query.then(utils.omitRows(omissions())); | ||||
|  | ||||
| 		if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { | ||||
| 			return internalHost.cleanAllRowsCertificateMeta(rows); | ||||
| 		} | ||||
|  | ||||
| 		return rows; | ||||
| 	}, | ||||
|  | ||||
|   | ||||
| @@ -131,7 +131,7 @@ export default function (tokenString) { | ||||
| 						const rows = await query; | ||||
| 						objects = []; | ||||
| 						_.forEach(rows, (ruleRow) => { | ||||
| 							result.push(ruleRow.id); | ||||
| 							objects.push(ruleRow.id); | ||||
| 						}); | ||||
|  | ||||
| 						// enum should not have less than 1 item | ||||
|   | ||||
| @@ -70,3 +70,7 @@ | ||||
| 	font-family: 'Courier New', Courier, monospace !important; | ||||
| 	resize: vertical; | ||||
| } | ||||
|  | ||||
| label.row { | ||||
| 	cursor: pointer; | ||||
| } | ||||
|   | ||||
| @@ -103,6 +103,7 @@ export interface ProxyHost { | ||||
| 	modifiedOn: string; | ||||
| 	ownerUserId: number; | ||||
| 	domainNames: string[]; | ||||
| 	forwardScheme: string; | ||||
| 	forwardHost: string; | ||||
| 	forwardPort: number; | ||||
| 	accessListId: number; | ||||
| @@ -114,9 +115,8 @@ export interface ProxyHost { | ||||
| 	meta: Record<string, any>; | ||||
| 	allowWebsocketUpgrade: boolean; | ||||
| 	http2Support: boolean; | ||||
| 	forwardScheme: string; | ||||
| 	enabled: boolean; | ||||
| 	locations: string[]; // todo: string or object? | ||||
| 	locations?: string[]; // todo: string or object? | ||||
| 	hstsEnabled: boolean; | ||||
| 	hstsSubdomains: boolean; | ||||
| 	// Expansions: | ||||
|   | ||||
| @@ -7,8 +7,9 @@ interface Props { | ||||
| 	forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields | ||||
| 	forceDNSForNew?: boolean; | ||||
| 	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 v: any = values || {}; | ||||
|  | ||||
| @@ -31,7 +32,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain | ||||
| 	}; | ||||
|  | ||||
| 	const toggleClasses = "form-check-input"; | ||||
| 	const toggleEnabled = cn(toggleClasses, "bg-cyan"); | ||||
| 	const toggleEnabled = cn(toggleClasses, color); | ||||
|  | ||||
| 	const getHttpOptions = () => ( | ||||
| 		<div> | ||||
|   | ||||
| @@ -7,6 +7,7 @@ export * from "./useDeadHosts"; | ||||
| export * from "./useDnsProviders"; | ||||
| export * from "./useHealth"; | ||||
| export * from "./useHostReport"; | ||||
| export * from "./useProxyHost"; | ||||
| export * from "./useProxyHosts"; | ||||
| export * from "./useRedirectionHost"; | ||||
| 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", | ||||
|   "column.access": "Access", | ||||
|   "column.authorization": "Authorization", | ||||
|   "column.custom-locations": "Custom Locations", | ||||
|   "column.destination": "Destination", | ||||
|   "column.details": "Details", | ||||
|   "column.email": "Email", | ||||
| @@ -88,9 +89,13 @@ | ||||
|   "event.updated-user": "Updated User", | ||||
|   "footer.github-fork": "Fork me on Github", | ||||
|   "host.flags.block-exploits": "Block Common Exploits", | ||||
|   "host.flags.cache-assets": "Cache Assets", | ||||
|   "host.flags.preserve-path": "Preserve Path", | ||||
|   "host.flags.protocols": "Protocols", | ||||
|   "host.flags.title": "Options", | ||||
|   "host.flags.websockets-upgrade": "Websockets Support", | ||||
|   "host.forward-port": "Forward Port", | ||||
|   "host.forward-scheme": "Scheme", | ||||
|   "hosts.title": "Hosts", | ||||
|   "http-only": "HTTP Only", | ||||
|   "lets-encrypt": "Let's Encrypt", | ||||
| @@ -128,13 +133,14 @@ | ||||
|   "permissions.visibility.all": "All Items", | ||||
|   "permissions.visibility.title": "Item Visibility", | ||||
|   "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.add": "Add Proxy Host", | ||||
|   "proxy-hosts.count": "{count} Proxy Hosts", | ||||
|   "proxy-hosts.empty": "There are no Proxy Hosts", | ||||
|   "proxy-hosts.title": "Proxy Hosts", | ||||
|   "redirect-host.forward-domain": "Forward Domain", | ||||
|   "redirect-host.forward-scheme": "Scheme", | ||||
|   "redirection-host.forward-domain": "Forward Domain", | ||||
|   "redirection-host.new": "New Redirection Host", | ||||
|   "redirection-hosts.actions-title": "Redirection Host #{id}", | ||||
|   "redirection-hosts.add": "Add Redirection Host", | ||||
| @@ -152,7 +158,6 @@ | ||||
|   "stream.delete.content": "Are you sure you want to delete this Stream?", | ||||
|   "stream.delete.title": "Delete Stream", | ||||
|   "stream.forward-host": "Forward Host", | ||||
|   "stream.forward-port": "Forward Port", | ||||
|   "stream.incoming-port": "Incoming Port", | ||||
|   "stream.new": "New Stream", | ||||
|   "streams.actions-title": "Stream #{id}", | ||||
|   | ||||
| @@ -71,6 +71,9 @@ | ||||
| 	"column.authorization": { | ||||
| 		"defaultMessage": "Authorization" | ||||
| 	}, | ||||
| 	"column.custom-locations": { | ||||
| 		"defaultMessage": "Custom Locations" | ||||
| 	}, | ||||
| 	"column.destination": { | ||||
| 		"defaultMessage": "Destination" | ||||
| 	}, | ||||
| @@ -266,6 +269,9 @@ | ||||
| 	"host.flags.block-exploits": { | ||||
| 		"defaultMessage": "Block Common Exploits" | ||||
| 	}, | ||||
| 	"host.flags.cache-assets": { | ||||
| 		"defaultMessage": "Cache Assets" | ||||
| 	}, | ||||
| 	"host.flags.preserve-path": { | ||||
| 		"defaultMessage": "Preserve Path" | ||||
| 	}, | ||||
| @@ -275,6 +281,15 @@ | ||||
| 	"host.flags.title": { | ||||
| 		"defaultMessage": "Options" | ||||
| 	}, | ||||
| 	"host.flags.websockets-upgrade": { | ||||
| 		"defaultMessage": "Websockets Support" | ||||
| 	}, | ||||
| 	"host.forward-port": { | ||||
| 		"defaultMessage": "Forward Port" | ||||
| 	}, | ||||
| 	"host.forward-scheme": { | ||||
| 		"defaultMessage": "Scheme" | ||||
| 	}, | ||||
| 	"hosts.title": { | ||||
| 		"defaultMessage": "Hosts" | ||||
| 	}, | ||||
| @@ -386,6 +401,12 @@ | ||||
| 	"permissions.visibility.user": { | ||||
| 		"defaultMessage": "Created Items Only" | ||||
| 	}, | ||||
| 	"proxy-host.forward-host": { | ||||
| 		"defaultMessage": "Forward Hostname / IP" | ||||
| 	}, | ||||
| 	"proxy-host.new": { | ||||
| 		"defaultMessage": "New Proxy Host" | ||||
| 	}, | ||||
| 	"proxy-hosts.actions-title": { | ||||
| 		"defaultMessage": "Proxy Host #{id}" | ||||
| 	}, | ||||
| @@ -401,12 +422,9 @@ | ||||
| 	"proxy-hosts.title": { | ||||
| 		"defaultMessage": "Proxy Hosts" | ||||
| 	}, | ||||
| 	"redirect-host.forward-domain": { | ||||
| 	"redirection-host.forward-domain": { | ||||
| 		"defaultMessage": "Forward Domain" | ||||
| 	}, | ||||
| 	"redirect-host.forward-scheme": { | ||||
| 		"defaultMessage": "Scheme" | ||||
| 	}, | ||||
| 	"redirection-host.new": { | ||||
| 		"defaultMessage": "New Redirection Host" | ||||
| 	}, | ||||
| @@ -458,9 +476,6 @@ | ||||
| 	"stream.forward-host": { | ||||
| 		"defaultMessage": "Forward Host" | ||||
| 	}, | ||||
| 	"stream.forward-port": { | ||||
| 		"defaultMessage": "Forward Port" | ||||
| 	}, | ||||
| 	"stream.incoming-port": { | ||||
| 		"defaultMessage": "Incoming Port" | ||||
| 	}, | ||||
|   | ||||
| @@ -136,7 +136,7 @@ export function DeadHostModal({ id, onClose }: Props) { | ||||
| 													label="ssl-certificate" | ||||
| 													allowNew | ||||
| 												/> | ||||
| 												<SSLOptionsFields /> | ||||
| 												<SSLOptionsFields color="bg-red" /> | ||||
| 											</div> | ||||
| 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||
| 												<NginxConfigField /> | ||||
| @@ -152,7 +152,7 @@ export function DeadHostModal({ id, onClose }: Props) { | ||||
| 								<Button | ||||
| 									type="submit" | ||||
| 									actionType="primary" | ||||
| 									className="ms-auto" | ||||
| 									className="ms-auto bg-red" | ||||
| 									data-bs-dismiss="modal" | ||||
| 									isLoading={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 cn from "classnames"; | ||||
| import { Field, Form, Formik } from "formik"; | ||||
| import { useState } from "react"; | ||||
| import { Alert } from "react-bootstrap"; | ||||
| @@ -150,7 +151,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | ||||
| 																		htmlFor="forwardScheme" | ||||
| 																	> | ||||
| 																		{intl.formatMessage({ | ||||
| 																			id: "redirect-host.forward-scheme", | ||||
| 																			id: "host.forward-scheme", | ||||
| 																		})} | ||||
| 																	</label> | ||||
| 																	<select | ||||
| @@ -187,7 +188,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | ||||
| 																		htmlFor="forwardDomainName" | ||||
| 																	> | ||||
| 																		{intl.formatMessage({ | ||||
| 																			id: "redirect-host.forward-domain", | ||||
| 																			id: "redirection-host.forward-domain", | ||||
| 																		})} | ||||
| 																	</label> | ||||
| 																	<input | ||||
| @@ -230,7 +231,9 @@ export function RedirectionHostModal({ id, onClose }: Props) { | ||||
| 																				<input | ||||
| 																					{...field} | ||||
| 																					id="preservePath" | ||||
| 																					className="form-check-input" | ||||
| 																					className={cn("form-check-input", { | ||||
| 																						"bg-yellow": field.checked, | ||||
| 																					})} | ||||
| 																					type="checkbox" | ||||
| 																				/> | ||||
| 																			</label> | ||||
| @@ -253,7 +256,9 @@ export function RedirectionHostModal({ id, onClose }: Props) { | ||||
| 																				<input | ||||
| 																					{...field} | ||||
| 																					id="blockExploits" | ||||
| 																					className="form-check-input" | ||||
| 																					className={cn("form-check-input", { | ||||
| 																						"bg-yellow": field.checked, | ||||
| 																					})} | ||||
| 																					type="checkbox" | ||||
| 																				/> | ||||
| 																			</label> | ||||
| @@ -271,7 +276,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | ||||
| 													label="ssl-certificate" | ||||
| 													allowNew | ||||
| 												/> | ||||
| 												<SSLOptionsFields /> | ||||
| 												<SSLOptionsFields color="bg-yellow" /> | ||||
| 											</div> | ||||
| 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||
| 												<NginxConfigField /> | ||||
| @@ -287,7 +292,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { | ||||
| 								<Button | ||||
| 									type="submit" | ||||
| 									actionType="primary" | ||||
| 									className="ms-auto" | ||||
| 									className="ms-auto bg-yellow" | ||||
| 									data-bs-dismiss="modal" | ||||
| 									isLoading={isSubmitting} | ||||
| 									disabled={isSubmitting} | ||||
|   | ||||
| @@ -179,7 +179,7 @@ export function StreamModal({ id, onClose }: Props) { | ||||
| 																		htmlFor="forwardingPort" | ||||
| 																	> | ||||
| 																		{intl.formatMessage({ | ||||
| 																			id: "stream.forward-port", | ||||
| 																			id: "host.forward-port", | ||||
| 																		})} | ||||
| 																	</label> | ||||
| 																	<input | ||||
| @@ -292,7 +292,12 @@ export function StreamModal({ id, onClose }: Props) { | ||||
| 													allowNew | ||||
| 													forHttp={false} | ||||
| 												/> | ||||
| 												<SSLOptionsFields forHttp={false} forceDNSForNew requireDomainNames /> | ||||
| 												<SSLOptionsFields | ||||
| 													color="bg-blue" | ||||
| 													forHttp={false} | ||||
| 													forceDNSForNew | ||||
| 													requireDomainNames | ||||
| 												/> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									</div> | ||||
|   | ||||
| @@ -3,6 +3,7 @@ export * from "./DeadHostModal"; | ||||
| export * from "./DeleteConfirmModal"; | ||||
| export * from "./EventDetailsModal"; | ||||
| export * from "./PermissionsModal"; | ||||
| export * from "./ProxyHostModal"; | ||||
| export * from "./RedirectionHostModal"; | ||||
| export * from "./SetPasswordModal"; | ||||
| export * from "./StreamModal"; | ||||
|   | ||||
| @@ -56,9 +56,9 @@ export default function TableWrapper() { | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "dead-hosts.title" })}</h2> | ||||
| 						</div> | ||||
| 						<div className="col-md-auto col-sm-12"> | ||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 								{data?.length ? ( | ||||
| 						{data?.length ? ( | ||||
| 							<div className="col-md-auto col-sm-12"> | ||||
| 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 									<div className="input-group input-group-flat w-auto"> | ||||
| 										<span className="input-group-text input-group-text-sm"> | ||||
| 											<IconSearch size={16} /> | ||||
| @@ -71,12 +71,13 @@ export default function TableWrapper() { | ||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||
| 										/> | ||||
| 									</div> | ||||
| 								) : null} | ||||
| 								<Button size="sm" className="btn-red" onClick={() => setEditId("new")}> | ||||
| 									{intl.formatMessage({ id: "dead-hosts.add" })} | ||||
| 								</Button> | ||||
|  | ||||
| 									<Button size="sm" className="btn-red" onClick={() => setEditId("new")}> | ||||
| 										{intl.formatMessage({ id: "dead-hosts.add" })} | ||||
| 									</Button> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						) : null} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table | ||||
|   | ||||
| @@ -2,22 +2,27 @@ import type { Table as ReactTable } from "@tanstack/react-table"; | ||||
| import { Button } from "src/components"; | ||||
| 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>; | ||||
| 	onNew?: () => void; | ||||
| 	isFiltered?: boolean; | ||||
| } | ||||
| export default function Empty({ tableInstance }: Props) { | ||||
| export default function Empty({ tableInstance, onNew, isFiltered }: Props) { | ||||
| 	return ( | ||||
| 		<tr> | ||||
| 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | ||||
| 				<div className="text-center my-4"> | ||||
| 					<h2>{intl.formatMessage({ id: "proxy-hosts.empty" })}</h2> | ||||
| 					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p> | ||||
| 					<Button className="btn-lime my-3">{intl.formatMessage({ id: "proxy-hosts.add" })}</Button> | ||||
| 					{isFiltered ? ( | ||||
| 						<h2>{intl.formatMessage({ id: "empty-search" })}</h2> | ||||
| 					) : ( | ||||
| 						<> | ||||
| 							<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> | ||||
| 			</td> | ||||
| 		</tr> | ||||
|   | ||||
| @@ -9,9 +9,14 @@ import Empty from "./Empty"; | ||||
|  | ||||
| interface Props { | ||||
| 	data: ProxyHost[]; | ||||
| 	isFiltered?: 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 columns = useMemo( | ||||
| 		() => [ | ||||
| @@ -64,7 +69,7 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.display({ | ||||
| 				id: "id", // todo: not needed for a display? | ||||
| 				id: "id", | ||||
| 				cell: (info: any) => { | ||||
| 					return ( | ||||
| 						<span className="dropdown"> | ||||
| @@ -85,16 +90,39 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 										{ id: info.row.original.id }, | ||||
| 									)} | ||||
| 								</span> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 								<a | ||||
| 									className="dropdown-item" | ||||
| 									href="#" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										onEdit?.(info.row.original.id); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconEdit size={16} /> | ||||
| 									{intl.formatMessage({ id: "action.edit" })} | ||||
| 								</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} /> | ||||
| 									{intl.formatMessage({ id: "action.disable" })} | ||||
| 									{intl.formatMessage({ | ||||
| 										id: info.row.original.enabled ? "action.disable" : "action.enable", | ||||
| 									})} | ||||
| 								</a> | ||||
| 								<div className="dropdown-divider" /> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 								<a | ||||
| 									className="dropdown-item" | ||||
| 									href="#" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										onDelete?.(info.row.original.id); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconTrash size={16} /> | ||||
| 									{intl.formatMessage({ id: "action.delete" })} | ||||
| 								</a> | ||||
| @@ -107,7 +135,7 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 				}, | ||||
| 			}), | ||||
| 		], | ||||
| 		[columnHelper], | ||||
| 		[columnHelper, onEdit, onDisableToggle, onDelete], | ||||
| 	); | ||||
|  | ||||
| 	const tableInstance = useReactTable<ProxyHost>({ | ||||
| @@ -121,5 +149,10 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 		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 { useQueryClient } from "@tanstack/react-query"; | ||||
| import { useState } from "react"; | ||||
| import Alert from "react-bootstrap/Alert"; | ||||
| import { deleteProxyHost, toggleProxyHost } from "src/api/backend"; | ||||
| import { Button, LoadingPage } from "src/components"; | ||||
| import { useProxyHosts } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
| import { DeleteConfirmModal, ProxyHostModal } from "src/modals"; | ||||
| import { showSuccess } from "src/notifications"; | ||||
| import Table from "./Table"; | ||||
|  | ||||
| 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"]); | ||||
|  | ||||
| 	if (isLoading) { | ||||
| @@ -16,6 +25,31 @@ export default function TableWrapper() { | ||||
| 		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 ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-lime" /> | ||||
| @@ -25,27 +59,48 @@ export default function TableWrapper() { | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "proxy-hosts.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" | ||||
| 									/> | ||||
| 						{data?.length ? ( | ||||
| 							<div className="col-md-auto col-sm-12"> | ||||
| 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 									<div className="input-group input-group-flat w-auto"> | ||||
| 										<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> | ||||
| 									<Button size="sm" className="btn-lime"> | ||||
| 										{intl.formatMessage({ id: "proxy-hosts.add" })} | ||||
| 									</Button> | ||||
| 								</div> | ||||
| 								<Button size="sm" className="btn-lime"> | ||||
| 									{intl.formatMessage({ id: "proxy-hosts.add" })} | ||||
| 								</Button> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						) : null} | ||||
| 					</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> | ||||
| 	); | ||||
|   | ||||
| @@ -74,7 +74,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.display({ | ||||
| 				id: "id", // todo: not needed for a display? | ||||
| 				id: "id", | ||||
| 				cell: (info: any) => { | ||||
| 					return ( | ||||
| 						<span className="dropdown"> | ||||
|   | ||||
| @@ -59,9 +59,9 @@ export default function TableWrapper() { | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "redirection-hosts.title" })}</h2> | ||||
| 						</div> | ||||
| 						<div className="col-md-auto col-sm-12"> | ||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 								{data?.length ? ( | ||||
| 						{data?.length ? ( | ||||
| 							<div className="col-md-auto col-sm-12"> | ||||
| 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 									<div className="input-group input-group-flat w-auto"> | ||||
| 										<span className="input-group-text input-group-text-sm"> | ||||
| 											<IconSearch size={16} /> | ||||
| @@ -74,12 +74,13 @@ export default function TableWrapper() { | ||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||
| 										/> | ||||
| 									</div> | ||||
| 								) : null} | ||||
| 								<Button size="sm" className="btn-yellow" onClick={() => setEditId("new")}> | ||||
| 									{intl.formatMessage({ id: "redirection-hosts.add" })} | ||||
| 								</Button> | ||||
|  | ||||
| 									<Button size="sm" className="btn-yellow" onClick={() => setEditId("new")}> | ||||
| 										{intl.formatMessage({ id: "redirection-hosts.add" })} | ||||
| 									</Button> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						) : null} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table | ||||
|   | ||||
| @@ -62,9 +62,9 @@ export default function TableWrapper() { | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "streams.title" })}</h2> | ||||
| 						</div> | ||||
| 						<div className="col-md-auto col-sm-12"> | ||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 								{data?.length ? ( | ||||
| 						{data?.length ? ( | ||||
| 							<div className="col-md-auto col-sm-12"> | ||||
| 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 									<div className="input-group input-group-flat w-auto"> | ||||
| 										<span className="input-group-text input-group-text-sm"> | ||||
| 											<IconSearch size={16} /> | ||||
| @@ -77,12 +77,12 @@ export default function TableWrapper() { | ||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||
| 										/> | ||||
| 									</div> | ||||
| 								) : null} | ||||
| 								<Button size="sm" className="btn-blue" onClick={() => setEditId("new")}> | ||||
| 									{intl.formatMessage({ id: "streams.add" })} | ||||
| 								</Button> | ||||
| 									<Button size="sm" className="btn-blue" onClick={() => setEditId("new")}> | ||||
| 										{intl.formatMessage({ id: "streams.add" })} | ||||
| 									</Button> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						) : null} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table | ||||
|   | ||||
| @@ -63,9 +63,9 @@ export default function TableWrapper() { | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "users.title" })}</h2> | ||||
| 						</div> | ||||
| 						<div className="col-md-auto col-sm-12"> | ||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 								{data?.length ? ( | ||||
| 						{data?.length ? ( | ||||
| 							<div className="col-md-auto col-sm-12"> | ||||
| 								<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 									<div className="input-group input-group-flat w-auto"> | ||||
| 										<span className="input-group-text input-group-text-sm"> | ||||
| 											<IconSearch size={16} /> | ||||
| @@ -78,12 +78,13 @@ export default function TableWrapper() { | ||||
| 											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} | ||||
| 										/> | ||||
| 									</div> | ||||
| 								) : null} | ||||
| 								<Button size="sm" className="btn-orange" onClick={() => setEditUserId("new")}> | ||||
| 									{intl.formatMessage({ id: "users.add" })} | ||||
| 								</Button> | ||||
|  | ||||
| 									<Button size="sm" className="btn-orange" onClick={() => setEditUserId("new")}> | ||||
| 										{intl.formatMessage({ id: "users.add" })} | ||||
| 									</Button> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						) : null} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table | ||||
|   | ||||
		Reference in New Issue
	
	Block a user