Skip to content

Commit

Permalink
Feat sa table info (#2848)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/2-543/show-relevant-information-on-the-service-accounts-table

Shows relevant information on the table, like total PATs and the last
time a service account was active based on latest seen PAT for that
account. Adapts to the latest related PR on enterprise.


![image](https://user-images.githubusercontent.com/14320932/211312719-c4ed940a-723b-4b2e-a79e-8e7cdbda7c58.png)
  • Loading branch information
nunogois committed Jan 9, 2023
1 parent 7b07595 commit 997dbbb
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 65 deletions.
@@ -1,6 +1,7 @@
import { Alert, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { IUser } from 'interfaces/user';
import { IServiceAccount } from 'interfaces/service-account';
import { ServiceAccountTokens } from '../ServiceAccountModal/ServiceAccountTokens/ServiceAccountTokens';

const StyledTableContainer = styled('div')(({ theme }) => ({
Expand All @@ -12,10 +13,10 @@ const StyledLabel = styled('p')(({ theme }) => ({
}));

interface IServiceAccountDeleteDialogProps {
serviceAccount?: IUser;
serviceAccount?: IServiceAccount;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm: (serviceAccount: IUser) => void;
onConfirm: (serviceAccount: IServiceAccount) => void;
}

export const ServiceAccountDeleteDialog = ({
Expand All @@ -39,13 +40,20 @@ export const ServiceAccountDeleteDialog = ({
Deleting this service account may break any existing
implementations currently using it.
</Alert>
<StyledLabel>Service account tokens</StyledLabel>
<StyledTableContainer>
<ServiceAccountTokens
serviceAccount={serviceAccount!}
readOnly
/>
</StyledTableContainer>
<ConditionallyRender
condition={Boolean(serviceAccount?.tokens.length)}
show={
<>
<StyledLabel>Service account tokens</StyledLabel>
<StyledTableContainer>
<ServiceAccountTokens
serviceAccount={serviceAccount!}
readOnly
/>
</StyledTableContainer>
</>
}
/>
<StyledLabel>
You are about to delete service account:{' '}
<strong>{serviceAccount?.name}</strong>
Expand Down
Expand Up @@ -32,6 +32,7 @@ import {
import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens';
import { IServiceAccount } from 'interfaces/service-account';

const StyledForm = styled('form')(() => ({
display: 'flex',
Expand Down Expand Up @@ -110,7 +111,7 @@ interface IServiceAccountModalErrors {
const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];

interface IServiceAccountModalProps {
serviceAccount?: IUser;
serviceAccount?: IServiceAccount;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
newToken: (token: INewPersonalAPIToken) => void;
Expand Down Expand Up @@ -222,7 +223,8 @@ export const ServiceAccountModal = ({
const isUnique = (value: string) =>
!users?.some((user: IUser) => user.username === value) &&
!serviceAccounts?.some(
(serviceAccount: IUser) => serviceAccount.username === value
(serviceAccount: IServiceAccount) =>
serviceAccount.username === value
);
const isPATValid =
tokenGeneration === TokenGeneration.LATER ||
Expand Down
Expand Up @@ -7,15 +7,14 @@ import {
PersonalAPITokenForm,
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
import { ICreatePersonalApiTokenPayload } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
import { IUser } from 'interfaces/user';
import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens';
import { IServiceAccount } from 'interfaces/service-account';

const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];

interface IServiceAccountCreateTokenDialogProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
serviceAccount: IUser;
serviceAccount: IServiceAccount;
onCreateClick: (newToken: ICreatePersonalApiTokenPayload) => void;
}

Expand All @@ -25,8 +24,6 @@ export const ServiceAccountCreateTokenDialog = ({
serviceAccount,
onCreateClick,
}: IServiceAccountCreateTokenDialogProps) => {
const { tokens = [] } = usePersonalAPITokens(serviceAccount.id);

const [patDescription, setPatDescription] = useState('');
const [patExpiration, setPatExpiration] =
useState<ExpirationOption>(DEFAULT_EXPIRATION);
Expand All @@ -43,7 +40,9 @@ export const ServiceAccountCreateTokenDialog = ({
}, [open]);

const isDescriptionUnique = (description: string) =>
!tokens?.some(token => token.description === description);
!serviceAccount.tokens?.some(
token => token.description === description
);

const isPATValid =
patDescription.length &&
Expand Down
Expand Up @@ -30,14 +30,15 @@ import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/Servi
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IUser } from 'interfaces/user';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import {
ICreatePersonalApiTokenPayload,
usePersonalAPITokensApi,
} from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { IServiceAccount } from 'interfaces/service-account';
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';

const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
Expand Down Expand Up @@ -70,21 +71,14 @@ const StyledPlaceholderSubtitle = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(1.5),
}));

export const tokensPlaceholder: IPersonalAPIToken[] = Array(15).fill({
description: 'Short description of the feature',
type: '-',
createdAt: new Date(2022, 1, 1),
project: 'projectID',
});

export type PageQueryType = Partial<
Record<'sort' | 'order' | 'search', string>
>;

const defaultSort: SortingRule<string> = { id: 'createdAt' };

interface IServiceAccountTokensProps {
serviceAccount: IUser;
serviceAccount: IServiceAccount;
readOnly?: boolean;
}

Expand All @@ -96,11 +90,10 @@ export const ServiceAccountTokens = ({
const { setToastData, setToastApiError } = useToast();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const {
tokens = [],
refetchTokens,
loading,
} = usePersonalAPITokens(serviceAccount.id);
const { tokens = [], refetchTokens } = usePersonalAPITokens(
serviceAccount.id
);
const { refetch } = useServiceAccounts();
const { createUserPersonalAPIToken, deleteUserPersonalAPIToken } =
usePersonalAPITokensApi();

Expand All @@ -121,6 +114,7 @@ export const ServiceAccountTokens = ({
serviceAccount.id,
newToken
);
refetch();
refetchTokens();
setCreateOpen(false);
setNewToken(token);
Expand All @@ -141,6 +135,7 @@ export const ServiceAccountTokens = ({
serviceAccount.id,
selectedToken?.id
);
refetch();
refetchTokens();
setDeleteOpen(false);
setToastData({
Expand Down Expand Up @@ -216,18 +211,10 @@ export const ServiceAccountTokens = ({
[setSelectedToken, setDeleteOpen]
);

const {
data: searchedData,
getSearchText,
getSearchContext,
} = useSearch(columns, searchValue, tokens);

const data = useMemo(
() =>
searchedData?.length === 0 && loading
? tokensPlaceholder
: searchedData,
[searchedData, loading]
const { data, getSearchText, getSearchContext } = useSearch(
columns,
searchValue,
tokens
);

const {
Expand Down
@@ -0,0 +1,64 @@
import { VFC } from 'react';
import { Link, styled, Typography } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { IServiceAccount } from 'interfaces/service-account';

const StyledItem = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
}));

const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));

interface IServiceAccountTokensCellProps {
row: {
original: IServiceAccount;
};
value: string;
}

export const ServiceAccountTokensCell: VFC<IServiceAccountTokensCellProps> = ({
row,
value,
}) => {
const { searchQuery } = useSearchHighlightContext();

if (!row.original.tokens || row.original.tokens.length === 0)
return <TextCell />;

return (
<TextCell>
<HtmlTooltip
title={
<>
{row.original.tokens?.map(({ id, description }) => (
<StyledItem key={id}>
<Highlighter search={searchQuery}>
{description}
</Highlighter>
</StyledItem>
))}
</>
}
>
<StyledLink
underline="always"
highlighted={
searchQuery.length > 0 &&
value.toLowerCase().includes(searchQuery.toLowerCase())
}
>
{row.original.tokens?.length === 1
? '1 token'
: `${row.original.tokens?.length} tokens`}
</StyledLink>
</HtmlTooltip>
</TextCell>
);
};
@@ -1,7 +1,6 @@
import { useMemo, useState } from 'react';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IUser } from 'interfaces/user';
import IRole from 'interfaces/role';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
Expand All @@ -26,6 +25,9 @@ import { ServiceAccountDeleteDialog } from './ServiceAccountDeleteDialog/Service
import { ServiceAccountsActionsCell } from './ServiceAccountsActionsCell/ServiceAccountsActionsCell';
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
import { ServiceAccountTokenDialog } from './ServiceAccountTokenDialog/ServiceAccountTokenDialog';
import { ServiceAccountTokensCell } from './ServiceAccountTokensCell/ServiceAccountTokensCell';
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { IServiceAccount } from 'interfaces/service-account';

export const ServiceAccountsTable = () => {
const { setToastData, setToastApiError } = useToast();
Expand All @@ -39,9 +41,9 @@ export const ServiceAccountsTable = () => {
const [newToken, setNewToken] = useState<INewPersonalAPIToken>();
const [deleteOpen, setDeleteOpen] = useState(false);
const [selectedServiceAccount, setSelectedServiceAccount] =
useState<IUser>();
useState<IServiceAccount>();

const onDeleteConfirm = async (serviceAccount: IUser) => {
const onDeleteConfirm = async (serviceAccount: IServiceAccount) => {
try {
await removeServiceAccount(serviceAccount.id);
setToastData({
Expand All @@ -60,14 +62,6 @@ export const ServiceAccountsTable = () => {

const columns = useMemo(
() => [
{
Header: 'Created',
accessor: 'createdAt',
Cell: DateCell,
sortType: 'date',
width: 120,
maxWidth: 120,
},
{
Header: 'Avatar',
accessor: 'imageUrl',
Expand All @@ -85,10 +79,7 @@ export const ServiceAccountsTable = () => {
accessor: (row: any) => row.name || '',
minWidth: 200,
Cell: ({ row: { original: user } }: any) => (
<HighlightCell
value={user.name}
subtitle={user.email || user.username}
/>
<HighlightCell value={user.name} subtitle={user.username} />
),
searchable: true,
},
Expand All @@ -100,6 +91,37 @@ export const ServiceAccountsTable = () => {
?.name || '',
maxWidth: 120,
},
{
id: 'tokens',
Header: 'Tokens',
accessor: (row: IServiceAccount) =>
row.tokens
?.map(({ description }) => description)
.join('\n') || '',
Cell: ServiceAccountTokensCell,
searchable: true,
},
{
Header: 'Created',
accessor: 'createdAt',
Cell: DateCell,
sortType: 'date',
width: 120,
maxWidth: 120,
},
{
id: 'seenAt',
Header: 'Last seen',
accessor: (row: IServiceAccount) =>
row.tokens.sort((a, b) => {
const aSeenAt = new Date(a.seenAt || 0);
const bSeenAt = new Date(b.seenAt || 0);
return bSeenAt?.getTime() - aSeenAt?.getTime();
})[0]?.seenAt,
Cell: TimeAgoCell,
sortType: 'date',
maxWidth: 150,
},
{
Header: 'Actions',
id: 'Actions',
Expand Down Expand Up @@ -149,6 +171,7 @@ export const ServiceAccountsTable = () => {
autoResetSortBy: false,
disableSortRemove: true,
disableMultiSort: true,
autoResetHiddenColumns: false,
defaultColumn: {
Cell: TextCell,
},
Expand All @@ -161,11 +184,11 @@ export const ServiceAccountsTable = () => {
[
{
condition: isExtraSmallScreen,
columns: ['imageUrl', 'role'],
columns: ['role', 'seenAt'],
},
{
condition: isSmallScreen,
columns: ['createdAt', 'last-login'],
columns: ['imageUrl', 'tokens', 'createdAt'],
},
],
setHiddenColumns,
Expand Down

0 comments on commit 997dbbb

Please sign in to comment.