Groups page ux refactor (#1183)

This commit is contained in:
Prakash Senthil Vel
2021-11-03 22:42:31 +05:30
committed by GitHub
parent acd785dfe0
commit 4e7559f354
6 changed files with 568 additions and 105 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -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<boolean>(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
: ""
}`,
}`
}}
/>
</div>
@@ -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));

View File

@@ -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 (
<ModalWrapper
modalOpen={open}
onClose={onClose}
title={title}
>
<PredefinedList
label={`Selected Group`}
content={selectedGroup}
/>
<br />
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
editMode={!selectedGroup}
/>
<Grid item xs={12} className={classes.buttonContainer}>
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={() => {
setSelectedUsers(preSelectedUsers);
}}
>
Clear
</button>
<Button
type="button"
variant="contained"
color="primary"
onClick={() => {
addMembersToGroup();
}}
>
Save
</Button>
</Grid>
</ModalWrapper>
);
};
const mapDispatchToProps = {
setModalErrorSnackMessage
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(AddGroupMember));

View File

@@ -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) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
)
}}
onChange={(e) => {
setFilter(e.target.value);
@@ -319,7 +313,7 @@ const Groups = ({ classes, setErrorSnackMessage }: IGroupsProps) => {
};
const mapDispatchToProps = {
setErrorSnackMessage,
setErrorSnackMessage
};
const connector = connect(null, mapDispatchToProps);

View File

@@ -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 (
<List component="nav" dense={true}>
<ListItem
button
selected={activeTab === 0}
onClick={() => {
onTabChange(0);
}}
>
<ListItemText primary="Members" />
</ListItem>
<ListItem
button
selected={activeTab === 1}
onClick={() => {
onTabChange(1);
}}
>
<ListItemText primary="Policies" />
</ListItem>
</List>
);
};
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 (
<PageHeader
label={
<Fragment>
<Link to={"/groups"} className={classes.breadcrumLink}>
Groups
</Link>
</Fragment>
}
actions={<React.Fragment />}
/>
);
};
const GroupsDetails = ({ classes }: IGroupDetailsProps) => {
const [currentTab, setCurrentTab] = useState<number>(0);
const [groupDetails, setGroupDetails] = useState<GroupInfo>({});
/*Modals*/
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const [usersOpen, setUsersOpen] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const {
groupName = ""
} = useParams<Record<string, string>>();
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 (
<React.Fragment>
<GroupDetailsHeader classes={classes} />
<Grid container className={classes.container}>
<Grid item xs={12}>
<ScreenTitle
icon={
<Fragment>
<UsersIcon width={40} />
</Fragment>
}
title={groupName}
subTitle={
<Fragment>Status: {isGroupEnabled ? "Enabled" : "Disabled"}</Fragment>
}
actions={
<Fragment>
<Button
onClick={() => {
toggleGroupStatus(!isGroupEnabled);
}}
color={"primary"}
>
{isGroupEnabled ? "Disable" : "Enable"}
</Button>
<Tooltip title="Delete User">
<IconButton
color="primary"
aria-label="Delete User"
component="span"
onClick={() => {
setDeleteOpen(true);
}}
size="large"
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Fragment>
}
/>
</Grid>
<Grid item xs={2}>
<TabItems activeTab={currentTab} onTabChange={(num) => {
setCurrentTab(num);
}} />
</Grid>
<Grid item xs={10}>
<Grid item xs={12}>
<TabPanel index={0} value={currentTab}>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Members</h1>
<Button
variant="contained"
color="primary"
startIcon={<UsersIcon />}
size="medium"
onClick={() => {
setUsersOpen(true);
}}
>
{memberActionText}
</Button>
</div>
<br />
<TableWrapper
//itemActions={tableActions}
columns={[{ label: "Access Key", elementKey: "" }]}
// onSelect={selectionChanged}
selectedItems={[]}
isLoading={false}
records={members}
entityName="Users"
idField=""
customPaperHeight={classes.twHeight}
/>
</TabPanel>
<TabPanel index={1} value={currentTab}>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Policies</h1>
<Button
variant="contained"
color="primary"
startIcon={<IAMPoliciesIcon />}
size="medium"
onClick={() => {
setPolicyOpen(true);
}}
>
Set Policies
</Button>
</div>
<br />
<TableWrapper
itemActions={[
{
type: "view",
onClick: (policy) => {
history.push(`/policies/${policy}`);
}
}
]}
columns={[{ label: "Policy", elementKey: "" }]}
isLoading={false}
records={groupPolicies}
entityName="Policies"
idField=""
/>
</TabPanel>
</Grid>
</Grid>
</Grid>
{/*Modals*/}
{policyOpen ? (
<SetPolicy
open={policyOpen}
selectedGroup={groupName}
selectedUser={null}
closeModalAndRefresh={() => {
setPolicyOpen(false);
fetchGroupInfo();
}}
/>
) : null}
{usersOpen ?
<AddGroupMember
selectedGroup={groupName}
onSaveClick={() => {
}}
title={memberActionText}
groupStatus={groupEnabled}
classes={classes}
preSelectedUsers={members}
open={usersOpen}
onClose={() => {
setUsersOpen(false);
fetchGroupInfo();
}} /> : null}
{deleteOpen && (
<DeleteGroup
deleteOpen={deleteOpen}
selectedGroup={groupName}
closeDeleteModalAndRefresh={(isDelSuccess: boolean) => {
setDeleteOpen(false);
if (isDelSuccess) {
history.push("/groups");
}
}}
/>
)}
{/*Modals*/}
</React.Fragment>
);
};
const mapDispatchToProps = {
setErrorSnackMessage
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(GroupsDetails));