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
|
* @param {Object} user
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getTokenFromUser: (user) => {
|
getTokenFromUser: async (user) => {
|
||||||
const expire = "1d";
|
const expire = "1d";
|
||||||
const Token = new TokenModel();
|
const Token = new TokenModel();
|
||||||
const expiry = parseDatePeriod(expire);
|
const expiry = parseDatePeriod(expire);
|
||||||
|
|
||||||
return Token.create({
|
const signed = await Token.create({
|
||||||
iss: "api",
|
iss: "api",
|
||||||
attrs: {
|
attrs: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
scope: ["user"],
|
scope: ["user"],
|
||||||
expiresIn: expire,
|
expiresIn: expire,
|
||||||
}).then((signed) => {
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: signed.token,
|
token: signed.token,
|
||||||
expires: expiry.toISOString(),
|
expires: expiry.toISOString(),
|
||||||
user: user,
|
user: user,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -337,11 +337,11 @@ const internalUser = {
|
|||||||
* @param {Integer} [id_requested]
|
* @param {Integer} [id_requested]
|
||||||
* @returns {[String]}
|
* @returns {[String]}
|
||||||
*/
|
*/
|
||||||
getUserOmisionsByAccess: (access, id_requested) => {
|
getUserOmisionsByAccess: (access, idRequested) => {
|
||||||
let response = []; // Admin response
|
let response = []; // Admin response
|
||||||
|
|
||||||
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== id_requested) {
|
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
|
||||||
response = ["roles", "is_deleted"]; // Restricted response
|
response = ["is_deleted"]; // Restricted response
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
@@ -123,16 +123,16 @@ export default () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param [default_value]
|
* @param [defaultValue]
|
||||||
* @returns {Integer}
|
* @returns {Integer}
|
||||||
*/
|
*/
|
||||||
getUserId: (default_value) => {
|
getUserId: (defaultValue) => {
|
||||||
const attrs = self.get("attrs");
|
const attrs = self.get("attrs");
|
||||||
if (attrs && typeof attrs.id !== "undefined" && attrs.id) {
|
if (attrs && typeof attrs.id !== "undefined" && attrs.id) {
|
||||||
return attrs.id;
|
return attrs.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return default_value || 0;
|
return defaultValue || 0;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -30,6 +30,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-intl": "^7.1.11",
|
"react-intl": "^7.1.11",
|
||||||
"react-router-dom": "^7.8.2",
|
"react-router-dom": "^7.8.2",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
"rooks": "^9.2.0"
|
"rooks": "^9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -5,3 +5,10 @@
|
|||||||
.domain-name {
|
.domain-name {
|
||||||
font-family: monospace;
|
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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { RawIntlProvider } from "react-intl";
|
import { RawIntlProvider } from "react-intl";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
|
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
|
||||||
import { intl } from "src/locale";
|
import { intl } from "src/locale";
|
||||||
import Router from "src/Router.tsx";
|
import Router from "src/Router.tsx";
|
||||||
@@ -16,6 +17,15 @@ function App() {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Router />
|
<Router />
|
||||||
|
<ToastContainer
|
||||||
|
position="top-right"
|
||||||
|
autoClose={5000}
|
||||||
|
hideProgressBar={true}
|
||||||
|
newestOnTop={true}
|
||||||
|
closeOnClick={true}
|
||||||
|
rtl={false}
|
||||||
|
closeButton={false}
|
||||||
|
/>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
<ReactQueryDevtools buttonPosition="bottom-right" position="right" />
|
<ReactQueryDevtools buttonPosition="bottom-right" position="right" />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
@@ -4,6 +4,7 @@ export * from "./createDeadHost";
|
|||||||
export * from "./createProxyHost";
|
export * from "./createProxyHost";
|
||||||
export * from "./createRedirectionHost";
|
export * from "./createRedirectionHost";
|
||||||
export * from "./createStream";
|
export * from "./createStream";
|
||||||
|
export * from "./createUser";
|
||||||
export * from "./deleteAccessList";
|
export * from "./deleteAccessList";
|
||||||
export * from "./deleteCertificate";
|
export * from "./deleteCertificate";
|
||||||
export * from "./deleteDeadHost";
|
export * from "./deleteDeadHost";
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import Alert from "react-bootstrap/Alert";
|
import Alert from "react-bootstrap/Alert";
|
||||||
|
import { Loading, LoadingPage } from "src/components";
|
||||||
import { useUser } from "src/hooks";
|
import { useUser } from "src/hooks";
|
||||||
import { intl } from "src/locale";
|
import { intl } from "src/locale";
|
||||||
|
|
||||||
@@ -8,11 +9,30 @@ interface Props {
|
|||||||
type: "manage" | "view";
|
type: "manage" | "view";
|
||||||
hideError?: boolean;
|
hideError?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
pageLoading?: boolean;
|
||||||
|
loadingNoLogo?: boolean;
|
||||||
}
|
}
|
||||||
function HasPermission({ permission, type, children, hideError = false }: Props) {
|
function HasPermission({
|
||||||
const { data } = useUser("me");
|
permission,
|
||||||
|
type,
|
||||||
|
children,
|
||||||
|
hideError = false,
|
||||||
|
pageLoading = false,
|
||||||
|
loadingNoLogo = false,
|
||||||
|
}: Props) {
|
||||||
|
const { data, isLoading } = useUser("me");
|
||||||
const perms = data?.permissions;
|
const perms = data?.permissions;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
if (hideError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (pageLoading) {
|
||||||
|
return <LoadingPage noLogo={loadingNoLogo} />;
|
||||||
|
}
|
||||||
|
return <Loading noLogo={loadingNoLogo} />;
|
||||||
|
}
|
||||||
|
|
||||||
let allowed = permission === "";
|
let allowed = permission === "";
|
||||||
const acceptable = ["manage", type];
|
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 { Loading, Page } from "src/components";
|
||||||
import { intl } from "src/locale";
|
|
||||||
import styles from "./LoadingPage.module.css";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -10,17 +8,7 @@ export function LoadingPage({ label, noLogo }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Page className="page-center">
|
<Page className="page-center">
|
||||||
<div className="container-tight py-4">
|
<div className="container-tight py-4">
|
||||||
<div className="empty text-center">
|
<Loading label={label} noLogo={noLogo} />
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
@@ -2,6 +2,7 @@ export * from "./Button";
|
|||||||
export * from "./ErrorNotFound";
|
export * from "./ErrorNotFound";
|
||||||
export * from "./Flag";
|
export * from "./Flag";
|
||||||
export * from "./HasPermission";
|
export * from "./HasPermission";
|
||||||
|
export * from "./Loading";
|
||||||
export * from "./LoadingPage";
|
export * from "./LoadingPage";
|
||||||
export * from "./LocalePicker";
|
export * from "./LocalePicker";
|
||||||
export * from "./NavLink";
|
export * from "./NavLink";
|
||||||
|
@@ -1,7 +1,20 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
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) => {
|
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" });
|
return getUser(id, { expand: "permissions" });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,8 +30,11 @@ const useUser = (id: string | number, options = {}) => {
|
|||||||
const useSetUser = () => {
|
const useSetUser = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (values: User) => updateUser(values),
|
mutationFn: (values: User) => (values.id ? updateUser(values) : createUser(values)),
|
||||||
onMutate: (values: User) => {
|
onMutate: (values: User) => {
|
||||||
|
if (!values.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const previousObject = queryClient.getQueryData(["user", values.id]);
|
const previousObject = queryClient.getQueryData(["user", values.id]);
|
||||||
queryClient.setQueryData(["user", values.id], (old: User) => ({
|
queryClient.setQueryData(["user", values.id], (old: User) => ({
|
||||||
...old,
|
...old,
|
||||||
|
@@ -51,6 +51,9 @@
|
|||||||
"notfound.action": "Take me home",
|
"notfound.action": "Take me home",
|
||||||
"notfound.text": "We are sorry but the page you are looking for was not found",
|
"notfound.text": "We are sorry but the page you are looking for was not found",
|
||||||
"notfound.title": "Oops… You just found an error page",
|
"notfound.title": "Oops… You just found an error page",
|
||||||
|
"notification.error": "Error",
|
||||||
|
"notification.success": "Success",
|
||||||
|
"notification.user-saved": "User has been saved",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
@@ -82,6 +85,7 @@
|
|||||||
"user.edit-profile": "Edit Profile",
|
"user.edit-profile": "Edit Profile",
|
||||||
"user.full-name": "Full Name",
|
"user.full-name": "Full Name",
|
||||||
"user.logout": "Logout",
|
"user.logout": "Logout",
|
||||||
|
"user.new": "New User",
|
||||||
"user.new-password": "New Password",
|
"user.new-password": "New Password",
|
||||||
"user.nickname": "Nickname",
|
"user.nickname": "Nickname",
|
||||||
"user.switch-dark": "Switch to Dark mode",
|
"user.switch-dark": "Switch to Dark mode",
|
||||||
|
@@ -155,6 +155,15 @@
|
|||||||
"notfound.title": {
|
"notfound.title": {
|
||||||
"defaultMessage": "Oops… You just found an error page"
|
"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": {
|
"offline": {
|
||||||
"defaultMessage": "Offline"
|
"defaultMessage": "Offline"
|
||||||
},
|
},
|
||||||
@@ -248,6 +257,9 @@
|
|||||||
"user.logout": {
|
"user.logout": {
|
||||||
"defaultMessage": "Logout"
|
"defaultMessage": "Logout"
|
||||||
},
|
},
|
||||||
|
"user.new": {
|
||||||
|
"defaultMessage": "New User"
|
||||||
|
},
|
||||||
"user.new-password": {
|
"user.new-password": {
|
||||||
"defaultMessage": "New Password"
|
"defaultMessage": "New Password"
|
||||||
},
|
},
|
||||||
|
@@ -2,31 +2,35 @@ import { Field, Form, Formik } from "formik";
|
|||||||
import { useState } from "react";
|
import { 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, Loading } from "src/components";
|
||||||
import { useSetUser, useUser } from "src/hooks";
|
import { useSetUser, useUser } from "src/hooks";
|
||||||
import { intl } from "src/locale";
|
import { intl } from "src/locale";
|
||||||
import { validateEmail, validateString } from "src/modules/Validations";
|
import { validateEmail, validateString } from "src/modules/Validations";
|
||||||
|
import { showSuccess } from "src/notifications";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userId: number | "me";
|
userId: number | "me" | "new";
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
export function UserModal({ userId, onClose }: Props) {
|
export function UserModal({ userId, onClose }: Props) {
|
||||||
const { data } = useUser(userId);
|
const { data, isLoading, error } = useUser(userId);
|
||||||
const { data: currentUser } = useUser("me");
|
const { data: currentUser, isLoading: currentIsLoading } = useUser("me");
|
||||||
const { mutate: setUser } = useSetUser();
|
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) => {
|
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||||
setError(null);
|
setErrorMsg(null);
|
||||||
const { ...payload } = {
|
const { ...payload } = {
|
||||||
id: userId,
|
id: userId === "new" ? undefined : userId,
|
||||||
roles: [],
|
roles: [],
|
||||||
...values,
|
...values,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("values", values);
|
|
||||||
|
|
||||||
if (data?.id === currentUser?.id) {
|
if (data?.id === currentUser?.id) {
|
||||||
// Prevent user from locking themselves out
|
// Prevent user from locking themselves out
|
||||||
delete payload.isDisabled;
|
delete payload.isDisabled;
|
||||||
@@ -39,21 +43,27 @@ export function UserModal({ userId, onClose }: Props) {
|
|||||||
delete payload.isAdmin;
|
delete payload.isAdmin;
|
||||||
|
|
||||||
setUser(payload, {
|
setUser(payload, {
|
||||||
onError: (err: any) => setError(err.message),
|
onError: (err: any) => setErrorMsg(err.message),
|
||||||
onSuccess: () => onClose(),
|
onSuccess: () => {
|
||||||
|
showSuccess(intl.formatMessage({ id: "notification.user-saved" }));
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
onSettled: () => setSubmitting(false),
|
onSettled: () => setSubmitting(false),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show onHide={onClose} animation={false}>
|
<Modal show onHide={onClose} animation={false}>
|
||||||
|
{!isLoading && error && <Alert variant="danger">{error?.message || "Unknown error"}</Alert>}
|
||||||
|
{(isLoading || currentIsLoading) && <Loading noLogo />}
|
||||||
|
{!isLoading && !currentIsLoading && data && currentUser && (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={
|
||||||
{
|
{
|
||||||
name: data?.name,
|
name: data?.name,
|
||||||
nickname: data?.nickname,
|
nickname: data?.nickname,
|
||||||
email: data?.email,
|
email: data?.email,
|
||||||
isAdmin: data?.roles.includes("admin"),
|
isAdmin: data?.roles?.includes("admin"),
|
||||||
isDisabled: data?.isDisabled,
|
isDisabled: data?.isDisabled,
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
@@ -62,11 +72,13 @@ export function UserModal({ userId, onClose }: Props) {
|
|||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>{intl.formatMessage({ id: "user.edit" })}</Modal.Title>
|
<Modal.Title>
|
||||||
|
{intl.formatMessage({ id: data?.id ? "user.edit" : "user.new" })}
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Alert variant="danger" show={!!error} onClose={() => setError(null)} dismissible>
|
<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
|
||||||
{error}
|
{errorMsg}
|
||||||
</Alert>
|
</Alert>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
@@ -133,10 +145,14 @@ export function UserModal({ userId, onClose }: Props) {
|
|||||||
placeholder={intl.formatMessage({ id: "email-address" })}
|
placeholder={intl.formatMessage({ id: "email-address" })}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="email">{intl.formatMessage({ id: "email-address" })}</label>
|
<label htmlFor="email">
|
||||||
|
{intl.formatMessage({ id: "email-address" })}
|
||||||
|
</label>
|
||||||
{form.errors.email ? (
|
{form.errors.email ? (
|
||||||
<div className="invalid-feedback">
|
<div className="invalid-feedback">
|
||||||
{form.errors.email && form.touched.email ? form.errors.email : null}
|
{form.errors.email && form.touched.email
|
||||||
|
? form.errors.email
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -208,6 +224,7 @@ export function UserModal({ userId, onClose }: Props) {
|
|||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
)}
|
||||||
</Modal>
|
</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 = () => {
|
const Access = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="accessLists" type="view">
|
<HasPermission permission="accessLists" type="view" pageLoading loadingNoLogo>
|
||||||
<TableWrapper />
|
<TableWrapper />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import AuditTable from "./AuditTable";
|
|||||||
|
|
||||||
const AuditLog = () => {
|
const AuditLog = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="admin" type="manage">
|
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||||
<AuditTable />
|
<AuditTable />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import CertificateTable from "./CertificateTable";
|
|||||||
|
|
||||||
const Certificates = () => {
|
const Certificates = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="certificates" type="view">
|
<HasPermission permission="certificates" type="view" pageLoading loadingNoLogo>
|
||||||
<CertificateTable />
|
<CertificateTable />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
|||||||
|
|
||||||
const DeadHosts = () => {
|
const DeadHosts = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="deadHosts" type="view">
|
<HasPermission permission="deadHosts" type="view" pageLoading loadingNoLogo>
|
||||||
<TableWrapper />
|
<TableWrapper />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
|||||||
|
|
||||||
const ProxyHosts = () => {
|
const ProxyHosts = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="proxyHosts" type="view">
|
<HasPermission permission="proxyHosts" type="view" pageLoading loadingNoLogo>
|
||||||
<TableWrapper />
|
<TableWrapper />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
|||||||
|
|
||||||
const RedirectionHosts = () => {
|
const RedirectionHosts = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="redirectionHosts" type="view">
|
<HasPermission permission="redirectionHosts" type="view" pageLoading loadingNoLogo>
|
||||||
<TableWrapper />
|
<TableWrapper />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
|||||||
|
|
||||||
const Streams = () => {
|
const Streams = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="streams" type="view">
|
<HasPermission permission="streams" type="view" pageLoading loadingNoLogo>
|
||||||
<TableWrapper />
|
<TableWrapper />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import SettingTable from "./SettingTable";
|
|||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="admin" type="manage">
|
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||||
<SettingTable />
|
<SettingTable />
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
);
|
);
|
||||||
|
@@ -4,15 +4,18 @@ import { intl } from "src/locale";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tableInstance: ReactTable<any>;
|
tableInstance: ReactTable<any>;
|
||||||
|
onNewUser?: () => void;
|
||||||
}
|
}
|
||||||
export default function Empty({ tableInstance }: Props) {
|
export default function Empty({ tableInstance, onNewUser }: Props) {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
|
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
|
||||||
<div className="text-center my-4">
|
<div className="text-center my-4">
|
||||||
<h2>{intl.formatMessage({ id: "proxy-hosts.empty" })}</h2>
|
<h2>{intl.formatMessage({ id: "proxy-hosts.empty" })}</h2>
|
||||||
<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@@ -12,8 +12,9 @@ interface Props {
|
|||||||
isFetching?: boolean;
|
isFetching?: boolean;
|
||||||
currentUserId?: number;
|
currentUserId?: number;
|
||||||
onEditUser?: (id: number) => void;
|
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 columnHelper = createColumnHelper<User>();
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -124,5 +125,10 @@ export default function Table({ data, isFetching, currentUserId, onEditUser }: P
|
|||||||
enableSortingRemoval: false,
|
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";
|
import Table from "./Table";
|
||||||
|
|
||||||
export default function TableWrapper() {
|
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 { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
|
||||||
const { data: currentUser } = useUser("me");
|
const { data: currentUser } = useUser("me");
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export default function TableWrapper() {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" className="btn-orange">
|
<Button size="sm" className="btn-orange" onClick={() => setEditUserId("new")}>
|
||||||
{intl.formatMessage({ id: "users.add" })}
|
{intl.formatMessage({ id: "users.add" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,6 +54,7 @@ export default function TableWrapper() {
|
|||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
currentUserId={currentUser?.id}
|
currentUserId={currentUser?.id}
|
||||||
onEditUser={(id: number) => setEditUserId(id)}
|
onEditUser={(id: number) => setEditUserId(id)}
|
||||||
|
onNewUser={() => setEditUserId("new")}
|
||||||
/>
|
/>
|
||||||
{editUserId ? <UserModal userId={editUserId} onClose={() => setEditUserId(0)} /> : null}
|
{editUserId ? <UserModal userId={editUserId} onClose={() => setEditUserId(0)} /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper";
|
|||||||
|
|
||||||
const Users = () => {
|
const Users = () => {
|
||||||
return (
|
return (
|
||||||
<HasPermission permission="admin" type="manage">
|
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||||
<TableWrapper />
|
<TableWrapper />
|
||||||
</HasPermission>
|
</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"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
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:
|
convert-source-map@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
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"
|
cookie "^1.0.1"
|
||||||
set-cookie-parser "^2.6.0"
|
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:
|
react-transition-group@^4.4.5:
|
||||||
version "4.4.5"
|
version "4.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
||||||
|
Reference in New Issue
Block a user