mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-11-04 17:35:15 +00:00
More react
- consolidated lang items - proxy host paths work
This commit is contained in:
48
frontend/src/components/EmptyData.tsx
Normal file
48
frontend/src/components/EmptyData.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Table as ReactTable } from "@tanstack/react-table";
|
||||
import cn from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "src/components";
|
||||
import { T } from "src/locale";
|
||||
|
||||
interface Props {
|
||||
tableInstance: ReactTable<any>;
|
||||
onNew?: () => void;
|
||||
isFiltered?: boolean;
|
||||
object: string;
|
||||
objects: string;
|
||||
color?: string;
|
||||
customAddBtn?: ReactNode;
|
||||
}
|
||||
function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color = "primary", customAddBtn }: Props) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
|
||||
<div className="text-center my-4">
|
||||
{isFiltered ? (
|
||||
<h2>
|
||||
<T id="empty-search" />
|
||||
</h2>
|
||||
) : (
|
||||
<>
|
||||
<h2>
|
||||
<T id="object.empty" tData={{ objects }} />
|
||||
</h2>
|
||||
<p className="text-muted">
|
||||
<T id="empty-subtitle" />
|
||||
</p>
|
||||
{customAddBtn ? (
|
||||
customAddBtn
|
||||
) : (
|
||||
<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
|
||||
<T id="object.add" tData={{ object }} />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export { EmptyData };
|
||||
@@ -12,7 +12,7 @@ export function ErrorNotFound() {
|
||||
<T id="notfound.title" />
|
||||
</p>
|
||||
<p className="empty-subtitle text-secondary">
|
||||
<T id="notfound.text" />
|
||||
<T id="notfound.content" />
|
||||
</p>
|
||||
<div className="empty-action">
|
||||
<Button type="button" size="md" onClick={() => navigate("/")}>
|
||||
|
||||
@@ -48,7 +48,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-muted">
|
||||
<T id="access.help.rules-order" />
|
||||
<T id="access-list.help.rules-order" />
|
||||
</p>
|
||||
{values.map((client: AccessListClient, idx: number) => (
|
||||
<div className="row mb-1" key={idx}>
|
||||
@@ -101,7 +101,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<p className="text-muted">
|
||||
<T id="access.help-rules-last" />
|
||||
<T id="access-list.help-rules-last" />
|
||||
</p>
|
||||
<div className="col-11">
|
||||
<div className="input-group mb-2">
|
||||
|
||||
@@ -31,7 +31,7 @@ interface Props {
|
||||
name?: string;
|
||||
label?: string;
|
||||
}
|
||||
export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) {
|
||||
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
|
||||
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
@@ -44,7 +44,7 @@ export function AccessField({ name = "accessListId", label = "access.title", id
|
||||
value: item.id || 0,
|
||||
label: item.name,
|
||||
subLabel: intl.formatMessage(
|
||||
{ id: "access.subtitle" },
|
||||
{ id: "access-list.subtitle" },
|
||||
{
|
||||
users: item?.items?.length,
|
||||
rules: item?.clients?.length,
|
||||
@@ -57,7 +57,7 @@ export function AccessField({ name = "accessListId", label = "access.title", id
|
||||
// Public option
|
||||
options?.unshift({
|
||||
value: 0,
|
||||
label: intl.formatMessage({ id: "access.public" }),
|
||||
label: intl.formatMessage({ id: "access-list.public" }),
|
||||
subLabel: "No basic auth required",
|
||||
icon: <IconLockOpen2 size={14} className="text-red" />,
|
||||
});
|
||||
|
||||
3
frontend/src/components/Form/LocationsFields.module.css
Normal file
3
frontend/src/components/Form/LocationsFields.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.locationCard {
|
||||
border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;
|
||||
}
|
||||
185
frontend/src/components/Form/LocationsFields.tsx
Normal file
185
frontend/src/components/Form/LocationsFields.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { IconSettings } from "@tabler/icons-react";
|
||||
import CodeEditor from "@uiw/react-textarea-code-editor";
|
||||
import cn from "classnames";
|
||||
import { useFormikContext } from "formik";
|
||||
import { useState } from "react";
|
||||
import type { ProxyLocation } from "src/api/backend";
|
||||
import { intl, T } from "src/locale";
|
||||
import styles from "./LocationsFields.module.css";
|
||||
|
||||
interface Props {
|
||||
initialValues: ProxyLocation[];
|
||||
name?: string;
|
||||
}
|
||||
export function LocationsFields({ initialValues, name = "items" }: Props) {
|
||||
const [values, setValues] = useState<ProxyLocation[]>(initialValues || []);
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [advVisible, setAdvVisible] = useState<number[]>([]);
|
||||
|
||||
const blankItem: ProxyLocation = {
|
||||
path: "",
|
||||
advancedConfig: "",
|
||||
forwardScheme: "http",
|
||||
forwardHost: "",
|
||||
forwardPort: 80,
|
||||
};
|
||||
|
||||
const toggleAdvVisible = (idx: number) => {
|
||||
setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setValues([...values, blankItem]);
|
||||
};
|
||||
|
||||
const handleRemove = (idx: number) => {
|
||||
const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx);
|
||||
setValues(newValues);
|
||||
setFormField(newValues);
|
||||
};
|
||||
|
||||
const handleChange = (idx: number, field: string, fieldValue: string) => {
|
||||
const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
|
||||
setValues(newValues);
|
||||
setFormField(newValues);
|
||||
};
|
||||
|
||||
const setFormField = (newValues: ProxyLocation[]) => {
|
||||
const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== "");
|
||||
setFieldValue(name, filtered);
|
||||
};
|
||||
|
||||
if (values.length === 0) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<button type="button" className="btn my-3" onClick={handleAdd}>
|
||||
<T id="action.add-location" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{values.map((item: ProxyLocation, idx: number) => (
|
||||
<div key={idx} className={cn("card", "card-active", "mb-3", styles.locationCard)}>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Location</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="/path"
|
||||
autoComplete="off"
|
||||
value={item.path}
|
||||
onChange={(e) => handleChange(idx, "path", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 text-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn p-0"
|
||||
title="Advanced"
|
||||
onClick={() => toggleAdvVisible(idx)}
|
||||
>
|
||||
<IconSettings size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="forwardScheme">
|
||||
<T id="host.forward-scheme" />
|
||||
</label>
|
||||
<select
|
||||
id="forwardScheme"
|
||||
className="form-control"
|
||||
value={item.forwardScheme}
|
||||
onChange={(e) => handleChange(idx, "forwardScheme", e.target.value)}
|
||||
>
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="forwardHost">
|
||||
<T id="proxy-host.forward-host" />
|
||||
</label>
|
||||
<input
|
||||
id="forwardHost"
|
||||
type="text"
|
||||
className="form-control"
|
||||
required
|
||||
placeholder="eg: 10.0.0.1/path/"
|
||||
value={item.forwardHost}
|
||||
onChange={(e) => handleChange(idx, "forwardHost", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="forwardPort">
|
||||
<T id="host.forward-port" />
|
||||
</label>
|
||||
<input
|
||||
id="forwardPort"
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
className="form-control"
|
||||
required
|
||||
placeholder="eg: 8081"
|
||||
value={item.forwardPort}
|
||||
onChange={(e) => handleChange(idx, "forwardPort", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{advVisible.includes(idx) && (
|
||||
<div className="">
|
||||
<CodeEditor
|
||||
language="nginx"
|
||||
placeholder={intl.formatMessage({ id: "nginx-config.placeholder" })}
|
||||
padding={15}
|
||||
data-color-mode="dark"
|
||||
minHeight={170}
|
||||
indentWidth={2}
|
||||
value={item.advancedConfig}
|
||||
onChange={(e) => handleChange(idx, "advancedConfig", e.target.value)}
|
||||
style={{
|
||||
fontFamily:
|
||||
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
|
||||
borderRadius: "0.3rem",
|
||||
minHeight: "170px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemove(idx);
|
||||
}}
|
||||
>
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<button type="button" className="btn btn-sm" onClick={handleAdd}>
|
||||
<T id="action.add-location" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export * from "./AccessField";
|
||||
export * from "./BasicAuthFields";
|
||||
export * from "./DNSProviderFields";
|
||||
export * from "./DomainNamesField";
|
||||
export * from "./LocationsFields";
|
||||
export * from "./NginxConfigField";
|
||||
export * from "./SSLCertificateField";
|
||||
export * from "./SSLOptionsFields";
|
||||
|
||||
@@ -25,33 +25,33 @@ const menuItems: MenuItem[] = [
|
||||
{
|
||||
to: "/",
|
||||
icon: IconHome,
|
||||
label: "dashboard.title",
|
||||
label: "dashboard",
|
||||
},
|
||||
{
|
||||
icon: IconDeviceDesktop,
|
||||
label: "hosts.title",
|
||||
label: "hosts",
|
||||
items: [
|
||||
{
|
||||
to: "/nginx/proxy",
|
||||
label: "proxy-hosts.title",
|
||||
label: "proxy-hosts",
|
||||
permission: "proxyHosts",
|
||||
permissionType: "view",
|
||||
},
|
||||
{
|
||||
to: "/nginx/redirection",
|
||||
label: "redirection-hosts.title",
|
||||
label: "redirection-hosts",
|
||||
permission: "redirectionHosts",
|
||||
permissionType: "view",
|
||||
},
|
||||
{
|
||||
to: "/nginx/stream",
|
||||
label: "streams.title",
|
||||
label: "streams",
|
||||
permission: "streams",
|
||||
permissionType: "view",
|
||||
},
|
||||
{
|
||||
to: "/nginx/404",
|
||||
label: "dead-hosts.title",
|
||||
label: "dead-hosts",
|
||||
permission: "deadHosts",
|
||||
permissionType: "view",
|
||||
},
|
||||
@@ -60,33 +60,33 @@ const menuItems: MenuItem[] = [
|
||||
{
|
||||
to: "/access",
|
||||
icon: IconLock,
|
||||
label: "access.title",
|
||||
label: "access-lists",
|
||||
permission: "accessLists",
|
||||
permissionType: "view",
|
||||
},
|
||||
{
|
||||
to: "/certificates",
|
||||
icon: IconShield,
|
||||
label: "certificates.title",
|
||||
label: "certificates",
|
||||
permission: "certificates",
|
||||
permissionType: "view",
|
||||
},
|
||||
{
|
||||
to: "/users",
|
||||
icon: IconUser,
|
||||
label: "users.title",
|
||||
label: "users",
|
||||
permission: "admin",
|
||||
},
|
||||
{
|
||||
to: "/audit-log",
|
||||
icon: IconBook,
|
||||
label: "auditlog.title",
|
||||
label: "auditlogs",
|
||||
permission: "admin",
|
||||
},
|
||||
{
|
||||
to: "/settings",
|
||||
icon: IconSettings,
|
||||
label: "settings.title",
|
||||
label: "settings",
|
||||
permission: "admin",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { AccessList } from "src/api/backend";
|
||||
import { T } from "src/locale";
|
||||
import { showAccessListModal } from "src/modals";
|
||||
|
||||
interface Props {
|
||||
access?: AccessList;
|
||||
}
|
||||
export function AccessListFormatter({ access }: Props) {
|
||||
if (!access) {
|
||||
return <T id="public" />;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-action btn-sm px-1"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showAccessListModal(access?.id || 0);
|
||||
}}
|
||||
>
|
||||
{access.name}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./AccessListformatter";
|
||||
export * from "./CertificateFormatter";
|
||||
export * from "./DomainsFormatter";
|
||||
export * from "./EmailFormatter";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./Button";
|
||||
export * from "./EmptyData";
|
||||
export * from "./ErrorNotFound";
|
||||
export * from "./Flag";
|
||||
export * from "./Form";
|
||||
|
||||
Reference in New Issue
Block a user