You've already forked nginx-proxy-manager
mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-11-01 05:31:05 +03:00
Certificates react work
- renewal and download - table columns rendering - searching - deleting
This commit is contained in:
@@ -80,8 +80,16 @@ export async function get(args: GetArgs, abortController?: AbortController) {
|
||||
return processResponse(await baseGet(args, abortController));
|
||||
}
|
||||
|
||||
export async function download(args: GetArgs, abortController?: AbortController) {
|
||||
return (await baseGet(args, abortController)).text();
|
||||
export async function download({ url, params }: GetArgs, filename = "download.file") {
|
||||
const headers = buildAuthHeader();
|
||||
const res = await fetch(buildUrl({ url, params }), { headers });
|
||||
const bl = await res.blob();
|
||||
const u = window.URL.createObjectURL(bl);
|
||||
const a = document.createElement("a");
|
||||
a.href = u;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
interface PostArgs {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as api from "./base";
|
||||
import type { Binary } from "./responseTypes";
|
||||
|
||||
export async function downloadCertificate(id: number): Promise<Binary> {
|
||||
return await api.get({
|
||||
export async function downloadCertificate(id: number): Promise<void> {
|
||||
await api.download(
|
||||
{
|
||||
url: `/nginx/certificates/${id}/download`,
|
||||
});
|
||||
},
|
||||
`certificate-${id}.zip`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,5 +15,3 @@ export interface ValidatedCertificateResponse {
|
||||
certificate: Record<string, any>;
|
||||
certificateKey: boolean;
|
||||
}
|
||||
|
||||
export type Binary = number & { readonly __brand: unique symbol };
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import OverlayTrigger from "react-bootstrap/OverlayTrigger";
|
||||
import Popover from "react-bootstrap/Popover";
|
||||
import type { DeadHost, ProxyHost, RedirectionHost } from "src/api/backend";
|
||||
import { T } from "src/locale";
|
||||
|
||||
const getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | DeadHost[]) => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<strong>
|
||||
<T id={title} />
|
||||
</strong>
|
||||
</div>
|
||||
{items.map((host) => (
|
||||
<div key={host.id} className="ms-1">
|
||||
{host.domainNames.join(", ")}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
proxyHosts: ProxyHost[];
|
||||
redirectionHosts: RedirectionHost[];
|
||||
deadHosts: DeadHost[];
|
||||
}
|
||||
export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts }: Props) {
|
||||
const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length;
|
||||
if (totalCount === 0) {
|
||||
return (
|
||||
<span className="badge bg-red-lt">
|
||||
<T id="certificate.not-in-use" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
proxyHosts.sort();
|
||||
redirectionHosts.sort();
|
||||
deadHosts.sort();
|
||||
|
||||
const popover = (
|
||||
<Popover id="popover-basic">
|
||||
<Popover.Body>
|
||||
{getSection("proxy-hosts", proxyHosts)}
|
||||
{getSection("redirection-hosts", redirectionHosts)}
|
||||
{getSection("dead-hosts", deadHosts)}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
|
||||
<span className="badge bg-lime-lt">
|
||||
<T id="certificate.in-use" />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
15
frontend/src/components/Table/Formatter/DateFormatter.tsx
Normal file
15
frontend/src/components/Table/Formatter/DateFormatter.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import cn from "classnames";
|
||||
import { isPast, parseISO } from "date-fns";
|
||||
import { DateTimeFormat } from "src/locale";
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
highlightPast?: boolean;
|
||||
}
|
||||
export function DateFormatter({ value, highlightPast }: Props) {
|
||||
const dateIsPast = isPast(parseISO(value));
|
||||
const cl = cn({
|
||||
"text-danger": highlightPast && dateIsPast,
|
||||
});
|
||||
return <span className={cl}>{DateTimeFormat(value)}</span>;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from "./AccessListformatter";
|
||||
export * from "./CertificateFormatter";
|
||||
export * from "./CertificateInUseFormatter";
|
||||
export * from "./DateFormatter";
|
||||
export * from "./DomainsFormatter";
|
||||
export * from "./EmailFormatter";
|
||||
export * from "./EnabledFormatter";
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from "./useAccessList";
|
||||
export * from "./useAccessLists";
|
||||
export * from "./useAuditLog";
|
||||
export * from "./useAuditLogs";
|
||||
export * from "./useCertificate";
|
||||
export * from "./useCertificates";
|
||||
export * from "./useDeadHost";
|
||||
export * from "./useDeadHosts";
|
||||
|
||||
17
frontend/src/hooks/useCertificate.ts
Normal file
17
frontend/src/hooks/useCertificate.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type Certificate, getCertificate } from "src/api/backend";
|
||||
|
||||
const fetchCertificate = (id: number) => {
|
||||
return getCertificate(id, ["owner"]);
|
||||
};
|
||||
|
||||
const useCertificate = (id: number, options = {}) => {
|
||||
return useQuery<Certificate, Error>({
|
||||
queryKey: ["certificate", id],
|
||||
queryFn: () => fetchCertificate(id),
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export { useCertificate };
|
||||
@@ -15,16 +15,20 @@
|
||||
"action.close": "Close",
|
||||
"action.delete": "Delete",
|
||||
"action.disable": "Disable",
|
||||
"action.download": "Download",
|
||||
"action.edit": "Edit",
|
||||
"action.enable": "Enable",
|
||||
"action.permissions": "Permissions",
|
||||
"action.renew": "Renew",
|
||||
"action.view-details": "View Details",
|
||||
"auditlogs": "Audit Logs",
|
||||
"cancel": "Cancel",
|
||||
"certificate": "Certificate",
|
||||
"certificate.in-use": "In Use",
|
||||
"certificate.none.subtitle": "No certificate assigned",
|
||||
"certificate.none.subtitle.for-http": "This host will not use HTTPS",
|
||||
"certificate.none.title": "None",
|
||||
"certificate.not-in-use": "Not Used",
|
||||
"certificates": "Certificates",
|
||||
"certificates.custom": "Custom Certificate",
|
||||
"certificates.dns.credentials": "Credentials File Content",
|
||||
@@ -121,6 +125,7 @@
|
||||
"notification.object-deleted": "{object} has been deleted",
|
||||
"notification.object-disabled": "{object} has been disabled",
|
||||
"notification.object-enabled": "{object} has been enabled",
|
||||
"notification.object-renewed": "{object} has been renewed",
|
||||
"notification.object-saved": "{object} has been saved",
|
||||
"notification.success": "Success",
|
||||
"object.actions-title": "{object} #{id}",
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
"action.disable": {
|
||||
"defaultMessage": "Disable"
|
||||
},
|
||||
"action.download": {
|
||||
"defaultMessage": "Download"
|
||||
},
|
||||
"action.edit": {
|
||||
"defaultMessage": "Edit"
|
||||
},
|
||||
@@ -56,6 +59,9 @@
|
||||
"action.permissions": {
|
||||
"defaultMessage": "Permissions"
|
||||
},
|
||||
"action.renew": {
|
||||
"defaultMessage": "Renew"
|
||||
},
|
||||
"action.view-details": {
|
||||
"defaultMessage": "View Details"
|
||||
},
|
||||
@@ -68,6 +74,9 @@
|
||||
"certificate": {
|
||||
"defaultMessage": "Certificate"
|
||||
},
|
||||
"certificate.in-use": {
|
||||
"defaultMessage": "In Use"
|
||||
},
|
||||
"certificate.none.subtitle": {
|
||||
"defaultMessage": "No certificate assigned"
|
||||
},
|
||||
@@ -77,6 +86,9 @@
|
||||
"certificate.none.title": {
|
||||
"defaultMessage": "None"
|
||||
},
|
||||
"certificate.not-in-use": {
|
||||
"defaultMessage": "Not Used"
|
||||
},
|
||||
"certificates": {
|
||||
"defaultMessage": "Certificates"
|
||||
},
|
||||
@@ -365,6 +377,9 @@
|
||||
"notification.object-enabled": {
|
||||
"defaultMessage": "{object} has been enabled"
|
||||
},
|
||||
"notification.object-renewed": {
|
||||
"defaultMessage": "{object} has been renewed"
|
||||
},
|
||||
"notification.object-saved": {
|
||||
"defaultMessage": "{object} has been saved"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import EasyModal, { type InnerModalProps } from "ez-modal-react";
|
||||
import { Form, Formik } from "formik";
|
||||
import { type ReactNode, useState } from "react";
|
||||
@@ -14,6 +15,7 @@ const showHTTPCertificateModal = () => {
|
||||
};
|
||||
|
||||
const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [domains, setDomains] = useState([] as string[]);
|
||||
@@ -32,6 +34,7 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPr
|
||||
} catch (err: any) {
|
||||
setErrorMsg(<T id={err.message} />);
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["certificates"] });
|
||||
setIsSubmitting(false);
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
74
frontend/src/modals/RenewCertificateModal.tsx
Normal file
74
frontend/src/modals/RenewCertificateModal.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import EasyModal, { type InnerModalProps } from "ez-modal-react";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { Alert } from "react-bootstrap";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import { renewCertificate } from "src/api/backend";
|
||||
import { Button, Loading } from "src/components";
|
||||
import { useCertificate } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
|
||||
interface Props extends InnerModalProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const showRenewCertificateModal = (id: number) => {
|
||||
EasyModal.show(RenewCertificateModal, { id });
|
||||
};
|
||||
|
||||
const RenewCertificateModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { data, isLoading, error } = useCertificate(id);
|
||||
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
|
||||
const [isFresh, setIsFresh] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !isFresh || isSubmitting) return;
|
||||
setIsFresh(false);
|
||||
setIsSubmitting(true);
|
||||
|
||||
renewCertificate(id)
|
||||
.then(() => {
|
||||
showObjectSuccess("certificate", "renewed");
|
||||
queryClient.invalidateQueries({ queryKey: ["certificates"] });
|
||||
remove();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setErrorMsg(<T id={err.message} />);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
}, [id, data, isFresh, isSubmitting, remove, queryClient.invalidateQueries]);
|
||||
|
||||
return (
|
||||
<Modal show={visible} onHide={isSubmitting ? undefined : remove}>
|
||||
<Modal.Header closeButton={!isSubmitting}>
|
||||
<Modal.Title>
|
||||
<T id="renew-certificate" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Alert variant="danger" show={!!errorMsg}>
|
||||
{errorMsg}
|
||||
</Alert>
|
||||
{isLoading && <Loading noLogo />}
|
||||
{!isLoading && error && (
|
||||
<Alert variant="danger" className="m-3">
|
||||
{error?.message || "Unknown error"}
|
||||
</Alert>
|
||||
)}
|
||||
{data && isSubmitting && !errorMsg ? <p className="text-center mt-3">Please wait ...</p> : null}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
|
||||
<T id="action.close" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export { showRenewCertificateModal };
|
||||
@@ -9,6 +9,7 @@ export * from "./HTTPCertificateModal";
|
||||
export * from "./PermissionsModal";
|
||||
export * from "./ProxyHostModal";
|
||||
export * from "./RedirectionHostModal";
|
||||
export * from "./RenewCertificateModal";
|
||||
export * from "./SetPasswordModal";
|
||||
export * from "./StreamModal";
|
||||
export * from "./UserModal";
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
|
||||
import { IconDotsVertical, IconDownload, IconRefresh, IconTrash } from "@tabler/icons-react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { Certificate } from "src/api/backend";
|
||||
import { DomainsFormatter, EmptyData, GravatarFormatter } from "src/components";
|
||||
import {
|
||||
CertificateInUseFormatter,
|
||||
DateFormatter,
|
||||
DomainsFormatter,
|
||||
EmptyData,
|
||||
GravatarFormatter,
|
||||
} from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
|
||||
|
||||
interface Props {
|
||||
data: Certificate[];
|
||||
isFiltered?: boolean;
|
||||
isFetching?: boolean;
|
||||
onDelete?: (id: number) => void;
|
||||
onRenew?: (id: number) => void;
|
||||
onDownload?: (id: number) => void;
|
||||
}
|
||||
export default function Table({ data, isFetching }: Props) {
|
||||
export default function Table({ data, isFetching, onDelete, onRenew, onDownload, isFiltered }: Props) {
|
||||
const columnHelper = createColumnHelper<Certificate>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
@@ -37,25 +47,35 @@ export default function Table({ data, isFetching }: Props) {
|
||||
id: "provider",
|
||||
header: intl.formatMessage({ id: "column.provider" }),
|
||||
cell: (info: any) => {
|
||||
return info.getValue();
|
||||
if (info.getValue() === "letsencrypt") {
|
||||
return <T id="lets-encrypt" />;
|
||||
}
|
||||
return <T id={info.getValue()} />;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor((row: any) => row.expires_on, {
|
||||
id: "expires_on",
|
||||
columnHelper.accessor((row: any) => row.expiresOn, {
|
||||
id: "expiresOn",
|
||||
header: intl.formatMessage({ id: "column.expires" }),
|
||||
cell: (info: any) => {
|
||||
return info.getValue();
|
||||
return <DateFormatter value={info.getValue()} highlightPast />;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "id",
|
||||
id: "proxyHosts",
|
||||
header: intl.formatMessage({ id: "column.status" }),
|
||||
cell: (info: any) => {
|
||||
return info.getValue();
|
||||
const r = info.getValue();
|
||||
return (
|
||||
<CertificateInUseFormatter
|
||||
proxyHosts={r.proxyHosts}
|
||||
redirectionHosts={r.redirectionHosts}
|
||||
deadHosts={r.deadHosts}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "id", // todo: not needed for a display?
|
||||
id: "id",
|
||||
cell: (info: any) => {
|
||||
return (
|
||||
<span className="dropdown">
|
||||
@@ -75,16 +95,37 @@ export default function Table({ data, isFetching }: Props) {
|
||||
data={{ id: info.row.original.id }}
|
||||
/>
|
||||
</span>
|
||||
<a className="dropdown-item" href="#">
|
||||
<IconEdit size={16} />
|
||||
<T id="action.edit" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onRenew?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconRefresh size={16} />
|
||||
<T id="action.renew" />
|
||||
</a>
|
||||
<a className="dropdown-item" href="#">
|
||||
<IconPower size={16} />
|
||||
<T id="action.disable" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDownload?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconDownload size={16} />
|
||||
<T id="action.download" />
|
||||
</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} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
@@ -97,7 +138,7 @@ export default function Table({ data, isFetching }: Props) {
|
||||
},
|
||||
}),
|
||||
],
|
||||
[columnHelper],
|
||||
[columnHelper, onDelete, onRenew, onDownload],
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<Certificate>({
|
||||
@@ -160,8 +201,7 @@ export default function Table({ data, isFetching }: Props) {
|
||||
object="certificate"
|
||||
objects="certificates"
|
||||
tableInstance={tableInstance}
|
||||
// onNew={onNew}
|
||||
// isFiltered={isFiltered}
|
||||
isFiltered={isFiltered}
|
||||
color="pink"
|
||||
customAddBtn={customAddBtn}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { 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 { useCertificates } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
|
||||
import {
|
||||
showCustomCertificateModal,
|
||||
showDeleteConfirmModal,
|
||||
showDNSCertificateModal,
|
||||
showHTTPCertificateModal,
|
||||
showRenewCertificateModal,
|
||||
} from "src/modals";
|
||||
import { showError, showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
export default function TableWrapper() {
|
||||
const [search, setSearch] = useState("");
|
||||
const { isFetching, isLoading, isError, error, data } = useCertificates([
|
||||
"owner",
|
||||
"dead_hosts",
|
||||
@@ -22,6 +32,31 @@ export default function TableWrapper() {
|
||||
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await deleteCertificate(id);
|
||||
showObjectSuccess("certificate", "deleted");
|
||||
};
|
||||
|
||||
const handleDownload = async (id: number) => {
|
||||
try {
|
||||
await downloadCertificate(id);
|
||||
} catch (err: any) {
|
||||
showError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
let filtered = null;
|
||||
if (search && data) {
|
||||
filtered = data?.filter(
|
||||
(item) =>
|
||||
item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||
|
||||
item.niceName.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-pink" />
|
||||
@@ -33,6 +68,7 @@ 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="input-group input-group-flat w-auto">
|
||||
@@ -44,6 +80,7 @@ export default function TableWrapper() {
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
autoComplete="off"
|
||||
onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
|
||||
/>
|
||||
</div>
|
||||
<div className="dropdown">
|
||||
@@ -90,9 +127,24 @@ export default function TableWrapper() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Table data={data ?? []} isFetching={isFetching} />
|
||||
<Table
|
||||
data={filtered ?? data ?? []}
|
||||
isFiltered={!!search}
|
||||
isFetching={isFetching}
|
||||
onRenew={showRenewCertificateModal}
|
||||
onDownload={handleDownload}
|
||||
onDelete={(id: number) =>
|
||||
showDeleteConfirmModal({
|
||||
title: <T id="object.delete" tData={{ object: "certificate" }} />,
|
||||
onConfirm: () => handleDelete(id),
|
||||
invalidations: [["certificates"], ["certificate", id]],
|
||||
children: <T id="object.delete.content" tData={{ object: "certificate" }} />,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -89,10 +89,10 @@ export default function TableWrapper() {
|
||||
onEdit={(id: number) => showProxyHostModal(id)}
|
||||
onDelete={(id: number) =>
|
||||
showDeleteConfirmModal({
|
||||
title: "proxy-host.delete.title",
|
||||
title: <T id="object.delete" tData={{ object: "proxy-host" }} />,
|
||||
onConfirm: () => handleDelete(id),
|
||||
invalidations: [["proxy-hosts"], ["proxy-host", id]],
|
||||
children: <T id="proxy-host.delete.content" />,
|
||||
children: <T id="object.delete.content" tData={{ object: "proxy-host" }} />,
|
||||
})
|
||||
}
|
||||
onDisableToggle={handleDisableToggle}
|
||||
|
||||
Reference in New Issue
Block a user