1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: Orphaned tokens - new API tokens list icon (#7693)

Moving warning icon from "projects" column, to left icon.
This commit is contained in:
Tymoteusz Czech 2024-08-01 16:31:42 +02:00 committed by GitHub
parent fc02581a10
commit 8aa812e0f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 95 deletions

View File

@ -0,0 +1,46 @@
import { render } from 'utils/testRenderer';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ApiTokenIcon } from './ApiTokenIcon';
describe('ApiTokenIcon', () => {
it('should show warning icon if it is an orphaned token', async () => {
render(
<ApiTokenIcon secret='test:development.be7536c3a160ff15e3a92da45de531dd54bc1ae15d8455c0476f086b' />,
);
const errorIcon = await screen.findByTestId('orphaned-token-icon');
expect(errorIcon).toBeInTheDocument();
});
it('should show tooltip with warning message if it is an orphaned token', async () => {
const user = userEvent.setup();
render(
<ApiTokenIcon secret='test:development.be7536c3a160ff15e3a92da45de531dd54bc1ae15d8455c0476f086b' />,
);
const errorIcon = await screen.findByTestId('orphaned-token-icon');
user.hover(errorIcon);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent(/orphaned token/);
});
it('should not show warning icon if token is in v1 format', async () => {
render(
<ApiTokenIcon secret='be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178' />,
);
const errorIcon = await screen.queryByTestId('orphaned-token-icon');
expect(errorIcon).toBeNull();
});
it('should not show warning for true wildcard tokens', async () => {
render(
<ApiTokenIcon secret='*:development.be7536c3a160ff15e3a92da45de531dd54bc1ae15d8455c0476f086b' />,
);
const errorIcon = await screen.queryByTestId('orphaned-token-icon');
expect(errorIcon).toBeNull();
});
});

View File

@ -0,0 +1,46 @@
import type { FC } from 'react';
import KeyIcon from '@mui/icons-material/Key';
import WarningIcon from '@mui/icons-material/WarningAmber';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
interface IApiTokenIconProps {
project?: string;
projects?: string | string[];
secret?: string;
}
export const ApiTokenIcon: FC<IApiTokenIconProps> = ({ secret }) => {
const tokenFormat = secret?.includes(':') ? 'v2' : 'v1'; // see https://docs.getunleash.io/reference/api-tokens-and-client-keys#format
const isWildcardToken = secret?.startsWith('*:');
const isOrphanedToken = tokenFormat === 'v2' && !isWildcardToken;
if (isOrphanedToken) {
return (
<IconCell
icon={
<HtmlTooltip
title={
<p>
This is an orphaned token. All of its original
projects have been deleted and it now has access
to all current and future projects. You should
stop using this token and delete it.
</p>
}
placement='bottom-start'
arrow
>
<WarningIcon
aria-label='Orphaned token'
color='warning'
data-testid='orphaned-token-icon'
/>
</HtmlTooltip>
}
/>
);
}
return <IconCell icon={<KeyIcon color='disabled' />} />;
};

View File

