1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-28 00:06:53 +01:00

feat: show orphaned API tokens (#7569)

Add a visual indication that a token was scoped to projects that have
been deleted.
This commit is contained in:
Tymoteusz Czech 2024-07-11 14:06:22 +02:00 committed by GitHub
parent b9c3d101ba
commit d440d3230a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 99 additions and 5 deletions

View File

@ -1,5 +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';
describe('ProjectsList', () => {
@ -55,4 +56,58 @@ 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

@ -1,11 +1,13 @@
import { Fragment, type FC } from 'react';
import { styled } from '@mui/material';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
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 { Fragment, type FC } from 'react';
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',
@ -15,12 +17,28 @@ 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',
});
interface IProjectsListProps {
project?: string;
projects?: string | string[];
secret?: string;
}
export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
export const ProjectsList: FC<IProjectsListProps> = ({
projects,
project,
secret,
}) => {
const { searchQuery } = useSearchHighlightContext();
const projectsList =
@ -67,16 +85,36 @@ export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
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='ALL current and future projects'
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.'
)
}
placement='bottom'
arrow
>
<span>
<StyledContainer>
<Highlighter search={searchQuery}>*</Highlighter>
</span>
<ConditionallyRender
condition={isOrphanedToken}
show={<StyledErrorIcon aria-label='Orphaned token' />}
/>
</StyledContainer>
</HtmlTooltip>
</TextCell>
);

View File

@ -61,6 +61,7 @@ export const useApiTokenTable = (
<ProjectsList
project={props.row.original.project}
projects={props.row.original.projects}
secret={props.row.original.secret}
/>
),
width: 160,