mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-11-04 09:25:15 +00:00 
			
		
		
		
	Tidy up
- Add help docs for most sections - Add translations documentation - Fix up todos - Remove german translation
This commit is contained in:
		@@ -4,7 +4,6 @@ import type { AccessList } from "./models";
 | 
			
		||||
export async function createAccessList(item: AccessList): Promise<AccessList> {
 | 
			
		||||
	return await api.post({
 | 
			
		||||
		url: "/nginx/access-lists",
 | 
			
		||||
		// todo: only use whitelist of fields for this data
 | 
			
		||||
		data: item,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import type { DeadHost } from "./models";
 | 
			
		||||
export async function createDeadHost(item: DeadHost): Promise<DeadHost> {
 | 
			
		||||
	return await api.post({
 | 
			
		||||
		url: "/nginx/dead-hosts",
 | 
			
		||||
		// todo: only use whitelist of fields for this data
 | 
			
		||||
		data: item,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import type { ProxyHost } from "./models";
 | 
			
		||||
export async function createProxyHost(item: ProxyHost): Promise<ProxyHost> {
 | 
			
		||||
	return await api.post({
 | 
			
		||||
		url: "/nginx/proxy-hosts",
 | 
			
		||||
		// todo: only use whitelist of fields for this data
 | 
			
		||||
		data: item,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import type { RedirectionHost } from "./models";
 | 
			
		||||
export async function createRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> {
 | 
			
		||||
	return await api.post({
 | 
			
		||||
		url: "/nginx/redirection-hosts",
 | 
			
		||||
		// todo: only use whitelist of fields for this data
 | 
			
		||||
		data: item,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import type { Stream } from "./models";
 | 
			
		||||
export async function createStream(item: Stream): Promise<Stream> {
 | 
			
		||||
	return await api.post({
 | 
			
		||||
		url: "/nginx/streams",
 | 
			
		||||
		// todo: only use whitelist of fields for this data
 | 
			
		||||
		data: item,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ export interface NewUser {
 | 
			
		||||
export async function createUser(item: NewUser, noAuth?: boolean): Promise<User> {
 | 
			
		||||
	return await api.post({
 | 
			
		||||
		url: "/users",
 | 
			
		||||
		// todo: only use whitelist of fields for this data
 | 
			
		||||
		data: item,
 | 
			
		||||
		noAuth,
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { createIntl, createIntlCache } from "react-intl";
 | 
			
		||||
import langDe from "./lang/de.json";
 | 
			
		||||
import langEn from "./lang/en.json";
 | 
			
		||||
import langFa from "./lang/fa.json";
 | 
			
		||||
import langList from "./lang/lang-list.json";
 | 
			
		||||
@@ -9,15 +8,12 @@ import langList from "./lang/lang-list.json";
 | 
			
		||||
// Remember when adding to this list, also update check-locales.js script
 | 
			
		||||
const localeOptions = [
 | 
			
		||||
	["en", "en-US"],
 | 
			
		||||
	["de", "de-DE"],
 | 
			
		||||
	["fa", "fa-IR"],
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
 | 
			
		||||
	const thisLocale = locale || "en";
 | 
			
		||||
	switch (thisLocale.slice(0, 2)) {
 | 
			
		||||
		case "de":
 | 
			
		||||
			return Object.assign({}, langList, langEn, langDe);
 | 
			
		||||
		case "fa":
 | 
			
		||||
			return Object.assign({}, langList, langEn, langFa);
 | 
			
		||||
		default:
 | 
			
		||||
@@ -27,9 +23,6 @@ const loadMessages = (locale?: string): typeof langList & typeof langEn => {
 | 
			
		||||
 | 
			
		||||
const getFlagCodeForLocale = (locale?: string) => {
 | 
			
		||||
	switch (locale) {
 | 
			
		||||
		case "de-DE":
 | 
			
		||||
		case "de":
 | 
			
		||||
			return "DE";
 | 
			
		||||
		case "fa-IR":
 | 
			
		||||
		case "fa":
 | 
			
		||||
			return "IR";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,48 @@
 | 
			
		||||
# Internationalisation support
 | 
			
		||||
 | 
			
		||||
## Before you start
 | 
			
		||||
 | 
			
		||||
It's highly recommended that you spin up a development instance of this project
 | 
			
		||||
on your docker capable server. It's pretty easy:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
git clone https://github.com/NginxProxyManager/nginx-proxy-manager.git
 | 
			
		||||
cd nginx-proxy-manager
 | 
			
		||||
./scripts/start-dev -f
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then after a while, you can access http://yourserverip:3081
 | 
			
		||||
 | 
			
		||||
This stack will watch the file system for changes, especially to language files,
 | 
			
		||||
and reload the site you have open in the browser.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Adding new translations
 | 
			
		||||
 | 
			
		||||
Modify the files in the `src` folder. Follow the conventions already there.
 | 
			
		||||
 | 
			
		||||
When the development stack is running, it will sort the locale lang files
 | 
			
		||||
for you when you save.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## After making changes
 | 
			
		||||
 | 
			
		||||
You will need to run `yarn locale-compile` in this frontend folder for
 | 
			
		||||
If you're NOT running the development stack, you will need to run
 | 
			
		||||
`yarn locale-compile` in the `frontend` folder for
 | 
			
		||||
the new translations to be compiled into the `lang` folder.
 | 
			
		||||
 | 
			
		||||
When running in dev mode, this should automatically happen within Vite.
 | 
			
		||||
 | 
			
		||||
## Adding a whole new language
 | 
			
		||||
 | 
			
		||||
There's a fair bit you'll need to touch. Here's a list that may
 | 
			
		||||
not be complete by the time you're reading this:
 | 
			
		||||
 | 
			
		||||
- frontend/src/locale/src/[yourlang].json
 | 
			
		||||
- frontend/src/locale/src/lang-list.json
 | 
			
		||||
- frontend/src/locale/src/HelpDoc/*
 | 
			
		||||
- frontend/src/locale/IntlProvider.tsx
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Checking for missing translations in other languages
 | 
			
		||||
## Checking for missing translations in languages
 | 
			
		||||
 | 
			
		||||
Run `node check-locales.cjs` in this frontend folder.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Adding new languages
 | 
			
		||||
 | 
			
		||||
todo
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "dashboard": "Armaturenbrett"
 | 
			
		||||
}
 | 
			
		||||
@@ -100,7 +100,11 @@
 | 
			
		||||
  "error.invalid-auth": "Invalid email or password",
 | 
			
		||||
  "error.invalid-domain": "Invalid domain: {domain}",
 | 
			
		||||
  "error.invalid-email": "Invalid email address",
 | 
			
		||||
  "error.max-character-length": "Maximum length is {max} character{max, plural, one {} other {s}}",
 | 
			
		||||
  "error.max-domains": "Too many domains, max is {max}",
 | 
			
		||||
  "error.maximum": "Maximum is {max}",
 | 
			
		||||
  "error.min-character-length": "Minimum length is {min} character{min, plural, one {} other {s}}",
 | 
			
		||||
  "error.minimum": "Minimum is {min}",
 | 
			
		||||
  "error.passwords-must-match": "Passwords must match",
 | 
			
		||||
  "error.required": "This is required",
 | 
			
		||||
  "expires.on": "Expires: {date}",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/src/locale/src/HelpDoc/en/AccessLists.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/locale/src/HelpDoc/en/AccessLists.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
## What is an Access List?
 | 
			
		||||
 | 
			
		||||
Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.
 | 
			
		||||
 | 
			
		||||
You can configure multiple client rules, usernames and passwords for a single Access List and then apply that to one or more _Proxy Hosts_.
 | 
			
		||||
 | 
			
		||||
This is most useful for forwarded web services that do not have authentication mechanisms built in or when you want to protect from unknown clients.
 | 
			
		||||
							
								
								
									
										32
									
								
								frontend/src/locale/src/HelpDoc/en/Certificates.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								frontend/src/locale/src/HelpDoc/en/Certificates.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
## Certificates Help
 | 
			
		||||
 | 
			
		||||
### HTTP Certificate
 | 
			
		||||
 | 
			
		||||
A HTTP validated certificate means Let's Encrypt servers will
 | 
			
		||||
attempt to reach your domains over HTTP (not HTTPS!) and if successful, they
 | 
			
		||||
will issue your certificate.
 | 
			
		||||
 | 
			
		||||
For this method, you will have to have a _Proxy Host_ created for your domains(s) that
 | 
			
		||||
is accessible with HTTP and pointing to this Nginx installation. After a certificate
 | 
			
		||||
has been given, you can modify the _Proxy Host_ to also use this certificate for HTTPS
 | 
			
		||||
connections. However, the _Proxy Host_ will still need to be configured for HTTP access
 | 
			
		||||
in order for the certificate to renew.
 | 
			
		||||
 | 
			
		||||
This process _does not_ support wildcard domains.
 | 
			
		||||
 | 
			
		||||
### DNS Certificate
 | 
			
		||||
 | 
			
		||||
A DNS validated certificate requires you to use a DNS Provider plugin. This DNS
 | 
			
		||||
Provider will be used to create temporary records on your domain and then Let's
 | 
			
		||||
Encrypt will query those records to be sure you're the owner and if successful, they
 | 
			
		||||
will issue your certificate.
 | 
			
		||||
 | 
			
		||||
You do not need a _Proxy Host_ to be created prior to requesting this type of
 | 
			
		||||
certificate. Nor do you need to have your _Proxy Host_ configured for HTTP access.
 | 
			
		||||
 | 
			
		||||
This process _does_ support wildcard domains.
 | 
			
		||||
 | 
			
		||||
### Custom Certificate
 | 
			
		||||
 | 
			
		||||
Use this option to upload your own SSL Certificate, as provided by your own
 | 
			
		||||
Certificate Authority.
 | 
			
		||||
							
								
								
									
										10
									
								
								frontend/src/locale/src/HelpDoc/en/DeadHosts.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/locale/src/HelpDoc/en/DeadHosts.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
## What is a 404 Host?
 | 
			
		||||
 | 
			
		||||
A 404 Host is simply a host setup that shows a 404 page.
 | 
			
		||||
 | 
			
		||||
This can be useful when your domain is listed in search engines and you want
 | 
			
		||||
to provide a nicer error page or specifically to tell the search indexers that
 | 
			
		||||
the domain pages no longer exist.
 | 
			
		||||
 | 
			
		||||
Another benefit of having this host is to track the logs for hits to it and
 | 
			
		||||
view the referrers.
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/src/locale/src/HelpDoc/en/ProxyHosts.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/locale/src/HelpDoc/en/ProxyHosts.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
## What is a Proxy Host?
 | 
			
		||||
 | 
			
		||||
A Proxy Host is the incoming endpoint for a web service that you want to forward.
 | 
			
		||||
 | 
			
		||||
It provides optional SSL termination for your service that might not have SSL support built in.
 | 
			
		||||
 | 
			
		||||
Proxy Hosts are the most common use for the Nginx Proxy Manager.
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/src/locale/src/HelpDoc/en/RedirectionHosts.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/locale/src/HelpDoc/en/RedirectionHosts.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
## What is a Redirection Host?
 | 
			
		||||
 | 
			
		||||
A Redirection Host will redirect requests from the incoming domain and push the
 | 
			
		||||
viewer to another domain.
 | 
			
		||||
 | 
			
		||||
The most common reason to use this type of host is when your website changes
 | 
			
		||||
domains but you still have search engine or referrer links pointing to the old domain.
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/src/locale/src/HelpDoc/en/Streams.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/locale/src/HelpDoc/en/Streams.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
## What is a Stream?
 | 
			
		||||
 | 
			
		||||
A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP
 | 
			
		||||
traffic directly to another computer on the network.
 | 
			
		||||
 | 
			
		||||
If you're running game servers, FTP or SSH servers this can come in handy.
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/src/locale/src/HelpDoc/en/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/locale/src/HelpDoc/en/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
export * as AccessLists from "./AccessLists.md";
 | 
			
		||||
export * as Certificates from "./Certificates.md";
 | 
			
		||||
export * as DeadHosts from "./DeadHosts.md";
 | 
			
		||||
export * as ProxyHosts from "./ProxyHosts.md";
 | 
			
		||||
export * as RedirectionHosts from "./RedirectionHosts.md";
 | 
			
		||||
export * as Streams from "./Streams.md";
 | 
			
		||||
							
								
								
									
										20
									
								
								frontend/src/locale/src/HelpDoc/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/locale/src/HelpDoc/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
// import * as de from "./de/index";
 | 
			
		||||
// import * as fa from "./fa/index";
 | 
			
		||||
import * as en from "./en/index";
 | 
			
		||||
 | 
			
		||||
const items: any = { en };
 | 
			
		||||
 | 
			
		||||
const fallbackLang = "en";
 | 
			
		||||
 | 
			
		||||
export const getHelpFile = (lang: string, section: string): string => {
 | 
			
		||||
	if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
 | 
			
		||||
		return items[lang][section].default;
 | 
			
		||||
	}
 | 
			
		||||
	// Fallback to English
 | 
			
		||||
	if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
 | 
			
		||||
		return items[fallbackLang][section].default;
 | 
			
		||||
	}
 | 
			
		||||
	throw new Error(`Cannot load help doc for ${lang}-${section}`);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default items;
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"dashboard": {
 | 
			
		||||
		"defaultMessage": "Armaturenbrett"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -302,9 +302,21 @@
 | 
			
		||||
	"error.invalid-email": {
 | 
			
		||||
		"defaultMessage": "Invalid email address"
 | 
			
		||||
	},
 | 
			
		||||
	"error.max-character-length": {
 | 
			
		||||
		"defaultMessage": "Maximum length is {max} character{max, plural, one {} other {s}}"
 | 
			
		||||
	},
 | 
			
		||||
	"error.max-domains": {
 | 
			
		||||
		"defaultMessage": "Too many domains, max is {max}"
 | 
			
		||||
	},
 | 
			
		||||
	"error.maximum": {
 | 
			
		||||
		"defaultMessage": "Maximum is {max}"
 | 
			
		||||
	},
 | 
			
		||||
	"error.min-character-length": {
 | 
			
		||||
		"defaultMessage": "Minimum length is {min} character{min, plural, one {} other {s}}"
 | 
			
		||||
	},
 | 
			
		||||
	"error.minimum": {
 | 
			
		||||
		"defaultMessage": "Minimum is {min}"
 | 
			
		||||
	},
 | 
			
		||||
	"error.passwords-must-match": {
 | 
			
		||||
		"defaultMessage": "Passwords must match"
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,27 @@ const DeleteConfirmModal = EasyModal.create(
 | 
			
		||||
					<Alert variant="danger" show={!!error} onClose={() => setError(null)} dismissible>
 | 
			
		||||
						{error}
 | 
			
		||||
					</Alert>
 | 
			
		||||
					{children}
 | 
			
		||||
					<div className="text-center mb-3">
 | 
			
		||||
						<svg
 | 
			
		||||
							role="img"
 | 
			
		||||
							aria-label="warning icon"
 | 
			
		||||
							xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							className="icon mb-2 text-danger icon-lg"
 | 
			
		||||
							width="24"
 | 
			
		||||
							height="24"
 | 
			
		||||
							viewBox="0 0 24 24"
 | 
			
		||||
							stroke-width="2"
 | 
			
		||||
							stroke="currentColor"
 | 
			
		||||
							fill="none"
 | 
			
		||||
							stroke-linecap="round"
 | 
			
		||||
							stroke-linejoin="round"
 | 
			
		||||
						>
 | 
			
		||||
							<path stroke="none" d="M0 0h24v24H0z" fill="none" />
 | 
			
		||||
							<path d="M12 9v2m0 4v.01" />
 | 
			
		||||
							<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75" />
 | 
			
		||||
						</svg>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div className="text-center mb-3">{children}</div>
 | 
			
		||||
				</Modal.Body>
 | 
			
		||||
				<Modal.Footer>
 | 
			
		||||
					<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										54
									
								
								frontend/src/modals/HelpModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								frontend/src/modals/HelpModal.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
import EasyModal, { type InnerModalProps } from "ez-modal-react";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import Modal from "react-bootstrap/Modal";
 | 
			
		||||
import ReactMarkdown from "react-markdown";
 | 
			
		||||
import { Button } from "src/components";
 | 
			
		||||
import { getLocale, T } from "src/locale";
 | 
			
		||||
import { getHelpFile } from "src/locale/src/HelpDoc";
 | 
			
		||||
 | 
			
		||||
interface Props extends InnerModalProps {
 | 
			
		||||
	section: string;
 | 
			
		||||
	color?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const showHelpModal = (section: string, color?: string) => {
 | 
			
		||||
	EasyModal.show(HelpModal, { section, color });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const HelpModal = EasyModal.create(({ section, color, visible, remove }: Props) => {
 | 
			
		||||
	const [markdownText, setMarkdownText] = useState("");
 | 
			
		||||
	const lang = getLocale(true);
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		try {
 | 
			
		||||
			const docFile = getHelpFile(lang, section) as any;
 | 
			
		||||
			fetch(docFile)
 | 
			
		||||
				.then((response) => response.text())
 | 
			
		||||
				.then(setMarkdownText);
 | 
			
		||||
		} catch (ex: any) {
 | 
			
		||||
			setMarkdownText(`**ERROR:** ${ex.message}`);
 | 
			
		||||
		}
 | 
			
		||||
	}, [lang, section]);
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<Modal show={visible} onHide={remove}>
 | 
			
		||||
			<Modal.Body>
 | 
			
		||||
				<ReactMarkdown>{markdownText}</ReactMarkdown>
 | 
			
		||||
			</Modal.Body>
 | 
			
		||||
			<Modal.Footer>
 | 
			
		||||
				<Button
 | 
			
		||||
					type="button"
 | 
			
		||||
					actionType="primary"
 | 
			
		||||
					className={cn("ms-auto", color ? `btn-${color}` : null)}
 | 
			
		||||
					data-bs-dismiss="modal"
 | 
			
		||||
					onClick={remove}
 | 
			
		||||
				>
 | 
			
		||||
					<T id="action.close" />
 | 
			
		||||
				</Button>
 | 
			
		||||
			</Modal.Footer>
 | 
			
		||||
		</Modal>
 | 
			
		||||
	);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export { showHelpModal };
 | 
			
		||||
@@ -5,6 +5,7 @@ export * from "./DeadHostModal";
 | 
			
		||||
export * from "./DeleteConfirmModal";
 | 
			
		||||
export * from "./DNSCertificateModal";
 | 
			
		||||
export * from "./EventDetailsModal";
 | 
			
		||||
export * from "./HelpModal";
 | 
			
		||||
export * from "./HTTPCertificateModal";
 | 
			
		||||
export * from "./PermissionsModal";
 | 
			
		||||
export * from "./ProxyHostModal";
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,10 @@ const validateString = (minLength = 0, maxLength = 0) => {
 | 
			
		||||
			return intl.formatMessage({ id: "error.required" });
 | 
			
		||||
		}
 | 
			
		||||
		if (minLength && value.length < minLength) {
 | 
			
		||||
			// TODO: i18n
 | 
			
		||||
			return `Minimum length is ${minLength} character${minLength === 1 ? "" : "s"}`;
 | 
			
		||||
			return intl.formatMessage({ id: "error.min-character-length" }, { min: minLength });
 | 
			
		||||
		}
 | 
			
		||||
		if (maxLength && (typeof value === "undefined" || value.length > maxLength)) {
 | 
			
		||||
			// TODO: i18n
 | 
			
		||||
			return `Maximum length is ${maxLength} character${maxLength === 1 ? "" : "s"}`;
 | 
			
		||||
			return intl.formatMessage({ id: "error.max-character-length" }, { max: maxLength });
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
@@ -33,12 +31,10 @@ const validateNumber = (min = -1, max = -1) => {
 | 
			
		||||
			return intl.formatMessage({ id: "error.required" });
 | 
			
		||||
		}
 | 
			
		||||
		if (min > -1 && int < min) {
 | 
			
		||||
			// TODO: i18n
 | 
			
		||||
			return `Minimum is ${min}`;
 | 
			
		||||
			return intl.formatMessage({ id: "error.minimum" }, { min });
 | 
			
		||||
		}
 | 
			
		||||
		if (max > -1 && int > max) {
 | 
			
		||||
			// TODO: i18n
 | 
			
		||||
			return `Maximum is ${max}`;
 | 
			
		||||
			return intl.formatMessage({ id: "error.maximum" }, { max });
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
				cell: (info: any) => <T id="proxy-hosts.count" data={{ count: info.getValue() }} />,
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.display({
 | 
			
		||||
				id: "id", // todo: not needed for a display?
 | 
			
		||||
				id: "id",
 | 
			
		||||
				cell: (info: any) => {
 | 
			
		||||
					return (
 | 
			
		||||
						<span className="dropdown">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import { IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { IconHelp, IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteAccessList } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useAccessLists } from "src/hooks";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showAccessListModal, showDeleteConfirmModal } from "src/modals";
 | 
			
		||||
import { showAccessListModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
@@ -47,9 +47,10 @@ export default function TableWrapper() {
 | 
			
		||||
								<T id="access-lists" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
							<div className="col-md-auto col-sm-12">
 | 
			
		||||
								<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
 | 
			
		||||
						<div className="col-md-auto col-sm-12">
 | 
			
		||||
							<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<div className="input-group input-group-flat w-auto">
 | 
			
		||||
										<span className="input-group-text input-group-text-sm">
 | 
			
		||||
											<IconSearch size={16} />
 | 
			
		||||
@@ -62,12 +63,17 @@ export default function TableWrapper() {
 | 
			
		||||
											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
								<Button size="sm" onClick={() => showHelpModal("AccessLists", "cyan")}>
 | 
			
		||||
									<IconHelp size={20} />
 | 
			
		||||
								</Button>
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<Button size="sm" className="btn-cyan" onClick={() => showAccessListModal("new")}>
 | 
			
		||||
										<T id="object.add" tData={{ object: "access-list" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
							</div>
 | 
			
		||||
						) : null}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table
 | 
			
		||||
@@ -77,7 +83,7 @@ export default function TableWrapper() {
 | 
			
		||||
					onEdit={(id: number) => showAccessListModal(id)}
 | 
			
		||||
					onDelete={(id: number) =>
 | 
			
		||||
						showDeleteConfirmModal({
 | 
			
		||||
							title: "access.delete.title",
 | 
			
		||||
							title: <T id="object.delete" tData={{ object: "access-list" }} />,
 | 
			
		||||
							onConfirm: () => handleDelete(id),
 | 
			
		||||
							invalidations: [["access-lists"], ["access-list", id]],
 | 
			
		||||
							children: <T id="object.delete.content" tData={{ object: "access-list" }} />,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
import { IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { IconHelp, IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteCertificate, downloadCertificate } from "src/api/backend";
 | 
			
		||||
import { LoadingPage } from "src/components";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useCertificates } from "src/hooks";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import {
 | 
			
		||||
	showCustomCertificateModal,
 | 
			
		||||
	showDeleteConfirmModal,
 | 
			
		||||
	showDNSCertificateModal,
 | 
			
		||||
	showHelpModal,
 | 
			
		||||
	showHTTPCertificateModal,
 | 
			
		||||
	showRenewCertificateModal,
 | 
			
		||||
} from "src/modals";
 | 
			
		||||
@@ -69,9 +70,10 @@ export default function TableWrapper() {
 | 
			
		||||
								<T id="certificates" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
							<div className="col-md-auto col-sm-12">
 | 
			
		||||
								<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
 | 
			
		||||
						<div className="col-md-auto col-sm-12">
 | 
			
		||||
							<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<div className="input-group input-group-flat w-auto">
 | 
			
		||||
										<span className="input-group-text input-group-text-sm">
 | 
			
		||||
											<IconSearch size={16} />
 | 
			
		||||
@@ -84,6 +86,11 @@ export default function TableWrapper() {
 | 
			
		||||
											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
								<Button size="sm" onClick={() => showHelpModal("Certificates", "pink")}>
 | 
			
		||||
									<IconHelp size={20} />
 | 
			
		||||
								</Button>
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<div className="dropdown">
 | 
			
		||||
										<button
 | 
			
		||||
											type="button"
 | 
			
		||||
@@ -126,9 +133,9 @@ export default function TableWrapper() {
 | 
			
		||||
											</a>
 | 
			
		||||
										</div>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
							</div>
 | 
			
		||||
						) : null}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table
 | 
			
		||||
 
 | 
			
		||||
@@ -116,12 +116,7 @@ const Dashboard = () => {
 | 
			
		||||
				<code>{`Todo:
 | 
			
		||||
 | 
			
		||||
- check mobile
 | 
			
		||||
- use statuses for table formatters where applicable: https://docs.tabler.io/ui/components/statuses
 | 
			
		||||
- add help docs for host types
 | 
			
		||||
- REDO SCREENSHOTS in docs folder
 | 
			
		||||
- search codebase for "TODO"
 | 
			
		||||
- update documentation to add development notes for translations
 | 
			
		||||
- double check output of access field selection on proxy host dialog, after access lists are completed
 | 
			
		||||
- check permissions in all places
 | 
			
		||||
 | 
			
		||||
More for api, then implement here:
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,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">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { IconHelp, IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Alert from "react-bootstrap/Alert";
 | 
			
		||||
@@ -6,7 +6,7 @@ import { deleteDeadHost, toggleDeadHost } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useDeadHosts } from "src/hooks";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeadHostModal, showDeleteConfirmModal } from "src/modals";
 | 
			
		||||
import { showDeadHostModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
@@ -56,9 +56,10 @@ export default function TableWrapper() {
 | 
			
		||||
								<T id="dead-hosts" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
							<div className="col-md-auto col-sm-12">
 | 
			
		||||
								<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
 | 
			
		||||
						<div className="col-md-auto col-sm-12">
 | 
			
		||||
							<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<div className="input-group input-group-flat w-auto">
 | 
			
		||||
										<span className="input-group-text input-group-text-sm">
 | 
			
		||||
											<IconSearch size={16} />
 | 
			
		||||
@@ -71,12 +72,17 @@ export default function TableWrapper() {
 | 
			
		||||
											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
								<Button size="sm" onClick={() => showHelpModal("DeadHosts", "red")}>
 | 
			
		||||
									<IconHelp size={20} />
 | 
			
		||||
								</Button>
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<Button size="sm" className="btn-red" onClick={() => showDeadHostModal("new")}>
 | 
			
		||||
										<T id="object.add" tData={{ object: "dead-host" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
							</div>
 | 
			
		||||
						) : null}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { IconHelp, IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Alert from "react-bootstrap/Alert";
 | 
			
		||||
@@ -6,7 +6,7 @@ import { deleteProxyHost, toggleProxyHost } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useProxyHosts } from "src/hooks";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeleteConfirmModal, showProxyHostModal } from "src/modals";
 | 
			
		||||
import { showDeleteConfirmModal, showHelpModal, showProxyHostModal } from "src/modals";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
@@ -59,9 +59,10 @@ export default function TableWrapper() {
 | 
			
		||||
								<T id="proxy-hosts" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
							<div className="col-md-auto col-sm-12">
 | 
			
		||||
								<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
 | 
			
		||||
						<div className="col-md-auto col-sm-12">
 | 
			
		||||
							<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<div className="input-group input-group-flat w-auto">
 | 
			
		||||
										<span className="input-group-text input-group-text-sm">
 | 
			
		||||
											<IconSearch size={16} />
 | 
			
		||||
@@ -74,12 +75,17 @@ export default function TableWrapper() {
 | 
			
		||||
											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
								<Button size="sm" onClick={() => showHelpModal("ProxyHosts", "lime")}>
 | 
			
		||||
									<IconHelp size={20} />
 | 
			
		||||
								</Button>
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<Button size="sm" className="btn-lime" onClick={() => showProxyHostModal("new")}>
 | 
			
		||||
										<T id="object.add" tData={{ object: "proxy-host" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
							</div>
 | 
			
		||||
						) : null}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { IconHelp, IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Alert from "react-bootstrap/Alert";
 | 
			
		||||
@@ -6,7 +6,7 @@ import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useRedirectionHosts } from "src/hooks";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeleteConfirmModal, showRedirectionHostModal } from "src/modals";
 | 
			
		||||
import { showDeleteConfirmModal, showHelpModal, showRedirectionHostModal } from "src/modals";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
@@ -59,9 +59,10 @@ export default function TableWrapper() {
 | 
			
		||||
								<T id="redirection-hosts" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
							<div className="col-md-auto col-sm-12">
 | 
			
		||||
								<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
 | 
			
		||||
						<div className="col-md-auto col-sm-12">
 | 
			
		||||
							<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<div className="input-group input-group-flat w-auto">
 | 
			
		||||
										<span className="input-group-text input-group-text-sm">
 | 
			
		||||
											<IconSearch size={16} />
 | 
			
		||||
@@ -74,6 +75,11 @@ export default function TableWrapper() {
 | 
			
		||||
											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
								<Button size="sm" onClick={() => showHelpModal("RedirectionHosts", "yellow")}>
 | 
			
		||||
									<IconHelp size={20} />
 | 
			
		||||
								</Button>
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<Button
 | 
			
		||||
										size="sm"
 | 
			
		||||
										className="btn-yellow"
 | 
			
		||||
@@ -81,9 +87,9 @@ export default function TableWrapper() {
 | 
			
		||||
									>
 | 
			
		||||
										<T id="object.add" tData={{ object: "redirection-host" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
							</div>
 | 
			
		||||
						) : null}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
				},
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.display({
 | 
			
		||||
				id: "id", // todo: not needed for a display?
 | 
			
		||||
				id: "id",
 | 
			
		||||
				cell: (info: any) => {
 | 
			
		||||
					return (
 | 
			
		||||
						<span className="dropdown">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { IconHelp, IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Alert from "react-bootstrap/Alert";
 | 
			
		||||
@@ -6,7 +6,7 @@ import { deleteStream, toggleStream } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useStreams } from "src/hooks";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeleteConfirmModal, showStreamModal } from "src/modals";
 | 
			
		||||
import { showDeleteConfirmModal, showHelpModal, showStreamModal } from "src/modals";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
@@ -61,9 +61,10 @@ export default function TableWrapper() {
 | 
			
		||||
								<T id="streams" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
							<div className="col-md-auto col-sm-12">
 | 
			
		||||
								<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
 | 
			
		||||
						<div className="col-md-auto col-sm-12">
 | 
			
		||||
							<div className="ms-auto d-flex flex-wrap btn-list">
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<div className="input-group input-group-flat w-auto">
 | 
			
		||||
										<span className="input-group-text input-group-text-sm">
 | 
			
		||||
											<IconSearch size={16} />
 | 
			
		||||
@@ -76,12 +77,17 @@ export default function TableWrapper() {
 | 
			
		||||
											onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
								<Button size="sm" onClick={() => showHelpModal("Streams", "blue")}>
 | 
			
		||||
									<IconHelp size={20} />
 | 
			
		||||
								</Button>
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<Button size="sm" className="btn-blue" onClick={() => showStreamModal("new")}>
 | 
			
		||||
										<T id="object.add" tData={{ object: "stream" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
								) : null}
 | 
			
		||||
							</div>
 | 
			
		||||
						) : null}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user