@ -1,7 +1,6 @@
import { render } from 'utils/testRenderer';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
import { ProjectsList } from './ProjectsList';
describe('ProjectsList', () => {
it('should prioritize new "projects" array over deprecated "project"', async () => {
@ -56,58 +55,4 @@ describe('ProjectsList', () => {
expect(container.textContent).toEqual('*');
});
describe('orphaned tokens', () => {
it('should show warning icon if it is an orphaned token', async () => {
render(
<ProjectsList
projects={[]}
secret='test:development.be7536c3a160ff15e3a92da45de531dd54bc1ae15d8455c0476f086b'
/>,
);
const errorIcon = await screen.findByTestId('ErrorIcon');
expect(errorIcon).toBeInTheDocument();
});
it('should show tooltip with warning message if it is an orphaned token', async () => {
const user = userEvent.setup();
render(
<ProjectsList
projects={[]}
secret='test:development.be7536c3a160ff15e3a92da45de531dd54bc1ae15d8455c0476f086b'
/>,
);
const errorIcon = await screen.findByTestId('ErrorIcon');
user.hover(errorIcon);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent(/orphaned token/);
});
it('should not show warning icon if token is in v1 format', async () => {
render(
<ProjectsList
projects={[]}
secret='be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178'
/>,
);
const errorIcon = await screen.queryByTestId('ErrorIcon');
expect(errorIcon).toBeNull();
});
it('should not show warning for wildcard tokens', async () => {
render(
<ProjectsList
projects={[]}
secret='*:development.be7536c3a160ff15e3a92da45de531dd54bc1ae15d8455c0476f086b'
/>,
);
const errorIcon = await screen.queryByTestId('ErrorIcon');
expect(errorIcon).toBeNull();
});
});
});

View File

@ -5,9 +5,7 @@ import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import ErrorIcon from '@mui/icons-material/Error';
import { Link } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',
@ -17,12 +15,6 @@ const StyledLink = styled(Link)(({ theme }) => ({
},
}));
const StyledErrorIcon = styled(ErrorIcon)(({ theme }) => ({
color: theme.palette.error.main,
marginBottom: theme.spacing(0.5),
marginLeft: theme.spacing(0.5),
}));
const StyledContainer = styled('div')({
display: 'flex',
alignItems: 'center',
@ -31,14 +23,9 @@ const StyledContainer = styled('div')({
interface IProjectsListProps {
project?: string;
projects?: string | string[];
secret?: string;
}
export const ProjectsList: FC<IProjectsListProps> = ({
projects,
project,
secret,
}) => {
export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
const { searchQuery } = useSearchHighlightContext();
const projectsList =
@ -85,35 +72,15 @@ export const ProjectsList: FC<IProjectsListProps> = ({
return <LinkCell to={`/projects/${item}`} title={item} />;
}
const tokenFormat = secret?.includes(':') ? 'v2' : 'v1'; // see https://docs.getunleash.io/reference/api-tokens-and-client-keys#format
const isWildcardToken = secret?.startsWith('*:');
const isOrphanedToken = tokenFormat === 'v2' && !isWildcardToken;
return (
<TextCell>
<HtmlTooltip
title={
isOrphanedToken ? (
<>
This is an orphaned token. All of its original
projects have been deleted and it now has access to
all current and future projects. You should stop
using this token and delete it. It will lose access
to all projects at a later date.
</>
) : (
'ALL current and future projects.'
)
}
title='ALL current and future projects.'
placement='bottom'
arrow
>
<StyledContainer>
<Highlighter search={searchQuery}>*</Highlighter>
<ConditionallyRender
condition={isOrphanedToken}
show={<StyledErrorIcon aria-label='Orphaned token' />}
/>
</StyledContainer>
</HtmlTooltip>
</TextCell>

View File

@ -2,7 +2,6 @@ import { useMemo } from 'react';
import type { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import {
useTable,
@ -12,7 +11,7 @@ import {
} from 'react-table';
import { sortTypes } from 'utils/sortTypes';
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
import Key from '@mui/icons-material/Key';
import { ApiTokenIcon } from 'component/admin/apiToken/ApiTokenIcon/ApiTokenIcon';
export const useApiTokenTable = (
tokens: IApiToken[],
@ -27,7 +26,9 @@ export const useApiTokenTable = (
return [
{
id: 'Icon',
Cell: () => <IconCell icon={<Key color='disabled' />} />,
Cell: (props: any) => (
<ApiTokenIcon secret={props.row.original.secret} />
),
disableSortBy: true,
disableGlobalFilter: true,
width: 50,
@ -61,7 +62,6 @@ export const useApiTokenTable = (
<ProjectsList
project={props.row.original.project}
projects={props.row.original.projects}
secret={props.row.original.secret}
/>
),
width: 160,