diff --git a/web/default/src/components/dialog.tsx b/web/default/src/components/dialog.tsx new file mode 100644 index 000000000000..0e93b17bf636 --- /dev/null +++ b/web/default/src/components/dialog.tsx @@ -0,0 +1,127 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import * as React from 'react' +import { cn } from '@/lib/utils' +import { + Dialog as DialogRoot, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' + +type DialogProps = React.ComponentProps & { + title: React.ReactNode + description?: React.ReactNode + children: React.ReactNode + trigger?: React.ReactElement + footer?: React.ReactNode + contentHeight?: React.CSSProperties['height'] + contentClassName?: string + headerClassName?: string + titleClassName?: string + descriptionClassName?: string + bodyClassName?: string + footerClassName?: string + initialFocus?: boolean + showCloseButton?: boolean +} + +const dialogContentMotionClassName = + 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 duration-100' + +export function Dialog({ + title, + description, + children, + trigger, + footer, + contentHeight = 'auto', + contentClassName, + headerClassName, + titleClassName, + descriptionClassName, + bodyClassName, + footerClassName, + initialFocus, + showCloseButton, + ...dialogProps +}: DialogProps) { + return ( + + {trigger ? : null} + + + {title} + {description ? ( + + {description} + + ) : null} + + +
+
+ {children} +
+
+ + {footer ? ( + + {footer} + + ) : null} +
+
+ ) +} diff --git a/web/default/src/components/layout/components/public-header.tsx b/web/default/src/components/layout/components/public-header.tsx index 5fad63703d3e..7030a7c54040 100644 --- a/web/default/src/components/layout/components/public-header.tsx +++ b/web/default/src/components/layout/components/public-header.tsx @@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications' import { useSystemConfig } from '@/hooks/use-system-config' import { useTopNavLinks } from '@/hooks/use-top-nav-links' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Skeleton } from '@/components/ui/skeleton' +import { Dialog } from '@/components/dialog' import { LanguageSwitcher } from '@/components/language-switcher' import { NotificationPopover } from '@/components/notification-popover' import { ProfileDropdown } from '@/components/profile-dropdown' @@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) { closeAuthPrompt() } }} - > - - - {t('Sign in required')} - - {t('Please sign in to view {{module}}.', { - module: authPromptTarget?.title || '', - })} - - -
- {t('Redirecting to sign in in {{seconds}} seconds.', { - seconds: authPromptSecondsLeft, - })} -
- + title={t('Sign in required')} + description={t('Please sign in to view {{module}}.', { + module: authPromptTarget?.title || '', + })} + contentClassName='sm:max-w-md' + contentHeight='auto' + footer={ + <> - -
+ + } + > +
+ {t('Redirecting to sign in in {{seconds}} seconds.', { + seconds: authPromptSecondsLeft, + })} +
) diff --git a/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx b/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx index 88dda89fde11..02e8c16d6e15 100644 --- a/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx +++ b/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx @@ -20,16 +20,9 @@ import { useMemo } from 'react' import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Dialog } from '@/components/dialog' import type { SecureVerificationState, VerificationMethod, @@ -91,122 +84,118 @@ export function SecureVerificationDialog({ (activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6)) return ( - - -
- - - - {title} - - - {description} - - - -
- {availableTabs.length === 0 ? ( -
-
- -
-

- {t( - 'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.' - )} -

-
- ) : ( - - onMethodChange(value as VerificationMethod) + + + {title} + + } + description={description} + contentClassName='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 overflow-hidden border-none shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl' + headerClassName='border-b pb-4 text-left' + titleClassName='flex items-center gap-2 text-lg font-semibold' + descriptionClassName='text-left' + contentHeight='auto' + bodyClassName='px-1 py-1' + showCloseButton={!state.loading} + footerClassName='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end' + footer={ + <> + + + + } + > + {availableTabs.length === 0 ? ( +
+
+ +
+

+ {t( + 'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.' + )} +

+
+ ) : ( + onMethodChange(value as VerificationMethod)} + className='gap-4' + > + + {methods.has2FA && ( + {t('Authenticator code')} + )} + {methods.hasPasskey && methods.passkeySupported && ( + {t('Passkey')} + )} + + + +

+ {t( + 'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.' + )} +

+ onCodeChange(event.target.value)} + placeholder={t('Enter verification code')} + disabled={state.loading} + autoFocus={activeMethod === '2fa'} + onKeyDown={(event) => { + if (event.key === 'Enter' && !verifyDisabled) { + event.preventDefault() + handleVerify() } - className='gap-4' - > - - {methods.has2FA && ( - - {t('Authenticator code')} - - )} - {methods.hasPasskey && methods.passkeySupported && ( - {t('Passkey')} - )} - - - -

+ }} + /> + + + +

+
+ +
+

+ {t('Use your Passkey')} +

+

{t( - 'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.' + 'We will prompt your device to confirm using biometrics or your hardware key.' )}

- onCodeChange(event.target.value)} - placeholder={t('Enter verification code')} - disabled={state.loading} - autoFocus={activeMethod === '2fa'} - onKeyDown={(event) => { - if (event.key === 'Enter' && !verifyDisabled) { - event.preventDefault() - handleVerify() - } - }} - /> - - - -
-
- -
-

- {t('Use your Passkey')} -

-

- {t( - 'We will prompt your device to confirm using biometrics or your hardware key.' - )} -

-
-
-
- {!methods.passkeySupported && ( -

- {t('This device does not support Passkey verification.')} -

- )} -
- +
+
+
+ {!methods.passkeySupported && ( +

+ {t('This device does not support Passkey verification.')} +

)} -
- - - - - -
-
+ + + )}
) } diff --git a/web/default/src/features/auth/sign-in/components/user-auth-form.tsx b/web/default/src/features/auth/sign-in/components/user-auth-form.tsx index 46591cccf383..1830517c22c0 100644 --- a/web/default/src/features/auth/sign-in/components/user-auth-form.tsx +++ b/web/default/src/features/auth/sign-in/components/user-auth-form.tsx @@ -32,14 +32,6 @@ import { import { cn } from '@/lib/utils' import { useStatus } from '@/hooks/use-status' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Form, FormControl, @@ -50,6 +42,7 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Dialog } from '@/components/dialog' import { PasswordInput } from '@/components/password-input' import { Turnstile } from '@/components/turnstile' import { login, wechatLoginByCode } from '@/features/auth/api' @@ -414,43 +407,16 @@ export function UserAuthForm({ - - - {t('WeChat sign in')} - - {t( - 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' - )} - - - - {wechatQrCodeUrl ? ( -
- {t('WeChat -
- ) : ( -

- {t('QR code is not configured. Please contact support.')} -

- )} - -
- - setWeChatCode(event.target.value)} - autoComplete='one-time-code' - /> -
- - + title={t('WeChat sign in')} + description={t( + 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' + )} + contentClassName='max-w-sm' + headerClassName='text-left' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <>
)} diff --git a/web/default/src/features/auth/sign-up/components/sign-up-form.tsx b/web/default/src/features/auth/sign-up/components/sign-up-form.tsx index 9466c15ec4d1..94e262a10941 100644 --- a/web/default/src/features/auth/sign-up/components/sign-up-form.tsx +++ b/web/default/src/features/auth/sign-up/components/sign-up-form.tsx @@ -26,14 +26,6 @@ import { toast } from 'sonner' import { cn } from '@/lib/utils' import { useStatus } from '@/hooks/use-status' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Form, FormControl, @@ -44,6 +36,7 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Dialog } from '@/components/dialog' import { PasswordInput } from '@/components/password-input' import { Turnstile } from '@/components/turnstile' import { register, wechatLoginByCode } from '@/features/auth/api' @@ -387,43 +380,16 @@ export function SignUpForm({ - - - {t('WeChat sign in')} - - {t( - 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' - )} - - - - {wechatQrCodeUrl ? ( -
- {t('WeChat -
- ) : ( -

- {t('QR code is not configured. Please contact support.')} -

- )} - -
- - setWeChatCode(event.target.value)} - autoComplete='one-time-code' - /> -
- - + title={t('WeChat sign in')} + description={t( + 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' + )} + contentClassName='max-w-sm' + headerClassName='text-left' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <>
)} diff --git a/web/default/src/features/channels/components/data-table-bulk-actions.tsx b/web/default/src/features/channels/components/data-table-bulk-actions.tsx index c5d5bd7ff554..31a30b28e653 100644 --- a/web/default/src/features/channels/components/data-table-bulk-actions.tsx +++ b/web/default/src/features/channels/components/data-table-bulk-actions.tsx @@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table' import { Power, PowerOff, Tag, Trash2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { @@ -38,6 +30,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' +import { Dialog } from '@/components/dialog' import { handleBatchDelete, handleBatchDisable, @@ -188,29 +181,21 @@ export function DataTableBulkActions({ {/* Set Tag Dialog */} - - - - {t('Set Tag')} - - {t('Set a tag for')} {selectedIds.length}{' '} - {t('selected channel(s). Leave empty to remove tag.')} - - - -
-
- - setTagValue(e.target.value)} - /> -
-
- - + + {t('Set a tag for')} + {selectedIds.length}{' '} + {t('selected channel(s). Leave empty to remove tag.')} + + } + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+
+ + setTagValue(e.target.value)} + /> +
+
{/* Delete Confirmation Dialog */} - - - - {t('Delete Channels?')} - - {t('Are you sure you want to delete')} {selectedIds.length}{' '} - {t('channel(s)? This action cannot be undone.')} - - - - + + {t('Are you sure you want to delete')} + {selectedIds.length}{' '} + {t('channel(s)? This action cannot be undone.')} + + } + contentHeight='auto' + footer={ + <> - - + + } + > + {' '} ) diff --git a/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx b/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx index f9e39d84d674..32b91f094bae 100644 --- a/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx @@ -24,14 +24,7 @@ import { toast } from 'sonner' import { formatCurrencyFromUSD } from '@/lib/currency' import { formatTimestampToDate } from '@/lib/format' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' +import { Dialog } from '@/components/dialog' import { getCodexUsage, updateChannelBalance } from '../../api' import { channelsQueryKeys } from '../../lib' import { useChannels } from '../channels-provider' @@ -161,53 +154,55 @@ export function BalanceQueryDialog({ } return ( - - - - {t('Query Balance')} - - {t('Update balance for:')} {currentRow.name} - - - -
- {/* Current Balance Display */} -
-
- - {t('Current Balance')} -
-
- {balance !== null - ? formatBalance(balance) - : formatBalance(currentRow.balance)} -
-
- {t('Last updated:')}{' '} - {formatDate( - balanceUpdatedTime ?? currentRow.balance_updated_time - )} -
-
- - {/* Balance Update Button */} - -
- - + + {t('Update balance for:')} + {currentRow.name} + + } + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+ {/* Current Balance Display */} +
+
+ + {t('Current Balance')} +
+
+ {balance !== null + ? formatBalance(balance) + : formatBalance(currentRow.balance)} +
+
+ {t('Last updated:')}{' '} + {formatDate(balanceUpdatedTime ?? currentRow.balance_updated_time)} +
+
+ + {/* Balance Update Button */} + +
) } diff --git a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx index d511128472f2..ae18648fe9fa 100644 --- a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx @@ -33,14 +33,6 @@ import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { useIsMobile } from '@/hooks/use-mobile' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { @@ -75,6 +67,7 @@ import { } from '@/components/ui/tooltip' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTablePagination } from '@/components/data-table/pagination' +import { Dialog } from '@/components/dialog' import { sideDrawerContentClassName, sideDrawerFooterClassName, @@ -529,179 +522,184 @@ export function ChannelTestDialog({ return ( <> - - - - {t('Test Channel Connection')} - - {t('Test connectivity for:')} {currentRow.name} - - - -
-
-
- - { + const itemValue = option.value + return { value: itemValue, label: t(option.label) } + }), + ]} + value={endpointType} + onValueChange={(v) => v !== null && setEndpointType(v)} + > + + + + + + {endpointTypeOptions.map((option) => { const itemValue = option.value - return { value: itemValue, label: t(option.label) } - }), - ]} - value={endpointType} - onValueChange={(v) => v !== null && setEndpointType(v)} - > - - - - - - {endpointTypeOptions.map((option) => { - const itemValue = option.value - return ( - - {t(option.label)} - - ) - })} - - - -

