Use a modal manager

This commit is contained in:
Jamie Curnow
2025-10-14 17:49:56 +10:00
parent e6f7ae3fba
commit 7af01d0fc7
32 changed files with 291 additions and 251 deletions

View File

@@ -24,6 +24,7 @@
"classnames": "^2.5.1", "classnames": "^2.5.1",
"country-flag-icons": "^1.5.20", "country-flag-icons": "^1.5.20",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"ez-modal-react": "^1.0.5",
"formik": "^2.4.6", "formik": "^2.4.6",
"generate-password-browser": "^1.1.0", "generate-password-browser": "^1.1.0",
"humps": "^2.0.1", "humps": "^2.0.1",

View File

@@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import EasyModal from "ez-modal-react";
import { RawIntlProvider } from "react-intl"; import { RawIntlProvider } from "react-intl";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context"; import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
@@ -16,7 +17,9 @@ function App() {
<ThemeProvider> <ThemeProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthProvider> <AuthProvider>
<EasyModal.Provider>
<Router /> <Router />
</EasyModal.Provider>
<ToastContainer <ToastContainer
position="top-right" position="top-right"
autoClose={5000} autoClose={5000}

View File

@@ -1,18 +1,15 @@
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react"; import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
import { useState } from "react";
import { LocalePicker, ThemeSwitcher } from "src/components"; import { LocalePicker, ThemeSwitcher } from "src/components";
import { useAuthState } from "src/context"; import { useAuthState } from "src/context";
import { useUser } from "src/hooks"; import { useUser } from "src/hooks";
import { T } from "src/locale"; import { T } from "src/locale";
import { ChangePasswordModal, UserModal } from "src/modals"; import { showChangePasswordModal, showUserModal } from "src/modals";
import styles from "./SiteHeader.module.css"; import styles from "./SiteHeader.module.css";
export function SiteHeader() { export function SiteHeader() {
const { data: currentUser } = useUser("me"); const { data: currentUser } = useUser("me");
const isAdmin = currentUser?.roles.includes("admin"); const isAdmin = currentUser?.roles.includes("admin");
const { logout } = useAuthState(); const { logout } = useAuthState();
const [showProfileEdit, setShowProfileEdit] = useState(false);
const [showChangePassword, setShowChangePassword] = useState(false);
return ( return (
<header className="navbar navbar-expand-md d-print-none"> <header className="navbar navbar-expand-md d-print-none">
@@ -76,7 +73,7 @@ export function SiteHeader() {
className="dropdown-item" className="dropdown-item"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setShowProfileEdit(true); showUserModal("me");
}} }}
> >
<IconUser width={18} /> <IconUser width={18} />
@@ -87,7 +84,7 @@ export function SiteHeader() {
className="dropdown-item" className="dropdown-item"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setShowChangePassword(true); showChangePasswordModal("me");
}} }}
> >
<IconLock width={18} /> <IconLock width={18} />
@@ -110,10 +107,6 @@ export function SiteHeader() {
</div> </div>
</div> </div>
</div> </div>
{showProfileEdit ? <UserModal userId="me" onClose={() => setShowProfileEdit(false)} /> : null}
{showChangePassword ? (
<ChangePasswordModal userId="me" onClose={() => setShowChangePassword(false)} />
) : null}
</header> </header>
); );
} }

View File

