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;