mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-14 10:52:34 +00:00
Notification toasts, nicer loading, add new user support
This commit is contained in:
@@ -134,24 +134,24 @@ export default {
|
||||
* @param {Object} user
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getTokenFromUser: (user) => {
|
||||
getTokenFromUser: async (user) => {
|
||||
const expire = "1d";
|
||||
const Token = new TokenModel();
|
||||
const expiry = parseDatePeriod(expire);
|
||||
|
||||
return Token.create({
|
||||
const signed = await Token.create({
|
||||
iss: "api",
|
||||
attrs: {
|
||||
id: user.id,
|
||||
},
|
||||
scope: ["user"],
|
||||
expiresIn: expire,
|
||||
}).then((signed) => {
|
||||
return {
|
||||
token: signed.token,
|
||||
expires: expiry.toISOString(),
|
||||
user: user,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
token: signed.token,
|
||||
expires: expiry.toISOString(),
|
||||
user: user,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
@@ -337,11 +337,11 @@ const internalUser = {
|
||||
* @param {Integer} [id_requested]
|
||||
* @returns {[String]}
|
||||
*/
|
||||
getUserOmisionsByAccess: (access, id_requested) => {
|
||||
getUserOmisionsByAccess: (access, idRequested) => {
|
||||
let response = []; // Admin response
|
||||
|
||||
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== id_requested) {
|
||||
response = ["roles", "is_deleted"]; // Restricted response
|
||||
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
|
||||
response = ["is_deleted"]; // Restricted response
|
||||
}
|
||||
|
||||
return response;
|
||||
|
@@ -123,16 +123,16 @@ export default () => {
|
||||
},
|
||||
|
||||
/**
|
||||
* @param [default_value]
|
||||
* @param [defaultValue]
|
||||
* @returns {Integer}
|
||||
*/
|
||||
getUserId: (default_value) => {
|
||||
getUserId: (defaultValue) => {
|
||||
const attrs = self.get("attrs");
|
||||
if (attrs && typeof attrs.id !== "undefined" && attrs.id) {
|
||||
return attrs.id;
|
||||
}
|
||||
|
||||
return default_value || 0;
|
||||
return defaultValue || 0;
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -30,6 +30,7 @@
|
||||
"react-dom": "^19.1.1",
|
||||
"react-intl": "^7.1.11",
|
||||
"react-router-dom": "^7.8.2",
|
||||
"react-toastify": "^11.0.5",
|
||||
"rooks": "^9.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -5,3 +5,10 @@
|
||||
.domain-name {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { RawIntlProvider } from "react-intl";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
|
||||
import { intl } from "src/locale";
|
||||
import Router from "src/Router.tsx";
|
||||
@@ -16,6 +17,15 @@ function App() {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<Router />
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar={true}
|
||||
newestOnTop={true}
|
||||
closeOnClick={true}
|
||||
rtl={false}
|
||||
closeButton={false}
|
||||
/>
|
||||
</AuthProvider>
|
||||
<ReactQueryDevtools buttonPosition="bottom-right" position="right" />
|
||||
</QueryClientProvider>
|
||||
|
@@ -4,6 +4,7 @@ export * from "./createDeadHost";
|
||||
export * from "./createProxyHost";
|
||||
export * from "./createRedirectionHost";
|
||||
export * from "./createStream";
|
||||
export * from "./createUser";
|
||||
export * from "./deleteAccessList";
|
||||
export * from "./deleteCertificate";
|
||||
export * from "./deleteDeadHost";
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { Loading, LoadingPage } from "src/components";
|
||||
import { useUser } from "src/hooks";
|
||||
import { intl } from "src/locale";
|
||||
|
||||
@@ -8,11 +9,30 @@ interface Props {
|
||||
type: "manage" | "view";
|
||||
hideError?: boolean;
|
||||
children?: ReactNode;
|
||||
pageLoading?: boolean;
|
||||
loadingNoLogo?: boolean;
|
||||
}
|
||||
function HasPermission({ permission, type, children, hideError = false }: Props) {
|
||||
const { data } = useUser("me");
|
||||
function HasPermission({
|
||||
permission,
|
||||
type,
|
||||
children,
|
||||
hideError = false,
|
||||
pageLoading = false,
|
||||
loadingNoLogo = false,
|
||||
}: Props) {
|
||||
const { data, isLoading } = useUser("me");
|
||||
const perms = data?.permissions;
|
||||
|
||||
if (isLoading) {
|
||||
if (hideError) {
|
||||
return null;
|
||||
}
|
||||
if (pageLoading) {
|
||||
return <LoadingPage noLogo={loadingNoLogo} />;
|
||||
}
|
||||
return <Loading noLogo={loadingNoLogo} />;
|
||||
}
|
||||
|
||||
let allowed = permission === "";
|
||||
const acceptable = ["manage", type];
|
||||
|
||||
|
22
frontend/src/components/Loading.tsx
Normal file
22
frontend/src/components/Loading.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { intl } from "src/locale";
|
||||
import styles from "./Loading.module.css";
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
noLogo?: boolean;
|
||||
}
|
||||
export function Loading({ label, noLogo }: Props) {
|
||||
return (
|
||||
<div className="empty text-center">
|
||||
{noLogo ? null : (
|
||||
<div className="mb-3">
|
||||
<img className={styles.logo} src="/images/logo-no-text.svg" alt="" />
|
||||
</div>
|
||||
)}
|
||||
<div className="text-secondary mb-3">{label || intl.formatMessage({ id: "loading" })}</div>
|
||||
<div className="progress progress-sm">
|
||||
<div className="progress-bar progress-bar-indeterminate" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,6 +1,4 @@
|
||||
import { Page } from "src/components";
|
||||
import { intl } from "src/locale";
|
||||
import styles from "./LoadingPage.module.css";
|
||||
import { Loading, Page } from "src/components";
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
@@ -10,17 +8,7 @@ export function LoadingPage({ label, noLogo }: Props) {
|
||||
return (
|
||||
<Page className="page-center">
|
||||
<div className="container-tight py-4">
|
||||
<div className="empty text-center">
|
||||
{noLogo ? null : (
|
||||
<div className="mb-3">
|
||||
<img className={styles.logo} src="/images/logo-no-text.svg" alt="" />
|
||||
</div>
|
||||
)}
|
||||
<div className="text-secondary mb-3">{label || intl.formatMessage({ id: "loading" })}</div>
|
||||
<div className="progress progress-sm">
|
||||
<div className="progress-bar progress-bar-indeterminate" />
|
||||
</div>
|
||||
</div>
|
||||
<Loading label={label} noLogo={noLogo} />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
@@ -2,6 +2,7 @@ export * from "./Button";
|
||||
export * from "./ErrorNotFound";
|
||||
export * from "./Flag";
|
||||
export * from "./HasPermission";
|
||||
export * from "./Loading";
|
||||
export * from "./LoadingPage";
|
||||
export * from "./LocalePicker";
|
||||
export * from "./NavLink";
|
||||
|
@@ -1,7 +1,20 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getUser, type User, updateUser } from "src/api/backend";
|
||||
import { createUser, getUser, type User, updateUser } from "src/api/backend";
|
||||
|
||||
const fetchUser = (id: number | string) => {
|
||||
if (id === "new") {
|
||||
return Promise.resolve({
|
||||
id: 0,
|
||||
createdOn: "",
|
||||
modifiedOn: "",
|
||||
isDisabled: false,
|
||||
email: "",
|
||||
name: "",
|
||||
nickname: "",
|
||||
roles: [],
|
||||
avatar: "",
|
||||
} as User);
|
||||
}
|
||||
return getUser(id, { expand: "permissions" });
|
||||
};
|
||||
|
||||
@@ -17,8 +30,11 @@ const useUser = (id: string | number, options = {}) => {
|
||||
const useSetUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (values: User) => updateUser(values),
|
||||
mutationFn: (values: User) => (values.id ? updateUser(values) : createUser(values)),
|
||||
onMutate: (values: User) => {
|
||||
if (!values.id) {
|
||||
return;
|
||||
}
|
||||
const previousObject = queryClient.getQueryData(["user", values.id]);
|
||||
queryClient.setQueryData(["user", values.id], (old: User) => ({
|
||||
...old,
|
||||
|
@@ -51,6 +51,9 @@
|
||||
"notfound.action": "Take me home",
|
||||
"notfound.text": "We are sorry but the page you are looking for was not found",
|
||||
"notfound.title": "Oops… You just found an error page",
|
||||
"notification.error": "Error",
|
||||
"notification.success": "Success",
|
||||
"notification.user-saved": "User has been saved",
|
||||
"offline": "Offline",
|
||||
"online": "Online",
|
||||
"password": "Password",
|
||||
@@ -82,6 +85,7 @@
|
||||
"user.edit-profile": "Edit Profile",
|
||||
"user.full-name": "Full Name",
|
||||
"user.logout": "Logout",
|
||||
"user.new": "New User",
|
||||
"user.new-password": "New Password",
|
||||
"user.nickname": "Nickname",
|
||||
"user.switch-dark": "Switch to Dark mode",
|
||||
|
@@ -155,6 +155,15 @@
|
||||
"notfound.title": {
|
||||
"defaultMessage": "Oops… You just found an error page"
|
||||
},
|
||||
"notification.error": {
|
||||
"defaultMessage": "Error"
|
||||
},
|
||||
"notification.user-saved": {
|
||||
"defaultMessage": "User has been saved"
|
||||
},
|
||||
"notification.success": {
|
||||
"defaultMessage": "Success"
|
||||
},
|
||||
"offline": {
|
||||
"defaultMessage": "Offline"
|
||||
},
|
||||
@@ -248,6 +257,9 @@
|
||||
"user.logout": {
|
||||
"defaultMessage": "Logout"
|
||||
},
|
||||
"user.new": {
|
||||
"defaultMessage": "New User"
|
||||
},
|
||||
"user.new-password": {
|
||||
"defaultMessage": "New Password"
|
||||
},
|
||||
|
@@ -2,31 +2,35 @@ import { Field, Form, Formik } from "formik";
|
||||
import { useState } from "react";
|
||||
import { Alert } from "react-bootstrap";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import { Button } from "src/components";
|
||||
import { Button, Loading } from "src/components";
|
||||
import { useSetUser, useUser } from "src/hooks";
|
||||
import { intl } from "src/locale";
|
||||
import { validateEmail, validateString } from "src/modules/Validations";
|
||||
import { showSuccess } from "src/notifications";
|
||||
|
||||
interface Props {
|
||||
userId: number | "me";
|
||||
userId: number | "me" | "new";
|
||||
onClose: () => void;
|
||||
}
|
||||
export function UserModal({ userId, onClose }: Props) {
|
||||
const { data } = useUser(userId);
|
||||
const { data: currentUser } = useUser("me");
|
||||
const { data, isLoading, error } = useUser(userId);
|
||||
const { data: currentUser, isLoading: currentIsLoading } = useUser("me");
|
||||
const { mutate: setUser } = useSetUser();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
if (data && currentUser) {
|
||||
console.log("DATA:", data);
|
||||
console.log("CURRENT:", currentUser);
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||
setError(null);
|
||||
setErrorMsg(null);
|
||||
const { ...payload } = {
|
||||
id: userId,
|
||||
id: userId === "new" ? undefined : userId,
|
||||
roles: [],
|
||||
...values,
|
||||
};
|
||||
|
||||
console.log("values", values);
|
||||
|
||||
if (data?.id === currentUser?.id) {
|
||||
// Prevent user from locking themselves out
|
||||
delete payload.isDisabled;
|
||||
@@ -39,175 +43,188 @@ export function UserModal({ userId, onClose }: Props) {
|
||||
delete payload.isAdmin;
|
||||
|
||||
setUser(payload, {
|
||||
onError: (err: any) => setError(err.message),
|
||||
onSuccess: () => onClose(),
|
||||
onError: (err: any) => setErrorMsg(err.message),
|
||||
onSuccess: () => {
|
||||
showSuccess(intl.formatMessage({ id: "notification.user-saved" }));
|
||||
onClose();
|
||||
},
|
||||
onSettled: () => setSubmitting(false),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show onHide={onClose} animation={false}>
|
||||
<Formik
|
||||
initialValues={
|
||||
{
|
||||
name: data?.name,
|
||||
nickname: data?.nickname,
|
||||
email: data?.email,
|
||||
isAdmin: data?.roles.includes("admin"),
|
||||
isDisabled: data?.isDisabled,
|
||||
} as any
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{intl.formatMessage({ id: "user.edit" })}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Alert variant="danger" show={!!error} onClose={() => setError(null)} dismissible>
|
||||
{error}
|
||||
</Alert>
|
||||
<div className="row">
|
||||
<div className="col-lg-6">
|
||||
<div className="mb-3">
|
||||
<Field name="name" validate={validateString(1, 50)}>
|
||||
{({ field, form }: any) => (
|
||||
<div className="form-floating mb-3">
|
||||
<input
|
||||
id="name"
|
||||
className={`form-control ${form.errors.name && form.touched.name ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "user.full-name" })}
|
||||
{...field}
|
||||
/>
|
||||
<label htmlFor="name">
|
||||
{intl.formatMessage({ id: "user.full-name" })}
|
||||
</label>
|
||||
{form.errors.name ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.name && form.touched.name
|
||||
? form.errors.name
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="mb-3">
|
||||
<Field name="nickname" validate={validateString(1, 30)}>
|
||||
{({ field, form }: any) => (
|
||||
<div className="form-floating mb-3">
|
||||
<input
|
||||
id="nickname"
|
||||
className={`form-control ${form.errors.nickname && form.touched.nickname ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "user.nickname" })}
|
||||
{...field}
|
||||
/>
|
||||
<label htmlFor="nickname">
|
||||
{intl.formatMessage({ id: "user.nickname" })}
|
||||
</label>
|
||||
{form.errors.nickname ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.nickname && form.touched.nickname
|
||||
? form.errors.nickname
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Field name="email" validate={validateEmail()}>
|
||||
{({ field, form }: any) => (
|
||||
<div className="form-floating mb-3">
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
className={`form-control ${form.errors.email && form.touched.email ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "email-address" })}
|
||||
{...field}
|
||||
/>
|
||||
<label htmlFor="email">{intl.formatMessage({ id: "email-address" })}</label>
|
||||
{form.errors.email ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.email && form.touched.email ? form.errors.email : null}
|
||||
</div>
|
||||
) : null}
|
||||
{!isLoading && error && <Alert variant="danger">{error?.message || "Unknown error"}</Alert>}
|
||||
{(isLoading || currentIsLoading) && <Loading noLogo />}
|
||||
{!isLoading && !currentIsLoading && data && currentUser && (
|
||||
<Formik
|
||||
initialValues={
|
||||
{
|
||||
name: data?.name,
|
||||
nickname: data?.nickname,
|
||||
email: data?.email,
|
||||
isAdmin: data?.roles?.includes("admin"),
|
||||
isDisabled: data?.isDisabled,
|
||||
} as any
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{intl.formatMessage({ id: data?.id ? "user.edit" : "user.new" })}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
|
||||
{errorMsg}
|
||||
</Alert>
|
||||
<div className="row">
|
||||
<div className="col-lg-6">
|
||||
<div className="mb-3">
|
||||
<Field name="name" validate={validateString(1, 50)}>
|
||||
{({ field, form }: any) => (
|
||||
<div className="form-floating mb-3">
|
||||
<input
|
||||
id="name"
|
||||
className={`form-control ${form.errors.name && form.touched.name ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "user.full-name" })}
|
||||
{...field}
|
||||
/>
|
||||
<label htmlFor="name">
|
||||
{intl.formatMessage({ id: "user.full-name" })}
|
||||
</label>
|
||||
{form.errors.name ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.name && form.touched.name
|
||||
? form.errors.name
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
{currentUser && data && currentUser?.id !== data?.id ? (
|
||||
<div className="my-3">
|
||||
<h3 className="py-2">Properties</h3>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="mb-3">
|
||||
<Field name="nickname" validate={validateString(1, 30)}>
|
||||
{({ field, form }: any) => (
|
||||
<div className="form-floating mb-3">
|
||||
<input
|
||||
id="nickname"
|
||||
className={`form-control ${form.errors.nickname && form.touched.nickname ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "user.nickname" })}
|
||||
{...field}
|
||||
/>
|
||||
<label htmlFor="nickname">
|
||||
{intl.formatMessage({ id: "user.nickname" })}
|
||||
</label>
|
||||
{form.errors.nickname ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.nickname && form.touched.nickname
|
||||
? form.errors.nickname
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Field name="email" validate={validateEmail()}>
|
||||
{({ field, form }: any) => (
|
||||
<div className="form-floating mb-3">
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
className={`form-control ${form.errors.email && form.touched.email ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "email-address" })}
|
||||
{...field}
|
||||
/>
|
||||
<label htmlFor="email">
|
||||
{intl.formatMessage({ id: "email-address" })}
|
||||
</label>
|
||||
{form.errors.email ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.email && form.touched.email
|
||||
? form.errors.email
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
{currentUser && data && currentUser?.id !== data?.id ? (
|
||||
<div className="my-3">
|
||||
<h3 className="py-2">Properties</h3>
|
||||
|
||||
<div className="divide-y">
|
||||
<div>
|
||||
<label className="row" htmlFor="isAdmin">
|
||||
<span className="col">Administrator</span>
|
||||
<span className="col-auto">
|
||||
<Field name="isAdmin" type="checkbox">
|
||||
{({ field }: any) => (
|
||||
<label className="form-check form-check-single form-switch">
|
||||
<input
|
||||
{...field}
|
||||
id="isAdmin"
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</Field>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="row" htmlFor="isDisabled">
|
||||
<span className="col">Disabled</span>
|
||||
<span className="col-auto">
|
||||
<Field name="isDisabled" type="checkbox">
|
||||
{({ field }: any) => (
|
||||
<label className="form-check form-check-single form-switch">
|
||||
<input
|
||||
{...field}
|
||||
id="isDisabled"
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</Field>
|
||||
</span>
|
||||
</label>
|
||||
<div className="divide-y">
|
||||
<div>
|
||||
<label className="row" htmlFor="isAdmin">
|
||||
<span className="col">Administrator</span>
|
||||
<span className="col-auto">
|
||||
<Field name="isAdmin" type="checkbox">
|
||||
{({ field }: any) => (
|
||||
<label className="form-check form-check-single form-switch">
|
||||
<input
|
||||
{...field}
|
||||
id="isAdmin"
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</Field>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="row" htmlFor="isDisabled">
|
||||
<span className="col">Disabled</span>
|
||||
<span className="col-auto">
|
||||
<Field name="isDisabled" type="checkbox">
|
||||
{({ field }: any) => (
|
||||
<label className="form-check form-check-single form-switch">
|
||||
<input
|
||||
{...field}
|
||||
id="isDisabled"
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</Field>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
|
||||
{intl.formatMessage({ id: "cancel" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
actionType="primary"
|
||||
className="ms-auto"
|
||||
data-bs-dismiss="modal"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{intl.formatMessage({ id: "save" })}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
) : null}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
|
||||
{intl.formatMessage({ id: "cancel" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
actionType="primary"
|
||||
className="ms-auto"
|
||||
data-bs-dismiss="modal"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{intl.formatMessage({ id: "save" })}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
14
frontend/src/notifications/Msg.module.css
Normal file
14
frontend/src/notifications/Msg.module.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.toaster {
|
||||
padding: 0;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
|
||||
&.toast {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
padding: 16px 24px;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
36
frontend/src/notifications/Msg.tsx
Normal file
36
frontend/src/notifications/Msg.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IconCheck, IconExclamationCircle } from "@tabler/icons-react";
|
||||
import cn from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
function Msg({ data }: any) {
|
||||
const cns = cn("toast", "show", data.type || null);
|
||||
|
||||
let icon: ReactNode = null;
|
||||
switch (data.type) {
|
||||
case "success":
|
||||
icon = <IconCheck className="text-green mr-1" size={16} />;
|
||||
break;
|
||||
case "error":
|
||||
icon = <IconExclamationCircle className="text-red mr-1" size={16} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cns}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
data-bs-autohide="false"
|
||||
data-bs-toggle="toast"
|
||||
>
|
||||
{data.title && (
|
||||
<div className="toast-header">
|
||||
{icon} {data.title}
|
||||
</div>
|
||||
)}
|
||||
<div className="toast-body">{data.message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export { Msg };
|
27
frontend/src/notifications/helpers.tsx
Normal file
27
frontend/src/notifications/helpers.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { toast } from "react-toastify";
|
||||
import { intl } from "src/locale";
|
||||
import { Msg } from "./Msg";
|
||||
import styles from "./Msg.module.css";
|
||||
|
||||
const showSuccess = (message: string) => {
|
||||
toast(Msg, {
|
||||
className: styles.toaster,
|
||||
data: {
|
||||
type: "success",
|
||||
title: intl.formatMessage({ id: "notification.success" }),
|
||||
message,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showError = (message: string) => {
|
||||
toast(<Msg />, {
|
||||
data: {
|
||||
type: "error",
|
||||
title: intl.formatMessage({ id: "notification.error" }),
|
||||
message,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { showSuccess, showError };
|
1
frontend/src/notifications/index.ts
Normal file
1
frontend/src/notifications/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./helpers";
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
||||
|
||||
const Access = () => {
|
||||
return (
|
||||
<HasPermission permission="accessLists" type="view">
|
||||
<HasPermission permission="accessLists" type="view" pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import AuditTable from "./AuditTable";
|
||||
|
||||
const AuditLog = () => {
|
||||
return (
|
||||
<HasPermission permission="admin" type="manage">
|
||||
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||
<AuditTable />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import CertificateTable from "./CertificateTable";
|
||||
|
||||
const Certificates = () => {
|
||||
return (
|
||||
<HasPermission permission="certificates" type="view">
|
||||
<HasPermission permission="certificates" type="view" pageLoading loadingNoLogo>
|
||||
<CertificateTable />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
||||
|
||||
const DeadHosts = () => {
|
||||
return (
|
||||
<HasPermission permission="deadHosts" type="view">
|
||||
<HasPermission permission="deadHosts" type="view" pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
||||
|
||||
const ProxyHosts = () => {
|
||||
return (
|
||||
<HasPermission permission="proxyHosts" type="view">
|
||||
<HasPermission permission="proxyHosts" type="view" pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
||||
|
||||
const RedirectionHosts = () => {
|
||||
return (
|
||||
<HasPermission permission="redirectionHosts" type="view">
|
||||
<HasPermission permission="redirectionHosts" type="view" pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
||||
|
||||
const Streams = () => {
|
||||
return (
|
||||
<HasPermission permission="streams" type="view">
|
||||
<HasPermission permission="streams" type="view" pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import SettingTable from "./SettingTable";
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<HasPermission permission="admin" type="manage">
|
||||
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||
<SettingTable />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -4,15 +4,18 @@ import { intl } from "src/locale";
|
||||
|
||||
interface Props {
|
||||
tableInstance: ReactTable<any>;
|
||||
onNewUser?: () => void;
|
||||
}
|
||||
export default function Empty({ tableInstance }: Props) {
|
||||
export default function Empty({ tableInstance, onNewUser }: Props) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
|
||||
<div className="text-center my-4">
|
||||
<h2>{intl.formatMessage({ id: "proxy-hosts.empty" })}</h2>
|
||||
<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
|
||||
<Button className="btn-lime my-3">{intl.formatMessage({ id: "proxy-hosts.add" })}</Button>
|
||||
<Button className="btn-lime my-3" onClick={onNewUser}>
|
||||
{intl.formatMessage({ id: "proxy-hosts.add" })}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@@ -12,8 +12,9 @@ interface Props {
|
||||
isFetching?: boolean;
|
||||
currentUserId?: number;
|
||||
onEditUser?: (id: number) => void;
|
||||
onNewUser?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, currentUserId, onEditUser }: Props) {
|
||||
export default function Table({ data, isFetching, currentUserId, onEditUser, onNewUser }: Props) {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
@@ -124,5 +125,10 @@ export default function Table({ data, isFetching, currentUserId, onEditUser }: P
|
||||
enableSortingRemoval: false,
|
||||
});
|
||||
|
||||
return <TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} />} />;
|
||||
return (
|
||||
<TableLayout
|
||||
tableInstance={tableInstance}
|
||||
emptyState={<Empty tableInstance={tableInstance} onNewUser={onNewUser} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { UserModal } from "src/modals";
|
||||
import Table from "./Table";
|
||||
|
||||
export default function TableWrapper() {
|
||||
const [editUserId, setEditUserId] = useState(0);
|
||||
const [editUserId, setEditUserId] = useState(0 as number | "new");
|
||||
const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
|
||||
const { data: currentUser } = useUser("me");
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function TableWrapper() {
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" className="btn-orange">
|
||||
<Button size="sm" className="btn-orange" onClick={() => setEditUserId("new")}>
|
||||
{intl.formatMessage({ id: "users.add" })}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -54,6 +54,7 @@ export default function TableWrapper() {
|
||||
isFetching={isFetching}
|
||||
currentUserId={currentUser?.id}
|
||||
onEditUser={(id: number) => setEditUserId(id)}
|
||||
onNewUser={() => setEditUserId("new")}
|
||||
/>
|
||||
{editUserId ? <UserModal userId={editUserId} onClose={() => setEditUserId(0)} /> : null}
|
||||
</div>
|
||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
||||
|
||||
const Users = () => {
|
||||
return (
|
||||
<HasPermission permission="admin" type="manage">
|
||||
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
@@ -1064,6 +1064,11 @@ classnames@^2.3.2, classnames@^2.5.1:
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
convert-source-map@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||
@@ -1607,6 +1612,13 @@ react-router@7.8.2:
|
||||
cookie "^1.0.1"
|
||||
set-cookie-parser "^2.6.0"
|
||||
|
||||
react-toastify@^11.0.5:
|
||||
version "11.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.5.tgz#ce4c42d10eeb433988ab2264d3e445c4e9d13313"
|
||||
integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==
|
||||
dependencies:
|
||||
clsx "^2.1.1"
|
||||
|
||||
react-transition-group@^4.4.5:
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
||||
|
Reference in New Issue
Block a user