1
0
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:
Simon Hornby 2023-09-15 11:51:29 +02:00 committed by GitHub
parent 53c40372dd
commit 7843c93dc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 80 additions and 1 deletions

View 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>
);
};

View File

@ -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"

View File

@ -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,
};
};

View File

@ -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;
}; };

View File

@ -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,

View File

@ -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 = {

View File

@ -44,6 +44,7 @@ process.nextTick(async () => {
doraMetrics: true, doraMetrics: true,
variantTypeNumber: true, variantTypeNumber: true,
privateProjects: true, privateProjects: true,
accessOverview: true,
}, },
}, },
authentication: { authentication: {