mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02: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:
parent
b9c3d101ba
commit
d440d3230a
@ -1,5 +1,6 @@
|
|||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
|
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
|
||||||
|
|
||||||
describe('ProjectsList', () => {
|
describe('ProjectsList', () => {
|
||||||
@ -55,4 +56,58 @@ describe('ProjectsList', () => {
|
|||||||
|
|
||||||
expect(container.textContent).toEqual('*');
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
import { Fragment, type FC } from 'react';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
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 { Link } from 'react-router-dom';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const StyledLink = styled(Link)(({ theme }) => ({
|
const StyledLink = styled(Link)(({ theme }) => ({
|
||||||
textDecoration: 'none',
|
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 {
|
interface IProjectsListProps {
|
||||||
project?: string;
|
project?: string;
|
||||||
projects?: string | 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 { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
const projectsList =
|
const projectsList =
|
||||||
@ -67,16 +85,36 @@ export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
|
|||||||
return <LinkCell to={`/projects/${item}`} title={item} />;
|
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 (
|
return (
|
||||||
<TextCell>
|
<TextCell>
|
||||||
<HtmlTooltip
|
<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'
|
placement='bottom'
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<span>
|
<StyledContainer>
|
||||||
<Highlighter search={searchQuery}>*</Highlighter>
|
<Highlighter search={searchQuery}>*</Highlighter>
|
||||||
</span>
|
<ConditionallyRender
|
||||||
|
condition={isOrphanedToken}
|
||||||
|
show={<StyledErrorIcon aria-label='Orphaned token' />}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
</HtmlTooltip>
|
</HtmlTooltip>
|
||||||
</TextCell>
|
</TextCell>
|
||||||
);
|
);
|
||||||
|
@ -61,6 +61,7 @@ export const useApiTokenTable = (
|
|||||||
<ProjectsList
|
<ProjectsList
|
||||||
project={props.row.original.project}
|
project={props.row.original.project}
|
||||||
projects={props.row.original.projects}
|
projects={props.row.original.projects}
|
||||||
|
secret={props.row.original.secret}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
width: 160,
|
width: 160,
|
||||||
|
Loading…
Reference in New Issue
Block a user