function RolesList()

in superset-frontend/src/pages/RolesList/index.tsx [71:505]


function RolesList({ addDangerToast, addSuccessToast, user }: RolesListProps) {
  const theme = useTheme();
  const {
    state: {
      loading,
      resourceCount: rolesCount,
      resourceCollection: roles,
      bulkSelectEnabled,
    },
    fetchData,
    refreshData,
    toggleBulkSelect,
  } = useListViewResource<RoleObject>(
    'security/roles/search',
    t('Role'),
    addDangerToast,
    false,
  );
  const [modalState, setModalState] = useState({
    edit: false,
    add: false,
    duplicate: false,
  });
  const openModal = (type: ModalType) =>
    setModalState(prev => ({ ...prev, [type]: true }));
  const closeModal = (type: ModalType) =>
    setModalState(prev => ({ ...prev, [type]: false }));

  const [currentRole, setCurrentRole] = useState<RoleObject | null>(null);
  const [roleCurrentlyDeleting, setRoleCurrentlyDeleting] =
    useState<RoleObject | null>(null);
  const [permissions, setPermissions] = useState<FormattedPermission[]>([]);
  const [users, setUsers] = useState<UserObject[]>([]);
  const [loadingState, setLoadingState] = useState({
    permissions: true,
    users: true,
  });

  const isAdmin = useMemo(() => isUserAdmin(user), [user]);

  const fetchPermissions = useCallback(async () => {
    try {
      const pageSize = 100;

      const fetchPage = async (pageIndex: number) => {
        const response = await SupersetClient.get({
          endpoint: `api/v1/security/permissions-resources/?q=(page_size:${pageSize},page:${pageIndex})`,
        });

        return {
          count: response.json.count,
          results: response.json.result.map(
            ({ permission, view_menu, id }: PermissionResource) => ({
              label: `${permission.name.replace(/_/g, ' ')} ${view_menu.name.replace(/_/g, ' ')}`,
              value: `${permission.name}__${view_menu.name}`,
              id,
            }),
          ),
        };
      };

      const initialResponse = await fetchPage(0);
      const totalPermissions = initialResponse.count;
      const firstPageResults = initialResponse.results;

      if (firstPageResults.length >= totalPermissions) {
        setPermissions(firstPageResults);
        return;
      }

      const totalPages = Math.ceil(totalPermissions / pageSize);

      const permissionRequests = Array.from(
        { length: totalPages - 1 },
        (_, i) => fetchPage(i + 1),
      );
      const remainingResults = await Promise.all(permissionRequests);

      setPermissions([
        ...firstPageResults,
        ...remainingResults.flatMap(res => res.results),
      ]);
    } catch (err) {
      addDangerToast(t('Error while fetching permissions'));
    } finally {
      setLoadingState(prev => ({ ...prev, permissions: false }));
    }
  }, []);

  const fetchUsers = useCallback(async () => {
    try {
      const pageSize = 100;

      const fetchPage = async (pageIndex: number) => {
        const response = await SupersetClient.get({
          endpoint: `api/v1/security/users/?q=(page_size:${pageSize},page:${pageIndex})`,
        });
        return response.json;
      };

      const initialResponse = await fetchPage(0);
      const totalUsers = initialResponse.count;
      const firstPageResults = initialResponse.result;

      if (pageSize >= totalUsers) {
        setUsers(firstPageResults);
        return;
      }

      const totalPages = Math.ceil(totalUsers / pageSize);

      const userRequests = Array.from({ length: totalPages - 1 }, (_, i) =>
        fetchPage(i + 1),
      );
      const remainingResults = await Promise.all(userRequests);

      setUsers([
        ...firstPageResults,
        ...remainingResults.flatMap(res => res.result),
      ]);
    } catch (err) {
      addDangerToast(t('Error while fetching users'));
    } finally {
      setLoadingState(prev => ({ ...prev, users: false }));
    }
  }, []);

  useEffect(() => {
    fetchPermissions();
  }, [fetchPermissions]);

  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  const handleRoleDelete = async ({ id, name }: RoleObject) => {
    try {
      await SupersetClient.delete({
        endpoint: `/api/v1/security/roles/${id}`,
      });

      refreshData();
      setRoleCurrentlyDeleting(null);
      addSuccessToast(t('Deleted role: %s', name));
    } catch (error) {
      addDangerToast(t('There was an issue deleting %s', name));
    }
  };

  const handleBulkRolesDelete = async (rolesToDelete: RoleObject[]) => {
    const deletedRoleNames: string[] = [];

    await Promise.all(
      rolesToDelete.map(async role => {
        try {
          await SupersetClient.delete({
            endpoint: `api/v1/security/roles/${role.id}`,
          });

          deletedRoleNames.push(role.name);
        } catch (error) {
          addDangerToast(t('Error deleting %s', role.name));
        }
      }),
    );

    if (deletedRoleNames.length > 0) {
      addSuccessToast(t('Deleted roles: %s', deletedRoleNames.join(', ')));
    }

    refreshData();
  };

  const initialSort = [{ id: 'name', desc: true }];
  const columns = useMemo(
    () => [
      {
        accessor: 'name',
        Header: t('Name'),
        Cell: ({
          row: {
            original: { name },
          },
        }: any) => <span>{name}</span>,
      },
      {
        accessor: 'user_ids',
        Header: t('Users'),
        hidden: true,
        Cell: ({ row: { original } }: any) => original.user_ids.join(', '),
      },
      {
        accessor: 'permission_ids',
        Header: t('Permissions'),
        hidden: true,
        Cell: ({ row: { original } }: any) =>
          original.permission_ids.join(', '),
      },
      {
        Cell: ({ row: { original } }: any) => {
          const handleEdit = () => {
            setCurrentRole(original);
            openModal(ModalType.EDIT);
          };
          const handleDelete = () => setRoleCurrentlyDeleting(original);
          const handleDuplicate = () => {
            setCurrentRole(original);
            openModal(ModalType.DUPLICATE);
          };

          const actions = isAdmin
            ? [
                {
                  label: 'role-list-edit-action',
                  tooltip: t('Edit role'),
                  placement: 'bottom',
                  icon: 'EditOutlined',
                  onClick: handleEdit,
                },
                {
                  label: 'role-list-duplicate-action',
                  tooltip: t('Duplicate role'),
                  placement: 'bottom',
                  icon: 'CopyOutlined',
                  onClick: handleDuplicate,
                },
                {
                  label: 'role-list-delete-action',
                  tooltip: t('Delete role'),
                  placement: 'bottom',
                  icon: 'DeleteOutlined',
                  onClick: handleDelete,
                },
              ]
            : [];

          return <ActionsBar actions={actions as ActionProps[]} />;
        },
        Header: t('Actions'),
        id: 'actions',
        disableSortBy: true,
        hidden: !isAdmin,
        size: 'xl',
      },
    ],
    [isAdmin],
  );

  const subMenuButtons: SubMenuProps['buttons'] = [];

  if (isAdmin) {
    subMenuButtons.push(
      {
        name: (
          <>
            <Icons.PlusOutlined
              iconColor={theme.colors.primary.light5}
              iconSize="m"
              css={css`
                margin: auto ${theme.gridUnit * 2}px auto 0;
                vertical-align: text-top;
              `}
            />
            {t('Role')}
          </>
        ),
        buttonStyle: 'primary',
        onClick: () => {
          openModal(ModalType.ADD);
        },
        loading: loadingState.permissions,
        'data-test': 'add-role-button',
      },
      {
        name: t('Bulk select'),
        onClick: toggleBulkSelect,
        buttonStyle: 'secondary',
      },
    );
  }

  const filters: Filters = useMemo(
    () => [
      {
        Header: t('Name'),
        key: 'name',
        id: 'name',
        input: 'search',
        operator: FilterOperator.Contains,
      },
      {
        Header: t('Users'),
        key: 'user_ids',
        id: 'user_ids',
        input: 'select',
        operator: FilterOperator.RelationOneMany,
        unfilteredLabel: t('All'),
        selects: users?.map(user => ({
          label: user.username,
          value: user.id,
        })),
        loading: loadingState.users,
      },
      {
        Header: t('Permissions'),
        key: 'permission_ids',
        id: 'permission_ids',
        input: 'select',
        operator: FilterOperator.RelationOneMany,
        unfilteredLabel: t('All'),
        selects: permissions?.map(permission => ({
          label: permission.label,
          value: permission.id,
        })),
        loading: loadingState.permissions,
      },
    ],
    [permissions, users, loadingState.users, loadingState.permissions],
  );

  const emptyState = {
    title: t('No roles yet'),
    image: 'filter-results.svg',
    ...(isAdmin && {
      buttonAction: () => {
        openModal(ModalType.ADD);
      },
      buttonText: (
        <>
          <Icons.PlusOutlined
            iconColor={theme.colors.primary.light5}
            iconSize="m"
            css={css`
              margin: auto ${theme.gridUnit * 2}px auto 0;
              vertical-align: text-top;
            `}
          />
          {t('Role')}
        </>
      ),
    }),
  };

  return (
    <>
      <SubMenu name={t('List Roles')} buttons={subMenuButtons} />
      <RoleListAddModal
        onHide={() => closeModal(ModalType.ADD)}
        show={modalState.add}
        onSave={() => {
          refreshData();
          closeModal(ModalType.ADD);
        }}
        permissions={permissions}
      />
      {modalState.edit && currentRole && (
        <RoleListEditModal
          role={currentRole}
          show={modalState.edit}
          onHide={() => closeModal(ModalType.EDIT)}
          onSave={() => {
            refreshData();
            closeModal(ModalType.EDIT);
            fetchUsers();
          }}
          permissions={permissions}
          users={users}
        />
      )}
      {modalState.duplicate && currentRole && (
        <RoleListDuplicateModal
          role={currentRole}
          show={modalState.duplicate}
          onHide={() => closeModal(ModalType.DUPLICATE)}
          onSave={() => {
            refreshData();
            closeModal(ModalType.DUPLICATE);
          }}
        />
      )}
      {roleCurrentlyDeleting && (
        <DeleteModal
          description={t('This action will permanently delete the role.')}
          onConfirm={() => {
            if (roleCurrentlyDeleting) {
              handleRoleDelete(roleCurrentlyDeleting);
            }
          }}
          onHide={() => setRoleCurrentlyDeleting(null)}
          open
          title={t('Delete Role?')}
        />
      )}
      <ConfirmStatusChange
        title={t('Please confirm')}
        description={t('Are you sure you want to delete the selected roles?')}
        onConfirm={handleBulkRolesDelete}
      >
        {confirmDelete => {
          const bulkActions: ListViewProps['bulkActions'] = isAdmin
            ? [
                {
                  key: 'delete',
                  name: t('Delete'),
                  onSelect: confirmDelete,
                  type: 'danger',
                },
              ]
            : [];

          return (
            <ListView<RoleObject>
              className="role-list-view"
              columns={columns}
              count={rolesCount}
              data={roles}
              fetchData={fetchData}
              filters={filters}
              initialSort={initialSort}
              loading={loading}
              pageSize={PAGE_SIZE}
              bulkActions={bulkActions}
              bulkSelectEnabled={bulkSelectEnabled}
              disableBulkSelect={toggleBulkSelect}
              addDangerToast={addDangerToast}
              addSuccessToast={addSuccessToast}
              emptyState={emptyState}
              refreshData={refreshData}
            />
          );
        }}
      </ConfirmStatusChange>
    </>
  );
}