diff --git a/web/default/src/components/data-table/mobile-card-list.tsx b/web/default/src/components/data-table/mobile-card-list.tsx index feefe183..1ec364c3 100644 --- a/web/default/src/components/data-table/mobile-card-list.tsx +++ b/web/default/src/components/data-table/mobile-card-list.tsx @@ -62,7 +62,7 @@ function ListSkeleton() { -
+
@@ -136,9 +136,9 @@ function CompactRow({ row }: { row: Row }) { )}
- {/* Row 2: Key fields side by side */} + {/* Row 2: Key fields wrap into compact columns instead of squeezing */} {fieldCells.length > 0 && ( -
+
{fieldCells.map((cell) => { const label = getCellLabel(cell) return ( @@ -260,7 +260,7 @@ export function MobileCardList(props: MobileCardListProps) { if (!rows || rows.length === 0) { return ( -
+
diff --git a/web/default/src/components/data-table/pagination.tsx b/web/default/src/components/data-table/pagination.tsx index e140be97..b2f3b049 100644 --- a/web/default/src/components/data-table/pagination.tsx +++ b/web/default/src/components/data-table/pagination.tsx @@ -32,12 +32,12 @@ export function DataTablePagination({
-
-
+
+
{t('Page {{current}} of {{total}}', { current: currentPage, total: totalPages, @@ -50,7 +50,7 @@ export function DataTablePagination({ table.setPageSize(Number(value)) }} > - + @@ -74,7 +74,7 @@ export function DataTablePagination({ total: totalPages, })}
-
+
diff --git a/web/default/src/components/layout/components/section-page-layout.tsx b/web/default/src/components/layout/components/section-page-layout.tsx index 5c7120c6..ca4c6eb8 100644 --- a/web/default/src/components/layout/components/section-page-layout.tsx +++ b/web/default/src/components/layout/components/section-page-layout.tsx @@ -70,15 +70,15 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
-
- {breadcrumb != null &&
{breadcrumb}
} -
+
+ {breadcrumb != null &&
{breadcrumb}
} +
-

+

{title}

{description != null && ( -

+

{description}

)} @@ -91,11 +91,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
-
{content}
+
+ {content} +
diff --git a/web/default/src/components/ui/titled-card.tsx b/web/default/src/components/ui/titled-card.tsx new file mode 100644 index 00000000..f7d1ecf7 --- /dev/null +++ b/web/default/src/components/ui/titled-card.tsx @@ -0,0 +1,81 @@ +import type { ReactNode } from 'react' +import { cn } from '@/lib/utils' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from './card' + +type TitledCardProps = { + title: ReactNode + description?: ReactNode + icon?: ReactNode + action?: ReactNode + children?: ReactNode + className?: string + headerClassName?: string + contentClassName?: string + iconClassName?: string + titleClassName?: string + descriptionClassName?: string +} + +export function TitledCard({ + title, + description, + icon, + action, + children, + className, + headerClassName, + contentClassName, + iconClassName, + titleClassName, + descriptionClassName, +}: TitledCardProps) { + return ( + + +
+
+ {icon != null && ( +
+ {icon} +
+ )} +
+ + {title} + + {description != null && ( + + {description} + + )} +
+
+ {action != null &&
{action}
} +
+
+ + {children} + +
+ ) +} diff --git a/web/default/src/features/auth/hooks/use-auth-redirect.ts b/web/default/src/features/auth/hooks/use-auth-redirect.ts index f0371cd9..34721ae4 100644 --- a/web/default/src/features/auth/hooks/use-auth-redirect.ts +++ b/web/default/src/features/auth/hooks/use-auth-redirect.ts @@ -5,6 +5,24 @@ import { getSelf } from '@/lib/api' import type { User } from '@/features/users/types' import { saveUserId } from '../lib/storage' +function getSavedLanguage(user: User): string | undefined { + const userData = user as Record + if (typeof userData.language === 'string') { + return userData.language + } + + if (typeof userData.setting !== 'string') { + return undefined + } + + try { + const setting = JSON.parse(userData.setting) as { language?: unknown } + return typeof setting.language === 'string' ? setting.language : undefined + } catch { + return undefined + } +} + /** * Hook for handling authentication redirects and user data management */ @@ -39,9 +57,7 @@ export function useAuthRedirect() { } // Restore saved language preference - const savedLang = (user as Record).language as - | string - | undefined + const savedLang = getSavedLanguage(user) if (savedLang && savedLang !== i18n.language) { i18n.changeLanguage(savedLang) } diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx index 7bb2f36c..86a5aaf4 100644 --- a/web/default/src/features/channels/components/channels-table.tsx +++ b/web/default/src/features/channels/components/channels-table.tsx @@ -87,7 +87,10 @@ export function ChannelsTable() { } = useTableUrlState({ search: route.useSearch(), navigate: route.useNavigate(), - pagination: { defaultPage: 1, defaultPageSize: DEFAULT_PAGE_SIZE }, + pagination: { + defaultPage: 1, + defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE, + }, globalFilter: { enabled: true, key: 'filter' }, columnFilters: [ { columnId: 'status', searchKey: 'status', type: 'array' }, @@ -329,7 +332,7 @@ export function ChannelsTable() { return ( <> -
+
- - + + {getLobeIcon(`${getChannelTypeIcon(currentType)}.Color`, 22)} @@ -1110,10 +1110,10 @@ export function ChannelMutateDrawer({
{/* ── Basic Information ── */} -
+
} @@ -3276,7 +3276,7 @@ export function ChannelMutateDrawer({ - +
-
+
{themeReady && spec && ( { const days = props.preferences.defaultTimeRangeDays - const { start, end } = getNormalizedDateRange(days) + const { start, end } = getRollingDateRange(days) setFilters({ ...buildDefaultDashboardFilters(props.preferences), start_timestamp: start, @@ -109,7 +109,7 @@ export function ModelsFilter(props: ModelsFilterProps) { } const handleQuickRange = (days: number) => { - const { start, end } = getNormalizedDateRange(days) + const { start, end } = getRollingDateRange(days) setFilters((prev) => ({ ...prev, @@ -127,7 +127,7 @@ export function ModelsFilter(props: ModelsFilterProps) { {t('Filter')} - + {t('Filter Dashboard Models')} @@ -137,15 +137,15 @@ export function ModelsFilter(props: ModelsFilterProps) { - -
+ +
{/* Quick time range selection */}
-
+
{TIME_RANGE_PRESETS.map((range) => ( 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 b8138aa5..8d19c292 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 @@ -79,18 +79,18 @@ function ApiKeyFormSection(props: ApiKeyFormSectionProps) { return (
-
-
- +
+
+

{props.title}

-

+

{props.description}

-
{props.children}
+
{props.children}
) } @@ -254,13 +254,13 @@ export function ApiKeysMutateDrawer({ > - - + + {isUpdate ? t('Update API Key') : t('Create API Key')} - + {isUpdate ? t('Update the API key by providing necessary info.') : t('Add a new API key by providing necessary info.')}{' '} @@ -271,7 +271,7 @@ export function ApiKeysMutateDrawer({
( - +
{t('Cross-group retry')} - + {t( 'When enabled, if channels in the current group fail, it will try channels in the next group in order.' )} @@ -353,7 +353,7 @@ export function ApiKeysMutateDrawer({ value={field.value} onChange={field.onChange} placeholder={t('Never expires')} - className='min-w-0' + className='min-w-0 [&_input[type=time]]:w-24 sm:[&_input[type=time]]:w-32' />
@@ -361,7 +361,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(0, 0, 0)} > {t('Never')} @@ -370,7 +370,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(1, 0, 0)} > {t('1 Month')} @@ -379,7 +379,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(0, 1, 0)} > {t('1 Day')} @@ -388,7 +388,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(0, 0, 1)} > {t('1 Hour')} @@ -470,7 +470,7 @@ export function ApiKeysMutateDrawer({ control={form.control} name='unlimited_quota' render={({ field }) => ( - +
{t('Unlimited Quota')} @@ -495,10 +495,10 @@ export function ApiKeysMutateDrawer({ -
+
- + - + - diff --git a/web/default/src/features/keys/components/api-keys-table.tsx b/web/default/src/features/keys/components/api-keys-table.tsx index 6c55e34f..51c9d12a 100644 --- a/web/default/src/features/keys/components/api-keys-table.tsx +++ b/web/default/src/features/keys/components/api-keys-table.tsx @@ -16,7 +16,9 @@ import { import { useMediaQuery } from '@/hooks' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' +import { formatQuota } from '@/lib/format' import { cn } from '@/lib/utils' +import { Database } from 'lucide-react' import { useTableUrlState } from '@/hooks/use-table-url-state' import { Table, @@ -33,16 +35,31 @@ import { DataTableToolbar, TableSkeleton, TableEmpty, - MobileCardList, } from '@/components/data-table' +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from '@/components/ui/empty' import { PageFooterPortal } from '@/components/layout' +import { Skeleton } from '@/components/ui/skeleton' +import { StatusBadge } from '@/components/status-badge' import { getApiKeys, searchApiKeys } from '../api' -import { API_KEY_STATUS, API_KEY_STATUS_OPTIONS, ERROR_MESSAGES } from '../constants' +import { + API_KEY_STATUS, + API_KEY_STATUS_OPTIONS, + API_KEY_STATUSES, + ERROR_MESSAGES, +} from '../constants' import { type ApiKey } from '../types' +import { ApiKeyCell } from './api-keys-cells' import { useApiKeysColumns } from './api-keys-columns' import { ApiKeysPrimaryButtons } from './api-keys-primary-buttons' import { useApiKeys } from './api-keys-provider' import { DataTableBulkActions } from './data-table-bulk-actions' +import { DataTableRowActions } from './data-table-row-actions' const route = getRouteApi('/_authenticated/keys/') @@ -50,6 +67,123 @@ function isDisabledApiKeyRow(apiKey: ApiKey) { return apiKey.status !== API_KEY_STATUS.ENABLED } +function ApiKeysMobileSkeleton() { + return ( +
+ {Array.from({ length: 5 }).map((_, index) => ( +
+
+ + +
+
+ + +
+ +
+ ))} +
+ ) +} + +function ApiKeysMobileList({ + table, + isLoading, +}: { + table: ReturnType> + isLoading: boolean +}) { + const { t } = useTranslation() + const rows = table.getRowModel().rows + + if (isLoading) return + + if (!rows.length) { + return ( +
+ + + + + + {t('No API Keys Found')} + + {t( + 'No API keys available. Create your first API key to get started.' + )} + + + +
+ ) + } + + return ( +
+ {rows.map((row) => { + const apiKey = row.original + const statusConfig = API_KEY_STATUSES[apiKey.status] + const total = apiKey.used_quota + apiKey.remain_quota + + return ( +
+
+
+
+ {apiKey.name} +
+
+ {t('API Key')} +
+
+ {statusConfig && ( + + )} +
+ +
+
+ +
+ +
+ +
+ {t('Quota')} + {apiKey.unlimited_quota ? ( + {t('Unlimited')} + ) : ( + + {formatQuota(apiKey.remain_quota)} + + {' / '} + {formatQuota(total)} + + + )} +
+
+ ) + })} +
+ ) +} + export function ApiKeysTable() { const { t } = useTranslation() const { refreshTrigger } = useApiKeys() @@ -166,7 +300,7 @@ export function ApiKeysTable() { return ( <> -
+
@@ -184,18 +318,9 @@ export function ApiKeysTable() {
{isMobile ? ( - - isDisabledApiKeyRow(row.original) - ? DISABLED_ROW_MOBILE - : undefined - } /> ) : (
-
+
- + {title} @@ -205,14 +205,14 @@ export function UpdateConfigDialog({
) : ( -
+
-
+
-
+
-
+
-
+
- +
- diff --git a/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx b/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx index e76055bb..14e44da0 100644 --- a/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx +++ b/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx @@ -124,7 +124,7 @@ export function ViewLogsDialog({ return ( - + @@ -132,11 +132,11 @@ export function ViewLogsDialog({ -
+
{t('Deployment ID')}: {deploymentId}
-
+
-
+
{t('Auto refresh')}
-
+
{t('Container')} @@ -234,7 +234,7 @@ export function ViewLogsDialog({
{ const target = e.target as HTMLDivElement const isAtBottom = diff --git a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx index ffbe8914..996de746 100644 --- a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx +++ b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx @@ -601,8 +601,8 @@ export function ModelMutateDrawer({ return ( - - + + {isEditing ? t('Edit Model') : t('Create Model')} @@ -621,7 +621,7 @@ export function ModelMutateDrawer({ onSubmit={form.handleSubmit( onSubmit as Parameters[0] )} - className='flex-1 space-y-6 overflow-y-auto px-4' + className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4' > {/* Basic Information */}
@@ -1232,7 +1232,7 @@ export function ModelMutateDrawer({ - +