nicolium: migrate tokens page

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-06 17:16:56 +01:00
parent 7a6ec96e9f
commit e3dfa04094
3 changed files with 135 additions and 100 deletions

View File

@ -1,16 +1,13 @@
import { useInfiniteQuery, useMutation } from '@tanstack/react-query';
import clsx from 'clsx';
import React from 'react';
import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import Badge from '@/components/badge';
import Button from '@/components/ui/button';
import Card, { CardBody, CardHeader, CardTitle } from '@/components/ui/card';
import Column from '@/components/ui/column';
import HStack from '@/components/ui/hstack';
import Icon from '@/components/ui/icon';
import Spinner from '@/components/ui/spinner';
import Stack from '@/components/ui/stack';
import Text from '@/components/ui/text';
import { useAppSelector } from '@/hooks/use-app-selector';
import {
oauthTokensQueryOptions,
@ -62,102 +59,93 @@ const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
};
return (
<div className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
<Stack space={2} className='h-full justify-between'>
<Stack space={1}>
<Text size='md' weight='medium'>
<HStack space={1} alignItems='center'>
{token.app_name}
{token.app_website && (
<a href={token.app_website} target='_blank' rel='noopener noreferrer'>
<Icon
src={require('@phosphor-icons/core/regular/arrow-square-out.svg')}
className='inline size-4 text-inherit'
<div className={clsx('⁂-token', { '⁂-token--current': isCurrent })}>
<div className='⁂-token__info'>
<p className='⁂-token__name'>
{token.app_name}
{token.app_website && (
<a href={token.app_website} target='_blank' rel='noopener noreferrer'>
<Icon src={require('@phosphor-icons/core/regular/arrow-square-out.svg')} />
</a>
)}
</p>
{token.scopes?.length > 0 && (
<div className='⁂-token__tokens'>
<p>
<FormattedMessage id='security.tokens.scopes' defaultMessage='Scopes:' />
</p>
{token.scopes.map((scope) => (
<Badge title={scope} slug='opaque' key={scope} />
))}
</div>
)}
{token.created_at && (
<p className='⁂-token__detail'>
<FormattedMessage
id='security.tokens.created_at'
defaultMessage='Created on {date}'
values={{
date: (
<FormattedDate
value={token.created_at}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
</a>
)}
</HStack>
</Text>
{token.scopes?.length > 0 && (
<HStack space={2} alignItems='center' wrap>
<Text size='sm' theme='muted'>
<FormattedMessage id='security.tokens.scopes' defaultMessage='Scopes:' />
</Text>
{token.scopes.map((scope) => (
<Badge title={scope} slug='opaque' key={scope} />
))}
</HStack>
)}
{token.created_at && (
<Text size='sm' theme='muted'>
<FormattedMessage
id='security.tokens.created_at'
defaultMessage='Created on {date}'
values={{
date: (
<FormattedDate
value={token.created_at}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
),
}}
/>
</Text>
)}
{token.last_used && (
<Text size='sm' theme='muted'>
<FormattedMessage
id='security.tokens.last_used'
defaultMessage='Last used on {date}'
values={{
date: (
<FormattedDate
value={token.last_used}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
),
}}
/>
</Text>
)}
{token.valid_until && (
<Text size='sm' theme='muted'>
<FormattedMessage
id='security.tokens.valid_until'
defaultMessage='Expires on {date}'
values={{
date: (
<FormattedDate
value={token.valid_until}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
),
}}
/>
</Text>
)}
</Stack>
<HStack justifyContent='end'>
<Button theme={isCurrent ? 'danger' : 'primary'} onClick={handleRevoke}>
{intl.formatMessage(messages.revoke)}
</Button>
</HStack>
</Stack>
),
}}
/>
</p>
)}
{token.last_used && (
<p className='⁂-token__detail'>
<FormattedMessage
id='security.tokens.last_used'
defaultMessage='Last used on {date}'
values={{
date: (
<FormattedDate
value={token.last_used}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
),
}}
/>
</p>
)}
{token.valid_until && (
<p className='⁂-token__detail'>
<FormattedMessage
id='security.tokens.valid_until'
defaultMessage='Expires on {date}'
values={{
date: (
<FormattedDate
value={token.valid_until}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
),
}}
/>
</p>
)}
</div>
<div className={clsx('⁂-token__actions')}>
<button onClick={handleRevoke}>{intl.formatMessage(messages.revoke)}</button>
</div>
</div>
);
};
@ -176,7 +164,7 @@ const AuthTokenListPage: React.FC = () => {
});
const body = tokens ? (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<div className='⁂-tokens'>
{tokens.map((token) => (
<AuthToken
key={token.id}

View File

@ -13,3 +13,4 @@
@use 'chats';
@use 'events';
@use 'directory';
@use 'settings';

View File

@ -0,0 +1,46 @@
@use 'mixins';
.-tokens {
@apply grid grid-cols-1 gap-4 sm:grid-cols-2;
}
.-token {
@apply rounded-lg bg-gray-100 p-4 dark:bg-primary-800 flex flex-col gap-2 h-full justify-between;
&__content {
@apply flex flex-col gap-1;
}
&__name {
@apply flex gap-1 items-center;
@include mixins.text($size: md, $weight: medium);
svg {
@apply inline size-4 text-inherit;
}
}
&__tokens {
@apply flex gap-2 items-center flex-wrap;
p {
@include mixins.text($size: sm, $theme: muted);
}
}
&__detail {
@include mixins.text($size: sm, $theme: muted);
}
&__actions {
@apply flex justify-end;
button {
@include mixins.button($theme: primary);
}
}
&--current button {
@apply border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:ring-danger-500;
}
}