- {t( - 'Override the endpoint used for testing. Leave empty to auto detect.' - )} -

+ return ( + + {t(option.label)} + + ) + })} +
+
+ +

+ {t( + 'Override the endpoint used for testing. Leave empty to auto detect.' + )} +

+
+
+ +
+ + + {isStreamTest ? t('Enabled') : t('Disabled')} +
-
- -
- - - {isStreamTest ? t('Enabled') : t('Disabled')} - -
+

+ {t('Enable streaming mode for the test request.')} +

+
+
+ +
+
+
+

{t('Channel models')}

- {t('Enable streaming mode for the test request.')} + {t('Select models to run batch tests.')}

+ setSearchTerm(e.target.value)} + className='sm:w-64' + />
-
-
-
-

{t('Channel models')}

-

- {t('Select models to run batch tests.')} -

-
- setSearchTerm(e.target.value)} - className='sm:w-64' - /> -
- -
-
-
- - - - - - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - +
+
+
+ + + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + ))} - ))} - - - {table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {models.length - ? 'No models matched your search.' - : 'This channel has no configured models.'} - - - )} - -
-
+ )) + ) : ( + + + {models.length + ? 'No models matched your search.' + : 'This channel has no configured models.'} + + + )} + +
- -
- +
-
- - - - + +
+
- - - {t('Codex Authorization')} - - {t( - 'Generate a Codex OAuth credential and paste it into the channel key field.' - )} - - - -
- - - {t( - '1) Click "Open authorization page" and complete login. 2) Your browser may redirect to localhost (it is OK if the page does not load). 3) Copy the full URL from the address bar and paste it below. 4) Click "Generate credential".' - )} - - - -
- - - -
- -
-
{t('Callback URL')}
- - setState((prev) => ({ ...prev, callbackUrl: e.target.value })) - } - placeholder={t( - 'Paste the full callback URL (includes code & state)' - )} - autoComplete='off' - spellCheck={false} - /> -
- {t( - 'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.' - )} -
-
-
- - + + + + + +
+
{t('Callback URL')}
+ + setState((prev) => ({ ...prev, callbackUrl: e.target.value })) + } + placeholder={t( + 'Paste the full callback URL (includes code & state)' + )} + autoComplete='off' + spellCheck={false} + /> +
+ {t( + 'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.' + )} +
+
+
) } diff --git a/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx b/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx index 442db3d77d6a..17440b5588b8 100644 --- a/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx @@ -31,16 +31,9 @@ import { useTranslation } from 'react-i18next' import dayjs from '@/lib/dayjs' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Progress } from '@/components/ui/progress' import { ScrollArea } from '@/components/ui/scroll-area' +import { Dialog } from '@/components/dialog' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' type CodexRateLimitWindow = { @@ -414,186 +407,190 @@ export function CodexUsageDialog({ }, [response]) return ( - - - - - {t('Codex Account & Usage')} - - - {t('Channel:')} {channelName || '-'}{' '} - {channelId ? `(#${channelId})` : ''} - - - -
- {errorMessage && ( -
- {errorMessage} -
- )} + + {t('Channel:')} + {channelName || '-'}{' '} + {channelId ? `(#${channelId})` : ''} + + } + contentClassName='sm:max-w-3xl' + titleClassName='flex items-center gap-2' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> + + + } + > +
+ {errorMessage && ( +
+ {errorMessage} +
+ )} - {/* Account summary */} -
-
-
+ {/* Account summary */} +
+
+
+ + {statusBadge} + {typeof response?.upstream_status === 'number' && ( - {statusBadge} - {typeof response?.upstream_status === 'number' && ( - - )} -
- {onRefresh && ( - )}
+ {onRefresh && ( + + )} +
- {/* Account identity info */} -
- } - label='User ID' - value={payload?.user_id} - mono - /> - } - label={t('Email')} - value={payload?.email} - /> - } - label='Account ID' - value={payload?.account_id} - mono - /> -
+ {/* Account identity info */} +
+ } + label='User ID' + value={payload?.user_id} + mono + /> + } + label={t('Email')} + value={payload?.email} + /> + } + label='Account ID' + value={payload?.account_id} + mono + />
+
- {/* Rate limit windows */} -
-
-
- {t('Rate Limit Windows')} -
-

