You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-11-04 04:11:42 +03:00 
			
		
		
		
	Proxy host modal basis, other improvements
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user