Skip to content

Commit

Permalink
feat: API Tokens limit - UI (#7561)
Browse files Browse the repository at this point in the history
When approaching limit or limit reached for the number of API tokens, we
show a corresponding message.
  • Loading branch information
Tymek authored Jul 12, 2024
1 parent f1b3758 commit e7627be
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { render } from 'utils/testRenderer';
import { screen, waitFor } from '@testing-library/react';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { CreateApiToken } from './CreateApiToken';
import {
ADMIN,
CREATE_CLIENT_API_TOKEN,
CREATE_FRONTEND_API_TOKEN,
} from '@server/types/permissions';

const permissions = [
{ permission: CREATE_CLIENT_API_TOKEN },
{ permission: CREATE_FRONTEND_API_TOKEN },
{ permission: ADMIN },
];

const server = testServerSetup();

const setupApi = (existingTokensCount: number) => {
testServerRoute(server, '/api/admin/ui-config', {
flags: {
resourceLimits: true,
},
resourceLimits: {
apiTokens: 1,
},
versionInfo: {
current: { enterprise: 'version' },
},
});

testServerRoute(server, '/api/admin/api-tokens', {
tokens: [...Array(existingTokensCount).keys()].map((_, i) => ({
secret: `token${i}`,
})),
});
};

test('Enabled new token button when limits, version and permission allow for it', async () => {
setupApi(0);
render(<CreateApiToken />, {
permissions,
});

const button = await screen.findByText('Create token');
expect(button).toBeDisabled();

await waitFor(async () => {
const button = await screen.findByText('Create token');
expect(button).not.toBeDisabled();
});
});

test('Token limit reached', async () => {
setupApi(1);
render(<CreateApiToken />, {
permissions,
});

await screen.findByText('You have reached the limit for API tokens');

const button = await screen.findByText('Create token');
expect(button).toBeDisabled();
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import ApiTokenForm from '../ApiTokenForm/ApiTokenForm';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
Expand All @@ -22,17 +25,45 @@ import {
CREATE_CLIENT_API_TOKEN,
CREATE_FRONTEND_API_TOKEN,
} from '@server/types/permissions';
import { Limit } from 'component/common/Limit/Limit';

const pageTitle = 'Create API token';
interface ICreateApiTokenProps {
modal?: boolean;
}

const StyledLimit = styled(Limit)(({ theme }) => ({
margin: theme.spacing(2, 0, 4),
}));

const useApiTokenLimit = () => {
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const { tokens, loading: loadingTokens } = useApiTokens();
const { uiConfig, loading: loadingConfig } = useUiConfig();
const apiTokensLimit = uiConfig.resourceLimits.apiTokens;

return {
resourceLimitsEnabled,
limit: apiTokensLimit,
currentValue: tokens.length,
limitReached: resourceLimitsEnabled && tokens.length >= apiTokensLimit,
loading: loadingConfig || loadingTokens,
};
};

export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const navigate = useNavigate();
const [showConfirm, setShowConfirm] = useState(false);
const [token, setToken] = useState('');
const {
resourceLimitsEnabled,
limit,
currentValue,
limitReached,
loading: loadingLimit,
} = useApiTokenLimit();

const {
getApiTokenPayload,
Expand All @@ -50,7 +81,7 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
apiTokenTypes,
} = useApiTokenForm();

const { createToken, loading } = useApiTokensApi();
const { createToken, loading: loadingCreateToken } = useApiTokensApi();
const { refetch } = useApiTokens();

usePageTitle(pageTitle);
Expand Down Expand Up @@ -96,7 +127,7 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {

return (
<FormTemplate
loading={loading}
loading={loadingCreateToken}
title={pageTitle}
modal={modal}
description="Unleash SDKs use API tokens to authenticate to the Unleash API. Client SDKs need a token with 'client privileges', which allows them to fetch feature flag configurations and post usage metrics."
Expand All @@ -116,6 +147,9 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
CREATE_CLIENT_API_TOKEN,
CREATE_FRONTEND_API_TOKEN,
]}
disabled={
limitReached || loadingLimit || loadingCreateToken
}
/>
}
>
Expand All @@ -142,6 +176,17 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
environment={environment}
setEnvironment={setEnvironment}
/>
<ConditionallyRender
condition={resourceLimitsEnabled}
show={
<StyledLimit
name='API tokens'
shortName='tokens'
currentValue={currentValue}
limit={limit}
/>
}
/>
</ApiTokenForm>
<ConfirmToken
open={showConfirm}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/component/common/Limit/Limit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export const Limit: FC<{
limit: number;
currentValue: number;
onClose?: () => void;
}> = ({ name, shortName, limit, currentValue, onClose }) => {
className?: string;
}> = ({ name, shortName, limit, currentValue, onClose, className }) => {
const percentageLimit = Math.floor((currentValue / limit) * 100);
const belowLimit = currentValue < limit;
const threshold = 80;
Expand All @@ -78,7 +79,7 @@ export const Limit: FC<{
}

return (
<StyledBox>
<StyledBox className={className}>
<Header>
<ConditionallyRender
condition={belowLimit}
Expand Down

0 comments on commit e7627be

Please sign in to comment.