mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00:00 
			
		
		
		
	Settings polish
This commit is contained in:
		| @@ -196,10 +196,10 @@ export interface Stream { | ||||
|  | ||||
| export interface Setting { | ||||
| 	id: string; | ||||
| 	name: string; | ||||
| 	description: string; | ||||
| 	name?: string; | ||||
| 	description?: string; | ||||
| 	value: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	meta?: Record<string, any>; | ||||
| } | ||||
|  | ||||
| export interface DNSProvider { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ export * from "./useProxyHost"; | ||||
| export * from "./useProxyHosts"; | ||||
| export * from "./useRedirectionHost"; | ||||
| export * from "./useRedirectionHosts"; | ||||
| export * from "./useSetting"; | ||||
| export * from "./useStream"; | ||||
| export * from "./useStreams"; | ||||
| export * from "./useTheme"; | ||||
|   | ||||
							
								
								
									
										40
									
								
								frontend/src/hooks/useSetting.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								frontend/src/hooks/useSetting.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||||
| import { getSetting, type Setting, updateSetting } from "src/api/backend"; | ||||
|  | ||||
| const fetchSetting = (id: string) => { | ||||
| 	return getSetting(id); | ||||
| }; | ||||
|  | ||||
| const useSetting = (id: string, options = {}) => { | ||||
| 	return useQuery<Setting, Error>({ | ||||
| 		queryKey: ["setting", id], | ||||
| 		queryFn: () => fetchSetting(id), | ||||
| 		staleTime: 60 * 1000, // 1 minute | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| const useSetSetting = () => { | ||||
| 	const queryClient = useQueryClient(); | ||||
| 	return useMutation({ | ||||
| 		mutationFn: (values: Setting) => updateSetting(values), | ||||
| 		onMutate: (values: Setting) => { | ||||
| 			if (!values.id) { | ||||
| 				return; | ||||
| 			} | ||||
| 			const previousObject = queryClient.getQueryData(["setting", values.id]); | ||||
| 			queryClient.setQueryData(["setting", values.id], (old: Setting) => ({ | ||||
| 				...old, | ||||
| 				...values, | ||||
| 			})); | ||||
| 			return () => queryClient.setQueryData(["setting", values.id], previousObject); | ||||
| 		}, | ||||
| 		onError: (_, __, rollback: any) => rollback(), | ||||
| 		onSuccess: async ({ id }: Setting) => { | ||||
| 			queryClient.invalidateQueries({ queryKey: ["setting", id] }); | ||||
| 			queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); | ||||
| 		}, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { useSetting, useSetSetting }; | ||||
| @@ -168,7 +168,16 @@ | ||||
|   "role.admin": "Administrator", | ||||
|   "role.standard-user": "Standard User", | ||||
|   "save": "Save", | ||||
|   "setting": "Setting", | ||||
|   "settings": "Settings", | ||||
|   "settings.default-site": "Default Site", | ||||
|   "settings.default-site.404": "404 Page", | ||||
|   "settings.default-site.444": "No Response (444)", | ||||
|   "settings.default-site.congratulations": "Congratulations Page", | ||||
|   "settings.default-site.description": "What to show when Nginx is hit with an unknown Host", | ||||
|   "settings.default-site.html": "Custom HTML", | ||||
|   "settings.default-site.html.placeholder": "<!-- Enter your custom HTML content here -->", | ||||
|   "settings.default-site.redirect": "Redirect", | ||||
|   "setup.preamble": "Get started by creating your admin account.", | ||||
|   "setup.title": "Welcome!", | ||||
|   "sign-in": "Sign in", | ||||
|   | ||||
| @@ -506,9 +506,36 @@ | ||||
| 	"save": { | ||||
| 		"defaultMessage": "Save" | ||||
| 	}, | ||||
| 	"setting": { | ||||
| 		"defaultMessage": "Setting" | ||||
| 	}, | ||||
| 	"settings": { | ||||
| 		"defaultMessage": "Settings" | ||||
| 	}, | ||||
| 	"settings.default-site": { | ||||
| 		"defaultMessage": "Default Site" | ||||
| 	}, | ||||
| 	"settings.default-site.404": { | ||||
| 		"defaultMessage": "404 Page" | ||||
| 	}, | ||||
| 	"settings.default-site.444": { | ||||
| 		"defaultMessage": "No Response (444)" | ||||
| 	}, | ||||
| 	"settings.default-site.congratulations": { | ||||
| 		"defaultMessage": "Congratulations Page" | ||||
| 	}, | ||||
| 	"settings.default-site.description": { | ||||
| 		"defaultMessage": "What to show when Nginx is hit with an unknown Host" | ||||
| 	}, | ||||
| 	"settings.default-site.html": { | ||||
| 		"defaultMessage": "Custom HTML" | ||||
| 	}, | ||||
| 	"settings.default-site.html.placeholder": { | ||||
| 		"defaultMessage": "<!-- Enter your custom HTML content here -->" | ||||
| 	}, | ||||
| 	"settings.default-site.redirect": { | ||||
| 		"defaultMessage": "Redirect" | ||||
| 	}, | ||||
| 	"setup.preamble": { | ||||
| 		"defaultMessage": "Get started by creating your admin account." | ||||
| 	}, | ||||
|   | ||||
							
								
								
									
										269
									
								
								frontend/src/pages/Settings/DefaultSite.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								frontend/src/pages/Settings/DefaultSite.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,269 @@ | ||||
| import CodeEditor from "@uiw/react-textarea-code-editor"; | ||||
| import { Field, Form, Formik } from "formik"; | ||||
| import { type ReactNode, useState } from "react"; | ||||
| import { Alert } from "react-bootstrap"; | ||||
| import { Button, Loading } from "src/components"; | ||||
| import { useSetSetting, useSetting } from "src/hooks"; | ||||
| import { intl, T } from "src/locale"; | ||||
| import { validateString } from "src/modules/Validations"; | ||||
| import { showObjectSuccess } from "src/notifications"; | ||||
|  | ||||
| export default function DefaultSite() { | ||||
| 	const { data, isLoading, error } = useSetting("default-site"); | ||||
| 	const { mutate: setSetting } = useSetSetting(); | ||||
| 	const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); | ||||
| 	const [isSubmitting, setIsSubmitting] = useState(false); | ||||
|  | ||||
| 	const onSubmit = async (values: any, { setSubmitting }: any) => { | ||||
| 		if (isSubmitting) return; | ||||
| 		setIsSubmitting(true); | ||||
| 		setErrorMsg(null); | ||||
|  | ||||
| 		const payload = { | ||||
| 			id: "default-site", | ||||
| 			value: values.value, | ||||
| 			meta: { | ||||
| 				redirect: values.redirect, | ||||
| 				html: values.html, | ||||
| 			}, | ||||
| 		}; | ||||
|  | ||||
| 		setSetting(payload, { | ||||
| 			onError: (err: any) => setErrorMsg(<T id={err.message} />), | ||||
| 			onSuccess: () => { | ||||
| 				showObjectSuccess("setting", "saved"); | ||||
| 			}, | ||||
| 			onSettled: () => { | ||||
| 				setIsSubmitting(false); | ||||
| 				setSubmitting(false); | ||||
| 			}, | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
| 	if (!isLoading && error) { | ||||
| 		return ( | ||||
| 			<div className="card-body"> | ||||
| 				<div className="mb-3"> | ||||
| 					<Alert variant="danger" show> | ||||
| 						{error.message} | ||||
| 					</Alert> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	if (isLoading) { | ||||
| 		return ( | ||||
| 			<div className="card-body"> | ||||
| 				<div className="mb-3"> | ||||
| 					<Loading noLogo /> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<Formik | ||||
| 			initialValues={ | ||||
| 				{ | ||||
| 					value: data?.value || "congratulations", | ||||
| 					redirect: data?.meta?.redirect || "", | ||||
| 					html: data?.meta?.html || "", | ||||
| 				} as any | ||||
| 			} | ||||
| 			onSubmit={onSubmit} | ||||
| 		> | ||||
| 			{({ values }) => ( | ||||
| 				<Form> | ||||
| 					<div className="card-body"> | ||||
| 						<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible> | ||||
| 							{errorMsg} | ||||
| 						</Alert> | ||||
| 						<Field name="value"> | ||||
| 							{({ field, form }: any) => ( | ||||
| 								<div className="mb-3"> | ||||
| 									<label className="form-label" htmlFor="setting-host-unknown"> | ||||
| 										<T id="settings.default-site.description" /> | ||||
| 									</label> | ||||
| 									<div className="form-selectgroup form-selectgroup-boxes d-flex flex-column"> | ||||
| 										<label className="form-selectgroup-item flex-fill"> | ||||
| 											<input | ||||
| 												type="radio" | ||||
| 												name={field.name} | ||||
| 												value="congratulations" | ||||
| 												className="form-selectgroup-input" | ||||
| 												checked={field.value === "congratulations"} | ||||
| 												onChange={(e) => form.setFieldValue(field.name, e.target.value)} | ||||
| 											/> | ||||
| 											<div className="form-selectgroup-label d-flex align-items-center p-3"> | ||||
| 												<div className="me-3"> | ||||
| 													<span className="form-selectgroup-check" /> | ||||
| 												</div> | ||||
| 												<div> | ||||
| 													<T id="settings.default-site.congratulations" /> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 										</label> | ||||
| 										<label className="form-selectgroup-item flex-fill"> | ||||
| 											<input | ||||
| 												type="radio" | ||||
| 												name={field.name} | ||||
| 												value="404" | ||||
| 												className="form-selectgroup-input" | ||||
| 												checked={field.value === "404"} | ||||
| 												onChange={(e) => form.setFieldValue(field.name, e.target.value)} | ||||
| 											/> | ||||
| 											<div className="form-selectgroup-label d-flex align-items-center p-3"> | ||||
| 												<div className="me-3"> | ||||
| 													<span className="form-selectgroup-check" /> | ||||
| 												</div> | ||||
| 												<div> | ||||
| 													<T id="settings.default-site.404" /> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 										</label> | ||||
| 										<label className="form-selectgroup-item flex-fill"> | ||||
| 											<input | ||||
| 												type="radio" | ||||
| 												name={field.name} | ||||
| 												value="444" | ||||
| 												className="form-selectgroup-input" | ||||
| 												checked={field.value === "444"} | ||||
| 												onChange={(e) => form.setFieldValue(field.name, e.target.value)} | ||||
| 											/> | ||||
| 											<div className="form-selectgroup-label d-flex align-items-center p-3"> | ||||
| 												<div className="me-3"> | ||||
| 													<span className="form-selectgroup-check" /> | ||||
| 												</div> | ||||
| 												<div> | ||||
| 													<T id="settings.default-site.444" /> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 										</label> | ||||
| 										<label className="form-selectgroup-item flex-fill"> | ||||
| 											<input | ||||
| 												type="radio" | ||||
| 												name={field.name} | ||||
| 												value="redirect" | ||||
| 												className="form-selectgroup-input" | ||||
| 												checked={field.value === "redirect"} | ||||
| 												onChange={(e) => form.setFieldValue(field.name, e.target.value)} | ||||
| 											/> | ||||
| 											<div className="form-selectgroup-label d-flex align-items-center p-3"> | ||||
| 												<div className="me-3"> | ||||
| 													<span className="form-selectgroup-check" /> | ||||
| 												</div> | ||||
| 												<div> | ||||
| 													<T id="settings.default-site.redirect" /> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 										</label> | ||||
| 										<label className="form-selectgroup-item flex-fill"> | ||||
| 											<input | ||||
| 												type="radio" | ||||
| 												name={field.name} | ||||
| 												value="html" | ||||
| 												className="form-selectgroup-input" | ||||
| 												checked={field.value === "html"} | ||||
| 												onChange={(e) => form.setFieldValue(field.name, e.target.value)} | ||||
| 											/> | ||||
| 											<div className="form-selectgroup-label d-flex align-items-center p-3"> | ||||
| 												<div className="me-3"> | ||||
| 													<span className="form-selectgroup-check" /> | ||||
| 												</div> | ||||
| 												<div> | ||||
| 													<T id="settings.default-site.redirect" /> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 										</label> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							)} | ||||
| 						</Field> | ||||
| 						{values.value === "redirect" && ( | ||||
| 							<Field name="redirect" validate={validateString(1, 255)}> | ||||
| 								{({ field, form }: any) => ( | ||||
| 									<div className="mt-5 mb-3"> | ||||
| 										<label className="form-label" htmlFor="setting-host-unknown"> | ||||
| 											<T id="settings.default-site.redirect" /> | ||||
| 										</label> | ||||
| 										<div> | ||||
| 											<input | ||||
| 												id="redirect" | ||||
| 												type="text" | ||||
| 												placeholder="https://" | ||||
| 												required | ||||
| 												autoComplete="off" | ||||
| 												className="form-control" | ||||
| 												{...field} | ||||
| 											/> | ||||
| 											{form.errors.redirect ? ( | ||||
| 												<div className="invalid-feedback"> | ||||
| 													{form.errors.redirect && form.touched.redirect | ||||
| 														? form.errors.redirect | ||||
| 														: null} | ||||
| 												</div> | ||||
| 											) : null} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								)} | ||||
| 							</Field> | ||||
| 						)} | ||||
| 						{values.value === "html" && ( | ||||
| 							<Field name="html" validate={validateString(1)}> | ||||
| 								{({ field, form }: any) => ( | ||||
| 									<div className="mt-5 mb-3"> | ||||
| 										<label className="form-label" htmlFor="setting-host-unknown"> | ||||
| 											<T id="settings.default-site.html" /> | ||||
| 										</label> | ||||
| 										<div> | ||||
| 											<CodeEditor | ||||
| 												// Believe it or not, 'html' sucks yet 'php' renders the html | ||||
| 												// content much nicer. | ||||
| 												language="php" | ||||
| 												placeholder={intl.formatMessage({ | ||||
| 													id: "settings.default-site.html.placeholder", | ||||
| 												})} | ||||
| 												padding={15} | ||||
| 												data-color-mode="dark" | ||||
| 												minHeight={300} | ||||
| 												indentWidth={2} | ||||
| 												style={{ | ||||
| 													fontFamily: | ||||
| 														"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", | ||||
| 													borderRadius: "0.3rem", | ||||
| 													minHeight: "300px", | ||||
| 													backgroundColor: "var(--tblr-bg-surface-dark)", | ||||
| 												}} | ||||
| 												{...field} | ||||
| 											/> | ||||
| 											{form.errors.html ? ( | ||||
| 												<div className="invalid-feedback"> | ||||
| 													{form.errors.html && form.touched.html ? form.errors.html : null} | ||||
| 												</div> | ||||
| 											) : null} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								)} | ||||
| 							</Field> | ||||
| 						)} | ||||
| 					</div> | ||||
| 					<div className="card-footer bg-transparent mt-auto"> | ||||
| 						<div className="btn-list justify-content-end"> | ||||
| 							<Button | ||||
| 								type="submit" | ||||
| 								actionType="primary" | ||||
| 								className="ms-auto bg-teal" | ||||
| 								data-bs-dismiss="modal" | ||||
| 								isLoading={isSubmitting} | ||||
| 								disabled={isSubmitting} | ||||
| 							> | ||||
| 								<T id="save" /> | ||||
| 							</Button> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</Form> | ||||
| 			)} | ||||
| 		</Formik> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										40
									
								
								frontend/src/pages/Settings/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								frontend/src/pages/Settings/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { T } from "src/locale"; | ||||
| import DefaultSite from "./DefaultSite"; | ||||
|  | ||||
| export default function Layout() { | ||||
| 	// Taken from https://preview.tabler.io/settings.html | ||||
| 	// Refer to that when updating this content | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-teal" /> | ||||
| 			<div className="card-table"> | ||||
| 				<div className="card-header"> | ||||
| 					<div className="row w-full"> | ||||
| 						<h2 className="mt-1 mb-0"> | ||||
| 							<T id="settings" /> | ||||
| 						</h2> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div className="row g-0"> | ||||
| 					<div className="col-12 col-md-3 border-end"> | ||||
| 						<div className="card-body mt-0 pt-0"> | ||||
| 							<div className="list-group list-group-transparent"> | ||||
| 								<a | ||||
| 									href="#" | ||||
| 									className="list-group-item list-group-item-action d-flex align-items-center active" | ||||
| 									onClick={(e) => e.preventDefault()} | ||||
| 								> | ||||
| 									<T id="settings.default-site" /> | ||||
| 								</a> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div className="col-12 col-md-9 d-flex flex-column"> | ||||
| 						<DefaultSite /> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -1,113 +0,0 @@ | ||||
| import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react"; | ||||
| import { T } from "src/locale"; | ||||
|  | ||||
| export default function SettingTable() { | ||||
| 	return ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-teal" /> | ||||
| 			<div className="card-table"> | ||||
| 				<div className="card-header"> | ||||
| 					<div className="row w-full"> | ||||
| 						<h2 className="mt-1 mb-0"> | ||||
| 							<T id="settings" /> | ||||
| 						</h2> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div id="advanced-table"> | ||||
| 					<div className="table-responsive"> | ||||
| 						<table className="table table-vcenter table-selectable"> | ||||
| 							<thead> | ||||
| 								<tr> | ||||
| 									<th className="w-1" /> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Source | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Destination | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											SSL | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Access | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Status | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th className="w-1" /> | ||||
| 								</tr> | ||||
| 							</thead> | ||||
| 							<tbody className="table-tbody"> | ||||
| 								<tr> | ||||
| 									<td data-label="Owner"> | ||||
| 										<div className="d-flex py-1 align-items-center"> | ||||
| 											<span | ||||
| 												className="avatar avatar-2 me-2" | ||||
| 												style={{ | ||||
| 													backgroundImage: | ||||
| 														"url(//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm)", | ||||
| 												}} | ||||
| 											/> | ||||
| 										</div> | ||||
| 									</td> | ||||
| 									<td data-label="Destination"> | ||||
| 										<div className="flex-fill"> | ||||
| 											<div className="font-weight-medium"> | ||||
| 												<span className="badge badge-lg domain-name">blog.jc21.com</span> | ||||
| 											</div> | ||||
| 											<div className="text-secondary mt-1">Created: 20th September 2024</div> | ||||
| 										</div> | ||||
| 									</td> | ||||
| 									<td data-label="Source">http://172.17.0.1:3001</td> | ||||
| 									<td data-label="SSL">Let's Encrypt</td> | ||||
| 									<td data-label="Access">Public</td> | ||||
| 									<td data-label="Status"> | ||||
| 										<span className="badge bg-lime-lt">Online</span> | ||||
| 									</td> | ||||
| 									<td data-label="Status" className="text-end"> | ||||
| 										<span className="dropdown"> | ||||
| 											<button | ||||
| 												type="button" | ||||
| 												className="btn dropdown-toggle btn-action btn-sm px-1" | ||||
| 												data-bs-boundary="viewport" | ||||
| 												data-bs-toggle="dropdown" | ||||
| 											> | ||||
| 												<IconDotsVertical /> | ||||
| 											</button> | ||||
| 											<div className="dropdown-menu dropdown-menu-end"> | ||||
| 												<span className="dropdown-header">Proxy Host #2</span> | ||||
| 												<a className="dropdown-item" href="#"> | ||||
| 													<IconEdit size={16} /> | ||||
| 													Edit | ||||
| 												</a> | ||||
| 												<a className="dropdown-item" href="#"> | ||||
| 													<IconPower size={16} /> | ||||
| 													Disable | ||||
| 												</a> | ||||
| 												<div className="dropdown-divider" /> | ||||
| 												<a className="dropdown-item" href="#"> | ||||
| 													<IconTrash size={16} /> | ||||
| 													Delete | ||||
| 												</a> | ||||
| 											</div> | ||||
| 										</span> | ||||
| 									</td> | ||||
| 								</tr> | ||||
| 							</tbody> | ||||
| 						</table> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { HasPermission } from "src/components"; | ||||
| import SettingTable from "./SettingTable"; | ||||
| import Layout from "./Layout"; | ||||
|  | ||||
| const Settings = () => { | ||||
| 	return ( | ||||
| 		<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo> | ||||
| 			<SettingTable /> | ||||
| 			<Layout /> | ||||
| 		</HasPermission> | ||||
| 	); | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user