mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-17 13:46:47 +02:00
feat: add a button to download user access information (#4746)
This commit is contained in:
parent
53c40372dd
commit
7843c93dc5
17
frontend/src/component/accessOverview/AccessOverview.tsx
Normal file
17
frontend/src/component/accessOverview/AccessOverview.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
|
import { IconButton } from '@mui/material';
|
||||||
|
import { Download } from '@mui/icons-material';
|
||||||
|
import { useAccessOverviewApi } from 'hooks/api/actions/useAccessOverviewApi/useAccessOverviewApi';
|
||||||
|
|
||||||
|
export const AccessOverview = () => {
|
||||||
|
const { downloadCSV } = useAccessOverviewApi();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton onClick={downloadCSV}>
|
||||||
|
<Download />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
@ -6,6 +6,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
|
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
||||||
|
import { useAccessOverviewApi } from 'hooks/api/actions/useAccessOverviewApi/useAccessOverviewApi';
|
||||||
import { IUser } from 'interfaces/user';
|
import { IUser } from 'interfaces/user';
|
||||||
import { IRole } from 'interfaces/role';
|
import { IRole } from 'interfaces/role';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
@ -13,7 +14,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
|||||||
import { useUsersPlan } from 'hooks/useUsersPlan';
|
import { useUsersPlan } from 'hooks/useUsersPlan';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { Button, useMediaQuery } from '@mui/material';
|
import { Button, IconButton, Tooltip, useMediaQuery } from '@mui/material';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { UserTypeCell } from './UserTypeCell/UserTypeCell';
|
import { UserTypeCell } from './UserTypeCell/UserTypeCell';
|
||||||
import { useFlexLayout, useSortBy, useTable } from 'react-table';
|
import { useFlexLayout, useSortBy, useTable } from 'react-table';
|
||||||
@ -31,12 +32,15 @@ import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColum
|
|||||||
import { UserLimitWarning } from './UserLimitWarning/UserLimitWarning';
|
import { UserLimitWarning } from './UserLimitWarning/UserLimitWarning';
|
||||||
import { RoleCell } from 'component/common/Table/cells/RoleCell/RoleCell';
|
import { RoleCell } from 'component/common/Table/cells/RoleCell/RoleCell';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
|
import { Download } from '@mui/icons-material';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const UsersList = () => {
|
const UsersList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { users, roles, refetch, loading } = useUsers();
|
const { users, roles, refetch, loading } = useUsers();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { removeUser, userLoading, userApiErrors } = useAdminUsersApi();
|
const { removeUser, userLoading, userApiErrors } = useAdminUsersApi();
|
||||||
|
const { downloadCSV } = useAccessOverviewApi();
|
||||||
const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({
|
const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({
|
||||||
open: false,
|
open: false,
|
||||||
});
|
});
|
||||||
@ -47,6 +51,8 @@ const UsersList = () => {
|
|||||||
const [delUser, setDelUser] = useState<IUser>();
|
const [delUser, setDelUser] = useState<IUser>();
|
||||||
const { planUsers, isBillingUsers } = useUsersPlan(users);
|
const { planUsers, isBillingUsers } = useUsersPlan(users);
|
||||||
|
|
||||||
|
const accessOverviewEnabled = useUiFlag('accessOverview');
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
@ -263,6 +269,23 @@ const UsersList = () => {
|
|||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
/>
|
/>
|
||||||
<PageHeader.Divider />
|
<PageHeader.Divider />
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(accessOverviewEnabled)}
|
||||||
|
show={() => (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
title="Exports user access information"
|
||||||
|
arrow
|
||||||
|
describeChild
|
||||||
|
>
|
||||||
|
<IconButton onClick={downloadCSV}>
|
||||||
|
<Download />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
export const useAccessOverviewApi = () => {
|
||||||
|
const { loading, makeRequest, createRequest, errors } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadCSV = async () => {
|
||||||
|
const requestId = 'downloadCSV';
|
||||||
|
const req = createRequest(
|
||||||
|
'api/admin/access/overview',
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: { Accept: 'text/csv' },
|
||||||
|
},
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
|
||||||
|
const file = await (await makeRequest(req.caller, req.id)).blob();
|
||||||
|
const url = window.URL.createObjectURL(file);
|
||||||
|
window.location.assign(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
downloadCSV,
|
||||||
|
errors,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
@ -66,6 +66,7 @@ export type UiFlags = {
|
|||||||
doraMetrics?: boolean;
|
doraMetrics?: boolean;
|
||||||
variantTypeNumber?: boolean;
|
variantTypeNumber?: boolean;
|
||||||
privateProjects?: boolean;
|
privateProjects?: boolean;
|
||||||
|
accessOverview?: boolean;
|
||||||
[key: string]: boolean | Variant | undefined;
|
[key: string]: boolean | Variant | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ exports[`should create default config 1`] = `
|
|||||||
"isEnabled": [Function],
|
"isEnabled": [Function],
|
||||||
},
|
},
|
||||||
"flags": {
|
"flags": {
|
||||||
|
"accessOverview": false,
|
||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
"customRootRolesKillSwitch": false,
|
"customRootRolesKillSwitch": false,
|
||||||
@ -108,6 +109,7 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
"flagResolver": FlagResolver {
|
"flagResolver": FlagResolver {
|
||||||
"experiments": {
|
"experiments": {
|
||||||
|
"accessOverview": false,
|
||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
"customRootRolesKillSwitch": false,
|
"customRootRolesKillSwitch": false,
|
||||||
|
@ -29,6 +29,7 @@ export type IFlagKey =
|
|||||||
| 'featureNamingPattern'
|
| 'featureNamingPattern'
|
||||||
| 'doraMetrics'
|
| 'doraMetrics'
|
||||||
| 'variantTypeNumber'
|
| 'variantTypeNumber'
|
||||||
|
| 'accessOverview'
|
||||||
| 'privateProjects';
|
| 'privateProjects';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
@ -137,6 +138,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
|
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
accessOverview: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_ACCESS_OVERVIEW,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -44,6 +44,7 @@ process.nextTick(async () => {
|
|||||||
doraMetrics: true,
|
doraMetrics: true,
|
||||||
variantTypeNumber: true,
|
variantTypeNumber: true,
|
||||||
privateProjects: true,
|
privateProjects: true,
|
||||||
|
accessOverview: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user