github.com/minio/console@v1.4.1/web-app/src/screens/Console/Groups/Groups.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 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 } from "react-router-dom"; 19 import { 20 AddIcon, 21 Button, 22 DeleteIcon, 23 GroupsIcon, 24 HelpBox, 25 IAMPoliciesIcon, 26 PageLayout, 27 UsersIcon, 28 DataTable, 29 Grid, 30 Box, 31 ProgressBar, 32 ActionLink, 33 } from "mds"; 34 35 import { api } from "api"; 36 import { stringSort } from "../../../utils/sortFunctions"; 37 import { actionsTray } from "../Common/FormComponents/common/styleLibrary"; 38 import { 39 applyPolicyPermissions, 40 CONSOLE_UI_RESOURCE, 41 createGroupPermissions, 42 deleteGroupPermissions, 43 displayGroupsPermissions, 44 getGroupPermissions, 45 IAM_PAGES, 46 permissionTooltipHelper, 47 } from "../../../common/SecureComponent/permissions"; 48 import { 49 hasPermission, 50 SecureComponent, 51 } from "../../../common/SecureComponent"; 52 import { errorToHandler } from "../../../api/errors"; 53 import withSuspense from "../Common/Components/withSuspense"; 54 import { encodeURLString } from "../../../common/utils"; 55 import { setErrorSnackMessage, setHelpName } from "../../../systemSlice"; 56 import { useAppDispatch } from "../../../store"; 57 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 58 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 59 import HelpMenu from "../HelpMenu"; 60 import SearchBox from "../Common/SearchBox"; 61 62 const DeleteGroup = withSuspense(React.lazy(() => import("./DeleteGroup"))); 63 const SetPolicy = withSuspense( 64 React.lazy(() => import("../Policies/SetPolicy")), 65 ); 66 67 const Groups = () => { 68 const dispatch = useAppDispatch(); 69 const navigate = useNavigate(); 70 71 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 72 const [loading, isLoading] = useState<boolean>(false); 73 const [records, setRecords] = useState<any[]>([]); 74 const [filter, setFilter] = useState<string>(""); 75 const [policyOpen, setPolicyOpen] = useState<boolean>(false); 76 const [checkedGroups, setCheckedGroups] = useState<string[]>([]); 77 78 useEffect(() => { 79 isLoading(true); 80 }, []); 81 82 useEffect(() => { 83 isLoading(true); 84 }, []); 85 86 useEffect(() => { 87 dispatch(setHelpName("groups")); 88 // eslint-disable-next-line react-hooks/exhaustive-deps 89 }, []); 90 91 const displayGroups = hasPermission( 92 CONSOLE_UI_RESOURCE, 93 displayGroupsPermissions, 94 ); 95 96 const deleteGroup = hasPermission( 97 CONSOLE_UI_RESOURCE, 98 deleteGroupPermissions, 99 ); 100 101 const getGroup = hasPermission(CONSOLE_UI_RESOURCE, getGroupPermissions); 102 103 const applyPolicy = hasPermission( 104 CONSOLE_UI_RESOURCE, 105 applyPolicyPermissions, 106 true, 107 ); 108 109 const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { 110 const { target: { value = "", checked = false } = {} } = e; 111 112 let elements: string[] = [...checkedGroups]; // We clone the checkedUsers array 113 114 if (checked) { 115 // If the user has checked this field we need to push this to checkedUsersList 116 elements.push(value); 117 } else { 118 // User has unchecked this field, we need to remove it from the list 119 elements = elements.filter((element) => element !== value); 120 } 121 122 setCheckedGroups(elements); 123 124 return elements; 125 }; 126 127 useEffect(() => { 128 if (loading) { 129 if (displayGroups) { 130 const fetchRecords = () => { 131 api.groups 132 .listGroups() 133 .then((res) => { 134 let resGroups: string[] = []; 135 if (res.data.groups) { 136 resGroups = res.data.groups.sort(stringSort); 137 } 138 setRecords(resGroups); 139 isLoading(false); 140 }) 141 .catch((err) => { 142 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 143 isLoading(false); 144 }); 145 }; 146 fetchRecords(); 147 } else { 148 isLoading(false); 149 } 150 } 151 }, [loading, dispatch, displayGroups]); 152 153 const closeDeleteModalAndRefresh = (refresh: boolean) => { 154 setDeleteOpen(false); 155 setCheckedGroups([]); 156 if (refresh) { 157 isLoading(true); 158 } 159 }; 160 161 const filteredRecords = records.filter((elementItem) => 162 elementItem.includes(filter), 163 ); 164 165 const viewAction = (group: any) => { 166 navigate(`${IAM_PAGES.GROUPS}/${encodeURLString(group)}`); 167 }; 168 169 const tableActions = [ 170 { 171 type: "view", 172 onClick: viewAction, 173 disableButtonFunction: () => !getGroup, 174 }, 175 { 176 type: "edit", 177 onClick: viewAction, 178 disableButtonFunction: () => !getGroup, 179 }, 180 ]; 181 182 return ( 183 <Fragment> 184 {deleteOpen && ( 185 <DeleteGroup 186 deleteOpen={deleteOpen} 187 selectedGroups={checkedGroups} 188 closeDeleteModalAndRefresh={closeDeleteModalAndRefresh} 189 /> 190 )} 191 {policyOpen && ( 192 <SetPolicy 193 open={policyOpen} 194 selectedGroups={checkedGroups} 195 selectedUser={null} 196 closeModalAndRefresh={() => { 197 setPolicyOpen(false); 198 }} 199 /> 200 )} 201 <PageHeaderWrapper label={"Groups"} actions={<HelpMenu />} /> 202 203 <PageLayout> 204 <Grid container> 205 <Grid item xs={12} sx={actionsTray.actionsTray}> 206 <SecureComponent 207 resource={CONSOLE_UI_RESOURCE} 208 scopes={displayGroupsPermissions} 209 errorProps={{ disabled: true }} 210 > 211 <SearchBox 212 placeholder={"Search Groups"} 213 onChange={setFilter} 214 value={filter} 215 sx={{ maxWidth: 380 }} 216 /> 217 </SecureComponent> 218 <Box 219 sx={{ 220 display: "flex", 221 }} 222 > 223 <SecureComponent 224 resource={CONSOLE_UI_RESOURCE} 225 scopes={applyPolicyPermissions} 226 matchAll 227 errorProps={{ disabled: true }} 228 > 229 <TooltipWrapper 230 tooltip={ 231 checkedGroups.length < 1 232 ? "Please select Groups on which you want to apply Policies" 233 : applyPolicy 234 ? "Select Policy" 235 : permissionTooltipHelper( 236 applyPolicyPermissions, 237 "apply policies to Groups", 238 ) 239 } 240 > 241 <Button 242 id={"assign-policy"} 243 onClick={() => { 244 setPolicyOpen(true); 245 }} 246 label={"Assign Policy"} 247 icon={<IAMPoliciesIcon />} 248 disabled={checkedGroups.length < 1 || !applyPolicy} 249 variant={"regular"} 250 /> 251 </TooltipWrapper> 252 </SecureComponent> 253 <SecureComponent 254 resource={CONSOLE_UI_RESOURCE} 255 scopes={deleteGroupPermissions} 256 matchAll 257 errorProps={{ disabled: true }} 258 > 259 <TooltipWrapper 260 tooltip={ 261 checkedGroups.length === 0 262 ? "Select Groups to delete" 263 : getGroup 264 ? "Delete Selected" 265 : permissionTooltipHelper( 266 getGroupPermissions, 267 "delete Groups", 268 ) 269 } 270 > 271 <Button 272 id="delete-selected-groups" 273 onClick={() => { 274 setDeleteOpen(true); 275 }} 276 label={"Delete Selected"} 277 icon={<DeleteIcon />} 278 variant="secondary" 279 disabled={checkedGroups.length === 0 || !getGroup} 280 /> 281 </TooltipWrapper> 282 </SecureComponent> 283 <SecureComponent 284 resource={CONSOLE_UI_RESOURCE} 285 scopes={createGroupPermissions} 286 matchAll 287 errorProps={{ disabled: true }} 288 > 289 <TooltipWrapper tooltip={"Create Group"}> 290 <Button 291 id={"create-group"} 292 label={"Create Group"} 293 variant="callAction" 294 icon={<AddIcon />} 295 onClick={() => { 296 navigate(`${IAM_PAGES.GROUPS_ADD}`); 297 }} 298 /> 299 </TooltipWrapper> 300 </SecureComponent> 301 </Box> 302 </Grid> 303 {loading && <ProgressBar />} 304 {!loading && ( 305 <Fragment> 306 {records.length > 0 && ( 307 <Fragment> 308 <Grid item xs={12} sx={{ marginBottom: 15 }}> 309 <SecureComponent 310 resource={CONSOLE_UI_RESOURCE} 311 scopes={displayGroupsPermissions} 312 errorProps={{ disabled: true }} 313 > 314 <DataTable 315 itemActions={tableActions} 316 columns={[{ label: "Name" }]} 317 isLoading={loading} 318 selectedItems={checkedGroups} 319 onSelect={ 320 deleteGroup || getGroup ? selectionChanged : undefined 321 } 322 records={filteredRecords} 323 entityName="Groups" 324 idField="" 325 /> 326 </SecureComponent> 327 </Grid> 328 <Grid item xs={12}> 329 <HelpBox 330 title={"Groups"} 331 iconComponent={<GroupsIcon />} 332 help={ 333 <Fragment> 334 A group can have one attached IAM policy, where all 335 users with membership in that group inherit that 336 policy. Groups support more simplified management of 337 user permissions on the MinIO Tenant. 338 <br /> 339 <br /> 340 You can learn more at our{" "} 341 <a 342 href="https://min.io/docs/minio/linux/administration/identity-access-management/minio-group-management.html?ref=con" 343 target="_blank" 344 rel="noopener" 345 > 346 documentation 347 </a> 348 . 349 </Fragment> 350 } 351 /> 352 </Grid> 353 </Fragment> 354 )} 355 {records.length === 0 && ( 356 <Grid container> 357 <Grid item xs={8}> 358 <HelpBox 359 title={"Groups"} 360 iconComponent={<UsersIcon />} 361 help={ 362 <Fragment> 363 A group can have one attached IAM policy, where all 364 users with membership in that group inherit that 365 policy. Groups support more simplified management of 366 user permissions on the MinIO Tenant. 367 <SecureComponent 368 resource={CONSOLE_UI_RESOURCE} 369 scopes={createGroupPermissions} 370 matchAll 371 > 372 <br /> 373 <br /> 374 To get started,{" "} 375 <ActionLink 376 onClick={() => { 377 navigate(`${IAM_PAGES.GROUPS_ADD}`); 378 }} 379 > 380 Create a Group 381 </ActionLink> 382 . 383 </SecureComponent> 384 </Fragment> 385 } 386 /> 387 </Grid> 388 </Grid> 389 )} 390 </Fragment> 391 )} 392 </Grid> 393 </PageLayout> 394 </Fragment> 395 ); 396 }; 397 398 export default Groups;