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:
parent
fc02581a10
commit
8aa812e0f5
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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' />} />;
|
||||||
|
};
|
@ -1,7 +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 './ProjectsList';
|
||||||
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
|
|
||||||
|
|
||||||
describe('ProjectsList', () => {
|
describe('ProjectsList', () => {
|
||||||
it('should prioritize new "projects" array over deprecated "project"', async () => {
|
it('should prioritize new "projects" array over deprecated "project"', async () => {
|
||||||
@ -56,58 +55,4 @@ 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -5,9 +5,7 @@ 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 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',
|
||||||
@ -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')({
|
const StyledContainer = styled('div')({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -31,14 +23,9 @@ const StyledContainer = styled('div')({
|
|||||||
interface IProjectsListProps {
|
interface IProjectsListProps {
|
||||||
project?: string;
|
project?: string;
|
||||||
projects?: string | string[];
|
projects?: string | string[];
|
||||||
secret?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectsList: FC<IProjectsListProps> = ({
|
export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
|
||||||
projects,
|
|
||||||
project,
|
|
||||||
secret,
|
|
||||||
}) => {
|
|
||||||
const { searchQuery } = useSearchHighlightContext();
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
const projectsList =
|
const projectsList =
|
||||||
@ -85,35 +72,15 @@ export const ProjectsList: FC<IProjectsListProps> = ({
|
|||||||
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={
|
title='ALL current and future projects.'
|
||||||
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
|
||||||
>
|
>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<Highlighter search={searchQuery}>*</Highlighter>
|
<Highlighter search={searchQuery}>*</Highlighter>
|
||||||
<ConditionallyRender
|
|
||||||
condition={isOrphanedToken}
|
|
||||||
show={<StyledErrorIcon aria-label='Orphaned token' />}
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
</HtmlTooltip>
|
</HtmlTooltip>
|
||||||
</TextCell>
|
</TextCell>
|
||||||
|
@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
|||||||
import type { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens';
|
import type { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens';
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
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 { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
import {
|
import {
|
||||||
useTable,
|
useTable,
|
||||||
@ -12,7 +11,7 @@ import {
|
|||||||
} from 'react-table';
|
} from 'react-table';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
|
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 = (
|
export const useApiTokenTable = (
|
||||||
tokens: IApiToken[],
|
tokens: IApiToken[],
|
||||||
@ -27,7 +26,9 @@ export const useApiTokenTable = (
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'Icon',
|
id: 'Icon',
|
||||||
Cell: () => <IconCell icon={<Key color='disabled' />} />,
|
Cell: (props: any) => (
|
||||||
|
<ApiTokenIcon secret={props.row.original.secret} />
|
||||||
|
),
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
disableGlobalFilter: true,
|
disableGlobalFilter: true,
|
||||||
width: 50,
|
width: 50,
|
||||||
@ -61,7 +62,6 @@ 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