github.com/minio/console@v1.4.1/web-app/src/screens/Console/Groups/GroupsDetails.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2023 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 import React, { Fragment, useEffect, useState } from "react"; 18 import { useNavigate, useParams } from "react-router-dom"; 19 import { 20 AddIcon, 21 BackLink, 22 Box, 23 Button, 24 DataTable, 25 Grid, 26 GroupsIcon, 27 IAMPoliciesIcon, 28 PageLayout, 29 ScreenTitle, 30 SectionTitle, 31 Switch, 32 Tabs, 33 TrashIcon, 34 } from "mds"; 35 import { api } from "api"; 36 import { errorToHandler } from "api/errors"; 37 import { Group } from "api/consoleApi"; 38 import { 39 addUserToGroupPermissions, 40 CONSOLE_UI_RESOURCE, 41 createGroupPermissions, 42 editGroupMembersPermissions, 43 enableDisableGroupPermissions, 44 getGroupPermissions, 45 IAM_PAGES, 46 listUsersPermissions, 47 permissionTooltipHelper, 48 setGroupPoliciesPermissions, 49 viewPolicyPermissions, 50 viewUserPermissions, 51 } from "../../../common/SecureComponent/permissions"; 52 import { 53 hasPermission, 54 SecureComponent, 55 } from "../../../common/SecureComponent"; 56 import { decodeURLString, encodeURLString } from "../../../common/utils"; 57 import { setHelpName, setModalErrorSnackMessage } from "../../../systemSlice"; 58 import { useAppDispatch } from "../../../store"; 59 import { setSelectedPolicies } from "../Users/AddUsersSlice"; 60 import SetPolicy from "../Policies/SetPolicy"; 61 import AddGroupMember from "./AddGroupMember"; 62 import DeleteGroup from "./DeleteGroup"; 63 import SearchBox from "../Common/SearchBox"; 64 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 65 import HelpMenu from "../HelpMenu"; 66 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 67 68 export const formatPolicy = (policy: string = ""): string[] => { 69 if (policy.length <= 0) return []; 70 return policy.split(","); 71 }; 72 73 const GroupsDetails = () => { 74 const dispatch = useAppDispatch(); 75 const navigate = useNavigate(); 76 const params = useParams(); 77 78 const [groupDetails, setGroupDetails] = useState<Group>({}); 79 const [policyOpen, setPolicyOpen] = useState<boolean>(false); 80 const [usersOpen, setUsersOpen] = useState<boolean>(false); 81 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 82 const [memberFilter, setMemberFilter] = useState<string>(""); 83 const [currentTab, setCurrentTab] = useState<string>("members"); 84 85 const groupName = decodeURLString(params.groupName || ""); 86 87 const { members = [], policy = "", status: groupEnabled } = groupDetails; 88 89 const filteredMembers = members.filter((elementItem) => 90 elementItem.includes(memberFilter), 91 ); 92 93 const viewUser = hasPermission( 94 CONSOLE_UI_RESOURCE, 95 viewUserPermissions, 96 true, 97 ); 98 99 useEffect(() => { 100 dispatch(setHelpName("group_details")); 101 // eslint-disable-next-line react-hooks/exhaustive-deps 102 }, []); 103 104 useEffect(() => { 105 if (groupName) { 106 fetchGroupInfo(); 107 } 108 // eslint-disable-next-line react-hooks/exhaustive-deps 109 }, [groupName]); 110 111 const groupPolicies = formatPolicy(policy); 112 const isGroupEnabled = groupEnabled === "enabled"; 113 const memberActionText = members.length > 0 ? "Edit Members" : "Add Members"; 114 115 const getGroupDetails = hasPermission( 116 CONSOLE_UI_RESOURCE, 117 getGroupPermissions, 118 ); 119 120 const canEditGroupMembers = hasPermission( 121 CONSOLE_UI_RESOURCE, 122 editGroupMembersPermissions, 123 true, 124 ); 125 126 const canSetPolicies = hasPermission( 127 CONSOLE_UI_RESOURCE, 128 setGroupPoliciesPermissions, 129 true, 130 ); 131 132 const canViewPolicy = hasPermission( 133 CONSOLE_UI_RESOURCE, 134 viewPolicyPermissions, 135 true, 136 ); 137 138 function fetchGroupInfo() { 139 if (getGroupDetails) { 140 api.group 141 .groupInfo(encodeURLString(groupName)) 142 .then((res) => { 143 setGroupDetails(res.data); 144 }) 145 .catch((err) => { 146 dispatch(setModalErrorSnackMessage(errorToHandler(err.error))); 147 setGroupDetails({}); 148 }); 149 } 150 } 151 152 function toggleGroupStatus(nextStatus: boolean) { 153 return api.group 154 .updateGroup(encodeURLString(groupName), { 155 members: members, 156 status: nextStatus ? "enabled" : "disabled", 157 }) 158 .then(() => { 159 fetchGroupInfo(); 160 }) 161 .catch((err) => { 162 dispatch(setModalErrorSnackMessage(errorToHandler(err.error))); 163 }); 164 } 165 166 const groupsTabContent = ( 167 <Box 168 onMouseMove={() => { 169 dispatch(setHelpName("groups_members")); 170 }} 171 > 172 <SectionTitle 173 separator 174 sx={{ marginBottom: 15 }} 175 actions={ 176 <Box 177 sx={{ 178 display: "flex", 179 gap: 10, 180 }} 181 > 182 <SearchBox 183 placeholder={"Search members"} 184 onChange={(searchText) => { 185 setMemberFilter(searchText); 186 }} 187 value={memberFilter} 188 sx={{ 189 maxWidth: 280, 190 }} 191 /> 192 <SecureComponent 193 resource={CONSOLE_UI_RESOURCE} 194 scopes={addUserToGroupPermissions} 195 errorProps={{ disabled: true }} 196 > 197 <TooltipWrapper 198 tooltip={ 199 canEditGroupMembers 200 ? memberActionText 201 : permissionTooltipHelper( 202 createGroupPermissions, 203 "edit Group membership", 204 ) 205 } 206 > 207 <Button 208 id={"add-user-group"} 209 label={memberActionText} 210 variant="callAction" 211 icon={<AddIcon />} 212 onClick={() => { 213 setUsersOpen(true); 214 }} 215 disabled={!canEditGroupMembers} 216 /> 217 </TooltipWrapper> 218 </SecureComponent> 219 </Box> 220 } 221 > 222 Members 223 </SectionTitle> 224 <Grid item xs={12}> 225 <SecureComponent 226 resource={CONSOLE_UI_RESOURCE} 227 scopes={listUsersPermissions} 228 errorProps={{ disabled: true }} 229 > 230 <TooltipWrapper 231 tooltip={ 232 viewUser 233 ? "" 234 : permissionTooltipHelper( 235 viewUserPermissions, 236 "view User details", 237 ) 238 } 239 > 240 <DataTable 241 itemActions={[ 242 { 243 type: "view", 244 onClick: (userName) => { 245 navigate(`${IAM_PAGES.USERS}/${encodeURLString(userName)}`); 246 }, 247 isDisabled: !viewUser, 248 }, 249 ]} 250 columns={[{ label: "Access Key" }]} 251 selectedItems={[]} 252 isLoading={false} 253 records={filteredMembers} 254 entityName="Users" 255 /> 256 </TooltipWrapper> 257 </SecureComponent> 258 </Grid> 259 </Box> 260 ); 261 262 const policiesTabContent = ( 263 <Fragment> 264 <Box 265 onMouseMove={() => { 266 dispatch(setHelpName("groups_policies")); 267 }} 268 > 269 <SectionTitle 270 separator 271 sx={{ marginBottom: 15 }} 272 actions={ 273 <TooltipWrapper 274 tooltip={ 275 canSetPolicies 276 ? "Set Policies" 277 : permissionTooltipHelper( 278 setGroupPoliciesPermissions, 279 "assign Policies", 280 ) 281 } 282 > 283 <Button 284 id={"set-policies"} 285 label={`Set Policies`} 286 variant="callAction" 287 icon={<IAMPoliciesIcon />} 288 onClick={() => { 289 setPolicyOpen(true); 290 }} 291 disabled={!canSetPolicies} 292 /> 293 </TooltipWrapper> 294 } 295 > 296 Policies 297 </SectionTitle> 298 </Box> 299 <Grid item xs={12}> 300 <TooltipWrapper 301 tooltip={ 302 canViewPolicy 303 ? "" 304 : permissionTooltipHelper( 305 viewPolicyPermissions, 306 "view Policy details", 307 ) 308 } 309 > 310 <DataTable 311 itemActions={[ 312 { 313 type: "view", 314 onClick: (policy) => { 315 navigate(`${IAM_PAGES.POLICIES}/${encodeURLString(policy)}`); 316 }, 317 isDisabled: !canViewPolicy, 318 }, 319 ]} 320 columns={[{ label: "Policy" }]} 321 isLoading={false} 322 records={groupPolicies} 323 entityName="Policies" 324 /> 325 </TooltipWrapper> 326 </Grid> 327 </Fragment> 328 ); 329 330 return ( 331 <Fragment> 332 {policyOpen ? ( 333 <SetPolicy 334 open={policyOpen} 335 selectedGroups={[groupName]} 336 selectedUser={null} 337 closeModalAndRefresh={() => { 338 setPolicyOpen(false); 339 fetchGroupInfo(); 340 dispatch(setSelectedPolicies([])); 341 }} 342 /> 343 ) : null} 344 345 {usersOpen ? ( 346 <AddGroupMember 347 selectedGroup={groupName} 348 onSaveClick={() => {}} 349 title={memberActionText} 350 groupStatus={groupEnabled} 351 preSelectedUsers={members} 352 open={usersOpen} 353 onClose={() => { 354 setUsersOpen(false); 355 fetchGroupInfo(); 356 }} 357 /> 358 ) : null} 359 360 {deleteOpen && ( 361 <DeleteGroup 362 deleteOpen={deleteOpen} 363 selectedGroups={[groupName]} 364 closeDeleteModalAndRefresh={(isDelSuccess: boolean) => { 365 setDeleteOpen(false); 366 if (isDelSuccess) { 367 navigate(IAM_PAGES.GROUPS); 368 } 369 }} 370 /> 371 )} 372 <PageHeaderWrapper 373 label={ 374 <Fragment> 375 <BackLink 376 label={"Groups"} 377 onClick={() => navigate(IAM_PAGES.GROUPS)} 378 /> 379 </Fragment> 380 } 381 actions={<HelpMenu />} 382 /> 383 <PageLayout> 384 <Grid item xs={12}> 385 <ScreenTitle 386 icon={ 387 <Fragment> 388 <GroupsIcon width={40} /> 389 </Fragment> 390 } 391 title={groupName} 392 subTitle={null} 393 bottomBorder 394 actions={ 395 <Box 396 sx={{ 397 display: "flex", 398 fontSize: 14, 399 alignItems: "center", 400 gap: 15, 401 }} 402 > 403 <span>Group Status:</span> 404 <span id="group-status-label" style={{ fontWeight: "bold" }}> 405 {isGroupEnabled ? "Enabled" : "Disabled"} 406 </span> 407 <TooltipWrapper 408 tooltip={ 409 hasPermission( 410 CONSOLE_UI_RESOURCE, 411 enableDisableGroupPermissions, 412 true, 413 ) 414 ? "" 415 : permissionTooltipHelper( 416 enableDisableGroupPermissions, 417 "enable or disable Groups", 418 ) 419 } 420 > 421 <SecureComponent 422 resource={CONSOLE_UI_RESOURCE} 423 scopes={enableDisableGroupPermissions} 424 errorProps={{ disabled: true }} 425 matchAll 426 > 427 <Switch 428 indicatorLabels={["Enabled", "Disabled"]} 429 checked={isGroupEnabled} 430 value={"group_enabled"} 431 id="group-status" 432 name="group-status" 433 onChange={() => { 434 toggleGroupStatus(!isGroupEnabled); 435 }} 436 switchOnly 437 /> 438 </SecureComponent> 439 </TooltipWrapper> 440 441 <TooltipWrapper tooltip={"Delete Group"}> 442 <Button 443 id={"delete-user-group"} 444 variant="secondary" 445 icon={<TrashIcon />} 446 onClick={() => { 447 setDeleteOpen(true); 448 }} 449 /> 450 </TooltipWrapper> 451 </Box> 452 } 453 sx={{ marginBottom: 15 }} 454 /> 455 </Grid> 456 457 <Grid item xs={12}> 458 <Tabs 459 options={[ 460 { 461 tabConfig: { id: "members", label: "Members" }, 462 content: groupsTabContent, 463 }, 464 { 465 tabConfig: { id: "policies", label: "Policies" }, 466 content: policiesTabContent, 467 }, 468 ]} 469 currentTabOrPath={currentTab} 470 onTabClick={setCurrentTab} 471 /> 472 </Grid> 473 </PageLayout> 474 </Fragment> 475 ); 476 }; 477 478 export default GroupsDetails;