From 4e7559f35467e9a6685653c98e1cd8f050aa3f11 Mon Sep 17 00:00:00 2001 From: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com> Date: Wed, 3 Nov 2021 22:42:31 +0530 Subject: [PATCH] Groups page ux refactor (#1183) --- pkg/acl/endpoints.go | 2 + pkg/acl/endpoints_test.go | 4 +- portal-ui/src/screens/Console/Console.tsx | 167 ++++----- .../screens/Console/Groups/AddGroupMember.tsx | 121 +++++++ .../src/screens/Console/Groups/Groups.tsx | 38 +- .../screens/Console/Groups/GroupsDetails.tsx | 341 ++++++++++++++++++ 6 files changed, 568 insertions(+), 105 deletions(-) create mode 100644 portal-ui/src/screens/Console/Groups/AddGroupMember.tsx create mode 100644 portal-ui/src/screens/Console/Groups/GroupsDetails.tsx diff --git a/pkg/acl/endpoints.go b/pkg/acl/endpoints.go index da87aea41..28f77614a 100644 --- a/pkg/acl/endpoints.go +++ b/pkg/acl/endpoints.go @@ -33,6 +33,7 @@ var ( users = "/users" usersDetail = "/users/:userName+" groups = "/groups" + groupsDetails = "/groups/:groupName+" iamPolicies = "/policies" policiesDetail = "/policies/*" dashboard = "/dashboard" @@ -317,6 +318,7 @@ var endpointRules = map[string]ConfigurationActionSet{ users: usersActionSet, usersDetail: usersActionSet, groups: groupsActionSet, + groupsDetails: groupsActionSet, iamPolicies: iamPoliciesActionSet, policiesDetail: iamPoliciesActionSet, dashboard: dashboardActionSet, diff --git a/pkg/acl/endpoints_test.go b/pkg/acl/endpoints_test.go index 70709f12b..87004ef1a 100644 --- a/pkg/acl/endpoints_test.go +++ b/pkg/acl/endpoints_test.go @@ -70,7 +70,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) { "admin:*", }, }, - want: 32, + want: 33, }, { name: "all s3 endpoints", @@ -89,7 +89,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) { "s3:*", }, }, - want: 34, + want: 35, }, { name: "Console User - default endpoints", diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index cb161ff0d..b4614ce6a 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -29,7 +29,7 @@ import { serverIsLoading, serverNeedsRestart, setMenuOpen, - setSnackBarMessage, + setSnackBarMessage } from "../../actions"; import { ISessionResponse } from "./types"; import { snackBarMessage } from "../../types"; @@ -65,6 +65,7 @@ import ListTenants from "./Tenants/ListTenants/ListTenants"; import Tools from "./Tools/Tools"; import ErrorLogs from "./Logs/ErrorLogs/ErrorLogs"; import LogsSearchMain from "./Logs/LogSearch/LogsSearchMain"; +import GroupsDetails from "./Groups/GroupsDetails"; const drawerWidth = 245; @@ -74,44 +75,44 @@ const styles = (theme: Theme) => display: "flex", "& .MuiPaper-root.MuiSnackbarContent-root": { borderRadius: "0px 0px 5px 5px", - boxShadow: "none", - }, + boxShadow: "none" + } }, toolbar: { background: theme.palette.background.default, color: "black", - paddingRight: 24, // keep right padding when drawer closed + paddingRight: 24 // keep right padding when drawer closed }, toolbarIcon: { display: "flex", alignItems: "center", justifyContent: "flex-end", padding: "0 8px", - ...theme.mixins.toolbar, + ...theme.mixins.toolbar }, appBar: { zIndex: theme.zIndex.drawer + 1, transition: theme.transitions.create(["width", "margin"], { easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), + duration: theme.transitions.duration.leavingScreen + }) }, appBarShift: { marginLeft: drawerWidth, width: `calc(100% - ${drawerWidth}px)`, transition: theme.transitions.create(["width", "margin"], { easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), + duration: theme.transitions.duration.enteringScreen + }) }, menuButton: { - marginRight: 36, + marginRight: 36 }, menuButtonHidden: { - display: "none", + display: "none" }, title: { - flexGrow: 1, + flexGrow: 1 }, drawerPaper: { position: "relative", @@ -119,44 +120,44 @@ const styles = (theme: Theme) => width: drawerWidth, transition: theme.transitions.create("width", { easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, + duration: theme.transitions.duration.enteringScreen }), overflowX: "hidden", background: "transparent linear-gradient(90deg, #073052 0%, #081C42 100%) 0% 0% no-repeat padding-box", - boxShadow: "0px 3px 7px #00000014", + boxShadow: "0px 3px 7px #00000014" }, drawerPaperClose: { overflowX: "hidden", transition: theme.transitions.create("width", { easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, + duration: theme.transitions.duration.leavingScreen }), width: theme.spacing(7), [theme.breakpoints.up("sm")]: { - width: theme.spacing(9), - }, + width: theme.spacing(9) + } }, content: { flexGrow: 1, height: "100vh", overflow: "auto", - position: "relative", + position: "relative" }, container: { paddingBottom: theme.spacing(4), margin: 0, width: "100%", - maxWidth: "initial", + maxWidth: "initial" }, paper: { padding: theme.spacing(2), display: "flex", overflow: "auto", - flexDirection: "column", + flexDirection: "column" }, fixedHeight: { - minHeight: 240, + minHeight: 240 }, warningBar: { background: theme.palette.primary.main, @@ -164,13 +165,13 @@ const styles = (theme: Theme) => heigh: "60px", widht: "100%", lineHeight: "60px", - textAlign: "center", + textAlign: "center" }, progress: { height: "3px", - backgroundColor: "#eaeaea", + backgroundColor: "#eaeaea" }, - ...snackBarCommon, + ...snackBarCommon }); interface IConsoleProps { @@ -189,17 +190,17 @@ interface IConsoleProps { } const Console = ({ - classes, - open, - needsRestart, - isServerLoading, - serverNeedsRestart, - serverIsLoading, - session, - loadingProgress, - snackBarMessage, - setSnackBarMessage, -}: IConsoleProps) => { + classes, + open, + needsRestart, + isServerLoading, + serverNeedsRestart, + serverIsLoading, + session, + loadingProgress, + snackBarMessage, + setSnackBarMessage + }: IConsoleProps) => { const [openSnackbar, setOpenSnackbar] = useState(false); const restartServer = () => { @@ -232,171 +233,175 @@ const Console = ({ const routes = [ { component: Dashboard, - path: "/dashboard", + path: "/dashboard" }, { component: Metrics, - path: "/metrics", + path: "/metrics" }, { component: Buckets, - path: "/buckets", + path: "/buckets" }, { component: Buckets, - path: "/buckets/*", + path: "/buckets/*" }, { component: Watch, - path: "/tools/watch", + path: "/tools/watch" }, { component: Users, - path: "/users/:userName+", + path: "/users/:userName+" }, { component: Users, - path: "/users", + path: "/users" }, { component: Groups, - path: "/groups", + path: "/groups" + }, + { + component: GroupsDetails, + path: "/groups/:groupName+" }, { component: Policies, - path: "/policies/*", + path: "/policies/*" }, { component: Policies, - path: "/policies", + path: "/policies" }, { component: Heal, - path: "/tools/heal", + path: "/tools/heal" }, { component: Trace, - path: "/tools/trace", + path: "/tools/trace" }, { component: HealthInfo, - path: "/tools/diagnostics", + path: "/tools/diagnostics" }, { component: ErrorLogs, - path: "/tools/logs", + path: "/tools/logs" }, { component: LogsSearchMain, - path: "/tools/audit-logs", + path: "/tools/audit-logs" }, { component: Tools, - path: "/tools", + path: "/tools" }, { component: ConfigurationMain, - path: "/settings", + path: "/settings" }, { component: ConfigurationMain, - path: "/settings/:option", + path: "/settings/:option" }, { component: AddNotificationEndpoint, - path: "/notification-endpoints/add/:service", + path: "/notification-endpoints/add/:service" }, { component: NotificationTypeSelector, - path: "/notification-endpoints/add", + path: "/notification-endpoints/add" }, { component: NotificationEndpoints, - path: "/notification-endpoints", + path: "/notification-endpoints" }, { component: AddTierConfiguration, - path: "/tiers/add/:service", + path: "/tiers/add/:service" }, { component: TierTypeSelector, - path: "/tiers/add", + path: "/tiers/add" }, { component: ListTiersConfiguration, - path: "/tiers", + path: "/tiers" }, { component: Account, path: "/account", props: { - changePassword: session.pages.includes("/account/change-password"), - }, + changePassword: session.pages.includes("/account/change-password") + } }, { component: ListTenants, - path: "/tenants", + path: "/tenants" }, { component: AddTenant, - path: "/tenants/add", + path: "/tenants/add" }, { component: Storage, - path: "/storage", + path: "/storage" }, { component: Storage, - path: "/storage/volumes", + path: "/storage/volumes" }, { component: Storage, - path: "/storage/drives", + path: "/storage/drives" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName", + path: "/namespaces/:tenantNamespace/tenants/:tenantName" }, { component: Hop, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/hop", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/hop" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/summary", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/summary" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/metrics", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/metrics" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/pools", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/pools" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/volumes", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/volumes" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/license", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/license" }, { component: TenantDetails, - path: "/namespaces/:tenantNamespace/tenants/:tenantName/security", + path: "/namespaces/:tenantNamespace/tenants/:tenantName/security" }, { component: License, - path: "/license", - }, + path: "/license" + } ]; const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]); @@ -481,7 +486,7 @@ const Console = ({ snackBarMessage.type === "error" ? classes.errorSnackBar : "" - }`, + }` }} /> @@ -515,14 +520,14 @@ const mapState = (state: AppState) => ({ isServerLoading: state.system.serverIsLoading, session: state.console.session, loadingProgress: state.system.loadingProgress, - snackBarMessage: state.system.snackBar, + snackBarMessage: state.system.snackBar }); const connector = connect(mapState, { setMenuOpen, serverNeedsRestart, serverIsLoading, - setSnackBarMessage, + setSnackBarMessage }); export default withStyles(styles)(connector(Console)); diff --git a/portal-ui/src/screens/Console/Groups/AddGroupMember.tsx b/portal-ui/src/screens/Console/Groups/AddGroupMember.tsx new file mode 100644 index 000000000..6889f72d7 --- /dev/null +++ b/portal-ui/src/screens/Console/Groups/AddGroupMember.tsx @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import UsersSelectors from "./UsersSelectors"; +import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; +import PredefinedList from "../Common/FormComponents/PredefinedList/PredefinedList"; +import Grid from "@mui/material/Grid"; +import { Button } from "@mui/material"; +import api from "../../../common/api"; +import { ErrorResponseHandler } from "../../../common/types"; +import { setModalErrorSnackMessage } from "../../../actions"; +import { connect } from "react-redux"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import { modalBasic } from "../Common/FormComponents/common/styleLibrary"; +import withStyles from "@mui/styles/withStyles"; + +type UserPickerModalProps = { + classes?: any, + title?: string + preSelectedUsers?: string[] + selectedGroup?: string, + open: boolean, + onClose: () => void, + onSaveClick: () => void, + groupStatus?: string, +} + + +const styles = (theme: Theme) => + createStyles({ + buttonContainer: { + textAlign: "right", + marginTop: "1rem" + }, + ...modalBasic + }); + +const AddGroupMember = ({ + classes, + title = "", + groupStatus = "enabled", + preSelectedUsers = [], + selectedGroup = "", + open, + onClose + }: UserPickerModalProps) => { + + const [selectedUsers, setSelectedUsers] = useState(preSelectedUsers); + + function addMembersToGroup() { + return api + .invoke("PUT", `/api/v1/group?name=${encodeURI(selectedGroup)}`, { + group: selectedGroup, + members: selectedUsers, + status: groupStatus + + }) + .then((res) => { + onClose(); + }) + .catch((err: ErrorResponseHandler) => { + onClose(); + setModalErrorSnackMessage(err); + }); + } + + return ( + + + + +
+ + + + + + + + + +
+ ); + +}; + + +const mapDispatchToProps = { + setModalErrorSnackMessage +}; + +const connector = connect(null, mapDispatchToProps); +export default withStyles(styles)(connector(AddGroupMember)); diff --git a/portal-ui/src/screens/Console/Groups/Groups.tsx b/portal-ui/src/screens/Console/Groups/Groups.tsx index 6ca2035f4..0cc8db06a 100644 --- a/portal-ui/src/screens/Console/Groups/Groups.tsx +++ b/portal-ui/src/screens/Console/Groups/Groups.tsx @@ -31,7 +31,7 @@ import { actionsTray, containerForHeader, linkStyles, - searchField, + searchField } from "../Common/FormComponents/common/styleLibrary"; import { ErrorResponseHandler } from "../../../common/types"; import api from "../../../common/api"; @@ -42,6 +42,7 @@ import SetPolicy from "../Policies/SetPolicy"; import PageHeader from "../Common/PageHeader/PageHeader"; import SearchIcon from "../../../icons/SearchIcon"; import HelpBox from "../../../common/HelpBox"; +import history from "../../../history"; interface IGroupsProps { classes: any; @@ -52,42 +53,42 @@ interface IGroupsProps { const styles = (theme: Theme) => createStyles({ seeMore: { - marginTop: theme.spacing(3), + marginTop: theme.spacing(3) }, paper: { // padding: theme.spacing(2), display: "flex", overflow: "auto", - flexDirection: "column", + flexDirection: "column" }, addSideBar: { width: "320px", - padding: "20px", + padding: "20px" }, tableToolbar: { paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(0), + paddingRight: theme.spacing(0) }, wrapCell: { maxWidth: "200px", whiteSpace: "normal", - wordWrap: "break-word", + wordWrap: "break-word" }, twHeight: { - minHeight: 600, + minHeight: 600 }, minTableHeader: { color: "#393939", "& tr": { "& th": { - fontWeight: "bold", - }, - }, + fontWeight: "bold" + } + } }, ...linkStyles(theme.palette.info.main), ...actionsTray, ...searchField, - ...containerForHeader(theme.spacing(4)), + ...containerForHeader(theme.spacing(4)) }); const Groups = ({ classes, setErrorSnackMessage }: IGroupsProps) => { @@ -147,8 +148,7 @@ const Groups = ({ classes, setErrorSnackMessage }: IGroupsProps) => { ); const viewAction = (group: any) => { - setGroupOpen(true); - setSelectedGroup(group); + history.push(`/groups/${group}`); }; const deleteAction = (group: any) => { @@ -156,15 +156,9 @@ const Groups = ({ classes, setErrorSnackMessage }: IGroupsProps) => { setSelectedGroup(group); }; - const setPolicyAction = (selectionElement: any): void => { - setPolicyOpen(true); - setSelectedGroup(selectionElement); - }; - const tableActions = [ { type: "view", onClick: viewAction }, - { type: "description", onClick: setPolicyAction }, - { type: "delete", onClick: deleteAction }, + { type: "delete", onClick: deleteAction } ]; return ( @@ -208,7 +202,7 @@ const Groups = ({ classes, setErrorSnackMessage }: IGroupsProps) => { - ), + ) }} onChange={(e) => { setFilter(e.target.value); @@ -319,7 +313,7 @@ const Groups = ({ classes, setErrorSnackMessage }: IGroupsProps) => { }; const mapDispatchToProps = { - setErrorSnackMessage, + setErrorSnackMessage }; const connector = connect(null, mapDispatchToProps); diff --git a/portal-ui/src/screens/Console/Groups/GroupsDetails.tsx b/portal-ui/src/screens/Console/Groups/GroupsDetails.tsx new file mode 100644 index 000000000..0177f639f --- /dev/null +++ b/portal-ui/src/screens/Console/Groups/GroupsDetails.tsx @@ -0,0 +1,341 @@ +import React, { Fragment, useEffect, useState } from "react"; +import PageHeader from "../Common/PageHeader/PageHeader"; +import { Link, useParams } from "react-router-dom"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import { actionsTray, containerForHeader, searchField } from "../Common/FormComponents/common/styleLibrary"; +import { setErrorSnackMessage, setModalErrorSnackMessage } from "../../../actions"; +import { connect } from "react-redux"; +import withStyles from "@mui/styles/withStyles"; +import { Button, Grid, IconButton, Tooltip } from "@mui/material"; +import ScreenTitle from "../Common/ScreenTitle/ScreenTitle"; +import { DeleteIcon, IAMPoliciesIcon, UsersIcon } from "../../../icons"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemText from "@mui/material/ListItemText"; +import { TabPanel } from "../../shared/tabs"; +import TableWrapper from "../Common/TableWrapper/TableWrapper"; +import history from "../../../history"; +import api from "../../../common/api"; +import SetPolicy from "../Policies/SetPolicy"; +import AddGroupMember from "./AddGroupMember"; +import { ErrorResponseHandler } from "../../../common/types"; +import DeleteGroup from "./DeleteGroup"; + + +const styles = (theme: Theme) => + createStyles({ + breadcrumLink: { + textDecoration: "none", + color: "black" + }, + ...actionsTray, + ...searchField, + actionsTray: { ...actionsTray.actionsTray }, + ...containerForHeader(theme.spacing(4)) + }); + +interface IGroupDetailsProps { + classes: any; + match: any; + setErrorSnackMessage: typeof setErrorSnackMessage; +} + +type TabItemsProps = { + activeTab: number, + onTabChange: (tab: number) => void +} + +type DetailsHeaderProps = { + classes: any +} + +type GroupInfo = { + members?: any[] + name?: string + policy?: string + status?: string +} + + +const TabItems = ({ activeTab, onTabChange }: TabItemsProps) => { + + return ( + + { + onTabChange(0); + }} + > + + + { + onTabChange(1); + }} + > + + + + + ); +}; + + +export const formatPolicy = (policy: string = ""): string[] => { + if (policy.length <= 0) return []; + return policy.split(","); +}; + +export const getPoliciesAsString = (policies: string[]): string => { + return policies.join(", "); +}; + + +const GroupDetailsHeader = ({ classes }: DetailsHeaderProps) => { + return ( + + + Groups + + + } + actions={} + /> + ); +}; + + +const GroupsDetails = ({ classes }: IGroupDetailsProps) => { + + const [currentTab, setCurrentTab] = useState(0); + const [groupDetails, setGroupDetails] = useState({}); + + /*Modals*/ + const [policyOpen, setPolicyOpen] = useState(false); + const [usersOpen, setUsersOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + + const { + groupName = "" + } = useParams>(); + + const { + members = [], + policy = "", + status: groupEnabled + } = groupDetails; + + useEffect(() => { + if (groupName) { + fetchGroupInfo(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [groupName]); + + const groupPolicies = formatPolicy(policy); + const isGroupEnabled = groupEnabled === "enabled"; + const memberActionText = members.length > 0 ? "Edit Members" : "Add Members"; + + function fetchGroupInfo() { + api + .invoke("GET", `/api/v1/group?name=${encodeURI(groupName)}`) + .then((res: any) => { + setGroupDetails(res); + }).catch(() => { + setGroupDetails({}); + }); + }; + + function toggleGroupStatus(nextStatus: boolean) { + + return api + .invoke("PUT", `/api/v1/group?name=${encodeURI(groupName)}`, { + group: groupName, + members: members, + status: nextStatus ? "enabled" : "disabled" + }) + .then((res) => { + fetchGroupInfo(); + }) + .catch((err: ErrorResponseHandler) => { + setModalErrorSnackMessage(err); + }); + } + + return ( + + + + + + + + + } + title={groupName} + subTitle={ + Status: {isGroupEnabled ? "Enabled" : "Disabled"} + } + actions={ + + + + { + setDeleteOpen(true); + }} + size="large" + > + + + + + } + /> + + + + { + setCurrentTab(num); + }} /> + + + + +
+

Members

+ +
+
+ +
+ +
+

Policies

+ +
+
+ { + history.push(`/policies/${policy}`); + } + } + ]} + columns={[{ label: "Policy", elementKey: "" }]} + isLoading={false} + records={groupPolicies} + entityName="Policies" + idField="" + /> +
+
+
+
+ + {/*Modals*/} + {policyOpen ? ( + { + setPolicyOpen(false); + fetchGroupInfo(); + }} + /> + ) : null} + + {usersOpen ? + { + + }} + title={memberActionText} + groupStatus={groupEnabled} + classes={classes} + preSelectedUsers={members} + open={usersOpen} + onClose={() => { + setUsersOpen(false); + fetchGroupInfo(); + }} /> : null} + + {deleteOpen && ( + { + setDeleteOpen(false); + if (isDelSuccess) { + history.push("/groups"); + } + }} + /> + )} + {/*Modals*/} +
+ ); + +}; + + +const mapDispatchToProps = { + setErrorSnackMessage +}; + +const connector = connect(null, mapDispatchToProps); + +export default withStyles(styles)(connector(GroupsDetails)); \ No newline at end of file