Updated menu component to use mds (#2866)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
@@ -33,7 +33,6 @@ import { useSelector } from "react-redux";
|
||||
import { AppState, useAppDispatch } from "../../store";
|
||||
import { snackBarCommon } from "./Common/FormComponents/common/styleLibrary";
|
||||
import { ErrorResponseHandler } from "../../common/types";
|
||||
import Menu from "./Menu/Menu";
|
||||
import api from "../../common/api";
|
||||
import MainError from "./Common/MainError/MainError";
|
||||
import {
|
||||
@@ -55,6 +54,7 @@ import {
|
||||
setSnackBarMessage,
|
||||
} from "../../systemSlice";
|
||||
import { selFeatures, selSession } from "./consoleSlice";
|
||||
import MenuWrapper from "./Menu/MenuWrapper";
|
||||
|
||||
const Trace = React.lazy(() => import("./Trace/Trace"));
|
||||
const Heal = React.lazy(() => import("./Heal/Heal"));
|
||||
@@ -481,7 +481,7 @@ const Console = ({ classes }: IConsoleProps) => {
|
||||
<Fragment>
|
||||
{session && session.status === "ok" ? (
|
||||
<MainContainer
|
||||
menu={!hideMenu ? <Menu /> : <Fragment />}
|
||||
menu={!hideMenu ? <MenuWrapper /> : <Fragment />}
|
||||
mobileModeAuto={false}
|
||||
>
|
||||
<Fragment>
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import { LogoutIcon } from "mds";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import List from "@mui/material/List";
|
||||
import {
|
||||
LogoutItemIconStyle,
|
||||
menuItemContainerStyles,
|
||||
menuItemMiniStyles,
|
||||
menuItemTextStyles,
|
||||
} from "./MenuStyleUtils";
|
||||
import MenuItem from "./MenuItem";
|
||||
|
||||
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
|
||||
import MenuSectionHeader from "./MenuSectionHeader";
|
||||
|
||||
const ConsoleMenuList = ({
|
||||
menuItems,
|
||||
isOpen,
|
||||
displayHeaders = false,
|
||||
}: {
|
||||
menuItems: any[];
|
||||
isOpen: boolean;
|
||||
displayHeaders?: boolean;
|
||||
}) => {
|
||||
const stateClsName = isOpen ? "wide" : "mini";
|
||||
const { pathname = "" } = useLocation();
|
||||
let groupToSelect = pathname.slice(1, pathname.length); //single path
|
||||
if (groupToSelect.indexOf("/") !== -1) {
|
||||
groupToSelect = groupToSelect.slice(0, groupToSelect.indexOf("/")); //nested path
|
||||
}
|
||||
|
||||
const [expandGroup, setExpandGroup] = useState(IAM_PAGES.BUCKETS);
|
||||
const [selectedMenuItem, setSelectedMenuItem] =
|
||||
useState<string>(groupToSelect);
|
||||
|
||||
const [previewMenuGroup, setPreviewMenuGroup] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
setExpandGroup(groupToSelect);
|
||||
setSelectedMenuItem(groupToSelect);
|
||||
}, [groupToSelect]);
|
||||
|
||||
let basename = document.baseURI.replace(window.location.origin, "");
|
||||
let header = "";
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={`${stateClsName} wrapper`}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
flex: 1,
|
||||
paddingRight: "8px",
|
||||
|
||||
"&.wide": {
|
||||
marginLeft: "30px",
|
||||
},
|
||||
|
||||
"&.mini": {
|
||||
marginLeft: "10px",
|
||||
"& .menuHeader": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<List
|
||||
sx={{
|
||||
flex: 1,
|
||||
paddingTop: 0,
|
||||
|
||||
"&.mini": {
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexFlow: "column",
|
||||
|
||||
"& .main-menu-item": {
|
||||
marginBottom: "20px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
className={`${stateClsName} group-wrapper main-list`}
|
||||
>
|
||||
<React.Fragment>
|
||||
{(menuItems || []).map((menuGroup: any, index) => {
|
||||
if (menuGroup) {
|
||||
let grHeader = null;
|
||||
|
||||
if (menuGroup.group !== header && displayHeaders) {
|
||||
grHeader = <MenuSectionHeader label={menuGroup.group} />;
|
||||
header = menuGroup.group;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={`${menuGroup.id}-${index.toString()}`}>
|
||||
{grHeader}
|
||||
<MenuItem
|
||||
stateClsName={stateClsName}
|
||||
page={menuGroup}
|
||||
id={menuGroup.id}
|
||||
selectedMenuItem={selectedMenuItem}
|
||||
setSelectedMenuItem={setSelectedMenuItem}
|
||||
pathValue={pathname}
|
||||
onExpand={setExpandGroup}
|
||||
expandedGroup={expandGroup}
|
||||
previewMenuGroup={previewMenuGroup}
|
||||
setPreviewMenuGroup={setPreviewMenuGroup}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</React.Fragment>
|
||||
</List>
|
||||
{/* List of Bottom anchored menus */}
|
||||
<List
|
||||
sx={{
|
||||
paddingTop: 0,
|
||||
"&.mini": {
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexFlow: "column",
|
||||
},
|
||||
}}
|
||||
className={`${stateClsName} group-wrapper bottom-list`}
|
||||
>
|
||||
<ListItem
|
||||
button
|
||||
component="a"
|
||||
href={`${window.location.origin}${basename}logout`}
|
||||
disableRipple
|
||||
sx={{
|
||||
...menuItemContainerStyles,
|
||||
...menuItemMiniStyles,
|
||||
marginBottom: "3px",
|
||||
}}
|
||||
className={`$ ${stateClsName} bottom-menu-item`}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
...LogoutItemIconStyle,
|
||||
}}
|
||||
>
|
||||
<LogoutIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Sign Out"
|
||||
id={"logout"}
|
||||
sx={{ ...menuItemTextStyles }}
|
||||
className={stateClsName}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default ConsoleMenuList;
|
||||
@@ -1,65 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "../../../store";
|
||||
import { Box } from "@mui/material";
|
||||
import { CircleIcon } from "mds";
|
||||
import { getLicenseConsent } from "../License/utils";
|
||||
import { registeredCluster } from "../../../config";
|
||||
|
||||
const LicenseBadge = () => {
|
||||
const licenseInfo = useSelector(
|
||||
(state: AppState) => state?.system?.licenseInfo
|
||||
);
|
||||
|
||||
const isAgplAckDone = getLicenseConsent();
|
||||
const clusterRegistered = registeredCluster();
|
||||
|
||||
const { plan = "" } = licenseInfo || {};
|
||||
|
||||
if (plan || isAgplAckDone || clusterRegistered) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 1,
|
||||
transform: "translateX(5px)",
|
||||
zIndex: 400,
|
||||
border: 0,
|
||||
}}
|
||||
style={{
|
||||
border: 0,
|
||||
}}
|
||||
>
|
||||
<CircleIcon
|
||||
style={{
|
||||
fill: "#FF3958",
|
||||
border: "1px solid #FF3958",
|
||||
borderRadius: "100%",
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LicenseBadge;
|
||||
@@ -1,126 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Drawer } from "@mui/material";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import clsx from "clsx";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
|
||||
import MenuToggle from "./MenuToggle";
|
||||
import ConsoleMenuList from "./ConsoleMenuList";
|
||||
import { validRoutes } from "../valid-routes";
|
||||
import { menuOpen } from "../../../systemSlice";
|
||||
import { selFeatures } from "../consoleSlice";
|
||||
|
||||
const drawerWidth = 250;
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: "nowrap",
|
||||
background:
|
||||
"transparent linear-gradient(90deg, #073052 0%, #081C42 100%) 0% 0% no-repeat padding-box !important",
|
||||
boxShadow: "0px 3px 7px #00000014",
|
||||
"& .MuiPaper-root": {
|
||||
backgroundColor: "inherit",
|
||||
},
|
||||
"& ::-webkit-scrollbar": {
|
||||
width: "5px",
|
||||
},
|
||||
"& ::-webkit-scrollbar-track": {
|
||||
background: "#F0F0F0",
|
||||
borderRadius: 0,
|
||||
boxShadow: "inset 0px 0px 0px 0px #F0F0F0",
|
||||
},
|
||||
"& ::-webkit-scrollbar-thumb": {
|
||||
background: "#5A6375",
|
||||
borderRadius: 0,
|
||||
},
|
||||
"& ::-webkit-scrollbar-thumb:hover": {
|
||||
background: "#081C42",
|
||||
},
|
||||
},
|
||||
drawerOpen: {
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerClose: {
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: "hidden",
|
||||
[theme.breakpoints.up("xs")]: {
|
||||
width: 75,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface IMenuProps {
|
||||
classes?: any;
|
||||
}
|
||||
|
||||
const Menu = ({ classes }: IMenuProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useSelector(selFeatures);
|
||||
|
||||
const sidebarOpen = useSelector(
|
||||
(state: AppState) => state.system.sidebarOpen
|
||||
);
|
||||
|
||||
const allowedMenuItems = validRoutes(features);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
id="app-menu"
|
||||
variant="permanent"
|
||||
className={clsx(classes.drawer, {
|
||||
[classes.drawerOpen]: sidebarOpen,
|
||||
[classes.drawerClose]: !sidebarOpen,
|
||||
})}
|
||||
classes={{
|
||||
paper: clsx({
|
||||
[classes.drawerOpen]: sidebarOpen,
|
||||
[classes.drawerClose]: !sidebarOpen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<MenuToggle
|
||||
onToggle={(nextState) => {
|
||||
dispatch(menuOpen(nextState));
|
||||
}}
|
||||
isOpen={sidebarOpen}
|
||||
/>
|
||||
|
||||
<ConsoleMenuList
|
||||
menuItems={allowedMenuItems}
|
||||
isOpen={sidebarOpen}
|
||||
displayHeaders
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Menu);
|
||||
74
portal-ui/src/screens/Console/Menu/MenuWrapper.tsx
Normal file
74
portal-ui/src/screens/Console/Menu/MenuWrapper.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Menu } from "mds";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
import { validRoutes } from "../valid-routes";
|
||||
import { menuOpen } from "../../../systemSlice";
|
||||
import { selFeatures } from "../consoleSlice";
|
||||
import { getLogoVar, registeredCluster } from "../../../config";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { getLicenseConsent } from "../License/utils";
|
||||
|
||||
const MenuWrapper = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useSelector(selFeatures);
|
||||
const navigate = useNavigate();
|
||||
const { pathname = "" } = useLocation();
|
||||
|
||||
const sidebarOpen = useSelector(
|
||||
(state: AppState) => state.system.sidebarOpen
|
||||
);
|
||||
const licenseInfo = useSelector(
|
||||
(state: AppState) => state?.system?.licenseInfo
|
||||
);
|
||||
|
||||
const isAgplAckDone = getLicenseConsent();
|
||||
const clusterRegistered = registeredCluster();
|
||||
|
||||
const { plan = "" } = licenseInfo || {};
|
||||
|
||||
let licenseNotification = true;
|
||||
if (plan || isAgplAckDone || clusterRegistered) {
|
||||
licenseNotification = false;
|
||||
}
|
||||
|
||||
const allowedMenuItems = validRoutes(features, licenseNotification);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
isOpen={sidebarOpen}
|
||||
displayGroupTitles
|
||||
options={allowedMenuItems}
|
||||
applicationLogo={{ applicationName: "console", subVariant: getLogoVar() }}
|
||||
callPathAction={(path) => {
|
||||
navigate(path);
|
||||
}}
|
||||
signOutAction={() => {
|
||||
navigate("/logout");
|
||||
}}
|
||||
collapseAction={() => {
|
||||
dispatch(menuOpen(!sidebarOpen));
|
||||
}}
|
||||
currentPath={pathname}
|
||||
mobileModeAuto={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuWrapper;
|
||||
@@ -14,21 +14,13 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export interface IMenuItem {
|
||||
group?: string;
|
||||
type?: string;
|
||||
component?: any;
|
||||
to?: string;
|
||||
name: string;
|
||||
id?: string;
|
||||
icon: any;
|
||||
onClick?: any;
|
||||
import { MenuItemProps } from "mds";
|
||||
|
||||
export interface IMenuItem extends MenuItemProps {
|
||||
forceDisplay?: boolean;
|
||||
extraMargin?: boolean;
|
||||
fsHidden?: boolean;
|
||||
customPermissionFnc?: any;
|
||||
customPermissionFnc?: () => boolean;
|
||||
children?: IMenuItem[];
|
||||
badge?: any;
|
||||
}
|
||||
|
||||
export interface IRouteRule {
|
||||
|
||||
@@ -34,8 +34,8 @@ export const routesAsKbarActions = (
|
||||
id: `${childI.id}`,
|
||||
name: childI.name,
|
||||
section: i.name,
|
||||
perform: () => navigate(`${childI.to}`),
|
||||
icon: <childI.icon />,
|
||||
perform: () => navigate(`${childI.path}`),
|
||||
icon: childI.icon,
|
||||
};
|
||||
initialActions.push(a);
|
||||
}
|
||||
@@ -44,8 +44,8 @@ export const routesAsKbarActions = (
|
||||
id: `${i.id}`,
|
||||
name: i.name,
|
||||
section: "Navigation",
|
||||
perform: () => navigate(`${i.to}`),
|
||||
icon: <i.icon />,
|
||||
perform: () => navigate(`${i.path}`),
|
||||
icon: i.icon,
|
||||
};
|
||||
initialActions.push(a);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import { IMenuItem } from "./Menu/types";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import {
|
||||
adminUserPermissions,
|
||||
CONSOLE_UI_RESOURCE,
|
||||
@@ -52,296 +52,272 @@ import {
|
||||
WatchIcon,
|
||||
} from "mds";
|
||||
import { hasPermission } from "../../common/SecureComponent";
|
||||
import React from "react";
|
||||
import LicenseBadge from "./Menu/LicenseBadge";
|
||||
import EncryptionIcon from "../../icons/SidebarMenus/EncryptionIcon";
|
||||
import EncryptionStatusIcon from "../../icons/SidebarMenus/EncryptionStatusIcon";
|
||||
import { LockOpen, Login } from "@mui/icons-material";
|
||||
|
||||
export const validRoutes = (features: string[] | null | undefined) => {
|
||||
const permissionsValidation = (item: IMenuItem) => {
|
||||
return (
|
||||
((item.customPermissionFnc
|
||||
? item.customPermissionFnc()
|
||||
: hasPermission(
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES_PERMISSIONS[item.path ?? ""]
|
||||
)) ||
|
||||
item.forceDisplay) &&
|
||||
!item.fsHidden
|
||||
);
|
||||
};
|
||||
|
||||
const validateItem = (item: IMenuItem) => {
|
||||
// We clean up child items first
|
||||
if (item.children && item.children.length > 0) {
|
||||
const childArray: IMenuItem[] = item.children.reduce(
|
||||
(acc: IMenuItem[], item) => {
|
||||
if (!validateItem(item)) {
|
||||
return [...acc];
|
||||
}
|
||||
|
||||
return [...acc, item];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const ret = { ...item, children: childArray };
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (permissionsValidation(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const validRoutes = (
|
||||
features: string[] | null | undefined,
|
||||
licenseNotification: boolean = false
|
||||
) => {
|
||||
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
|
||||
const kmsIsEnabled = (features && features.includes("kms")) || false;
|
||||
|
||||
let consoleMenus: IMenuItem[] = [
|
||||
{
|
||||
group: "User",
|
||||
name: "Object Browser",
|
||||
id: "object-browser",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.OBJECT_BROWSER_VIEW,
|
||||
icon: ObjectBrowserIcon,
|
||||
path: IAM_PAGES.OBJECT_BROWSER_VIEW,
|
||||
icon: <ObjectBrowserIcon />,
|
||||
forceDisplay: true,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
group: "User",
|
||||
component: NavLink,
|
||||
id: "nav-accesskeys",
|
||||
to: IAM_PAGES.ACCOUNT,
|
||||
path: IAM_PAGES.ACCOUNT,
|
||||
name: "Access Keys",
|
||||
icon: AccountsMenuIcon,
|
||||
icon: <AccountsMenuIcon />,
|
||||
forceDisplay: true,
|
||||
},
|
||||
{
|
||||
group: "User",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.DOCUMENTATION,
|
||||
path: "https://min.io/docs/minio/linux/index.html?ref=con",
|
||||
name: "Documentation",
|
||||
icon: DocumentationIcon,
|
||||
icon: <DocumentationIcon />,
|
||||
forceDisplay: true,
|
||||
onClick: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLLIElement>
|
||||
| React.MouseEvent<HTMLAnchorElement>
|
||||
| React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
e.preventDefault();
|
||||
window.open(
|
||||
"https://min.io/docs/minio/linux/index.html?ref=con",
|
||||
"_blank",
|
||||
"noopener"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
name: "Buckets",
|
||||
id: "buckets",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.BUCKETS,
|
||||
icon: BucketsMenuIcon,
|
||||
path: IAM_PAGES.BUCKETS,
|
||||
icon: <BucketsMenuIcon />,
|
||||
forceDisplay: true,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
name: "Policies",
|
||||
component: NavLink,
|
||||
id: "policies",
|
||||
to: IAM_PAGES.POLICIES,
|
||||
icon: AccessMenuIcon,
|
||||
path: IAM_PAGES.POLICIES,
|
||||
icon: <AccessMenuIcon />,
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
name: "Identity",
|
||||
id: "identity",
|
||||
icon: IdentityMenuIcon,
|
||||
icon: <IdentityMenuIcon />,
|
||||
children: [
|
||||
{
|
||||
component: NavLink,
|
||||
id: "users",
|
||||
to: IAM_PAGES.USERS,
|
||||
path: IAM_PAGES.USERS,
|
||||
customPermissionFnc: () =>
|
||||
hasPermission(CONSOLE_UI_RESOURCE, adminUserPermissions) ||
|
||||
hasPermission(S3_ALL_RESOURCES, adminUserPermissions) ||
|
||||
hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.ADMIN_ALL_ACTIONS]),
|
||||
name: "Users",
|
||||
icon: UsersMenuIcon,
|
||||
icon: <UsersMenuIcon />,
|
||||
fsHidden: ldapIsEnabled,
|
||||
},
|
||||
{
|
||||
component: NavLink,
|
||||
id: "groups",
|
||||
to: IAM_PAGES.GROUPS,
|
||||
path: IAM_PAGES.GROUPS,
|
||||
name: "Groups",
|
||||
icon: GroupsMenuIcon,
|
||||
icon: <GroupsMenuIcon />,
|
||||
fsHidden: ldapIsEnabled,
|
||||
},
|
||||
{
|
||||
name: "OpenID",
|
||||
component: NavLink,
|
||||
id: "openID",
|
||||
to: IAM_PAGES.IDP_OPENID_CONFIGURATIONS,
|
||||
icon: LockOpen,
|
||||
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS,
|
||||
icon: <LockOpen />,
|
||||
},
|
||||
{
|
||||
name: "LDAP",
|
||||
component: NavLink,
|
||||
id: "ldap",
|
||||
to: IAM_PAGES.IDP_LDAP_CONFIGURATIONS,
|
||||
icon: Login,
|
||||
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS,
|
||||
icon: <Login />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
group: "Administrator",
|
||||
name: "Monitoring",
|
||||
id: "tools",
|
||||
icon: MonitoringMenuIcon,
|
||||
icon: <MonitoringMenuIcon />,
|
||||
children: [
|
||||
{
|
||||
name: "Metrics",
|
||||
id: "monitorMetrics",
|
||||
to: IAM_PAGES.DASHBOARD,
|
||||
icon: MetricsMenuIcon,
|
||||
component: NavLink,
|
||||
path: IAM_PAGES.DASHBOARD,
|
||||
icon: <MetricsMenuIcon />,
|
||||
},
|
||||
{
|
||||
name: "Logs ",
|
||||
id: "monitorLogs",
|
||||
to: IAM_PAGES.TOOLS_LOGS,
|
||||
icon: LogsMenuIcon,
|
||||
component: NavLink,
|
||||
path: IAM_PAGES.TOOLS_LOGS,
|
||||
icon: <LogsMenuIcon />,
|
||||
},
|
||||
{
|
||||
name: "Audit",
|
||||
id: "monitorAudit",
|
||||
to: IAM_PAGES.TOOLS_AUDITLOGS,
|
||||
icon: AuditLogsMenuIcon,
|
||||
component: NavLink,
|
||||
path: IAM_PAGES.TOOLS_AUDITLOGS,
|
||||
icon: <AuditLogsMenuIcon />,
|
||||
},
|
||||
{
|
||||
name: "Trace",
|
||||
id: "monitorTrace",
|
||||
to: IAM_PAGES.TOOLS_TRACE,
|
||||
icon: TraceMenuIcon,
|
||||
component: NavLink,
|
||||
path: IAM_PAGES.TOOLS_TRACE,
|
||||
icon: <TraceMenuIcon />,
|
||||
},
|
||||
{
|
||||
name: "Watch",
|
||||
id: "watch",
|
||||
component: NavLink,
|
||||
icon: WatchIcon,
|
||||
to: IAM_PAGES.TOOLS_WATCH,
|
||||
id: "monitorWatch",
|
||||
icon: <WatchIcon />,
|
||||
path: IAM_PAGES.TOOLS_WATCH,
|
||||
},
|
||||
{
|
||||
name: "Drives",
|
||||
id: "monitorDrives",
|
||||
to: IAM_PAGES.TOOLS_HEAL,
|
||||
icon: DrivesMenuIcon,
|
||||
component: NavLink,
|
||||
path: IAM_PAGES.TOOLS_HEAL,
|
||||
icon: <DrivesMenuIcon />,
|
||||
},
|
||||
{
|
||||
name: "Encryption",
|
||||
id: "monitorEncryption",
|
||||
to: IAM_PAGES.KMS_STATUS,
|
||||
icon: EncryptionStatusIcon,
|
||||
component: NavLink,
|
||||
path: IAM_PAGES.KMS_STATUS,
|
||||
icon: <EncryptionStatusIcon />,
|
||||
fsHidden: !kmsIsEnabled,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.EVENT_DESTINATIONS,
|
||||
path: IAM_PAGES.EVENT_DESTINATIONS,
|
||||
name: "Events",
|
||||
icon: LambdaIcon,
|
||||
icon: <LambdaIcon />,
|
||||
id: "lambda",
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.TIERS,
|
||||
path: IAM_PAGES.TIERS,
|
||||
name: "Tiering",
|
||||
icon: TiersIcon,
|
||||
icon: <TiersIcon />,
|
||||
id: "tiers",
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.SITE_REPLICATION,
|
||||
path: IAM_PAGES.SITE_REPLICATION,
|
||||
name: "Site Replication",
|
||||
icon: RecoverIcon,
|
||||
icon: <RecoverIcon />,
|
||||
id: "sitereplication",
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.KMS_KEYS,
|
||||
path: IAM_PAGES.KMS_KEYS,
|
||||
name: "Encryption",
|
||||
icon: EncryptionIcon,
|
||||
icon: <EncryptionIcon />,
|
||||
id: "encryption",
|
||||
fsHidden: !kmsIsEnabled,
|
||||
},
|
||||
{
|
||||
group: "Administrator",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.SETTINGS,
|
||||
path: IAM_PAGES.SETTINGS,
|
||||
name: "Settings",
|
||||
id: "configurations",
|
||||
icon: SettingsIcon,
|
||||
icon: <SettingsIcon />,
|
||||
},
|
||||
{
|
||||
group: "Subscription",
|
||||
component: NavLink,
|
||||
to: IAM_PAGES.LICENSE,
|
||||
path: IAM_PAGES.LICENSE,
|
||||
name: "License",
|
||||
id: "license",
|
||||
icon: LicenseIcon,
|
||||
badge: LicenseBadge,
|
||||
icon: <LicenseIcon />,
|
||||
badge: licenseNotification,
|
||||
forceDisplay: true,
|
||||
},
|
||||
{
|
||||
group: "Subscription",
|
||||
name: "Health",
|
||||
id: "diagnostics",
|
||||
component: NavLink,
|
||||
icon: HealthMenuIcon,
|
||||
to: IAM_PAGES.TOOLS_DIAGNOSTICS,
|
||||
icon: <HealthMenuIcon />,
|
||||
path: IAM_PAGES.TOOLS_DIAGNOSTICS,
|
||||
},
|
||||
{
|
||||
group: "Subscription",
|
||||
name: "Performance",
|
||||
id: "performance",
|
||||
component: NavLink,
|
||||
icon: PerformanceMenuIcon,
|
||||
to: IAM_PAGES.TOOLS_SPEEDTEST,
|
||||
icon: <PerformanceMenuIcon />,
|
||||
path: IAM_PAGES.TOOLS_SPEEDTEST,
|
||||
},
|
||||
{
|
||||
group: "Subscription",
|
||||
name: "Profile",
|
||||
id: "profile",
|
||||
component: NavLink,
|
||||
icon: ProfileMenuIcon,
|
||||
to: IAM_PAGES.PROFILE,
|
||||
icon: <ProfileMenuIcon />,
|
||||
path: IAM_PAGES.PROFILE,
|
||||
},
|
||||
{
|
||||
group: "Subscription",
|
||||
name: "Inspect",
|
||||
id: "inspectObjects",
|
||||
to: IAM_PAGES.SUPPORT_INSPECT,
|
||||
icon: InspectMenuIcon,
|
||||
component: NavLink,
|
||||
path: IAM_PAGES.SUPPORT_INSPECT,
|
||||
icon: <InspectMenuIcon />,
|
||||
},
|
||||
{
|
||||
group: "Subscription",
|
||||
name: "Call Home",
|
||||
id: "callhome",
|
||||
component: NavLink,
|
||||
icon: CallHomeMenuIcon,
|
||||
to: IAM_PAGES.CALL_HOME,
|
||||
icon: <CallHomeMenuIcon />,
|
||||
path: IAM_PAGES.CALL_HOME,
|
||||
},
|
||||
];
|
||||
|
||||
const allowedItems = consoleMenus.filter((item: IMenuItem) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
const c = item.children?.filter((childItem: IMenuItem) => {
|
||||
return (
|
||||
((childItem.customPermissionFnc
|
||||
? childItem.customPermissionFnc()
|
||||
: hasPermission(
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES_PERMISSIONS[childItem.to ?? ""]
|
||||
)) ||
|
||||
childItem.forceDisplay) &&
|
||||
!childItem.fsHidden
|
||||
);
|
||||
});
|
||||
return c.length > 0;
|
||||
return consoleMenus.reduce((acc: IMenuItem[], item) => {
|
||||
const validation = validateItem(item);
|
||||
if (!validation) {
|
||||
return [...acc];
|
||||
}
|
||||
|
||||
const res =
|
||||
((item.customPermissionFnc
|
||||
? item.customPermissionFnc()
|
||||
: hasPermission(
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES_PERMISSIONS[item.to ?? ""]
|
||||
)) ||
|
||||
item.forceDisplay) &&
|
||||
!item.fsHidden;
|
||||
return res;
|
||||
});
|
||||
return allowedItems;
|
||||
return [...acc, validation];
|
||||
}, []);
|
||||
};
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { Role, Selector } from "testcafe";
|
||||
import { readFileSync } from "fs";
|
||||
import { getMenuElement } from "../utils/elements-menu";
|
||||
|
||||
const data = readFileSync(__dirname + "/../constants/timestamp.txt", "utf-8");
|
||||
const $TIMESTAMP = data.trim();
|
||||
@@ -39,14 +40,7 @@ const bucketsScreenUrl = `${testDomainUrl}/buckets`;
|
||||
|
||||
const loginSubmitBtn = Selector("button").withAttribute("id", "do-login");
|
||||
|
||||
export const bucketsSidebarEl = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#buckets");
|
||||
|
||||
export const menuListchildren = Selector("#tools-children");
|
||||
export const bucketsEl = menuListchildren
|
||||
.find("a")
|
||||
.withAttribute("href", "/buckets");
|
||||
export const bucketsSidebarEl = getMenuElement("buckets");
|
||||
|
||||
export const inspectAllowedRole = Role(
|
||||
loginUrl,
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
import * as roles from "../utils/roles";
|
||||
import { IAM_PAGES } from "../../src/common/SecureComponent/permissions";
|
||||
import { Selector } from "testcafe";
|
||||
import { getMenuElement } from "../utils/elements-menu";
|
||||
|
||||
let testDomainUrl = "http://localhost:9090";
|
||||
const screenUrl = `${testDomainUrl}${IAM_PAGES.SITE_REPLICATION}`;
|
||||
const siteReplicationEl = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#sitereplication");
|
||||
const siteReplicationEl = getMenuElement("sitereplication");
|
||||
export const addSitesBtn = Selector("button").withText("Add Sites");
|
||||
|
||||
/* Begin Local Testing config block */
|
||||
|
||||
@@ -15,85 +15,70 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Selector } from "testcafe";
|
||||
import { IAM_PAGES } from "../../src/common/SecureComponent/permissions";
|
||||
|
||||
//----------------------------------------------------
|
||||
// Functions to get elements
|
||||
//----------------------------------------------------
|
||||
|
||||
export const getMenuElement = (item) => {
|
||||
return Selector("div.menuItems").find("button").withAttribute("id", item);
|
||||
};
|
||||
|
||||
export const getSubmenuBlock = (item) => {
|
||||
return getMenuElement(item).sibling("div.subItemsBox");
|
||||
};
|
||||
|
||||
//----------------------------------------------------
|
||||
// General sidebar element
|
||||
//----------------------------------------------------
|
||||
export const sidebarItem = Selector(".MuiPaper-root").find("ul").child("a");
|
||||
export const logoutItem = Selector(".MuiPaper-root").find("ul").child("div");
|
||||
export const logoutItem = getMenuElement("sign-out");
|
||||
|
||||
//----------------------------------------------------
|
||||
// Specific sidebar elements
|
||||
//----------------------------------------------------
|
||||
export const monitoringElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#tools");
|
||||
export const monitoringChildren = Selector("#tools-children");
|
||||
export const monitoringElement = getMenuElement("tools");
|
||||
export const monitoringChildren = getSubmenuBlock("tools");
|
||||
|
||||
export const dashboardElement = monitoringChildren
|
||||
.find("a")
|
||||
.withAttribute("href", IAM_PAGES.DASHBOARD);
|
||||
.find("button")
|
||||
.withAttribute("id", "monitorMetrics");
|
||||
export const logsElement = monitoringChildren
|
||||
.find("a")
|
||||
.withAttribute("href", "/tools/logs");
|
||||
.find("button")
|
||||
.withAttribute("id", "monitorLogs");
|
||||
export const traceElement = monitoringChildren
|
||||
.find("a")
|
||||
.withAttribute("href", "/tools/trace");
|
||||
.find("button")
|
||||
.withAttribute("id", "monitorTrace");
|
||||
export const drivesElement = monitoringChildren
|
||||
.find("a")
|
||||
.withAttribute("href", "/tools/heal");
|
||||
.find("button")
|
||||
.withAttribute("id", "monitorDrives");
|
||||
export const watchElement = monitoringChildren
|
||||
.find("a")
|
||||
.withAttribute("href", "/tools/watch");
|
||||
.find("button")
|
||||
.withAttribute("id", "monitorWatch");
|
||||
|
||||
export const bucketsElement = sidebarItem.withAttribute("href", "/buckets");
|
||||
export const bucketsElement = getMenuElement("buckets");
|
||||
|
||||
export const serviceAcctsElement = sidebarItem.withAttribute(
|
||||
"href",
|
||||
IAM_PAGES.ACCOUNT
|
||||
);
|
||||
export const serviceAcctsElement = getMenuElement("nav-accesskeys");
|
||||
|
||||
export const identityElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#identity");
|
||||
export const identityChildren = Selector("#identity-children");
|
||||
export const identityElement = getMenuElement("identity");
|
||||
export const identityChildren = getSubmenuBlock("identity");
|
||||
|
||||
export const usersElement = identityChildren
|
||||
.find("a")
|
||||
.withAttribute("href", IAM_PAGES.USERS);
|
||||
.find("button")
|
||||
.withAttribute("id", "users");
|
||||
export const groupsElement = identityChildren
|
||||
.find("a")
|
||||
.withAttribute("href", IAM_PAGES.GROUPS);
|
||||
.find("button")
|
||||
.withAttribute("id", "groups");
|
||||
|
||||
export const iamPoliciesElement = sidebarItem.withAttribute(
|
||||
"href",
|
||||
IAM_PAGES.POLICIES
|
||||
);
|
||||
export const iamPoliciesElement = getMenuElement("policies");
|
||||
|
||||
export const configurationsElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#configurations");
|
||||
export const configurationsElement = getMenuElement("configurations");
|
||||
|
||||
export const notificationEndpointsElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#lambda");
|
||||
export const notificationEndpointsElement = getMenuElement("lambda");
|
||||
|
||||
export const tiersElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#tiers");
|
||||
export const tiersElement = getMenuElement("tiers");
|
||||
|
||||
export const diagnosticsElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#diagnostics");
|
||||
export const performanceElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#performance");
|
||||
export const profileElement = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#profile");
|
||||
export const inspectElement = sidebarItem.withAttribute(
|
||||
"href",
|
||||
"/support/inspect"
|
||||
);
|
||||
export const diagnosticsElement = getMenuElement("diagnostics");
|
||||
export const performanceElement = getMenuElement("performance");
|
||||
export const inspectElement = getMenuElement("inspectObjects");
|
||||
|
||||
export const licenseElement = sidebarItem.withAttribute("href", "/license");
|
||||
export const licenseElement = getMenuElement("license");
|
||||
|
||||
@@ -382,7 +382,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
|
||||
integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
|
||||
|
||||
|
||||
"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e"
|
||||
@@ -4873,9 +4872,9 @@ destroy@1.2.0:
|
||||
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||
|
||||
detect-gpu@^5.0.27:
|
||||
version "5.0.28"
|
||||
resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.28.tgz#e7762c04cc3b5a33d902eb5719add195494df60a"
|
||||
integrity sha512-sdT5Ti9ZHBBq39mK0DRwnm/5xZOVAz2+vxYLdPcFP83+3DGkzucEK0lzw1XFwct4zWDAXYrSTFUjC33qsoRAoQ==
|
||||
version "5.0.27"
|
||||
resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.27.tgz#821d9331c87e32568c483d85e12a9adee43d7bb2"
|
||||
integrity sha512-IDjjqTkS+f0xm/ntbD21IPYiF0srzpePC/hhUMmctEsoklZwJwStJiMi/KN0pnH0LjSsgjwbP+QwW7y+Qf4/SQ==
|
||||
dependencies:
|
||||
webgl-constants "^1.1.1"
|
||||
|
||||
@@ -5093,7 +5092,6 @@ electron-to-chromium@^1.4.411:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.425.tgz#399df13091b836d28283a545c25c8e4d9da86da8"
|
||||
integrity sha512-wv1NufHxu11zfDbY4fglYQApMswleE9FL/DSeyOyauVXDZ+Kco96JK/tPfBUaDqfRarYp2WH2hJ/5UnVywp9Jg==
|
||||
|
||||
|
||||
elegant-spinner@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
|
||||
@@ -8294,7 +8292,6 @@ memfs@^3.1.2, memfs@^3.4.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.5.3.tgz#d9b40fe4f8d5788c5f895bda804cd0d9eeee9f3b"
|
||||
integrity sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==
|
||||
|
||||
dependencies:
|
||||
fs-monkey "^1.0.4"
|
||||
|
||||
@@ -12534,7 +12531,6 @@ webpack@^5.64.4:
|
||||
version "5.86.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.86.0.tgz#b0eb81794b62aee0b7e7eb8c5073495217d9fc6d"
|
||||
integrity sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==
|
||||
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.3"
|
||||
"@types/estree" "^1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user