mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-09 01:17:06 +02:00
Feat sa table info (#2848)
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. 
This commit is contained in:
parent
7b075954a1
commit
997dbbbea5
@ -1,6 +1,7 @@
|
|||||||
import { Alert, styled } from '@mui/material';
|
import { Alert, styled } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { IUser } from 'interfaces/user';
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
import { ServiceAccountTokens } from '../ServiceAccountModal/ServiceAccountTokens/ServiceAccountTokens';
|
import { ServiceAccountTokens } from '../ServiceAccountModal/ServiceAccountTokens/ServiceAccountTokens';
|
||||||
|
|
||||||
const StyledTableContainer = styled('div')(({ theme }) => ({
|
const StyledTableContainer = styled('div')(({ theme }) => ({
|
||||||
@ -12,10 +13,10 @@ const StyledLabel = styled('p')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IServiceAccountDeleteDialogProps {
|
interface IServiceAccountDeleteDialogProps {
|
||||||
serviceAccount?: IUser;
|
serviceAccount?: IServiceAccount;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
onConfirm: (serviceAccount: IUser) => void;
|
onConfirm: (serviceAccount: IServiceAccount) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceAccountDeleteDialog = ({
|
export const ServiceAccountDeleteDialog = ({
|
||||||
@ -39,6 +40,10 @@ export const ServiceAccountDeleteDialog = ({
|
|||||||
Deleting this service account may break any existing
|
Deleting this service account may break any existing
|
||||||
implementations currently using it.
|
implementations currently using it.
|
||||||
</Alert>
|
</Alert>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(serviceAccount?.tokens.length)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
<StyledLabel>Service account tokens</StyledLabel>
|
<StyledLabel>Service account tokens</StyledLabel>
|
||||||
<StyledTableContainer>
|
<StyledTableContainer>
|
||||||
<ServiceAccountTokens
|
<ServiceAccountTokens
|
||||||
@ -46,6 +51,9 @@ export const ServiceAccountDeleteDialog = ({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</StyledTableContainer>
|
</StyledTableContainer>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<StyledLabel>
|
<StyledLabel>
|
||||||
You are about to delete service account:{' '}
|
You are about to delete service account:{' '}
|
||||||
<strong>{serviceAccount?.name}</strong>
|
<strong>{serviceAccount?.name}</strong>
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
||||||
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
||||||
import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens';
|
import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens';
|
||||||
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
|
|
||||||
const StyledForm = styled('form')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -110,7 +111,7 @@ interface IServiceAccountModalErrors {
|
|||||||
const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];
|
const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];
|
||||||
|
|
||||||
interface IServiceAccountModalProps {
|
interface IServiceAccountModalProps {
|
||||||
serviceAccount?: IUser;
|
serviceAccount?: IServiceAccount;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
newToken: (token: INewPersonalAPIToken) => void;
|
newToken: (token: INewPersonalAPIToken) => void;
|
||||||
@ -222,7 +223,8 @@ export const ServiceAccountModal = ({
|
|||||||
const isUnique = (value: string) =>
|
const isUnique = (value: string) =>
|
||||||
!users?.some((user: IUser) => user.username === value) &&
|
!users?.some((user: IUser) => user.username === value) &&
|
||||||
!serviceAccounts?.some(
|
!serviceAccounts?.some(
|
||||||
(serviceAccount: IUser) => serviceAccount.username === value
|
(serviceAccount: IServiceAccount) =>
|
||||||
|
serviceAccount.username === value
|
||||||
);
|
);
|
||||||
const isPATValid =
|
const isPATValid =
|
||||||
tokenGeneration === TokenGeneration.LATER ||
|
tokenGeneration === TokenGeneration.LATER ||
|
||||||
|
@ -7,15 +7,14 @@ import {
|
|||||||
PersonalAPITokenForm,
|
PersonalAPITokenForm,
|
||||||
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
|
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
|
||||||
import { ICreatePersonalApiTokenPayload } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
import { ICreatePersonalApiTokenPayload } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
||||||
import { IUser } from 'interfaces/user';
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens';
|
|
||||||
|
|
||||||
const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];
|
const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];
|
||||||
|
|
||||||
interface IServiceAccountCreateTokenDialogProps {
|
interface IServiceAccountCreateTokenDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
serviceAccount: IUser;
|
serviceAccount: IServiceAccount;
|
||||||
onCreateClick: (newToken: ICreatePersonalApiTokenPayload) => void;
|
onCreateClick: (newToken: ICreatePersonalApiTokenPayload) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,8 +24,6 @@ export const ServiceAccountCreateTokenDialog = ({
|
|||||||
serviceAccount,
|
serviceAccount,
|
||||||
onCreateClick,
|
onCreateClick,
|
||||||
}: IServiceAccountCreateTokenDialogProps) => {
|
}: IServiceAccountCreateTokenDialogProps) => {
|
||||||
const { tokens = [] } = usePersonalAPITokens(serviceAccount.id);
|
|
||||||
|
|
||||||
const [patDescription, setPatDescription] = useState('');
|
const [patDescription, setPatDescription] = useState('');
|
||||||
const [patExpiration, setPatExpiration] =
|
const [patExpiration, setPatExpiration] =
|
||||||
useState<ExpirationOption>(DEFAULT_EXPIRATION);
|
useState<ExpirationOption>(DEFAULT_EXPIRATION);
|
||||||
@ -43,7 +40,9 @@ export const ServiceAccountCreateTokenDialog = ({
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const isDescriptionUnique = (description: string) =>
|
const isDescriptionUnique = (description: string) =>
|
||||||
!tokens?.some(token => token.description === description);
|
!serviceAccount.tokens?.some(
|
||||||
|
token => token.description === description
|
||||||
|
);
|
||||||
|
|
||||||
const isPATValid =
|
const isPATValid =
|
||||||
patDescription.length &&
|
patDescription.length &&
|
||||||
|
@ -30,7 +30,6 @@ import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/Servi
|
|||||||
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { IUser } from 'interfaces/user';
|
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import {
|
import {
|
||||||
ICreatePersonalApiTokenPayload,
|
ICreatePersonalApiTokenPayload,
|
||||||
@ -38,6 +37,8 @@ import {
|
|||||||
} from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
} from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
|
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
|
||||||
|
|
||||||
const StyledHeader = styled('div')(({ theme }) => ({
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -70,13 +71,6 @@ const StyledPlaceholderSubtitle = styled(Typography)(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(1.5),
|
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<
|
export type PageQueryType = Partial<
|
||||||
Record<'sort' | 'order' | 'search', string>
|
Record<'sort' | 'order' | 'search', string>
|
||||||
>;
|
>;
|
||||||
@ -84,7 +78,7 @@ export type PageQueryType = Partial<
|
|||||||
const defaultSort: SortingRule<string> = { id: 'createdAt' };
|
const defaultSort: SortingRule<string> = { id: 'createdAt' };
|
||||||
|
|
||||||
interface IServiceAccountTokensProps {
|
interface IServiceAccountTokensProps {
|
||||||
serviceAccount: IUser;
|
serviceAccount: IServiceAccount;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,11 +90,10 @@ export const ServiceAccountTokens = ({
|
|||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const {
|
const { tokens = [], refetchTokens } = usePersonalAPITokens(
|
||||||
tokens = [],
|
serviceAccount.id
|
||||||
refetchTokens,
|
);
|
||||||
loading,
|
const { refetch } = useServiceAccounts();
|
||||||
} = usePersonalAPITokens(serviceAccount.id);
|
|
||||||
const { createUserPersonalAPIToken, deleteUserPersonalAPIToken } =
|
const { createUserPersonalAPIToken, deleteUserPersonalAPIToken } =
|
||||||
usePersonalAPITokensApi();
|
usePersonalAPITokensApi();
|
||||||
|
|
||||||
@ -121,6 +114,7 @@ export const ServiceAccountTokens = ({
|
|||||||
serviceAccount.id,
|
serviceAccount.id,
|
||||||
newToken
|
newToken
|
||||||
);
|
);
|
||||||
|
refetch();
|
||||||
refetchTokens();
|
refetchTokens();
|
||||||
setCreateOpen(false);
|
setCreateOpen(false);
|
||||||
setNewToken(token);
|
setNewToken(token);
|
||||||
@ -141,6 +135,7 @@ export const ServiceAccountTokens = ({
|
|||||||
serviceAccount.id,
|
serviceAccount.id,
|
||||||
selectedToken?.id
|
selectedToken?.id
|
||||||
);
|
);
|
||||||
|
refetch();
|
||||||
refetchTokens();
|
refetchTokens();
|
||||||
setDeleteOpen(false);
|
setDeleteOpen(false);
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -216,18 +211,10 @@ export const ServiceAccountTokens = ({
|
|||||||
[setSelectedToken, setDeleteOpen]
|
[setSelectedToken, setDeleteOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { data, getSearchText, getSearchContext } = useSearch(
|
||||||
data: searchedData,
|
columns,
|
||||||
getSearchText,
|
searchValue,
|
||||||
getSearchContext,
|
tokens
|
||||||
} = useSearch(columns, searchValue, tokens);
|
|
||||||
|
|
||||||
const data = useMemo(
|
|
||||||
() =>
|
|
||||||
searchedData?.length === 0 && loading
|
|
||||||
? tokensPlaceholder
|
|
||||||
: searchedData,
|
|
||||||
[searchedData, loading]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -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 { useMemo, useState } from 'react';
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { IUser } from 'interfaces/user';
|
|
||||||
import IRole from 'interfaces/role';
|
import IRole from 'interfaces/role';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
@ -26,6 +25,9 @@ import { ServiceAccountDeleteDialog } from './ServiceAccountDeleteDialog/Service
|
|||||||
import { ServiceAccountsActionsCell } from './ServiceAccountsActionsCell/ServiceAccountsActionsCell';
|
import { ServiceAccountsActionsCell } from './ServiceAccountsActionsCell/ServiceAccountsActionsCell';
|
||||||
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
||||||
import { ServiceAccountTokenDialog } from './ServiceAccountTokenDialog/ServiceAccountTokenDialog';
|
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 = () => {
|
export const ServiceAccountsTable = () => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
@ -39,9 +41,9 @@ export const ServiceAccountsTable = () => {
|
|||||||
const [newToken, setNewToken] = useState<INewPersonalAPIToken>();
|
const [newToken, setNewToken] = useState<INewPersonalAPIToken>();
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [selectedServiceAccount, setSelectedServiceAccount] =
|
const [selectedServiceAccount, setSelectedServiceAccount] =
|
||||||
useState<IUser>();
|
useState<IServiceAccount>();
|
||||||
|
|
||||||
const onDeleteConfirm = async (serviceAccount: IUser) => {
|
const onDeleteConfirm = async (serviceAccount: IServiceAccount) => {
|
||||||
try {
|
try {
|
||||||
await removeServiceAccount(serviceAccount.id);
|
await removeServiceAccount(serviceAccount.id);
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -60,14 +62,6 @@ export const ServiceAccountsTable = () => {
|
|||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
|
||||||
Header: 'Created',
|
|
||||||
accessor: 'createdAt',
|
|
||||||
Cell: DateCell,
|
|
||||||
sortType: 'date',
|
|
||||||
width: 120,
|
|
||||||
maxWidth: 120,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Header: 'Avatar',
|
Header: 'Avatar',
|
||||||
accessor: 'imageUrl',
|
accessor: 'imageUrl',
|
||||||
@ -85,10 +79,7 @@ export const ServiceAccountsTable = () => {
|
|||||||
accessor: (row: any) => row.name || '',
|
accessor: (row: any) => row.name || '',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
Cell: ({ row: { original: user } }: any) => (
|
Cell: ({ row: { original: user } }: any) => (
|
||||||
<HighlightCell
|
<HighlightCell value={user.name} subtitle={user.username} />
|
||||||
value={user.name}
|
|
||||||
subtitle={user.email || user.username}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
@ -100,6 +91,37 @@ export const ServiceAccountsTable = () => {
|
|||||||
?.name || '',
|
?.name || '',
|
||||||
maxWidth: 120,
|
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',
|
Header: 'Actions',
|
||||||
id: 'Actions',
|
id: 'Actions',
|
||||||
@ -149,6 +171,7 @@ export const ServiceAccountsTable = () => {
|
|||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
disableSortRemove: true,
|
disableSortRemove: true,
|
||||||
disableMultiSort: true,
|
disableMultiSort: true,
|
||||||
|
autoResetHiddenColumns: false,
|
||||||
defaultColumn: {
|
defaultColumn: {
|
||||||
Cell: TextCell,
|
Cell: TextCell,
|
||||||
},
|
},
|
||||||
@ -161,11 +184,11 @@ export const ServiceAccountsTable = () => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
condition: isExtraSmallScreen,
|
condition: isExtraSmallScreen,
|
||||||
columns: ['imageUrl', 'role'],
|
columns: ['role', 'seenAt'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: isSmallScreen,
|
condition: isSmallScreen,
|
||||||
columns: ['createdAt', 'last-login'],
|
columns: ['imageUrl', 'tokens', 'createdAt'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import IRole from 'interfaces/role';
|
import IRole from 'interfaces/role';
|
||||||
import { IUser } from 'interfaces/user';
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
@ -11,14 +11,14 @@ export const useServiceAccounts = () => {
|
|||||||
|
|
||||||
const { data, error, mutate } = useConditionalSWR(
|
const { data, error, mutate } = useConditionalSWR(
|
||||||
Boolean(uiConfig.flags.serviceAccounts) && isEnterprise(),
|
Boolean(uiConfig.flags.serviceAccounts) && isEnterprise(),
|
||||||
{ users: [], rootRoles: [] },
|
{ serviceAccounts: [], rootRoles: [] },
|
||||||
formatApiPath(`api/admin/service-account`),
|
formatApiPath(`api/admin/service-account`),
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
serviceAccounts: (data?.users ?? []) as IUser[],
|
serviceAccounts: (data?.serviceAccounts ?? []) as IServiceAccount[],
|
||||||
roles: (data?.rootRoles ?? []) as IRole[],
|
roles: (data?.rootRoles ?? []) as IRole[],
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
refetch: () => mutate(),
|
refetch: () => mutate(),
|
||||||
|
@ -3,6 +3,7 @@ export interface IPersonalAPIToken {
|
|||||||
description: string;
|
description: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
seenAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INewPersonalAPIToken extends IPersonalAPIToken {
|
export interface INewPersonalAPIToken extends IPersonalAPIToken {
|
||||||
|
6
frontend/src/interfaces/service-account.ts
Normal file
6
frontend/src/interfaces/service-account.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IPersonalAPIToken } from './personalAPIToken';
|
||||||
|
import { IUser } from './user';
|
||||||
|
|
||||||
|
export interface IServiceAccount extends IUser {
|
||||||
|
tokens: IPersonalAPIToken[];
|
||||||
|
}
|
@ -225,7 +225,7 @@ class UserStore implements IUserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserByPersonalAccessToken(secret: string): Promise<User> {
|
async getUserByPersonalAccessToken(secret: string): Promise<User> {
|
||||||
const row = await this.activeUsers()
|
const row = await this.activeAll()
|
||||||
.select(USER_COLUMNS.map((column) => `${TABLE}.${column}`))
|
.select(USER_COLUMNS.map((column) => `${TABLE}.${column}`))
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
'personal_access_tokens',
|
'personal_access_tokens',
|
||||||
|
Loading…
Reference in New Issue
Block a user