- {t( - 'Tracks current account base limits and additional metered usage on Codex upstream.' - )} -

- + {/* Rate limit windows */} +
+
+
+ {t('Rate Limit Windows')}
- - {additionalRateLimits.length > 0 && ( -
-
-
- {t('Additional Limits')} -
-

- {t( - 'Per-feature metered windows split by model or capability.' - )} -

-
-
- {additionalRateLimits.map((item, index) => { - const limitName = - item.limit_name || - item.metered_feature || - `${t('Additional Limit')} ${index + 1}` - return ( -
0 ? 'border-t pt-4' : ''} - > - -
- ) - })} -
-
- )} +

+ {t( + 'Tracks current account base limits and additional metered usage on Codex upstream.' + )} +

+
- {/* Raw JSON collapsible */} -
- - {showRawJson && ( - <> -
- + {additionalRateLimits.length > 0 && ( +
+
+
+ {t('Additional Limits')}
- -
-                    {rawJsonText || '-'}
-                  
-
- - )} -
+

+ {t( + 'Per-feature metered windows split by model or capability.' + )} +

+
+
+ {additionalRateLimits.map((item, index) => { + const limitName = + item.limit_name || + item.metered_feature || + `${t('Additional Limit')} ${index + 1}` + return ( +
0 ? 'border-t pt-4' : ''} + > + +
+ ) + })} +
+
+ )}
- - - - +
{t('Raw JSON')}
+ {showRawJson ? ( + + ) : ( + + )} + + {showRawJson && ( + <> +
+ +
+ +
+                  {rawJsonText || '-'}
+                
+
+ + )} +
+
) } diff --git a/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx b/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx index 36a1a8e38ed0..9edaaaebe3f9 100644 --- a/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx @@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Dialog } from '@/components/dialog' import { handleCopyChannel } from '../../lib' import { useChannels } from '../channels-provider' @@ -74,45 +67,20 @@ export function CopyChannelDialog({ } return ( - - - - {t('Copy Channel')} - - {t('Create a copy of:')} {currentRow.name} - - - -
-
- - setSuffix(e.target.value)} - disabled={isCopying} - /> -

- {t('New name will be:')} {currentRow.name} - {suffix} -

-
- -
- setResetBalance(!!checked)} - disabled={isCopying} - /> - -
-
- - + + {t('Create a copy of:')} + {currentRow.name} + + } + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+
+ + setSuffix(e.target.value)} + disabled={isCopying} + /> +

