From 75af3db11fb00eca6c495e2692b12e5e04279d0c Mon Sep 17 00:00:00 2001 From: CaIon Date: Wed, 29 Apr 2026 09:52:45 +0800 Subject: [PATCH] feat(logs): add username to TaskLog interface and implement log avatar styling --- .../components/api-key-group-combobox.tsx | 6 +- .../keys/components/api-keys-columns.tsx | 27 +- .../components/api-keys-mutate-drawer.tsx | 581 ++++++++++-------- .../components/columns/column-helpers.tsx | 111 +++- .../columns/common-logs-columns.tsx | 109 ++-- .../columns/drawing-logs-columns.tsx | 225 ++++--- .../components/columns/task-logs-columns.tsx | 236 ++++--- .../components/common-logs-filter-bar.tsx | 13 +- .../components/dialogs/details-dialog.tsx | 31 +- .../components/usage-logs-table.tsx | 25 +- .../features/usage-logs/lib/avatar-color.ts | 24 + .../src/features/usage-logs/lib/format.ts | 43 +- web/default/src/features/usage-logs/types.ts | 1 + web/default/src/i18n/locales/en.json | 4 +- web/default/src/i18n/locales/fr.json | 4 +- web/default/src/i18n/locales/ja.json | 4 +- web/default/src/i18n/locales/ru.json | 4 +- web/default/src/i18n/locales/vi.json | 4 +- web/default/src/i18n/locales/zh.json | 8 +- 19 files changed, 899 insertions(+), 561 deletions(-) create mode 100644 web/default/src/features/usage-logs/lib/avatar-color.ts diff --git a/web/default/src/features/keys/components/api-key-group-combobox.tsx b/web/default/src/features/keys/components/api-key-group-combobox.tsx index b1fd143c..0b0e38fe 100644 --- a/web/default/src/features/keys/components/api-key-group-combobox.tsx +++ b/web/default/src/features/keys/components/api-key-group-combobox.tsx @@ -110,7 +110,7 @@ export function ApiKeyGroupCombobox({ role='combobox' aria-expanded={open} disabled={disabled} - className='h-auto min-h-10 w-full justify-between gap-3 px-3 py-2 text-start' + className='border-input bg-muted/40 h-auto min-h-20 w-full justify-between gap-3 rounded-lg px-4 py-3 text-start shadow-none transition-[background-color,border-color,box-shadow] duration-150 hover:bg-muted/55 hover:text-foreground active:bg-background data-[state=open]:border-ring data-[state=open]:bg-background data-[state=open]:ring-ring/20 data-[state=open]:ring-[3px]' > @@ -128,7 +128,7 @@ export function ApiKeyGroupCombobox({ - + 1) { + return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300' + } + if (ratio < 1) { + return 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900/60 dark:bg-sky-950/40 dark:text-sky-300' + } + return 'border-border bg-muted text-muted-foreground' +} + function useGroupRatios(): Record { const isAdmin = useAuthStore((s) => Boolean(s.auth.user?.role && s.auth.user.role >= 10) @@ -242,15 +252,18 @@ export function useApiKeysColumns(): ColumnDef[] { ) } return ( - + {group || t('Default')} {ratio != null && ( - <> - · - - {ratio}x - - + + + {ratio}x + )} ) diff --git a/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx b/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx index 752a6f3b..c78bdac0 100644 --- a/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx +++ b/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx @@ -1,12 +1,19 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, type ReactNode } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useQuery } from '@tanstack/react-query' -import { ChevronDown } from 'lucide-react' +import { + ChevronDown, + KeyRound, + Settings2, + WalletCards, + type LucideIcon, +} from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { getUserModels, getUserGroups } from '@/lib/api' import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency' +import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Collapsible, @@ -59,6 +66,34 @@ type ApiKeyMutateDrawerProps = { side?: 'left' | 'right' } +type ApiKeyFormSectionProps = { + title: string + description: string + icon: LucideIcon + children: ReactNode +} + +function ApiKeyFormSection(props: ApiKeyFormSectionProps) { + const Icon = props.icon + + return ( +
+
+
+ +
+
+

{props.title}

+

+ {props.description} +

+
+
+
{props.children}
+
+ ) +} + export function ApiKeysMutateDrawer({ open, onOpenChange, @@ -201,6 +236,8 @@ export function ApiKeysMutateDrawer({ const quotaPlaceholder = tokensOnly ? t('Enter quota in tokens') : t('Enter quota in {{currency}}', { currency: currencyLabel }) + const selectedGroup = form.watch('group') + const unlimitedQuota = form.watch('unlimited_quota') return ( - - + + {isUpdate ? t('Update API Key') : t('Create API Key')} @@ -231,278 +268,314 @@ export function ApiKeysMutateDrawer({
- ( - - {t('Name')} - - - - - - )} - /> - - ( - - {t('Group')} - - - - - {t('Auto group enables circuit breaker mechanism')} - - - - )} - /> - - {form.watch('group') === 'auto' && ( + ( - -
- - {t('Cross-group retry')} - + + {t('Name')} + + + + + + )} + /> + + ( + + {t('Group')} + + + + + + )} + /> + + {selectedGroup === 'auto' && ( + ( + +
+ + {t('Cross-group retry')} + + + {t( + 'When enabled, if channels in the current group fail, it will try channels in the next group in order.' + )} + +
+ + + +
+ )} + /> + )} + + ( + + {t('Expiration Time')} +
+ + + +
+ + + + +
+
+ +
+ )} + /> + + {!isUpdate && ( + ( + + {t('Quantity')} + + + field.onChange(parseInt(e.target.value, 10) || 1) + } + /> + {t( - 'When enabled, if channels in the current group fail, it will try channels in the next group in order.' + 'Create multiple API keys at once (random suffix will be added to names)' )} + + + )} + /> + )} + + + + {!unlimitedQuota && ( + ( + + {quotaLabel} + + + field.onChange(parseFloat(e.target.value) || 0) + } + /> + + + {tokensOnly + ? t('Enter the quota amount in tokens') + : t('Enter the quota amount in {{currency}}', { + currency: currencyLabel, + })} + + + + )} + /> + )} + + ( + +
+ + {t('Unlimited Quota')} + + + {t('Enable unlimited quota for this API key')} +
)} /> - )} - - ( - -
- - {t('Unlimited Quota')} - - - {t('Enable unlimited quota for this API key')} - -
- - - -
- )} - /> - - {!form.watch('unlimited_quota') && ( - ( - - {quotaLabel} - - - field.onChange(parseFloat(e.target.value) || 0) - } - /> - - - {tokensOnly - ? t('Enter the quota amount in tokens') - : t('Enter the quota amount in {{currency}}', { - currency: currencyLabel, - })} - - - - )} - /> - )} - - ( - - {t('Expiration Time')} -
- - - -
- - - - -
-
- - {t('Leave empty for never expires')} - - -
- )} - /> - - {!isUpdate && ( - ( - - {t('Quantity')} - - - field.onChange(parseInt(e.target.value, 10) || 1) - } - /> - - - {t( - 'Create multiple API keys at once (random suffix will be added to names)' - )} - - - - )} - /> - )} +
- - - - - ( - - {t('Model Limits')} - - ({ label: m, value: m }))} - selected={field.value} - onChange={field.onChange} - placeholder={t('Select models (empty for allow all)')} - /> - - - {t('Limit which models can be used with this key')} - - - - )} - /> +
+ + + + +
+ ( + + {t('Model Limits')} + + ({ + label: m, + value: m, + }))} + selected={field.value} + onChange={field.onChange} + placeholder={t( + 'Select models (empty for allow all)' + )} + /> + + + {t('Limit which models can be used with this key')} + + + + )} + /> - ( - - {t('IP Whitelist (supports CIDR)')} - -