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 
			
		
		
		
	Streams modal
This commit is contained in:
		@@ -5,17 +5,24 @@ import { intl } from "src/locale";
 | 
			
		||||
interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNew?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNew }: 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: "dead-hosts.empty" })}</h2>
 | 
			
		||||
					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
 | 
			
		||||
					<Button className="btn-red my-3" onClick={onNew}>
 | 
			
		||||
						{intl.formatMessage({ id: "dead-hosts.add" })}
 | 
			
		||||
					</Button>
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>{intl.formatMessage({ id: "empty-search" })}</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>{intl.formatMessage({ id: "dead-hosts.empty" })}</h2>
 | 
			
		||||
							<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
 | 
			
		||||
							<Button className="btn-red my-3" onClick={onNew}>
 | 
			
		||||
								{intl.formatMessage({ id: "dead-hosts.add" })}
 | 
			
		||||
							</Button>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,13 +9,14 @@ import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: DeadHost[];
 | 
			
		||||
	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, onEdit, onDelete, onDisableToggle, onNew }: Props) {
 | 
			
		||||
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
 | 
			
		||||
	const columnHelper = createColumnHelper<DeadHost>();
 | 
			
		||||
	const columns = useMemo(
 | 
			
		||||
		() => [
 | 
			
		||||
@@ -133,6 +134,9 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} onNew={onNew} />} />
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,7 @@ export default function TableWrapper() {
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table
 | 
			
		||||
					data={filtered ?? data ?? []}
 | 
			
		||||
					isFiltered={!!search}
 | 
			
		||||
					isFetching={isFetching}
 | 
			
		||||
					onEdit={(id: number) => setEditId(id)}
 | 
			
		||||
					onDelete={(id: number) => setDeleteId(id)}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,25 @@ import { intl } from "src/locale";
 | 
			
		||||
 | 
			
		||||
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: "streams.empty" })}</h2>
 | 
			
		||||
					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
 | 
			
		||||
					<Button className="btn-blue my-3">{intl.formatMessage({ id: "streams.add" })}</Button>
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>{intl.formatMessage({ id: "empty-search" })}</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>{intl.formatMessage({ id: "streams.empty" })}</h2>
 | 
			
		||||
							<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
 | 
			
		||||
							<Button className="btn-blue my-3" onClick={onNew}>
 | 
			
		||||
								{intl.formatMessage({ id: "streams.add" })}
 | 
			
		||||
							</Button>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,21 @@ import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-
 | 
			
		||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import type { Stream } from "src/api/backend";
 | 
			
		||||
import { CertificateFormatter, DomainsFormatter, GravatarFormatter, StatusFormatter } from "src/components";
 | 
			
		||||
import { CertificateFormatter, GravatarFormatter, StatusFormatter, ValueWithDateFormatter } from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: Stream[];
 | 
			
		||||
	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, isFiltered, onEdit, onDelete, onDisableToggle, onNew }: Props) {
 | 
			
		||||
	const columnHelper = createColumnHelper<Stream>();
 | 
			
		||||
	const columns = useMemo(
 | 
			
		||||
		() => [
 | 
			
		||||
@@ -30,8 +35,7 @@ export default function Table({ data, isFetching }: Props) {
 | 
			
		||||
				header: intl.formatMessage({ id: "column.incoming-port" }),
 | 
			
		||||
				cell: (info: any) => {
 | 
			
		||||
					const value = info.getValue();
 | 
			
		||||
					// Bit of a hack to reuse the DomainsFormatter component
 | 
			
		||||
					return <DomainsFormatter domains={[value.incomingPort]} createdOn={value.createdOn} />;
 | 
			
		||||
					return <ValueWithDateFormatter value={value.incomingPort} createdOn={value.createdOn} />;
 | 
			
		||||
				},
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.accessor((row: any) => row, {
 | 
			
		||||
@@ -99,16 +103,37 @@ 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" })}
 | 
			
		||||
								</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>
 | 
			
		||||
@@ -121,7 +146,7 @@ export default function Table({ data, isFetching }: Props) {
 | 
			
		||||
				},
 | 
			
		||||
			}),
 | 
			
		||||
		],
 | 
			
		||||
		[columnHelper],
 | 
			
		||||
		[columnHelper, onEdit, onDisableToggle, onDelete],
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	const tableInstance = useReactTable<Stream>({
 | 
			
		||||
@@ -135,5 +160,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,19 @@
 | 
			
		||||
import { IconSearch } from "@tabler/icons-react";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useStreams } from "src/hooks";
 | 
			
		||||
import { intl } from "src/locale";
 | 
			
		||||
import { DeleteConfirmModal, StreamModal } from "src/modals";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
export default function TableWrapper() {
 | 
			
		||||
	const queryClient = useQueryClient();
 | 
			
		||||
	const [search, setSearch] = useState("");
 | 
			
		||||
	const [editId, setEditId] = useState(0 as number | "new");
 | 
			
		||||
	const [deleteId, setDeleteId] = useState(0);
 | 
			
		||||
	const { isFetching, isLoading, isError, error, data } = useStreams(["owner", "certificate"]);
 | 
			
		||||
 | 
			
		||||
	if (isLoading) {
 | 
			
		||||
@@ -16,6 +24,29 @@ export default function TableWrapper() {
 | 
			
		||||
		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const handleDelete = async () => {
 | 
			
		||||
		// await deleteDeadHost(deleteId);
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDisableToggle = async (id: number, enabled: boolean) => {
 | 
			
		||||
		// await toggleDeadHost(id, enabled);
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["dead-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;
 | 
			
		||||
			// return item.domainNames.some((domain: string) => domain.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-blue" />
 | 
			
		||||
@@ -27,25 +58,46 @@ export default function TableWrapper() {
 | 
			
		||||
						</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"
 | 
			
		||||
									/>
 | 
			
		||||
								</div>
 | 
			
		||||
								<Button size="sm" className="btn-blue">
 | 
			
		||||
								{data?.length ? (
 | 
			
		||||
									<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"
 | 
			
		||||
											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>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<Table data={data ?? []} isFetching={isFetching} />
 | 
			
		||||
				<Table
 | 
			
		||||
					data={filtered ?? data ?? []}
 | 
			
		||||
					isFetching={isFetching}
 | 
			
		||||
					onEdit={(id: number) => setEditId(id)}
 | 
			
		||||
					onDelete={(id: number) => setDeleteId(id)}
 | 
			
		||||
					onDisableToggle={handleDisableToggle}
 | 
			
		||||
					onNew={() => setEditId("new")}
 | 
			
		||||
				/>
 | 
			
		||||
				{editId ? <StreamModal id={editId} onClose={() => setEditId(0)} /> : null}
 | 
			
		||||
				{deleteId ? (
 | 
			
		||||
					<DeleteConfirmModal
 | 
			
		||||
						title={intl.formatMessage({ id: "stream.delete.title" })}
 | 
			
		||||
						onConfirm={handleDelete}
 | 
			
		||||
						onClose={() => setDeleteId(0)}
 | 
			
		||||
						invalidations={[["streams"], ["stream", deleteId]]}
 | 
			
		||||
					>
 | 
			
		||||
						{intl.formatMessage({ id: "stream.delete.content" })}
 | 
			
		||||
					</DeleteConfirmModal>
 | 
			
		||||
				) : null}
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user