@@ -1,9 +1,10 @@
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconUser } from "@tabler/icons-react"; import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconUser } from "@tabler/icons-react";
import type { AuditLog } from "src/api/backend"; import type { AuditLog } from "src/api/backend";
import { DateTimeFormat, T } from "src/locale"; import { DateTimeFormat, T } from "src/locale";
const getEventValue = (event: AuditLog) => { const getEventValue = (event: AuditLog) => {
switch (event.objectType) { switch (event.objectType) {
case "access-list":
case "user": case "user":
return event.meta?.name; return event.meta?.name;
case "proxy-host": case "proxy-host":
@@ -47,6 +48,9 @@ const getIcon = (row: AuditLog) => {
case "stream": case "stream":
ico = <IconDisc size={16} className={c} />; ico = <IconDisc size={16} className={c} />;
break; break;
case "access-list":
ico = <IconLock size={16} className={c} />;
break;
} }
return ico; return ico;

View File

@@ -85,6 +85,7 @@
"error.max-domains": "Too many domains, max is {max}", "error.max-domains": "Too many domains, max is {max}",
"error.passwords-must-match": "Passwords must match", "error.passwords-must-match": "Passwords must match",
"error.required": "This is required", "error.required": "This is required",
"event.created-access-list": "Created Access List",
"event.created-dead-host": "Created 404 Host", "event.created-dead-host": "Created 404 Host",
"event.created-redirection-host": "Created Redirection Host", "event.created-redirection-host": "Created Redirection Host",
"event.created-stream": "Created Stream", "event.created-stream": "Created Stream",
@@ -127,6 +128,7 @@
"notification.host-deleted": "Host has been deleted", "notification.host-deleted": "Host has been deleted",
"notification.host-disabled": "Host has been disabled", "notification.host-disabled": "Host has been disabled",
"notification.host-enabled": "Host has been enabled", "notification.host-enabled": "Host has been enabled",
"notification.proxy-host-saved": "Proxy Host has been saved",
"notification.redirection-host-saved": "Redirection Host has been saved", "notification.redirection-host-saved": "Redirection Host has been saved",
"notification.stream-deleted": "Stream has been deleted", "notification.stream-deleted": "Stream has been deleted",
"notification.stream-disabled": "Stream has been disabled", "notification.stream-disabled": "Stream has been disabled",
@@ -148,6 +150,7 @@
"permissions.visibility.all": "All Items", "permissions.visibility.all": "All Items",
"permissions.visibility.title": "Item Visibility", "permissions.visibility.title": "Item Visibility",
"permissions.visibility.user": "Created Items Only", "permissions.visibility.user": "Created Items Only",
"proxy-host.edit": "Edit Proxy Host",
"proxy-host.forward-host": "Forward Hostname / IP", "proxy-host.forward-host": "Forward Hostname / IP",
"proxy-host.new": "New Proxy Host", "proxy-host.new": "New Proxy Host",
"proxy-hosts.actions-title": "Proxy Host #{id}", "proxy-hosts.actions-title": "Proxy Host #{id}",

View File

@@ -257,6 +257,9 @@
"error.required": { "error.required": {
"defaultMessage": "This is required" "defaultMessage": "This is required"
}, },
"event.created-access-list": {
"defaultMessage": "Created Access List"
},
"event.created-dead-host": { "event.created-dead-host": {
"defaultMessage": "Created 404 Host" "defaultMessage": "Created 404 Host"
}, },
@@ -383,6 +386,9 @@
"notification.host-enabled": { "notification.host-enabled": {
"defaultMessage": "Host has been enabled" "defaultMessage": "Host has been enabled"
}, },
"notification.proxy-host-saved": {
"defaultMessage": "Proxy Host has been saved"
},
"notification.redirection-host-saved": { "notification.redirection-host-saved": {
"defaultMessage": "Redirection Host has been saved" "defaultMessage": "Redirection Host has been saved"
}, },
@@ -446,6 +452,9 @@
"permissions.visibility.user": { "permissions.visibility.user": {
"defaultMessage": "Created Items Only" "defaultMessage": "Created Items Only"
}, },
"proxy-host.edit": {
"defaultMessage": "Edit Proxy Host"
},
"proxy-host.forward-host": { "proxy-host.forward-host": {
"defaultMessage": "Forward Hostname / IP" "defaultMessage": "Forward Hostname / IP"
}, },

View File

@@ -1,4 +1,5 @@
import cn from "classnames"; import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -10,11 +11,14 @@ import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations"; import { validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
interface Props { const showAccessListModal = (id: number | "new") => {
EasyModal.show(AccessListModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new"; id: number | "new";
onClose: () => void;
} }
export function AccessListModal({ id, onClose }: Props) { const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useAccessList(id, ["items", "clients"]); const { data, isLoading, error } = useAccessList(id, ["items", "clients"]);
const { mutate: setAccessList } = useSetAccessList(); const { mutate: setAccessList } = useSetAccessList();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -69,7 +73,7 @@ export function AccessListModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />), onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => { onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.access-saved" })); showSuccess(intl.formatMessage({ id: "notification.access-saved" }));
onClose(); remove();
}, },
onSettled: () => { onSettled: () => {
setIsSubmitting(false); setIsSubmitting(false);
@@ -82,7 +86,7 @@ export function AccessListModal({ id, onClose }: Props) {
const toggleEnabled = cn(toggleClasses, "bg-cyan"); const toggleEnabled = cn(toggleClasses, "bg-cyan");
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -263,7 +267,7 @@ export function AccessListModal({ id, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -283,4 +287,6 @@ export function AccessListModal({ id, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showAccessListModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -7,11 +8,14 @@ import { Button } from "src/components";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations"; import { validateString } from "src/modules/Validations";
interface Props { const showChangePasswordModal = (id: number | "me") => {
userId: number | "me"; EasyModal.show(ChangePasswordModal, { id });
onClose: () => void; };
interface Props extends InnerModalProps {
id: number | "me";
} }
export function ChangePasswordModal({ userId, onClose }: Props) { const ChangePasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState<ReactNode | null>(null); const [error, setError] = useState<ReactNode | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -27,8 +31,8 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
setError(null); setError(null);
try { try {
await updateAuth(userId, values.new, values.current); await updateAuth(id, values.new, values.current);
onClose(); remove();
} catch (err: any) { } catch (err: any) {
setError(<T id={err.message} />); setError(<T id={err.message} />);
} }
@@ -37,7 +41,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
<Formik <Formik
initialValues={ initialValues={
{ {
@@ -142,7 +146,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -161,4 +165,6 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
</Formik> </Formik>
</Modal> </Modal>
); );
} });
export { showChangePasswordModal };

View File

@@ -1,4 +1,5 @@
import { IconSettings } from "@tabler/icons-react"; import { IconSettings } from "@tabler/icons-react";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik"; import { Form, Formik } from "formik";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -15,11 +16,14 @@ import { useDeadHost, useSetDeadHost } from "src/hooks";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
interface Props { const showDeadHostModal = (id: number | "new") => {
EasyModal.show(DeadHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new"; id: number | "new";
onClose: () => void;
} }
export function DeadHostModal({ id, onClose }: Props) { const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useDeadHost(id); const { data, isLoading, error } = useDeadHost(id);
const { mutate: setDeadHost } = useSetDeadHost(); const { mutate: setDeadHost } = useSetDeadHost();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -39,7 +43,7 @@ export function DeadHostModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />), onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => { onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" })); showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" }));
onClose(); remove();
}, },
onSettled: () => { onSettled: () => {
setIsSubmitting(false); setIsSubmitting(false);
@@ -49,7 +53,7 @@ export function DeadHostModal({ id, onClose }: Props) {
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -145,7 +149,7 @@ export function DeadHostModal({ id, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -165,4 +169,6 @@ export function DeadHostModal({ id, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showDeadHostModal };

View File

@@ -1,18 +1,25 @@
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal"; import Modal from "react-bootstrap/Modal";
import { Button } from "src/components"; import { Button } from "src/components";
import { T } from "src/locale"; import { T } from "src/locale";
interface Props { interface ShowProps {
title: string; title: string;
children: ReactNode; children: ReactNode;
onConfirm: () => Promise<void> | void; onConfirm: () => Promise<void> | void;
onClose: () => void;
invalidations?: any[]; invalidations?: any[];
} }
export function DeleteConfirmModal({ title, children, onConfirm, onClose, invalidations }: Props) {
interface Props extends InnerModalProps, ShowProps {}
const showDeleteConfirmModal = (props: ShowProps) => {
EasyModal.show(DeleteConfirmModal, props);
};
const DeleteConfirmModal = EasyModal.create(({ title, children, onConfirm, invalidations, visible, remove }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [error, setError] = useState<ReactNode | null>(null); const [error, setError] = useState<ReactNode | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -23,7 +30,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
setError(null); setError(null);
try { try {
await onConfirm(); await onConfirm();
onClose(); remove();
// invalidate caches as requested // invalidate caches as requested
invalidations?.forEach((inv) => { invalidations?.forEach((inv) => {
queryClient.invalidateQueries({ queryKey: inv }); queryClient.invalidateQueries({ queryKey: inv });
@@ -35,7 +42,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
<T id={title} /> <T id={title} />
@@ -48,7 +55,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
{children} {children}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -65,4 +72,6 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
); );
} });
export { showDeleteConfirmModal };

View File

@@ -1,18 +1,22 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal"; import Modal from "react-bootstrap/Modal";
import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components"; import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components";
import { useAuditLog } from "src/hooks"; import { useAuditLog } from "src/hooks";
import { T } from "src/locale"; import { T } from "src/locale";
interface Props { const showEventDetailsModal = (id: number) => {
EasyModal.show(EventDetailsModal, { id });
};
interface Props extends InnerModalProps {
id: number; id: number;
onClose: () => void;
} }
export function EventDetailsModal({ id, onClose }: Props) { const EventDetailsModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useAuditLog(id); const { data, isLoading, error } = useAuditLog(id);
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -41,7 +45,7 @@ export function EventDetailsModal({ id, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose}> <Button data-bs-dismiss="modal" onClick={remove}>
<T id="close" /> <T id="close" />
</Button> </Button>
</Modal.Footer> </Modal.Footer>
@@ -49,4 +53,6 @@ export function EventDetailsModal({ id, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showEventDetailsModal };

View File

@@ -1,5 +1,6 @@
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import cn from "classnames"; import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -9,14 +10,17 @@ import { Button, Loading } from "src/components";
import { useUser } from "src/hooks"; import { useUser } from "src/hooks";
import { T } from "src/locale"; import { T } from "src/locale";
interface Props { const showPermissionsModal = (id: number) => {
userId: number; EasyModal.show(PermissionsModal, { id });
onClose: () => void; };
interface Props extends InnerModalProps {
id: number;
} }
export function PermissionsModal({ userId, onClose }: Props) { const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
const { data, isLoading, error } = useUser(userId); const { data, isLoading, error } = useUser(id);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => { const onSubmit = async (values: any, { setSubmitting }: any) => {
@@ -24,8 +28,8 @@ export function PermissionsModal({ userId, onClose }: Props) {
setIsSubmitting(true); setIsSubmitting(true);
setErrorMsg(null); setErrorMsg(null);
try { try {
await setPermissions(userId, values); await setPermissions(id, values);
onClose(); remove();
queryClient.invalidateQueries({ queryKey: ["users"] }); queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["user"] });
} catch (err: any) { } catch (err: any) {
@@ -86,7 +90,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
const isAdmin = data?.roles.indexOf("admin") !== -1; const isAdmin = data?.roles.indexOf("admin") !== -1;
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -216,7 +220,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
)} )}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -236,4 +240,6 @@ export function PermissionsModal({ userId, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showPermissionsModal };

View File

@@ -1,5 +1,6 @@
import { IconSettings } from "@tabler/icons-react"; import { IconSettings } from "@tabler/icons-react";
import cn from "classnames"; import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -18,11 +19,14 @@ import { intl, T } from "src/locale";
import { validateNumber, validateString } from "src/modules/Validations"; import { validateNumber, validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
interface Props { const showProxyHostModal = (id: number | "new") => {
EasyModal.show(ProxyHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new"; id: number | "new";
onClose: () => void;
} }
export function ProxyHostModal({ id, onClose }: Props) { const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useProxyHost(id); const { data, isLoading, error } = useProxyHost(id);
const { mutate: setProxyHost } = useSetProxyHost(); const { mutate: setProxyHost } = useSetProxyHost();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -42,7 +46,7 @@ export function ProxyHostModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />), onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => { onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.proxy-host-saved" })); showSuccess(intl.formatMessage({ id: "notification.proxy-host-saved" }));
onClose(); remove();
}, },
onSettled: () => { onSettled: () => {
setIsSubmitting(false); setIsSubmitting(false);
@@ -52,7 +56,7 @@ export function ProxyHostModal({ id, onClose }: Props) {
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -341,7 +345,7 @@ export function ProxyHostModal({ id, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -361,4 +365,6 @@ export function ProxyHostModal({ id, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showProxyHostModal };

View File

@@ -1,5 +1,6 @@
import { IconSettings } from "@tabler/icons-react"; import { IconSettings } from "@tabler/icons-react";
import cn from "classnames"; import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -17,11 +18,14 @@ import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations"; import { validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
interface Props { const showRedirectionHostModal = (id: number | "new") => {
EasyModal.show(RedirectionHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new"; id: number | "new";
onClose: () => void;
} }
export function RedirectionHostModal({ id, onClose }: Props) { const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useRedirectionHost(id); const { data, isLoading, error } = useRedirectionHost(id);
const { mutate: setRedirectionHost } = useSetRedirectionHost(); const { mutate: setRedirectionHost } = useSetRedirectionHost();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -41,7 +45,7 @@ export function RedirectionHostModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />), onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => { onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.redirection-host-saved" })); showSuccess(intl.formatMessage({ id: "notification.redirection-host-saved" }));
onClose(); remove();
}, },
onSettled: () => { onSettled: () => {
setIsSubmitting(false); setIsSubmitting(false);
@@ -51,7 +55,7 @@ export function RedirectionHostModal({ id, onClose }: Props) {
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -275,7 +279,7 @@ export function RedirectionHostModal({ id, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -295,4 +299,6 @@ export function RedirectionHostModal({ id, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showRedirectionHostModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { generate } from "generate-password-browser"; import { generate } from "generate-password-browser";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
@@ -8,21 +9,24 @@ import { Button } from "src/components";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations"; import { validateString } from "src/modules/Validations";
interface Props { const showSetPasswordModal = (id: number) => {
userId: number; EasyModal.show(SetPasswordModal, { id });
onClose: () => void; };
interface Props extends InnerModalProps {
id: number;
} }
export function SetPasswordModal({ userId, onClose }: Props) { const SetPasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState<ReactNode | null>(null); const [error, setError] = useState<ReactNode | null>(null);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const _onSubmit = async (values: any, { setSubmitting }: any) => { const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return; if (isSubmitting) return;
setError(null); setError(null);
try { try {
await updateAuth(userId, values.new); await updateAuth(id, values.new);
onClose(); remove();
} catch (err: any) { } catch (err: any) {
setError(<T id={err.message} />); setError(<T id={err.message} />);
} }
@@ -31,14 +35,14 @@ export function SetPasswordModal({ userId, onClose }: Props) {
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
<Formik <Formik
initialValues={ initialValues={
{ {
new: "", new: "",
} as any } as any
} }
onSubmit={_onSubmit} onSubmit={onSubmit}
> >
{() => ( {() => (
<Form> <Form>
@@ -110,7 +114,7 @@ export function SetPasswordModal({ userId, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -129,4 +133,6 @@ export function SetPasswordModal({ userId, onClose }: Props) {
</Formik> </Formik>
</Modal> </Modal>
); );
} });
export { showSetPasswordModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -8,11 +9,14 @@ import { intl, T } from "src/locale";
import { validateNumber, validateString } from "src/modules/Validations"; import { validateNumber, validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
interface Props { const showStreamModal = (id: number | "new") => {
EasyModal.show(StreamModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new"; id: number | "new";
onClose: () => void;
} }
export function StreamModal({ id, onClose }: Props) { const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useStream(id); const { data, isLoading, error } = useStream(id);
const { mutate: setStream } = useSetStream(); const { mutate: setStream } = useSetStream();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null); const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -32,7 +36,7 @@ export function StreamModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />), onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => { onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.stream-saved" })); showSuccess(intl.formatMessage({ id: "notification.stream-saved" }));
onClose(); remove();
}, },
onSettled: () => { onSettled: () => {
setIsSubmitting(false); setIsSubmitting(false);
@@ -42,7 +46,7 @@ export function StreamModal({ id, onClose }: Props) {
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -296,7 +300,7 @@ export function StreamModal({ id, onClose }: Props) {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -316,4 +320,6 @@ export function StreamModal({ id, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showStreamModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { useState } from "react"; import { useState } from "react";
import { Alert } from "react-bootstrap"; import { Alert } from "react-bootstrap";
@@ -8,12 +9,15 @@ import { intl, T } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations"; import { validateEmail, validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
interface Props { const showUserModal = (id: number | "me" | "new") => {
userId: number | "me" | "new"; EasyModal.show(UserModal, { id });
onClose: () => void; };
interface Props extends InnerModalProps {
id: number | "me" | "new";
} }
export function UserModal({ userId, onClose }: Props) { const UserModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useUser(userId); const { data, isLoading, error } = useUser(id);
const { data: currentUser, isLoading: currentIsLoading } = useUser("me"); const { data: currentUser, isLoading: currentIsLoading } = useUser("me");
const { mutate: setUser } = useSetUser(); const { mutate: setUser } = useSetUser();
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorMsg, setErrorMsg] = useState<string | null>(null);
@@ -25,7 +29,7 @@ export function UserModal({ userId, onClose }: Props) {
setErrorMsg(null); setErrorMsg(null);
const { ...payload } = { const { ...payload } = {
id: userId === "new" ? undefined : userId, id: id === "new" ? undefined : id,
roles: [], roles: [],
...values, ...values,
}; };
@@ -45,7 +49,7 @@ export function UserModal({ userId, onClose }: Props) {
onError: (err: any) => setErrorMsg(err.message), onError: (err: any) => setErrorMsg(err.message),
onSuccess: () => { onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.user-saved" })); showSuccess(intl.formatMessage({ id: "notification.user-saved" }));
onClose(); remove();
}, },
onSettled: () => { onSettled: () => {
setIsSubmitting(false); setIsSubmitting(false);
@@ -55,7 +59,7 @@ export function UserModal({ userId, onClose }: Props) {
}; };
return ( return (
<Modal show onHide={onClose} animation={false}> <Modal show={visible} onHide={remove}>
{!isLoading && error && ( {!isLoading && error && (
<Alert variant="danger" className="m-3"> <Alert variant="danger" className="m-3">
{error?.message || "Unknown error"} {error?.message || "Unknown error"}
@@ -218,7 +222,7 @@ export function UserModal({ userId, onClose }: Props) {
) : null} ) : null}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> <Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" /> <T id="cancel" />
</Button> </Button>
<Button <Button
@@ -238,4 +242,6 @@ export function UserModal({ userId, onClose }: Props) {
)} )}
</Modal> </Modal>
); );
} });
export { showUserModal };

View File

@@ -14,7 +14,7 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
<div className="text-center my-4"> <div className="text-center my-4">
{isFiltered ? ( {isFiltered ? (
<h2> <h2>
<T id="empty.search" /> <T id="empty-search" />
</h2> </h2>
) : ( ) : (
<> <>

View File

@@ -5,14 +5,12 @@ import { deleteAccessList } from "src/api/backend";
import { Button, LoadingPage } from "src/components"; import { Button, LoadingPage } from "src/components";
import { useAccessLists } from "src/hooks"; import { useAccessLists } from "src/hooks";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { AccessListModal, DeleteConfirmModal } from "src/modals"; import { showAccessListModal, showDeleteConfirmModal } from "src/modals";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [editId, setEditId] = useState(0 as number | "new");
const [deleteId, setDeleteId] = useState(0);
const { isFetching, isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); const { isFetching, isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
if (isLoading) { if (isLoading) {
@@ -23,21 +21,15 @@ export default function TableWrapper() {
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
} }
const handleDelete = async () => { const handleDelete = async (id: number) => {
await deleteAccessList(deleteId); await deleteAccessList(id);
showSuccess(intl.formatMessage({ id: "notification.access-deleted" })); showSuccess(intl.formatMessage({ id: "notification.access-deleted" }));
}; };
let filtered = null; let filtered = null;
if (search && data) { if (search && data) {
filtered = data?.filter((_item) => { filtered = data?.filter((item) => {
return true; return item.name.toLowerCase().includes(search);
// TODO
// return (
// `${item.incomingPort}`.includes(search) ||
// `${item.forwardingPort}`.includes(search) ||
// item.forwardingHost.includes(search)
// );
}); });
} else if (search !== "") { } else if (search !== "") {
// this can happen if someone deletes the last item while searching // this can happen if someone deletes the last item while searching
@@ -70,7 +62,7 @@ export default function TableWrapper() {
onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
/> />
</div> </div>
<Button size="sm" className="btn-cyan" onClick={() => setEditId("new")}> <Button size="sm" className="btn-cyan" onClick={() => showAccessListModal("new")}>
<T id="access.add" /> <T id="access.add" />
</Button> </Button>
</div> </div>
@@ -82,21 +74,17 @@ export default function TableWrapper() {
data={filtered ?? data ?? []} data={filtered ?? data ?? []}
isFetching={isFetching} isFetching={isFetching}
isFiltered={!!filtered} isFiltered={!!filtered}
onEdit={(id: number) => setEditId(id)} onEdit={(id: number) => showAccessListModal(id)}
onDelete={(id: number) => setDeleteId(id)} onDelete={(id: number) =>
onNew={() => setEditId("new")} showDeleteConfirmModal({
title: "access.delete.title",
onConfirm: () => handleDelete(id),
invalidations: [["access-lists"], ["access-list", id]],
children: <T id="access.delete.content" />,
})
}
onNew={() => showAccessListModal("new")}
/> />
{editId ? <AccessListModal id={editId} onClose={() => setEditId(0)} /> : null}
{deleteId ? (
<DeleteConfirmModal
title="access.delete.title"
onConfirm={handleDelete}
onClose={() => setDeleteId(0)}
invalidations={[["access-lists"], ["access-list", deleteId]]}
>
<T id="access.delete.content" />
</DeleteConfirmModal>
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -1,13 +1,11 @@
import { useState } from "react";
import Alert from "react-bootstrap/Alert"; import Alert from "react-bootstrap/Alert";
import { LoadingPage } from "src/components"; import { LoadingPage } from "src/components";
import { useAuditLogs } from "src/hooks"; import { useAuditLogs } from "src/hooks";
import { T } from "src/locale"; import { T } from "src/locale";
import { EventDetailsModal } from "src/modals"; import { showEventDetailsModal } from "src/modals";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const [eventId, setEventId] = useState(0);
const { isFetching, isLoading, isError, error, data } = useAuditLogs(["user"]); const { isFetching, isLoading, isError, error, data } = useAuditLogs(["user"]);
if (isLoading) { if (isLoading) {
@@ -31,8 +29,7 @@ export default function TableWrapper() {
</div> </div>
</div> </div>
</div> </div>
<Table data={data ?? []} isFetching={isFetching} onSelectItem={setEventId} /> <Table data={data ?? []} isFetching={isFetching} onSelectItem={showEventDetailsModal} />
{eventId ? <EventDetailsModal id={eventId} onClose={() => setEventId(0)} /> : null}
</div> </div>
</div> </div>
); );

View File

@@ -14,7 +14,7 @@ export default function Empty({ tableInstance, onNew, onNewCustom, isFiltered }:
<div className="text-center my-4"> <div className="text-center my-4">
{isFiltered ? ( {isFiltered ? (
<h2> <h2>
<T id="empty.search" /> <T id="empty-search" />
</h2> </h2>
) : ( ) : (
<> <>

View File

@@ -14,7 +14,7 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
<div className="text-center my-4"> <div className="text-center my-4">
{isFiltered ? ( {isFiltered ? (
<h2> <h2>
<T id="empty.search" /> <T id="empty-search" />
</h2> </h2>
) : ( ) : (
<> <>

View File

@@ -6,15 +6,13 @@ import { deleteDeadHost, toggleDeadHost } from "src/api/backend";
import { Button, LoadingPage } from "src/components"; import { Button, LoadingPage } from "src/components";
import { useDeadHosts } from "src/hooks"; import { useDeadHosts } from "src/hooks";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { DeadHostModal, DeleteConfirmModal } from "src/modals"; import { showDeadHostModal, showDeleteConfirmModal } from "src/modals";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [deleteId, setDeleteId] = useState(0);
const [editId, setEditId] = useState(0 as number | "new");
const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]); const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]);
if (isLoading) { if (isLoading) {
@@ -25,8 +23,8 @@ export default function TableWrapper() {
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
} }
const handleDelete = async () => { const handleDelete = async (id: number) => {
await deleteDeadHost(deleteId); await deleteDeadHost(id);
showSuccess(intl.formatMessage({ id: "notification.host-deleted" })); showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
}; };
@@ -73,7 +71,7 @@ export default function TableWrapper() {
onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
/> />
</div> </div>
<Button size="sm" className="btn-red" onClick={() => setEditId("new")}> <Button size="sm" className="btn-red" onClick={() => showDeadHostModal("new")}>
<T id="dead-hosts.add" /> <T id="dead-hosts.add" />
</Button> </Button>
</div> </div>
@@ -85,22 +83,18 @@ export default function TableWrapper() {
data={filtered ?? data ?? []} data={filtered ?? data ?? []}
isFiltered={!!search} isFiltered={!!search}
isFetching={isFetching} isFetching={isFetching}
onEdit={(id: number) => setEditId(id)} onEdit={(id: number) => showDeadHostModal(id)}
onDelete={(id: number) => setDeleteId(id)} onDelete={(id: number) =>
showDeleteConfirmModal({
title: "dead-host.delete.title",
onConfirm: () => handleDelete(id),
invalidations: [["dead-hosts"], ["dead-host", id]],
children: <T id="dead-host.delete.content" />,
})
}
onDisableToggle={handleDisableToggle} onDisableToggle={handleDisableToggle}
onNew={() => setEditId("new")} onNew={() => showDeadHostModal("new")}
/> />
{editId ? <DeadHostModal id={editId} onClose={() => setEditId(0)} /> : null}
{deleteId ? (
<DeleteConfirmModal
title="dead-host.delete.title"
onConfirm={handleDelete}
onClose={() => setDeleteId(0)}
invalidations={[["dead-hosts"], ["dead-host", deleteId]]}
>
<T id="dead-host.delete.content" />
</DeleteConfirmModal>
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -14,7 +14,7 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
<div className="text-center my-4"> <div className="text-center my-4">
{isFiltered ? ( {isFiltered ? (
<h2> <h2>
<T id="empty.search" /> <T id="empty-search" />
</h2> </h2>
) : ( ) : (
<> <>

View File

@@ -6,15 +6,13 @@ import { deleteProxyHost, toggleProxyHost } from "src/api/backend";
import { Button, LoadingPage } from "src/components"; import { Button, LoadingPage } from "src/components";
import { useProxyHosts } from "src/hooks"; import { useProxyHosts } from "src/hooks";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { DeleteConfirmModal, ProxyHostModal } from "src/modals"; import { showDeleteConfirmModal, showProxyHostModal } from "src/modals";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [search, setSearch] = useState(""); 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"]); const { isFetching, isLoading, isError, error, data } = useProxyHosts(["owner", "access_list", "certificate"]);
if (isLoading) { if (isLoading) {
@@ -25,8 +23,8 @@ export default function TableWrapper() {
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
} }
const handleDelete = async () => { const handleDelete = async (id: number) => {
await deleteProxyHost(deleteId); await deleteProxyHost(id);
showSuccess(intl.formatMessage({ id: "notification.host-deleted" })); showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
}; };
@@ -74,9 +72,10 @@ export default function TableWrapper() {
type="text" type="text"
className="form-control form-control-sm" className="form-control form-control-sm"
autoComplete="off" autoComplete="off"
onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
/> />
</div> </div>
<Button size="sm" className="btn-lime"> <Button size="sm" className="btn-lime" onClick={() => showProxyHostModal("new")}>
<T id="proxy-hosts.add" /> <T id="proxy-hosts.add" />
</Button> </Button>
</div> </div>
@@ -88,22 +87,18 @@ export default function TableWrapper() {
data={filtered ?? data ?? []} data={filtered ?? data ?? []}
isFiltered={!!search} isFiltered={!!search}
isFetching={isFetching} isFetching={isFetching}
onEdit={(id: number) => setEditId(id)} onEdit={(id: number) => showProxyHostModal(id)}
onDelete={(id: number) => setDeleteId(id)} onDelete={(id: number) =>
showDeleteConfirmModal({
title: "proxy-host.delete.title",
onConfirm: () => handleDelete(id),
invalidations: [["proxy-hosts"], ["proxy-host", id]],
children: <T id="proxy-host.delete.content" />,
})
}
onDisableToggle={handleDisableToggle} onDisableToggle={handleDisableToggle}
onNew={() => setEditId("new")} onNew={() => showProxyHostModal("new")}
/> />
{editId ? <ProxyHostModal id={editId} onClose={() => setEditId(0)} /> : null}
{deleteId ? (
<DeleteConfirmModal
title="proxy-host.delete.title"
onConfirm={handleDelete}
onClose={() => setDeleteId(0)}
invalidations={[["proxy-hosts"], ["proxy-host", deleteId]]}
>
<T id="proxy-host.delete.content" />
</DeleteConfirmModal>
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -14,7 +14,7 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
<div className="text-center my-4"> <div className="text-center my-4">
{isFiltered ? ( {isFiltered ? (
<h2> <h2>
<T id="empty.search" /> <T id="empty-search" />
</h2> </h2>
) : ( ) : (
<> <>

View File

@@ -6,15 +6,13 @@ import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend";
import { Button, LoadingPage } from "src/components"; import { Button, LoadingPage } from "src/components";
import { useRedirectionHosts } from "src/hooks"; import { useRedirectionHosts } from "src/hooks";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { DeleteConfirmModal, RedirectionHostModal } from "src/modals"; import { showDeleteConfirmModal, showRedirectionHostModal } from "src/modals";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [deleteId, setDeleteId] = useState(0);
const [editId, setEditId] = useState(0 as number | "new");
const { isFetching, isLoading, isError, error, data } = useRedirectionHosts(["owner", "certificate"]); const { isFetching, isLoading, isError, error, data } = useRedirectionHosts(["owner", "certificate"]);
if (isLoading) { if (isLoading) {
@@ -25,8 +23,8 @@ export default function TableWrapper() {
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
} }
const handleDelete = async () => { const handleDelete = async (id: number) => {
await deleteRedirectionHost(deleteId); await deleteRedirectionHost(id);
showSuccess(intl.formatMessage({ id: "notification.host-deleted" })); showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
}; };
@@ -76,7 +74,11 @@ export default function TableWrapper() {
onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
/> />
</div> </div>
<Button size="sm" className="btn-yellow" onClick={() => setEditId("new")}> <Button
size="sm"
className="btn-yellow"
onClick={() => showRedirectionHostModal("new")}
>
<T id="redirection-hosts.add" /> <T id="redirection-hosts.add" />
</Button> </Button>
</div> </div>
@@ -88,22 +90,18 @@ export default function TableWrapper() {
data={filtered ?? data ?? []} data={filtered ?? data ?? []}
isFiltered={!!search} isFiltered={!!search}
isFetching={isFetching} isFetching={isFetching}
onEdit={(id: number) => setEditId(id)} onEdit={(id: number) => showRedirectionHostModal(id)}
onDelete={(id: number) => setDeleteId(id)} onDelete={(id: number) =>
showDeleteConfirmModal({
title: "redirection-host.delete.title",
onConfirm: () => handleDelete(id),
invalidations: [["redirection-hosts"], ["redirection-host", id]],
children: <T id="redirection-host.delete.content" />,
})
}
onDisableToggle={handleDisableToggle} onDisableToggle={handleDisableToggle}
onNew={() => setEditId("new")} onNew={() => showRedirectionHostModal("new")}
/> />
{editId ? <RedirectionHostModal id={editId} onClose={() => setEditId(0)} /> : null}
{deleteId ? (
<DeleteConfirmModal
title="redirection-host.delete.title"
onConfirm={handleDelete}
onClose={() => setDeleteId(0)}
invalidations={[["redirection-hosts"], ["redirection-host", deleteId]]}
>
<T id="redirection-host.delete.content" />
</DeleteConfirmModal>
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -14,7 +14,7 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
<div className="text-center my-4"> <div className="text-center my-4">
{isFiltered ? ( {isFiltered ? (
<h2> <h2>
<T id="empty.search" /> <T id="empty-search" />
</h2> </h2>
) : ( ) : (
<> <>

View File

@@ -6,15 +6,14 @@ import { deleteStream, toggleStream } from "src/api/backend";
import { Button, LoadingPage } from "src/components"; import { Button, LoadingPage } from "src/components";
import { useStreams } from "src/hooks"; import { useStreams } from "src/hooks";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { DeleteConfirmModal, StreamModal } from "src/modals"; import { showDeleteConfirmModal, showStreamModal } from "src/modals";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [editId, setEditId] = useState(0 as number | "new"); const [_deleteId, _setDeleteIdd] = useState(0);
const [deleteId, setDeleteId] = useState(0);
const { isFetching, isLoading, isError, error, data } = useStreams(["owner", "certificate"]); const { isFetching, isLoading, isError, error, data } = useStreams(["owner", "certificate"]);
if (isLoading) { if (isLoading) {
@@ -25,8 +24,8 @@ export default function TableWrapper() {
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
} }
const handleDelete = async () => { const handleDelete = async (id: number) => {
await deleteStream(deleteId); await deleteStream(id);
showSuccess(intl.formatMessage({ id: "notification.stream-deleted" })); showSuccess(intl.formatMessage({ id: "notification.stream-deleted" }));
}; };
@@ -79,7 +78,7 @@ export default function TableWrapper() {
onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())} onChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}
/> />
</div> </div>
<Button size="sm" className="btn-blue" onClick={() => setEditId("new")}> <Button size="sm" className="btn-blue" onClick={() => showStreamModal("new")}>
<T id="streams.add" /> <T id="streams.add" />
</Button> </Button>
</div> </div>
@@ -91,22 +90,18 @@ export default function TableWrapper() {
data={filtered ?? data ?? []} data={filtered ?? data ?? []}
isFetching={isFetching} isFetching={isFetching}
isFiltered={!!filtered} isFiltered={!!filtered}
onEdit={(id: number) => setEditId(id)} onEdit={(id: number) => showStreamModal(id)}
onDelete={(id: number) => setDeleteId(id)} onDelete={(id: number) =>
showDeleteConfirmModal({
title: "stream.delete.title",
onConfirm: () => handleDelete(id),
invalidations: [["streams"], ["stream", id]],
children: <T id="stream.delete.content" />,
})
}
onDisableToggle={handleDisableToggle} onDisableToggle={handleDisableToggle}
onNew={() => setEditId("new")} onNew={() => showStreamModal("new")}
/> />
{editId ? <StreamModal id={editId} onClose={() => setEditId(0)} /> : null}
{deleteId ? (
<DeleteConfirmModal
title="stream.delete.title"
onConfirm={handleDelete}
onClose={() => setDeleteId(0)}
invalidations={[["streams"], ["stream", deleteId]]}
>
<T id="stream.delete.content" />
</DeleteConfirmModal>
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -87,7 +87,7 @@ export default function Table({
}, },
}), }),
columnHelper.display({ columnHelper.display({
id: "id", // todo: not needed for a display? id: "id",
cell: (info: any) => { cell: (info: any) => {
return ( return (
<span className="dropdown"> <span className="dropdown">
@@ -112,7 +112,7 @@ export default function Table({
}} }}
> >
<IconEdit size={16} /> <IconEdit size={16} />
<T id="users.edit" /> <T id="user.edit" />
</a> </a>
{currentUserId !== info.row.original.id ? ( {currentUserId !== info.row.original.id ? (
<> <>

View File

@@ -6,17 +6,13 @@ import { deleteUser, toggleUser } from "src/api/backend";
import { Button, LoadingPage } from "src/components"; import { Button, LoadingPage } from "src/components";
import { useUser, useUsers } from "src/hooks"; import { useUser, useUsers } from "src/hooks";
import { intl, T } from "src/locale"; import { intl, T } from "src/locale";
import { DeleteConfirmModal, PermissionsModal, SetPasswordModal, UserModal } from "src/modals"; import { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from "src/modals";
import { showSuccess } from "src/notifications"; import { showSuccess } from "src/notifications";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [editUserId, setEditUserId] = useState(0 as number | "new");
const [editUserPermissionsId, setEditUserPermissionsId] = useState(0);
const [editUserPasswordId, setEditUserPasswordId] = useState(0);
const [deleteUserId, setDeleteUserId] = useState(0);
const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]); const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
const { data: currentUser } = useUser("me"); const { data: currentUser } = useUser("me");
@@ -28,8 +24,8 @@ export default function TableWrapper() {
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
} }
const handleDelete = async () => { const handleDelete = async (id: number) => {
await deleteUser(deleteUserId); await deleteUser(id);
showSuccess(intl.formatMessage({ id: "notification.user-deleted" })); showSuccess(intl.formatMessage({ id: "notification.user-deleted" }));
}; };
@@ -81,7 +77,7 @@ export default function TableWrapper() {
/> />
</div> </div>
<Button size="sm" className="btn-orange" onClick={() => setEditUserId("new")}> <Button size="sm" className="btn-orange" onClick={() => showUserModal("new")}>
<T id="users.add" /> <T id="users.add" />
</Button> </Button>
</div> </div>
@@ -94,30 +90,20 @@ export default function TableWrapper() {
isFiltered={!!search} isFiltered={!!search}
isFetching={isFetching} isFetching={isFetching}
currentUserId={currentUser?.id} currentUserId={currentUser?.id}
onEditUser={(id: number) => setEditUserId(id)} onEditUser={(id: number) => showUserModal(id)}
onEditPermissions={(id: number) => setEditUserPermissionsId(id)} onEditPermissions={(id: number) => showPermissionsModal(id)}
onSetPassword={(id: number) => setEditUserPasswordId(id)} onSetPassword={(id: number) => showSetPasswordModal(id)}
onDeleteUser={(id: number) => setDeleteUserId(id)} onDeleteUser={(id: number) =>
showDeleteConfirmModal({
title: "user.delete.title",
onConfirm: () => handleDelete(id),
invalidations: [["users"], ["user", id]],
children: <T id="user.delete.content" />,
})
}
onDisableToggle={handleDisableToggle} onDisableToggle={handleDisableToggle}
onNewUser={() => setEditUserId("new")} onNewUser={() => showUserModal("new")}
/> />
{editUserId ? <UserModal userId={editUserId} onClose={() => setEditUserId(0)} /> : null}
{editUserPermissionsId ? (
<PermissionsModal userId={editUserPermissionsId} onClose={() => setEditUserPermissionsId(0)} />
) : null}
{deleteUserId ? (
<DeleteConfirmModal
title="user.delete.title"
onConfirm={handleDelete}
onClose={() => setDeleteUserId(0)}
invalidations={[["users"], ["user", deleteUserId]]}
>
<T id="user.delete.content" />
</DeleteConfirmModal>
) : null}
{editUserPasswordId ? (
<SetPasswordModal userId={editUserPasswordId} onClose={() => setEditUserPasswordId(0)} />
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -1530,6 +1530,11 @@ extend@^3.0.0:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
ez-modal-react@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/ez-modal-react/-/ez-modal-react-1.0.5.tgz#38d36c5e31f54f6b7cb7afa0cc79a8d1190c2805"
integrity sha512-/A8yLK54tpmWCMkW8Pwqc2xxspmimGOOw/m+1Y+tNtUIheuDHhLynHP1Q0utciJEGDAK849aQcd+6DrJ88hggQ==
fast-deep-equal@^3.1.3: fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"