+ {t('New name will be:')} {currentRow.name} + {suffix} +

+
+ +
+ setResetBalance(!!checked)} + disabled={isCopying} + /> + +
+
) } diff --git a/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx b/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx index 30fa660f96c5..9967fa98c8f7 100644 --- a/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx @@ -22,14 +22,6 @@ import { Loader2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ScrollArea } from '@/components/ui/scroll-area' @@ -43,6 +35,7 @@ import { } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Textarea } from '@/components/ui/textarea' +import { Dialog } from '@/components/dialog' import { GroupBadge } from '@/components/group-badge' import { StatusBadge } from '@/components/status-badge' import { @@ -222,225 +215,228 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) { if (!currentTag) return null return ( - - - - - {t('Edit Tag:')} {currentTag} - - - {t( - 'Batch edit all channels with this tag. Leave fields empty to keep current values.' - )} - - - - -
- {/* Tag Name */} -
- - setNewTag(e.target.value)} - placeholder={t('Enter new tag name or leave empty')} - /> -
- - + + {t('Edit Tag:')} + {currentTag} + + } + description={t( + 'Batch edit all channels with this tag. Leave fields empty to keep current values.' + )} + contentClassName='max-h-[90vh] max-w-2xl' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> + + + + } + > + +
+ {/* Tag Name */} +
+ + setNewTag(e.target.value)} + placeholder={t('Enter new tag name or leave empty')} + /> +
- {/* Models */} -
-