mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-14 02:42:34 +00:00
User table polishing, user delete modal
This commit is contained in:
@@ -13,7 +13,7 @@ import { global as logger } from "../logger.js";
|
||||
const ALGO = "RS256";
|
||||
|
||||
export default () => {
|
||||
let token_data = {};
|
||||
let tokenData = {};
|
||||
|
||||
const self = {
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ export default () => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
token_data = payload;
|
||||
tokenData = payload;
|
||||
resolve({
|
||||
token: token,
|
||||
payload: payload,
|
||||
@@ -72,18 +72,18 @@ export default () => {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
token_data = result;
|
||||
tokenData = result;
|
||||
|
||||
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
|
||||
// For 30 days at least, we need to replace 'all' with user.
|
||||
if (
|
||||
typeof token_data.scope !== "undefined" &&
|
||||
_.indexOf(token_data.scope, "all") !== -1
|
||||
typeof tokenData.scope !== "undefined" &&
|
||||
_.indexOf(tokenData.scope, "all") !== -1
|
||||
) {
|
||||
token_data.scope = ["user"];
|
||||
tokenData.scope = ["user"];
|
||||
}
|
||||
|
||||
resolve(token_data);
|
||||
resolve(tokenData);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -100,15 +100,15 @@ export default () => {
|
||||
* @param {String} scope
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasScope: (scope) => typeof token_data.scope !== "undefined" && _.indexOf(token_data.scope, scope) !== -1,
|
||||
hasScope: (scope) => typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, scope) !== -1,
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @return {*}
|
||||
*/
|
||||
get: (key) => {
|
||||
if (typeof token_data[key] !== "undefined") {
|
||||
return token_data[key];
|
||||
if (typeof tokenData[key] !== "undefined") {
|
||||
return tokenData[key];
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -119,7 +119,7 @@ export default () => {
|
||||
* @param {*} value
|
||||
*/
|
||||
set: (key, value) => {
|
||||
token_data[key] = value;
|
||||
tokenData[key] = value;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@@ -66,7 +66,9 @@ export function SiteHeader() {
|
||||
<div className="d-none d-xl-block ps-2">
|
||||
<div>{currentUser?.nickname}</div>
|
||||
<div className="mt-1 small text-secondary">
|
||||
{intl.formatMessage({ id: isAdmin ? "administrator" : "standard-user" })}
|
||||
{intl.formatMessage({
|
||||
id: isAdmin ? "role.admin" : "role.standard-user",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@@ -10,9 +10,9 @@ export function DomainsFormatter({ domains, createdOn }: Props) {
|
||||
<div className="flex-fill">
|
||||
<div className="font-weight-medium">
|
||||
{domains.map((domain: string) => (
|
||||
<span key={domain} className="badge badge-lg domain-name">
|
||||
<a key={domain} href={`http://${domain}`} className="badge bg-yellow-lt domain-name">
|
||||
{domain}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{createdOn ? (
|
||||
|
10
frontend/src/components/Table/Formatter/EmailFormatter.tsx
Normal file
10
frontend/src/components/Table/Formatter/EmailFormatter.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
}
|
||||
export function EmailFormatter({ email }: Props) {
|
||||
return (
|
||||
<a href={`mailto:${email}`} className="badge bg-yellow-lt">
|
||||
{email}
|
||||
</a>
|
||||
);
|
||||
}
|
20
frontend/src/components/Table/Formatter/RolesFormatter.tsx
Normal file
20
frontend/src/components/Table/Formatter/RolesFormatter.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { intl } from "src/locale";
|
||||
|
||||
interface Props {
|
||||
roles: string[];
|
||||
}
|
||||
export function RolesFormatter({ roles }: Props) {
|
||||
const r = roles || [];
|
||||
if (r.length === 0) {
|
||||
r[0] = "standard-user";
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{r.map((role: string) => (
|
||||
<span key={role} className="badge bg-yellow-lt me-1">
|
||||
{intl.formatMessage({ id: `role.${role}` })}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -4,16 +4,19 @@ import { intl } from "src/locale";
|
||||
interface Props {
|
||||
value: string;
|
||||
createdOn?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
export function ValueWithDateFormatter({ value, createdOn }: Props) {
|
||||
export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
|
||||
return (
|
||||
<div className="flex-fill">
|
||||
<div className="font-weight-medium">
|
||||
<div className="font-weight-medium">{value}</div>
|
||||
<div className={`font-weight-medium ${disabled ? "text-red" : ""}`}>{value}</div>
|
||||
</div>
|
||||
{createdOn ? (
|
||||
<div className="text-secondary mt-1">
|
||||
{intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })}
|
||||
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
|
||||
{disabled
|
||||
? intl.formatMessage({ id: "disabled" })
|
||||
: intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
@@ -1,5 +1,7 @@
|
||||
export * from "./CertificateFormatter";
|
||||
export * from "./DomainsFormatter";
|
||||
export * from "./EmailFormatter";
|
||||
export * from "./GravatarFormatter";
|
||||
export * from "./RolesFormatter";
|
||||
export * from "./StatusFormatter";
|
||||
export * from "./ValueWithDateFormatter";
|
||||
|
@@ -12,7 +12,6 @@
|
||||
"action.edit": "Edit",
|
||||
"action.enable": "Enable",
|
||||
"action.permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"auditlog.title": "Audit Log",
|
||||
"cancel": "Cancel",
|
||||
"certificates.title": "SSL Certificates",
|
||||
@@ -37,6 +36,7 @@
|
||||
"dead-hosts.count": "{count} 404 Hosts",
|
||||
"dead-hosts.empty": "There are no 404 Hosts",
|
||||
"dead-hosts.title": "404 Hosts",
|
||||
"disabled": "Disabled",
|
||||
"email-address": "Email address",
|
||||
"empty-subtitle": "Why don't you create one?",
|
||||
"error.invalid-auth": "Invalid email or password",
|
||||
@@ -53,6 +53,7 @@
|
||||
"notfound.title": "Oops… You just found an error page",
|
||||
"notification.error": "Error",
|
||||
"notification.success": "Success",
|
||||
"notification.user-deleted": "User has been deleted",
|
||||
"notification.user-saved": "User has been saved",
|
||||
"offline": "Offline",
|
||||
"online": "Online",
|
||||
@@ -67,10 +68,11 @@
|
||||
"redirection-hosts.count": "{count} Redirection Hosts",
|
||||
"redirection-hosts.empty": "There are no Redirection Hosts",
|
||||
"redirection-hosts.title": "Redirection Hosts",
|
||||
"role.admin": "Administrator",
|
||||
"role.standard-user": "Standard User",
|
||||
"save": "Save",
|
||||
"settings.title": "Settings",
|
||||
"sign-in": "Sign in",
|
||||
"standard-user": "Apache Helicopter",
|
||||
"streams.actions-title": "Stream #{id}",
|
||||
"streams.add": "Add Stream",
|
||||
"streams.count": "{count} Streams",
|
||||
@@ -81,8 +83,11 @@
|
||||
"user.change-password": "Change Password",
|
||||
"user.confirm-password": "Confirm Password",
|
||||
"user.current-password": "Current Password",
|
||||
"user.delete.content": "Are you sure you want to delete this user?",
|
||||
"user.delete.title": "Delete User",
|
||||
"user.edit": "Edit User",
|
||||
"user.edit-profile": "Edit Profile",
|
||||
"user.flags.title": "Properties",
|
||||
"user.full-name": "Full Name",
|
||||
"user.logout": "Logout",
|
||||
"user.new": "New User",
|
||||
|
@@ -38,9 +38,6 @@
|
||||
"action.permissions": {
|
||||
"defaultMessage": "Permissions"
|
||||
},
|
||||
"administrator": {
|
||||
"defaultMessage": "Administrator"
|
||||
},
|
||||
"auditlog.title": {
|
||||
"defaultMessage": "Audit Log"
|
||||
},
|
||||
@@ -113,6 +110,9 @@
|
||||
"dead-hosts.title": {
|
||||
"defaultMessage": "404 Hosts"
|
||||
},
|
||||
"disabled": {
|
||||
"defaultMessage": "Disabled"
|
||||
},
|
||||
"email-address": {
|
||||
"defaultMessage": "Email address"
|
||||
},
|
||||
@@ -158,6 +158,9 @@
|
||||
"notification.error": {
|
||||
"defaultMessage": "Error"
|
||||
},
|
||||
"notification.user-deleted": {
|
||||
"defaultMessage": "User has been deleted"
|
||||
},
|
||||
"notification.user-saved": {
|
||||
"defaultMessage": "User has been saved"
|
||||
},
|
||||
@@ -203,6 +206,12 @@
|
||||
"redirection-hosts.title": {
|
||||
"defaultMessage": "Redirection Hosts"
|
||||
},
|
||||
"role.admin": {
|
||||
"defaultMessage": "Administrator"
|
||||
},
|
||||
"role.standard-user": {
|
||||
"defaultMessage": "Standard User"
|
||||
},
|
||||
"save": {
|
||||
"defaultMessage": "Save"
|
||||
},
|
||||
@@ -212,9 +221,6 @@
|
||||
"sign-in": {
|
||||
"defaultMessage": "Sign in"
|
||||
},
|
||||
"standard-user": {
|
||||
"defaultMessage": "Apache Helicopter"
|
||||
},
|
||||
"streams.actions-title": {
|
||||
"defaultMessage": "Stream #{id}"
|
||||
},
|
||||
@@ -245,12 +251,21 @@
|
||||
"user.current-password": {
|
||||
"defaultMessage": "Current Password"
|
||||
},
|
||||
"user.delete.title": {
|
||||
"defaultMessage": "Delete User"
|
||||
},
|
||||
"user.delete.content": {
|
||||
"defaultMessage": "Are you sure you want to delete this user?"
|
||||
},
|
||||
"user.edit": {
|
||||
"defaultMessage": "Edit User"
|
||||
},
|
||||
"user.edit-profile": {
|
||||
"defaultMessage": "Edit Profile"
|
||||
},
|
||||
"user.flags.title": {
|
||||
"defaultMessage": "Properties"
|
||||
},
|
||||
"user.full-name": {
|
||||
"defaultMessage": "Full Name"
|
||||
},
|
||||
|
65
frontend/src/modals/DeleteConfirmModal.tsx
Normal file
65
frontend/src/modals/DeleteConfirmModal.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { Alert } from "react-bootstrap";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import { Button } from "src/components";
|
||||
import { intl } from "src/locale";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
onConfirm: () => Promise<void> | void;
|
||||
onClose: () => void;
|
||||
invalidations?: any[];
|
||||
}
|
||||
export function DeleteConfirmModal({ title, children, onConfirm, onClose, invalidations }: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const onSubmit = async () => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await onConfirm();
|
||||
onClose();
|
||||
// invalidate caches as requested
|
||||
invalidations?.forEach((inv) => {
|
||||
queryClient.invalidateQueries({ queryKey: inv });
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(intl.formatMessage({ id: err.message }));
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show onHide={onClose} animation={false}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Alert variant="danger" show={!!error} onClose={() => setError(null)} dismissible>
|
||||
{error}
|
||||
</Alert>
|
||||
{children}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button data-bs-dismiss="modal" onClick={onClose} disabled={submitting}>
|
||||
{intl.formatMessage({ id: "cancel" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
actionType="primary"
|
||||
className="ms-auto btn-red"
|
||||
data-bs-dismiss="modal"
|
||||
isLoading={submitting}
|
||||
disabled={submitting}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{intl.formatMessage({ id: "action.delete" })}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@@ -18,11 +18,6 @@ export function UserModal({ userId, onClose }: Props) {
|
||||
const { mutate: setUser } = useSetUser();
|
||||
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) => {
|
||||
setErrorMsg(null);
|
||||
const { ...payload } = {
|
||||
@@ -161,12 +156,13 @@ export function UserModal({ userId, onClose }: Props) {
|
||||
</div>
|
||||
{currentUser && data && currentUser?.id !== data?.id ? (
|
||||
<div className="my-3">
|
||||
<h3 className="py-2">Properties</h3>
|
||||
|
||||
<h3 className="py-2">{intl.formatMessage({ id: "user.flags.title" })}</h3>
|
||||
<div className="divide-y">
|
||||
<div>
|
||||
<label className="row" htmlFor="isAdmin">
|
||||
<span className="col">Administrator</span>
|
||||
<span className="col">
|
||||
{intl.formatMessage({ id: "role.admin" })}
|
||||
</span>
|
||||
<span className="col-auto">
|
||||
<Field name="isAdmin" type="checkbox">
|
||||
{({ field }: any) => (
|
||||
@@ -185,7 +181,9 @@ export function UserModal({ userId, onClose }: Props) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="row" htmlFor="isDisabled">
|
||||
<span className="col">Disabled</span>
|
||||
<span className="col">
|
||||
{intl.formatMessage({ id: "disabled" })}
|
||||
</span>
|
||||
<span className="col-auto">
|
||||
<Field name="isDisabled" type="checkbox">
|
||||
{({ field }: any) => (
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export * from "./ChangePasswordModal";
|
||||
export * from "./DeleteConfirmModal";
|
||||
export * from "./UserModal";
|
||||
|
@@ -2,7 +2,7 @@ import { IconDotsVertical, IconEdit, IconLock, IconShield, IconTrash } from "@ta
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { User } from "src/api/backend";
|
||||
import { GravatarFormatter, ValueWithDateFormatter } from "src/components";
|
||||
import { EmailFormatter, GravatarFormatter, RolesFormatter, ValueWithDateFormatter } from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl } from "src/locale";
|
||||
import Empty from "./Empty";
|
||||
@@ -12,9 +12,10 @@ interface Props {
|
||||
isFetching?: boolean;
|
||||
currentUserId?: number;
|
||||
onEditUser?: (id: number) => void;
|
||||
onDeleteUser?: (id: number) => void;
|
||||
onNewUser?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, currentUserId, onEditUser, onNewUser }: Props) {
|
||||
export default function Table({ data, isFetching, currentUserId, onEditUser, onDeleteUser, onNewUser }: Props) {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
@@ -34,14 +35,20 @@ export default function Table({ data, isFetching, currentUserId, onEditUser, onN
|
||||
cell: (info: any) => {
|
||||
const value = info.getValue();
|
||||
// Hack to reuse domains formatter
|
||||
return <ValueWithDateFormatter value={value.name} createdOn={value.createdOn} />;
|
||||
return (
|
||||
<ValueWithDateFormatter
|
||||
value={value.name}
|
||||
createdOn={value.createdOn}
|
||||
disabled={value.isDisabled}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor((row: any) => row.email, {
|
||||
id: "email",
|
||||
header: intl.formatMessage({ id: "column.email" }),
|
||||
cell: (info: any) => {
|
||||
return info.getValue();
|
||||
return <EmailFormatter email={info.getValue()} />;
|
||||
},
|
||||
}),
|
||||
// TODO: formatter for roles
|
||||
@@ -49,7 +56,7 @@ export default function Table({ data, isFetching, currentUserId, onEditUser, onN
|
||||
id: "roles",
|
||||
header: intl.formatMessage({ id: "column.roles" }),
|
||||
cell: (info: any) => {
|
||||
return JSON.stringify(info.getValue());
|
||||
return <RolesFormatter roles={info.getValue()} />;
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
@@ -96,7 +103,14 @@ export default function Table({ data, isFetching, currentUserId, onEditUser, onN
|
||||
{currentUserId !== info.row.original.id ? (
|
||||
<>
|
||||
<div className="dropdown-divider" />
|
||||
<a className="dropdown-item" href="#">
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDeleteUser?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
{intl.formatMessage({ id: "action.delete" })}
|
||||
</a>
|
||||
@@ -111,7 +125,7 @@ export default function Table({ data, isFetching, currentUserId, onEditUser, onN
|
||||
},
|
||||
}),
|
||||
],
|
||||
[columnHelper, currentUserId, onEditUser],
|
||||
[columnHelper, currentUserId, onEditUser, onDeleteUser],
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<User>({
|
||||
|
@@ -1,14 +1,17 @@
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteUser } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { useUser, useUsers } from "src/hooks";
|
||||
import { intl } from "src/locale";
|
||||
import { UserModal } from "src/modals";
|
||||
import { DeleteConfirmModal, UserModal } from "src/modals";
|
||||
import { showSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
export default function TableWrapper() {
|
||||
const [editUserId, setEditUserId] = useState(0 as number | "new");
|
||||
const [deleteUserId, setDeleteUserId] = useState(0);
|
||||
const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
|
||||
const { data: currentUser } = useUser("me");
|
||||
|
||||
@@ -20,6 +23,11 @@ export default function TableWrapper() {
|
||||
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteUser(deleteUserId);
|
||||
showSuccess(intl.formatMessage({ id: "notification.user-deleted" }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card mt-4">
|
||||
<div className="card-status-top bg-orange" />
|
||||
@@ -54,9 +62,20 @@ export default function TableWrapper() {
|
||||
isFetching={isFetching}
|
||||
currentUserId={currentUser?.id}
|
||||
onEditUser={(id: number) => setEditUserId(id)}
|
||||
onDeleteUser={(id: number) => setDeleteUserId(id)}
|
||||
onNewUser={() => setEditUserId("new")}
|
||||
/>
|
||||
{editUserId ? <UserModal userId={editUserId} onClose={() => setEditUserId(0)} /> : null}
|
||||
{deleteUserId ? (
|
||||
<DeleteConfirmModal
|
||||
title={intl.formatMessage({ id: "user.delete.title" })}
|
||||
onConfirm={handleDelete}
|
||||
onClose={() => setDeleteUserId(0)}
|
||||
invalidations={[["users"], ["user", deleteUserId]]}
|
||||
>
|
||||
{intl.formatMessage({ id: "user.delete.content" })}
|
||||
</DeleteConfirmModal>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
Reference in New Issue
Block a user