Commit 15ff8e02 by 同語 Committed by GitHub

chore(web): improve frontend dialog layout and sizing (#5346)

Merge pull request #5346 from QuantumNous/perf/ui-dialog
parents a1c82841 2eaa943d
/*
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 <https://www.gnu.org/licenses/>.
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<typeof DialogRoot> & {
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 (
<DialogRoot {...dialogProps}>
{trigger ? <DialogTrigger render={trigger} /> : null}
<DialogContent
className={cn(
'flex max-h-[calc(100vh-2rem)] w-full flex-col gap-4 overflow-hidden p-4 sm:max-w-2xl sm:p-6',
contentClassName,
dialogContentMotionClassName
)}
initialFocus={initialFocus}
showCloseButton={showCloseButton}
style={
{
'--dialog-content-height': contentHeight,
} as React.CSSProperties
}
>
<DialogHeader
className={cn('flex-shrink-0 text-start', headerClassName)}
>
<DialogTitle className={titleClassName}>{title}</DialogTitle>
{description ? (
<DialogDescription className={descriptionClassName}>
{description}
</DialogDescription>
) : null}
</DialogHeader>
<div
className={cn(
'-mx-1 min-h-0 overflow-x-hidden overflow-y-auto overscroll-contain',
'h-[var(--dialog-content-height)] max-h-[calc(100vh-14rem)]'
)}
>
<div
className={cn(
'min-w-0 px-1 py-1',
'[&_form]:overflow-x-visible',
'[&_[data-slot=scroll-area-viewport]]:px-1 [&_[data-slot=scroll-area-viewport]]:py-1',
bodyClassName
)}
>
{children}
</div>
</div>
{footer ? (
<DialogFooter
className={cn(
'flex-shrink-0 gap-2 sm:-mx-6 sm:-mb-6 sm:justify-end sm:p-6',
footerClassName
)}
>
{footer}
</DialogFooter>
) : null}
</DialogContent>
</DialogRoot>
)
}
...@@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications' ...@@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications'
import { useSystemConfig } from '@/hooks/use-system-config' import { useSystemConfig } from '@/hooks/use-system-config'
import { useTopNavLinks } from '@/hooks/use-top-nav-links' import { useTopNavLinks } from '@/hooks/use-top-nav-links'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Dialog } from '@/components/dialog'
import { LanguageSwitcher } from '@/components/language-switcher' import { LanguageSwitcher } from '@/components/language-switcher'
import { NotificationPopover } from '@/components/notification-popover' import { NotificationPopover } from '@/components/notification-popover'
import { ProfileDropdown } from '@/components/profile-dropdown' import { ProfileDropdown } from '@/components/profile-dropdown'
...@@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) { ...@@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) {
closeAuthPrompt() closeAuthPrompt()
} }
}} }}
> title={t('Sign in required')}
<DialogContent className='sm:max-w-md'> description={t('Please sign in to view {{module}}.', {
<DialogHeader>
<DialogTitle>{t('Sign in required')}</DialogTitle>
<DialogDescription>
{t('Please sign in to view {{module}}.', {
module: authPromptTarget?.title || '', module: authPromptTarget?.title || '',
})} })}
</DialogDescription> contentClassName='sm:max-w-md'
</DialogHeader> contentHeight='auto'
footer={
<>
<Button variant='outline' onClick={closeAuthPrompt}>
{t('Cancel')}
</Button>
<Button onClick={navigateToSignIn}>{t('Sign in now')}</Button>
</>
}
>
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'> <div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
{t('Redirecting to sign in in {{seconds}} seconds.', { {t('Redirecting to sign in in {{seconds}} seconds.', {
seconds: authPromptSecondsLeft, seconds: authPromptSecondsLeft,
})} })}
</div> </div>
<DialogFooter>
<Button variant='outline' onClick={closeAuthPrompt}>
{t('Cancel')}
</Button>
<Button onClick={navigateToSignIn}>{t('Sign in now')}</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
</> </>
) )
......
...@@ -20,16 +20,9 @@ import { useMemo } from 'react' ...@@ -20,16 +20,9 @@ import { useMemo } from 'react'
import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react' import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Dialog } from '@/components/dialog'
import type { import type {
SecureVerificationState, SecureVerificationState,
VerificationMethod, VerificationMethod,
...@@ -91,23 +84,45 @@ export function SecureVerificationDialog({ ...@@ -91,23 +84,45 @@ export function SecureVerificationDialog({
(activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6)) (activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6))
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent open={open}
className='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 gap-0 overflow-hidden border-none p-0 shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl' onOpenChange={onOpenChange}
showCloseButton={!state.loading} title={
> <>
<div className='bg-background flex max-h-[calc(100dvh-2rem)] flex-col'>
<DialogHeader className='border-b px-6 py-5 text-left'>
<DialogTitle className='flex items-center gap-2 text-lg font-semibold'>
<ShieldCheck className='text-primary h-5 w-5' /> <ShieldCheck className='text-primary h-5 w-5' />
{title} {title}
</DialogTitle> </>
<DialogDescription className='text-left'> }
{description} description={description}
</DialogDescription> 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'
</DialogHeader> headerClassName='border-b pb-4 text-left'
titleClassName='flex items-center gap-2 text-lg font-semibold'
<div className='flex-1 overflow-y-auto px-6 py-5'> 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={
<>
<Button
type='button'
variant='outline'
disabled={state.loading}
onClick={onCancel}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleVerify}
disabled={availableTabs.length === 0 || verifyDisabled}
>
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</>
}
>
{availableTabs.length === 0 ? ( {availableTabs.length === 0 ? (
<div className='grid place-items-center gap-4 text-center'> <div className='grid place-items-center gap-4 text-center'>
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'> <div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
...@@ -122,16 +137,12 @@ export function SecureVerificationDialog({ ...@@ -122,16 +137,12 @@ export function SecureVerificationDialog({
) : ( ) : (
<Tabs <Tabs
value={activeMethod ?? availableTabs[0]} value={activeMethod ?? availableTabs[0]}
onValueChange={(value) => onValueChange={(value) => onMethodChange(value as VerificationMethod)}
onMethodChange(value as VerificationMethod)
}
className='gap-4' className='gap-4'
> >
<TabsList> <TabsList>
{methods.has2FA && ( {methods.has2FA && (
<TabsTrigger value='2fa'> <TabsTrigger value='2fa'>{t('Authenticator code')}</TabsTrigger>
{t('Authenticator code')}
</TabsTrigger>
)} )}
{methods.hasPasskey && methods.passkeySupported && ( {methods.hasPasskey && methods.passkeySupported && (
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger> <TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
...@@ -185,28 +196,6 @@ export function SecureVerificationDialog({ ...@@ -185,28 +196,6 @@ export function SecureVerificationDialog({
</TabsContent> </TabsContent>
</Tabs> </Tabs>
)} )}
</div>
<DialogFooter className='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end'>
<Button
type='button'
variant='outline'
disabled={state.loading}
onClick={onCancel}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleVerify}
disabled={availableTabs.length === 0 || verifyDisabled}
>
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -33,14 +33,6 @@ import { cn } from '@/lib/utils' ...@@ -33,14 +33,6 @@ import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status' import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
...@@ -50,6 +42,7 @@ import { ...@@ -50,6 +42,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile' import { Turnstile } from '@/components/turnstile'
import { login, wechatLoginByCode } from '@/features/auth/api' import { login, wechatLoginByCode } from '@/features/auth/api'
...@@ -414,43 +407,16 @@ export function UserAuthForm({ ...@@ -414,43 +407,16 @@ export function UserAuthForm({
<Dialog <Dialog
open={isWeChatDialogOpen} open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange} onOpenChange={handleWeChatDialogChange}
> title={t('WeChat sign in')}
<DialogContent className='max-w-sm'> description={t(
<DialogHeader className='text-left'>
<DialogTitle>{t('WeChat sign in')}</DialogTitle>
<DialogDescription>
{t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)} )}
</DialogDescription> contentClassName='max-w-sm'
</DialogHeader> headerClassName='text-left'
contentHeight='auto'
{wechatQrCodeUrl ? ( bodyClassName='space-y-4'
<div className='flex justify-center'> footer={
<img <>
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -474,8 +440,32 @@ export function UserAuthForm({ ...@@ -474,8 +440,32 @@ export function UserAuthForm({
) : null} ) : null}
{t('Confirm')} {t('Confirm')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog> </Dialog>
)} )}
</Form> </Form>
......
...@@ -27,14 +27,6 @@ import { cn } from '@/lib/utils' ...@@ -27,14 +27,6 @@ import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status' import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
...@@ -44,6 +36,7 @@ import { ...@@ -44,6 +36,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile' import { Turnstile } from '@/components/turnstile'
import { register, wechatLoginByCode } from '@/features/auth/api' import { register, wechatLoginByCode } from '@/features/auth/api'
...@@ -387,43 +380,16 @@ export function SignUpForm({ ...@@ -387,43 +380,16 @@ export function SignUpForm({
<Dialog <Dialog
open={isWeChatDialogOpen} open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange} onOpenChange={handleWeChatDialogChange}
> title={t('WeChat sign in')}
<DialogContent className='max-w-sm'> description={t(
<DialogHeader className='text-left'>
<DialogTitle>{t('WeChat sign in')}</DialogTitle>
<DialogDescription>
{t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)} )}
</DialogDescription> contentClassName='max-w-sm'
</DialogHeader> headerClassName='text-left'
contentHeight='auto'
{wechatQrCodeUrl ? ( bodyClassName='space-y-4'
<div className='flex justify-center'> footer={
<img <>
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -447,8 +413,32 @@ export function SignUpForm({ ...@@ -447,8 +413,32 @@ export function SignUpForm({
) : null} ) : null}
{t('Confirm')} {t('Confirm')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog> </Dialog>
)} )}
</Form> </Form>
......
...@@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table' ...@@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table'
import { Power, PowerOff, Tag, Trash2 } from 'lucide-react' import { Power, PowerOff, Tag, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
...@@ -38,6 +30,7 @@ import { ...@@ -38,6 +30,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { Dialog } from '@/components/dialog'
import { import {
handleBatchDelete, handleBatchDelete,
handleBatchDisable, handleBatchDisable,
...@@ -188,16 +181,34 @@ export function DataTableBulkActions<TData>({ ...@@ -188,16 +181,34 @@ export function DataTableBulkActions<TData>({
</BulkActionsToolbar> </BulkActionsToolbar>
{/* Set Tag Dialog */} {/* Set Tag Dialog */}
<Dialog open={showTagDialog} onOpenChange={setShowTagDialog}> <Dialog
<DialogContent> open={showTagDialog}
<DialogHeader> onOpenChange={setShowTagDialog}
<DialogTitle>{t('Set Tag')}</DialogTitle> title={t('Set Tag')}
<DialogDescription> description={
{t('Set a tag for')} {selectedIds.length}{' '} <>
{t('Set a tag for')}
{selectedIds.length}{' '}
{t('selected channel(s). Leave empty to remove tag.')} {t('selected channel(s). Leave empty to remove tag.')}
</DialogDescription> </>
</DialogHeader> }
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => {
setShowTagDialog(false)
setTagValue('')
}}
>
{t('Cancel')}
</Button>
<Button onClick={handleSetTag}>{t('Set Tag')}</Button>
</>
}
>
<div className='grid gap-4 py-4'> <div className='grid gap-4 py-4'>
<div className='grid gap-2'> <div className='grid gap-2'>
<Label htmlFor='tag'>{t('Tag')}</Label> <Label htmlFor='tag'>{t('Tag')}</Label>
...@@ -209,34 +220,23 @@ export function DataTableBulkActions<TData>({ ...@@ -209,34 +220,23 @@ export function DataTableBulkActions<TData>({
/> />
</div> </div>
</div> </div>
<DialogFooter>
<Button
variant='outline'
onClick={() => {
setShowTagDialog(false)
setTagValue('')
}}
>
{t('Cancel')}
</Button>
<Button onClick={handleSetTag}>{t('Set Tag')}</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> <Dialog
<DialogContent> open={showDeleteConfirm}
<DialogHeader> onOpenChange={setShowDeleteConfirm}
<DialogTitle>{t('Delete Channels?')}</DialogTitle> title={t('Delete Channels?')}
<DialogDescription> description={
{t('Are you sure you want to delete')} {selectedIds.length}{' '} <>
{t('Are you sure you want to delete')}
{selectedIds.length}{' '}
{t('channel(s)? This action cannot be undone.')} {t('channel(s)? This action cannot be undone.')}
</DialogDescription> </>
</DialogHeader> }
contentHeight='auto'
<DialogFooter> footer={
<>
<Button <Button
variant='outline' variant='outline'
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
...@@ -246,8 +246,10 @@ export function DataTableBulkActions<TData>({ ...@@ -246,8 +246,10 @@ export function DataTableBulkActions<TData>({
<Button variant='destructive' onClick={handleDeleteAll}> <Button variant='destructive' onClick={handleDeleteAll}>
{t('Delete')} {t('Delete')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{' '}
</Dialog> </Dialog>
</> </>
) )
......
...@@ -24,14 +24,7 @@ import { toast } from 'sonner' ...@@ -24,14 +24,7 @@ import { toast } from 'sonner'
import { formatCurrencyFromUSD } from '@/lib/currency' import { formatCurrencyFromUSD } from '@/lib/currency'
import { formatTimestampToDate } from '@/lib/format' import { formatTimestampToDate } from '@/lib/format'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import { Dialog } from '@/components/dialog'
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { getCodexUsage, updateChannelBalance } from '../../api' import { getCodexUsage, updateChannelBalance } from '../../api'
import { channelsQueryKeys } from '../../lib' import { channelsQueryKeys } from '../../lib'
import { useChannels } from '../channels-provider' import { useChannels } from '../channels-provider'
...@@ -161,15 +154,26 @@ export function BalanceQueryDialog({ ...@@ -161,15 +154,26 @@ export function BalanceQueryDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog
<DialogContent> open={open}
<DialogHeader> onOpenChange={handleClose}
<DialogTitle>{t('Query Balance')}</DialogTitle> title={t('Query Balance')}
<DialogDescription> description={
{t('Update balance for:')} <strong>{currentRow.name}</strong> <>
</DialogDescription> {t('Update balance for:')}
</DialogHeader> <strong>{currentRow.name}</strong>
</>
}
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={handleClose} disabled={isQuerying}>
{t('Close')}
</Button>
</>
}
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
{/* Current Balance Display */} {/* Current Balance Display */}
<div className='bg-muted/50 rounded-lg border p-4'> <div className='bg-muted/50 rounded-lg border p-4'>
...@@ -184,9 +188,7 @@ export function BalanceQueryDialog({ ...@@ -184,9 +188,7 @@ export function BalanceQueryDialog({
</div> </div>
<div className='text-muted-foreground mt-2 text-xs'> <div className='text-muted-foreground mt-2 text-xs'>
{t('Last updated:')}{' '} {t('Last updated:')}{' '}
{formatDate( {formatDate(balanceUpdatedTime ?? currentRow.balance_updated_time)}
balanceUpdatedTime ?? currentRow.balance_updated_time
)}
</div> </div>
</div> </div>
...@@ -201,13 +203,6 @@ export function BalanceQueryDialog({ ...@@ -201,13 +203,6 @@ export function BalanceQueryDialog({
{isQuerying ? t('Querying...') : t('Update Balance')} {isQuerying ? t('Querying...') : t('Update Balance')}
</Button> </Button>
</div> </div>
<DialogFooter>
<Button variant='outline' onClick={handleClose} disabled={isQuerying}>
{t('Close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -33,14 +33,6 @@ import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' ...@@ -33,14 +33,6 @@ import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { useIsMobile } from '@/hooks/use-mobile' import { useIsMobile } from '@/hooks/use-mobile'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
...@@ -75,6 +67,7 @@ import { ...@@ -75,6 +67,7 @@ import {
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { DataTablePagination } from '@/components/data-table/pagination' import { DataTablePagination } from '@/components/data-table/pagination'
import { Dialog } from '@/components/dialog'
import { import {
sideDrawerContentClassName, sideDrawerContentClassName,
sideDrawerFooterClassName, sideDrawerFooterClassName,
...@@ -529,15 +522,27 @@ export function ChannelTestDialog({ ...@@ -529,15 +522,27 @@ export function ChannelTestDialog({
return ( return (
<> <>
<Dialog open={open} onOpenChange={handleClose}> <Dialog
<DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'> open={open}
<DialogHeader> onOpenChange={handleClose}
<DialogTitle>{t('Test Channel Connection')}</DialogTitle> title={t('Test Channel Connection')}
<DialogDescription> description={
{t('Test connectivity for:')} <strong>{currentRow.name}</strong> <>
</DialogDescription> {t('Test connectivity for:')}
</DialogHeader> <strong>{currentRow.name}</strong>
</>
}
contentClassName='max-h-[90vh] overflow-hidden sm:max-w-3xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={handleClose}>
{t('Close')}
</Button>
</>
}
>
<div className='max-h-[78vh] space-y-4 overflow-y-auto py-4 pr-1'> <div className='max-h-[78vh] space-y-4 overflow-y-auto py-4 pr-1'>
<div className='grid gap-4 md:grid-cols-2'> <div className='grid gap-4 md:grid-cols-2'>
<div className='grid gap-2'> <div className='grid gap-2'>
...@@ -695,13 +700,6 @@ export function ChannelTestDialog({ ...@@ -695,13 +700,6 @@ export function ChannelTestDialog({
/> />
</div> </div>
</div> </div>
<DialogFooter>
<Button variant='outline' onClick={handleClose}>
{t('Close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
<FailureDetailsSheet <FailureDetailsSheet
details={failureDetails} details={failureDetails}
......
...@@ -24,15 +24,8 @@ import { tryPrettyJson } from '@/lib/utils' ...@@ -24,15 +24,8 @@ import { tryPrettyJson } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
import { completeCodexOAuth, startCodexOAuth } from '../../api' import { completeCodexOAuth, startCodexOAuth } from '../../api'
type CodexOAuthDialogProps = { type CodexOAuthDialogProps = {
...@@ -129,17 +122,35 @@ export function CodexOAuthDialog({ ...@@ -129,17 +122,35 @@ export function CodexOAuthDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-2xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Codex Authorization')}</DialogTitle> title={t('Codex Authorization')}
<DialogDescription> description={t(
{t(
'Generate a Codex OAuth credential and paste it into the channel key field.' 'Generate a Codex OAuth credential and paste it into the channel key field.'
)} )}
</DialogDescription> contentClassName='sm:max-w-2xl'
</DialogHeader> contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={state.isStarting || state.isCompleting}
>
{t('Cancel')}
</Button>
<Button onClick={handleComplete} disabled={!canComplete}>
{state.isCompleting && (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
)}
{state.isCompleting ? t('Generating...') : t('Generate credential')}
</Button>
</>
}
>
<div className='space-y-4'> <div className='space-y-4'>
<Alert> <Alert>
<AlertDescription> <AlertDescription>
...@@ -199,24 +210,6 @@ export function CodexOAuthDialog({ ...@@ -199,24 +210,6 @@ export function CodexOAuthDialog({
</div> </div>
</div> </div>
</div> </div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={state.isStarting || state.isCompleting}
>
{t('Cancel')}
</Button>
<Button onClick={handleComplete} disabled={!canComplete}>
{state.isCompleting && (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
)}
{state.isCompleting ? t('Generating...') : t('Generate credential')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -31,16 +31,9 @@ import { useTranslation } from 'react-i18next' ...@@ -31,16 +31,9 @@ import { useTranslation } from 'react-i18next'
import dayjs from '@/lib/dayjs' import dayjs from '@/lib/dayjs'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
type CodexRateLimitWindow = { type CodexRateLimitWindow = {
...@@ -414,18 +407,33 @@ export function CodexUsageDialog({ ...@@ -414,18 +407,33 @@ export function CodexUsageDialog({
}, [response]) }, [response])
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-3xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle className='flex items-center gap-2'> title={t('Codex Account & Usage')}
{t('Codex Account & Usage')} description={
</DialogTitle> <>
<DialogDescription> {t('Channel:')}
{t('Channel:')} <strong>{channelName || '-'}</strong>{' '} <strong>{channelName || '-'}</strong>{' '}
{channelId ? `(#${channelId})` : ''} {channelId ? `(#${channelId})` : ''}
</DialogDescription> </>
</DialogHeader> }
contentClassName='sm:max-w-3xl'
titleClassName='flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Close')}
</Button>
</>
}
>
<div className='space-y-4'> <div className='space-y-4'>
{errorMessage && ( {errorMessage && (
<div className='rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400'> <div className='rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400'>
...@@ -583,17 +591,6 @@ export function CodexUsageDialog({ ...@@ -583,17 +591,6 @@ export function CodexUsageDialog({
)} )}
</div> </div>
</div> </div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react' ...@@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { handleCopyChannel } from '../../lib' import { handleCopyChannel } from '../../lib'
import { useChannels } from '../channels-provider' import { useChannels } from '../channels-provider'
...@@ -74,15 +67,34 @@ export function CopyChannelDialog({ ...@@ -74,15 +67,34 @@ export function CopyChannelDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Copy Channel')}</DialogTitle> title={t('Copy Channel')}
<DialogDescription> description={
{t('Create a copy of:')} <strong>{currentRow.name}</strong> <>
</DialogDescription> {t('Create a copy of:')}
</DialogHeader> <strong>{currentRow.name}</strong>
</>
}
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isCopying}
>
{t('Cancel')}
</Button>
<Button onClick={handleCopy} disabled={isCopying}>
{isCopying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isCopying ? t('Copying...') : t('Copy Channel')}
</Button>
</>
}
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='suffix'>{t('Name Suffix')}</Label> <Label htmlFor='suffix'>{t('Name Suffix')}</Label>
...@@ -111,21 +123,6 @@ export function CopyChannelDialog({ ...@@ -111,21 +123,6 @@ export function CopyChannelDialog({
</Label> </Label>
</div> </div>
</div> </div>
<DialogFooter>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isCopying}
>
{t('Cancel')}
</Button>
<Button onClick={handleCopy} disabled={isCopying}>
{isCopying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isCopying ? 'Copying...' : 'Copy Channel'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -22,14 +22,6 @@ import { Loader2 } from 'lucide-react' ...@@ -22,14 +22,6 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
...@@ -43,6 +35,7 @@ import { ...@@ -43,6 +35,7 @@ import {
} from '@/components/ui/select' } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { import {
...@@ -222,19 +215,33 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) { ...@@ -222,19 +215,33 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
if (!currentTag) return null if (!currentTag) return null
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog
<DialogContent className='max-h-[90vh] max-w-2xl'> open={open}
<DialogHeader> onOpenChange={handleClose}
<DialogTitle> title={
{t('Edit Tag:')} {currentTag} <>
</DialogTitle> {t('Edit Tag:')}
<DialogDescription> {currentTag}
{t( </>
}
description={t(
'Batch edit all channels with this tag. Leave fields empty to keep current values.' 'Batch edit all channels with this tag. Leave fields empty to keep current values.'
)} )}
</DialogDescription> contentClassName='max-h-[90vh] max-w-2xl'
</DialogHeader> contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={handleClose}>
{t('Cancel')}
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{t('Save Changes')}
</Button>
</>
}
>
<ScrollArea className='max-h-[60vh] pr-4'> <ScrollArea className='max-h-[60vh] pr-4'>
<div className='space-y-6'> <div className='space-y-6'>
{/* Tag Name */} {/* Tag Name */}
...@@ -430,17 +437,6 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) { ...@@ -430,17 +437,6 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
</div> </div>
</div> </div>
</ScrollArea> </ScrollArea>
<DialogFooter>
<Button variant='outline' onClick={handleClose}>
{t('Cancel')}
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{t('Save Changes')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -28,14 +28,6 @@ import { ...@@ -28,14 +28,6 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
...@@ -44,6 +36,7 @@ import { ...@@ -44,6 +36,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { Dialog } from '@/components/dialog'
import { fetchUpstreamModels, updateChannel } from '../../api' import { fetchUpstreamModels, updateChannel } from '../../api'
import { import {
channelsQueryKeys, channelsQueryKeys,
...@@ -365,28 +358,46 @@ export function FetchModelsDialog({ ...@@ -365,28 +358,46 @@ export function FetchModelsDialog({
) )
} }
const showFooterActions =
!!(activeChannel || customFetcher) &&
!isFetching &&
(fetchedModels.length > 0 || removedModels.length > 0)
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog
<DialogContent className='max-w-3xl'> open={open}
<DialogHeader> onOpenChange={handleClose}
<DialogTitle>{t('Fetch Models')}</DialogTitle> title={t('Fetch Models')}
<DialogDescription> description={
{activeChannel ? ( activeChannel ? (
<> <>
{t('Fetch available models for:')}{' '} {t('Channel:')} <strong>{activeChannel.name}</strong>
<strong>{activeChannel.name}</strong>
</> </>
) : channelName ? ( ) : channelName ? (
<> <>
{t('Fetch available models for:')}{' '} {t('Channel:')} <strong>{channelName}</strong>
<strong>{channelName}</strong>
</> </>
) : ( ) : (
t('Fetch available models from upstream') t('Fetch available models from upstream')
)} )
</DialogDescription> }
</DialogHeader> contentClassName='max-w-3xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
showFooterActions ? (
<>
<Button variant='outline' onClick={handleClose} disabled={isSaving}>
{t('Cancel')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isSaving ? t('Saving...') : t('Save Models')}
</Button>
</>
) : null
}
>
{!activeChannel && !customFetcher ? ( {!activeChannel && !customFetcher ? (
<div className='text-muted-foreground py-8 text-center'> <div className='text-muted-foreground py-8 text-center'>
{t('No channel selected')} {t('No channel selected')}
...@@ -459,8 +470,7 @@ export function FetchModelsDialog({ ...@@ -459,8 +470,7 @@ export function FetchModelsDialog({
className='max-h-96 space-y-2 overflow-y-auto' className='max-h-96 space-y-2 overflow-y-auto'
> >
{getSortedCategoryEntries(newModelsByCategory).map( {getSortedCategoryEntries(newModelsByCategory).map(
([category, models]) => ([category, models]) => renderModelCategory(category, models)
renderModelCategory(category, models)
)} )}
</TabsContent> </TabsContent>
...@@ -469,8 +479,7 @@ export function FetchModelsDialog({ ...@@ -469,8 +479,7 @@ export function FetchModelsDialog({
className='max-h-96 space-y-2 overflow-y-auto' className='max-h-96 space-y-2 overflow-y-auto'
> >
{getSortedCategoryEntries(existingModelsByCategory).map( {getSortedCategoryEntries(existingModelsByCategory).map(
([category, models]) => ([category, models]) => renderModelCategory(category, models)
renderModelCategory(category, models)
)} )}
</TabsContent> </TabsContent>
...@@ -494,23 +503,8 @@ export function FetchModelsDialog({ ...@@ -494,23 +503,8 @@ export function FetchModelsDialog({
{t('{{n}} model(s) selected', { n: selectedModels.length })} {t('{{n}} model(s) selected', { n: selectedModels.length })}
</div> </div>
</div> </div>
<DialogFooter>
<Button
variant='outline'
onClick={handleClose}
disabled={isSaving}
>
{t('Cancel')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isSaving ? t('Saving...') : t('Save Models')}
</Button>
</DialogFooter>
</> </>
)} )}
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -23,13 +23,6 @@ import { useTranslation } from 'react-i18next' ...@@ -23,13 +23,6 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select, Select,
SelectContent, SelectContent,
SelectGroup, SelectGroup,
...@@ -47,6 +40,7 @@ import { ...@@ -47,6 +40,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { ConfirmDialog } from '@/components/confirm-dialog' import { ConfirmDialog } from '@/components/confirm-dialog'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { import {
getMultiKeyStatus, getMultiKeyStatus,
...@@ -228,10 +222,11 @@ export function MultiKeyManageDialog({ ...@@ -228,10 +222,11 @@ export function MultiKeyManageDialog({
return ( return (
<> <>
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='flex max-h-[90vh] max-w-5xl flex-col'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle className='flex items-center gap-2'> title={
<>
{t('Multi-Key Management')} {t('Multi-Key Management')}
<StatusBadge <StatusBadge
label={currentRow.name} label={currentRow.name}
...@@ -249,12 +244,16 @@ export function MultiKeyManageDialog({ ...@@ -249,12 +244,16 @@ export function MultiKeyManageDialog({
copyable={false} copyable={false}
/> />
)} )}
</DialogTitle> </>
<DialogDescription> }
{t('Manage multi-key status and configuration for this channel')} description={t(
</DialogDescription> 'Manage multi-key status and configuration for this channel'
</DialogHeader> )}
contentClassName='flex max-h-[90vh] max-w-5xl flex-col'
titleClassName='flex items-center gap-2'
contentHeight='min(72vh, 720px)'
bodyClassName='space-y-4'
>
<div className='flex min-h-0 flex-1 flex-col space-y-4 overflow-hidden'> <div className='flex min-h-0 flex-1 flex-col space-y-4 overflow-hidden'>
{/* Statistics */} {/* Statistics */}
<div className='grid shrink-0 grid-cols-3 gap-3'> <div className='grid shrink-0 grid-cols-3 gap-3'>
...@@ -339,9 +338,7 @@ export function MultiKeyManageDialog({ ...@@ -339,9 +338,7 @@ export function MultiKeyManageDialog({
<Button <Button
variant='destructive' variant='destructive'
size='sm' size='sm'
onClick={() => onClick={() => setConfirmAction({ type: 'delete-disabled' })}
setConfirmAction({ type: 'delete-disabled' })
}
> >
<Trash2 className='mr-2 h-4 w-4' /> <Trash2 className='mr-2 h-4 w-4' />
{t('Delete Auto-Disabled')} {t('Delete Auto-Disabled')}
...@@ -436,7 +433,6 @@ export function MultiKeyManageDialog({ ...@@ -436,7 +433,6 @@ export function MultiKeyManageDialog({
</div> </div>
)} )}
</div> </div>
</DialogContent>
</Dialog> </Dialog>
{/* Confirmation Dialog */} {/* Confirmation Dialog */}
......
...@@ -34,18 +34,11 @@ import { ...@@ -34,18 +34,11 @@ import {
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Dialog } from '@/components/dialog'
import { import {
deleteOllamaModel, deleteOllamaModel,
fetchModels as fetchModelsFromEndpoint, fetchModels as fetchModelsFromEndpoint,
...@@ -375,21 +368,30 @@ export function OllamaModelsDialog({ ...@@ -375,21 +368,30 @@ export function OllamaModelsDialog({
if (!open) return null if (!open) return null
return ( return (
<Dialog open={open} onOpenChange={close}> <Dialog
<DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'> open={open}
<DialogHeader> onOpenChange={close}
<DialogTitle>{t('Ollama Models')}</DialogTitle> title={t('Ollama Models')}
<DialogDescription> description={
<>
{t('Manage local models for:')} <strong>{currentRow?.name}</strong> {t('Manage local models for:')} <strong>{currentRow?.name}</strong>
</DialogDescription> </>
</DialogHeader> }
contentClassName='sm:max-w-3xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<Button variant='outline' onClick={close}>
{t('Close')}
</Button>
}
>
{!isOllamaChannel ? ( {!isOllamaChannel ? (
<div className='text-muted-foreground py-8 text-center'> <div className='text-muted-foreground py-8 text-center'>
{t('This channel is not an Ollama channel.')} {t('This channel is not an Ollama channel.')}
</div> </div>
) : ( ) : (
<div className='max-h-[78vh] space-y-4 overflow-y-auto py-2 pr-1'> <div className='space-y-4 py-2 pr-1'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between'> <div className='flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between'>
<div className='flex-1 space-y-2'> <div className='flex-1 space-y-2'>
<Label htmlFor='ollama-pull'>{t('Pull model')}</Label> <Label htmlFor='ollama-pull'>{t('Pull model')}</Label>
...@@ -521,9 +523,7 @@ export function OllamaModelsDialog({ ...@@ -521,9 +523,7 @@ export function OllamaModelsDialog({
<div className='flex min-w-0 items-start gap-3'> <div className='flex min-w-0 items-start gap-3'>
<Checkbox <Checkbox
checked={checked} checked={checked}
onCheckedChange={(v) => onCheckedChange={(v) => toggleSelected(m.id, !!v)}
toggleSelected(m.id, !!v)
}
aria-label={`Select model ${m.id}`} aria-label={`Select model ${m.id}`}
/> />
<div className='min-w-0'> <div className='min-w-0'>
...@@ -566,13 +566,6 @@ export function OllamaModelsDialog({ ...@@ -566,13 +566,6 @@ export function OllamaModelsDialog({
</div> </div>
)} )}
<DialogFooter>
<Button variant='outline' onClick={close}>
{t('Close')}
</Button>
</DialogFooter>
</DialogContent>
<AlertDialog <AlertDialog
open={deleteOpen} open={deleteOpen}
onOpenChange={(v) => { onOpenChange={(v) => {
......
...@@ -43,14 +43,6 @@ import { ...@@ -43,14 +43,6 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { import {
...@@ -63,6 +55,7 @@ import { ...@@ -63,6 +55,7 @@ import {
} from '@/components/ui/select' } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
...@@ -1701,17 +1694,33 @@ export function ParamOverrideEditorDialog( ...@@ -1701,17 +1694,33 @@ export function ParamOverrideEditorDialog(
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
return ( return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}> <Dialog
<DialogContent className='flex max-h-[90vh] flex-col gap-0 p-0 sm:max-w-5xl'> open={props.open}
<DialogHeader className='border-b px-6 py-4'> onOpenChange={props.onOpenChange}
<DialogTitle>{t('Parameter Override')}</DialogTitle> title={t('Parameter Override')}
<DialogDescription> description={t(
{t(
'Create request parameter override rules with a visual editor or raw JSON.' 'Create request parameter override rules with a visual editor or raw JSON.'
)} )}
</DialogDescription> contentClassName='flex max-h-[90vh] flex-col gap-0 p-0 sm:max-w-5xl'
</DialogHeader> headerClassName='border-b px-6 py-4'
footerClassName='border-t px-6 py-4'
contentHeight='min(72vh, 720px)'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => props.onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='button' onClick={handleSave}>
{t('Save')}
</Button>
</>
}
>
{/* Toolbar */} {/* Toolbar */}
<div className='bg-muted/30 border-b px-4 py-3'> <div className='bg-muted/30 border-b px-4 py-3'>
<div className='flex flex-wrap items-center gap-2'> <div className='flex flex-wrap items-center gap-2'>
...@@ -1791,7 +1800,6 @@ export function ParamOverrideEditorDialog( ...@@ -1791,7 +1800,6 @@ export function ParamOverrideEditorDialog(
</Button> </Button>
</div> </div>
</div> </div>
{/* Content */} {/* Content */}
<div className='min-h-0 flex-1 overflow-hidden'> <div className='min-h-0 flex-1 overflow-hidden'>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
...@@ -1885,15 +1893,11 @@ export function ParamOverrideEditorDialog( ...@@ -1885,15 +1893,11 @@ export function ParamOverrideEditorDialog(
role='button' role='button'
tabIndex={0} tabIndex={0}
draggable={operations.length > 1} draggable={operations.length > 1}
onClick={() => onClick={() => setSelectedOperationId(operation.id)}
setSelectedOperationId(operation.id)
}
onDragStart={(e) => onDragStart={(e) =>
handleDragStart(e, operation.id) handleDragStart(e, operation.id)
} }
onDragOver={(e) => onDragOver={(e) => handleDragOver(e, operation.id)}
handleDragOver(e, operation.id)
}
onDrop={(e) => handleDrop(e, operation.id)} onDrop={(e) => handleDrop(e, operation.id)}
onDragEnd={resetDragState} onDragEnd={resetDragState}
onKeyDown={(e: KeyboardEvent) => { onKeyDown={(e: KeyboardEvent) => {
...@@ -1948,9 +1952,7 @@ export function ParamOverrideEditorDialog( ...@@ -1948,9 +1952,7 @@ export function ParamOverrideEditorDialog(
<span <span
className={cn( className={cn(
'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium', 'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
getModeTagTailwind( getModeTagTailwind(operation.mode || 'set')
operation.mode || 'set'
)
)} )}
> >
{t( {t(
...@@ -2038,9 +2040,7 @@ export function ParamOverrideEditorDialog( ...@@ -2038,9 +2040,7 @@ export function ParamOverrideEditorDialog(
className='font-mono text-xs' className='font-mono text-xs'
/> />
<p className='text-muted-foreground mt-2 text-xs'> <p className='text-muted-foreground mt-2 text-xs'>
{t( {t('Edit JSON text directly. Format will be validated on save.')}
'Edit JSON text directly. Format will be validated on save.'
)}
</p> </p>
{jsonError && ( {jsonError && (
<p className='text-destructive mt-1 text-xs'>{jsonError}</p> <p className='text-destructive mt-1 text-xs'>{jsonError}</p>
...@@ -2048,21 +2048,7 @@ export function ParamOverrideEditorDialog( ...@@ -2048,21 +2048,7 @@ export function ParamOverrideEditorDialog(
</div> </div>
)} )}
</div> </div>
{/* Footer */} {/* Footer */}
<DialogFooter className='border-t px-6 py-4'>
<Button
type='button'
variant='outline'
onClick={() => props.onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='button' onClick={handleSave}>
{t('Save')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
......
...@@ -21,16 +21,9 @@ import { AlertTriangle } from 'lucide-react' ...@@ -21,16 +21,9 @@ import { AlertTriangle } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
interface StatusCodeRiskDialogProps { interface StatusCodeRiskDialogProps {
open: boolean open: boolean
...@@ -84,18 +77,35 @@ export function StatusCodeRiskDialog({ ...@@ -84,18 +77,35 @@ export function StatusCodeRiskDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle className='text-destructive flex items-center gap-2'> title={
<>
<AlertTriangle className='h-5 w-5' /> <AlertTriangle className='h-5 w-5' />
{t('High-risk operation confirmation')} {t('High-risk operation confirmation')}
</DialogTitle> </>
<DialogDescription> }
{t('High-risk status code retry risk disclaimer')} description={t('High-risk status code retry risk disclaimer')}
</DialogDescription> contentClassName='max-w-lg'
</DialogHeader> titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={handleCancel}>
{t('Cancel')}
</Button>
<Button
variant='destructive'
disabled={!canConfirm}
onClick={handleConfirm}
>
{t('I confirm enabling high-risk retry')}
</Button>
</>
}
>
<div className='space-y-4'> <div className='space-y-4'>
{detailItems.length > 0 && ( {detailItems.length > 0 && (
<div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'> <div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'>
...@@ -149,20 +159,6 @@ export function StatusCodeRiskDialog({ ...@@ -149,20 +159,6 @@ export function StatusCodeRiskDialog({
)} )}
</div> </div>
</div> </div>
<DialogFooter>
<Button variant='outline' onClick={handleCancel}>
{t('Cancel')}
</Button>
<Button
variant='destructive'
disabled={!canConfirm}
onClick={handleConfirm}
>
{t('I confirm enabling high-risk retry')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -23,18 +23,11 @@ import { useTranslation } from 'react-i18next' ...@@ -23,18 +23,11 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { MultiSelect } from '@/components/multi-select' import { MultiSelect } from '@/components/multi-select'
import { import {
getTagModels, getTagModels,
...@@ -190,15 +183,35 @@ export function TagBatchEditDialog({ ...@@ -190,15 +183,35 @@ export function TagBatchEditDialog({
if (!currentTag) return null if (!currentTag) return null
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog
<DialogContent className='max-h-[90vh] max-w-2xl overflow-y-auto'> open={open}
<DialogHeader> onOpenChange={handleClose}
<DialogTitle>{t('Batch Edit by Tag')}</DialogTitle> title={t('Batch Edit by Tag')}
<DialogDescription> description={
{t('Edit all channels with tag:')} <strong>{currentTag}</strong> <>
</DialogDescription> {t('Edit all channels with tag:')}
</DialogHeader> <strong>{currentTag}</strong>
</>
}
contentClassName='max-w-2xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
!isLoading ? (
<>
<Button variant='outline' onClick={handleClose} disabled={isSaving}>
{t('Cancel')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{isSaving ? t('Saving...') : t('Save Changes')}
</Button>
</>
) : null
}
>
{isLoading ? ( {isLoading ? (
<div className='flex items-center justify-center py-12'> <div className='flex items-center justify-center py-12'>
<Loader2 className='text-muted-foreground h-8 w-8 animate-spin' /> <Loader2 className='text-muted-foreground h-8 w-8 animate-spin' />
...@@ -272,9 +285,7 @@ export function TagBatchEditDialog({ ...@@ -272,9 +285,7 @@ export function TagBatchEditDialog({
options={groupOptions} options={groupOptions}
selected={groups} selected={groups}
onChange={setGroups} onChange={setGroups}
placeholder={t( placeholder={t('Select groups (leave empty to keep current)')}
'Select groups (leave empty to keep current)'
)}
/> />
)} )}
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
...@@ -282,23 +293,8 @@ export function TagBatchEditDialog({ ...@@ -282,23 +293,8 @@ export function TagBatchEditDialog({
</p> </p>
</div> </div>
</div> </div>
<DialogFooter>
<Button
variant='outline'
onClick={handleClose}
disabled={isSaving}
>
{t('Cancel')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isSaving ? t('Saving...') : t('Save Changes')}
</Button>
</DialogFooter>
</> </>
)} )}
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -21,17 +21,11 @@ import { Search } from 'lucide-react' ...@@ -21,17 +21,11 @@ import { Search } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ConfirmDialog } from '@/components/confirm-dialog' import { ConfirmDialog } from '@/components/confirm-dialog'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
interface UpstreamUpdateDialogProps { interface UpstreamUpdateDialogProps {
...@@ -120,18 +114,36 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) { ...@@ -120,18 +114,36 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
return ( return (
<> <>
<Dialog open={props.open} onOpenChange={(v) => !v && props.onCancel()}> <Dialog
<DialogContent className='sm:max-w-lg'> open={props.open}
<DialogHeader> onOpenChange={(v) => !v && props.onCancel()}
<DialogTitle>{t('Upstream Model Updates')}</DialogTitle> title={t('Upstream Model Updates')}
</DialogHeader> contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={props.onCancel}>
{t('Cancel')}
</Button>
<Button
onClick={handleConfirm}
disabled={
props.confirmLoading ||
(props.addModels.length === 0 &&
props.removeModels.length === 0)
}
>
{t('Confirm')}
</Button>
</>
}
>
<p className='text-muted-foreground text-sm'> <p className='text-muted-foreground text-sm'>
{t( {t(
'Select models to process. Unselected "add" models will be ignored.' 'Select models to process. Unselected "add" models will be ignored.'
)} )}
</p> </p>
<Tabs <Tabs
value={activeTab} value={activeTab}
onValueChange={(v) => setActiveTab(v as 'add' | 'remove')} onValueChange={(v) => setActiveTab(v as 'add' | 'remove')}
...@@ -139,21 +151,13 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) { ...@@ -139,21 +151,13 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
<TabsList className='grid w-full grid-cols-2'> <TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='add' className='gap-1'> <TabsTrigger value='add' className='gap-1'>
{t('Add Models')} {t('Add Models')}
<StatusBadge <StatusBadge variant='neutral' className='ml-1' copyable={false}>
variant='neutral'
className='ml-1'
copyable={false}
>
{selectedAdd.size}/{props.addModels.length} {selectedAdd.size}/{props.addModels.length}
</StatusBadge> </StatusBadge>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value='remove' className='gap-1'> <TabsTrigger value='remove' className='gap-1'>
{t('Remove Models')} {t('Remove Models')}
<StatusBadge <StatusBadge variant='neutral' className='ml-1' copyable={false}>
variant='neutral'
className='ml-1'
copyable={false}
>
{selectedRemove.size}/{props.removeModels.length} {selectedRemove.size}/{props.removeModels.length}
</StatusBadge> </StatusBadge>
</TabsTrigger> </TabsTrigger>
...@@ -248,11 +252,7 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) { ...@@ -248,11 +252,7 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
<Checkbox <Checkbox
checked={selectedRemove.has(model)} checked={selectedRemove.has(model)}
onCheckedChange={() => onCheckedChange={() =>
toggleModel( toggleModel(model, selectedRemove, setSelectedRemove)
model,
selectedRemove,
setSelectedRemove
)
} }
/> />
<span className='truncate text-sm'>{model}</span> <span className='truncate text-sm'>{model}</span>
...@@ -269,23 +269,6 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) { ...@@ -269,23 +269,6 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
</ScrollArea> </ScrollArea>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<DialogFooter>
<Button variant='outline' onClick={props.onCancel}>
{t('Cancel')}
</Button>
<Button
onClick={handleConfirm}
disabled={
props.confirmLoading ||
(props.addModels.length === 0 &&
props.removeModels.length === 0)
}
>
{t('Confirm')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
<ConfirmDialog <ConfirmDialog
......
...@@ -21,15 +21,6 @@ import { Save, Settings2 } from 'lucide-react' ...@@ -21,15 +21,6 @@ import { Save, Settings2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { TimeGranularity } from '@/lib/time' import type { TimeGranularity } from '@/lib/time'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
Select, Select,
...@@ -39,6 +30,7 @@ import { ...@@ -39,6 +30,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Dialog } from '@/components/dialog'
import { import {
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS, CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
MODEL_ANALYTICS_CHART_OPTIONS, MODEL_ANALYTICS_CHART_OPTIONS,
...@@ -74,23 +66,28 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) { ...@@ -74,23 +66,28 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogTrigger render={<Button variant='outline' size='sm' />}> open={open}
onOpenChange={handleOpenChange}
trigger={
<Button variant='outline' size='sm'>
<Settings2 className='mr-2 h-4 w-4' /> <Settings2 className='mr-2 h-4 w-4' />
{t('Preferences')} {t('Preferences')}
</DialogTrigger> </Button>
<DialogContent className='sm:max-w-md'> }
<DialogHeader> title={t('Model Analytics Defaults')}
<DialogTitle>{t('Dashboard Preferences')}</DialogTitle> description={t('Set default ranges and charts for model analytics.')}
<DialogDescription> contentClassName='sm:max-w-md'
{t( contentHeight='auto'
'Choose the default charts, range, and time granularity for model analytics.' bodyClassName='grid gap-3'
)} footer={
</DialogDescription> <Button onClick={handleSave} type='button'>
</DialogHeader> <Save className='mr-2 h-4 w-4' />
{t('Save Preferences')}
<div className='grid gap-4 py-2'> </Button>
<div className='grid gap-2'> }
>
<div className='grid gap-1.5'>
<Label htmlFor='default-time-range'>{t('Default range')}</Label> <Label htmlFor='default-time-range'>{t('Default range')}</Label>
<Select <Select
items={[ items={[
...@@ -121,8 +118,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) { ...@@ -121,8 +118,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className='grid gap-1.5'>
<div className='grid gap-2'>
<Label htmlFor='default-time-granularity'> <Label htmlFor='default-time-granularity'>
{t('Default time granularity')} {t('Default time granularity')}
</Label> </Label>
...@@ -155,8 +151,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) { ...@@ -155,8 +151,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className='grid gap-1.5'>
<div className='grid gap-2'>
<Label htmlFor='consumption-distribution-chart'> <Label htmlFor='consumption-distribution-chart'>
{t('Default consumption chart')} {t('Default consumption chart')}
</Label> </Label>
...@@ -190,8 +185,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) { ...@@ -190,8 +185,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className='grid gap-1.5'>
<div className='grid gap-2'>
<Label htmlFor='model-analytics-chart'> <Label htmlFor='model-analytics-chart'>
{t('Default model call chart')} {t('Default model call chart')}
</Label> </Label>
...@@ -224,15 +218,6 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) { ...@@ -224,15 +218,6 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
<DialogFooter>
<Button onClick={handleSave} type='button'>
<Save className='mr-2 h-4 w-4' />
{t('Save Preferences')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -23,15 +23,6 @@ import { useAuthStore } from '@/stores/auth-store' ...@@ -23,15 +23,6 @@ import { useAuthStore } from '@/stores/auth-store'
import { getRollingDateRange, type TimeGranularity } from '@/lib/time' import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
...@@ -44,6 +35,7 @@ import { ...@@ -44,6 +35,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { DateTimePicker } from '@/components/datetime-picker' import { DateTimePicker } from '@/components/datetime-picker'
import { Dialog } from '@/components/dialog'
import { import {
TIME_GRANULARITY_OPTIONS, TIME_GRANULARITY_OPTIONS,
TIME_RANGE_PRESETS, TIME_RANGE_PRESETS,
...@@ -144,23 +136,35 @@ export function ModelsFilter(props: ModelsFilterProps) { ...@@ -144,23 +136,35 @@ export function ModelsFilter(props: ModelsFilterProps) {
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogTrigger render={<Button variant='outline' size='sm' />}> open={open}
onOpenChange={handleOpenChange}
trigger={
<Button variant='outline' size='sm'>
<Filter className='mr-2 h-4 w-4' /> <Filter className='mr-2 h-4 w-4' />
{t('Filter')} {t('Filter')}
</DialogTrigger> </Button>
<DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-lg'> }
<DialogHeader> title={t('Model Analytics Filters')}
<DialogTitle>{t('Filter Dashboard Models')}</DialogTitle> description={t('Filter the model analytics view by time range and user.')}
<DialogDescription> contentClassName='max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-lg'
{t( contentHeight='min(48vh, 460px)'
'Set filters to customize your dashboard statistics and charts.' footerClassName='grid grid-cols-2 gap-2 sm:flex'
)} footer={
</DialogDescription> <>
</DialogHeader> <Button onClick={handleReset} variant='outline' type='button'>
<RotateCcw className='mr-2 h-4 w-4' />
<ScrollArea className='flex-1 pr-3 sm:pr-4'> {t('Reset')}
<div className='grid gap-3 py-3 sm:gap-4 sm:py-4'> </Button>
<Button onClick={handleApply} type='submit'>
<Search className='mr-2 h-4 w-4' />
{t('Apply Filters')}
</Button>
</>
}
>
<ScrollArea className='h-full pr-3 sm:pr-4'>
<div className='grid gap-2.5 py-2'>
{/* Quick time range selection */} {/* Quick time range selection */}
<div className='grid gap-2'> <div className='grid gap-2'>
<Label className='flex items-center gap-2'> <Label className='flex items-center gap-2'>
...@@ -173,9 +177,7 @@ export function ModelsFilter(props: ModelsFilterProps) { ...@@ -173,9 +177,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
key={range.days} key={range.days}
type='button' type='button'
size='sm' size='sm'
variant={ variant={selectedRange === range.days ? 'default' : 'outline'}
selectedRange === range.days ? 'default' : 'outline'
}
onClick={() => handleQuickRange(range.days)} onClick={() => handleQuickRange(range.days)}
className={cn( className={cn(
'flex-1', 'flex-1',
...@@ -192,7 +194,7 @@ export function ModelsFilter(props: ModelsFilterProps) { ...@@ -192,7 +194,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
<SectionDivider label={t('Custom Time Range')} /> <SectionDivider label={t('Custom Time Range')} />
{/* Custom time range */} {/* Custom time range */}
<div className='grid gap-3 sm:gap-4'> <div className='grid gap-2.5'>
<div className='grid gap-2'> <div className='grid gap-2'>
<Label htmlFor='start_timestamp'>{t('Start Time')}</Label> <Label htmlFor='start_timestamp'>{t('Start Time')}</Label>
<DateTimePicker <DateTimePicker
...@@ -265,18 +267,6 @@ export function ModelsFilter(props: ModelsFilterProps) { ...@@ -265,18 +267,6 @@ export function ModelsFilter(props: ModelsFilterProps) {
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
<DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
<Button onClick={handleReset} variant='outline' type='button'>
<RotateCcw className='mr-2 h-4 w-4' />
{t('Reset')}
</Button>
<Button onClick={handleApply} type='submit'>
<Search className='mr-2 h-4 w-4' />
{t('Apply Filters')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatDateTimeObject } from '@/lib/time' import { formatDateTimeObject } from '@/lib/time'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Markdown } from '@/components/ui/markdown' import { Markdown } from '@/components/ui/markdown'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
interface AnnouncementDetailModalProps { interface AnnouncementDetailModalProps {
open: boolean open: boolean
...@@ -47,18 +41,20 @@ export function AnnouncementDetailModal({ ...@@ -47,18 +41,20 @@ export function AnnouncementDetailModal({
}: AnnouncementDetailModalProps) { }: AnnouncementDetailModalProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Announcement Details')}</DialogTitle> title={t('Announcement Details')}
{announcement?.publishDate && ( description={
<DialogDescription> announcement?.publishDate
{t('Published:')}{' '} ? `${t('Published:')} ${formatDateTimeObject(new Date(announcement.publishDate))}`
{formatDateTimeObject(new Date(announcement.publishDate))} : undefined
</DialogDescription> }
)} contentClassName='sm:max-w-lg'
</DialogHeader> contentHeight='auto'
<ScrollArea className='max-h-[60vh] pr-4'> bodyClassName='space-y-4'
>
<ScrollArea className='max-h-[min(58vh,520px)] pr-4'>
<div className='space-y-4'> <div className='space-y-4'>
{announcement?.content && ( {announcement?.content && (
<div> <div>
...@@ -78,7 +74,6 @@ export function AnnouncementDetailModal({ ...@@ -78,7 +74,6 @@ export function AnnouncementDetailModal({
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -23,15 +23,9 @@ import { toast } from 'sonner' ...@@ -23,15 +23,9 @@ import { toast } from 'sonner'
import { getUserModels } from '@/lib/api' import { getUserModels } from '@/lib/api'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ComboboxInput } from '@/components/ui/combobox-input' import { ComboboxInput } from '@/components/ui/combobox-input'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Dialog } from '@/components/dialog'
const APP_CONFIGS = { const APP_CONFIGS = {
claude: { claude: {
...@@ -151,12 +145,22 @@ export function CCSwitchDialog(props: Props) { ...@@ -151,12 +145,22 @@ export function CCSwitchDialog(props: Props) {
} }
return ( return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={props.open}
<DialogHeader> onOpenChange={props.onOpenChange}
<DialogTitle>{t('Import to CC Switch')}</DialogTitle> title={t('Import to CC Switch')}
</DialogHeader> contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={() => props.onOpenChange(false)}>
{t('Cancel')}
</Button>
<Button onClick={handleSubmit}>{t('Open CC Switch')}</Button>
</>
}
>
<div className='space-y-4'> <div className='space-y-4'>
<div className='space-y-2'> <div className='space-y-2'>
<Label>{t('Application')}</Label> <Label>{t('Application')}</Label>
...@@ -213,14 +217,6 @@ export function CCSwitchDialog(props: Props) { ...@@ -213,14 +217,6 @@ export function CCSwitchDialog(props: Props) {
</div> </div>
))} ))}
</div> </div>
<DialogFooter>
<Button variant='outline' onClick={() => props.onOpenChange(false)}>
{t('Cancel')}
</Button>
<Button onClick={handleSubmit}>{t('Open CC Switch')}</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -25,19 +25,12 @@ import { toast } from 'sonner' ...@@ -25,19 +25,12 @@ import { toast } from 'sonner'
import { copyToClipboard } from '@/lib/copy-to-clipboard' import { copyToClipboard } from '@/lib/copy-to-clipboard'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { Dialog } from '@/components/dialog'
import { import {
handleBatchEnableModels, handleBatchEnableModels,
handleBatchDisableModels, handleBatchDisableModels,
...@@ -187,19 +180,17 @@ export function DataTableBulkActions<TData>({ ...@@ -187,19 +180,17 @@ export function DataTableBulkActions<TData>({
</BulkActionsToolbar> </BulkActionsToolbar>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> <Dialog
<DialogContent> open={showDeleteConfirm}
<DialogHeader> onOpenChange={setShowDeleteConfirm}
<DialogTitle>{t('Delete Models?')}</DialogTitle> title={t('Delete Models?')}
<DialogDescription> description={t(
{t(
'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.', 'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.',
{ count: selectedIds.length } { count: selectedIds.length }
)} )}
</DialogDescription> contentHeight='auto'
</DialogHeader> footer={
<>
<DialogFooter>
<Button <Button
variant='outline' variant='outline'
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
...@@ -209,8 +200,10 @@ export function DataTableBulkActions<TData>({ ...@@ -209,8 +200,10 @@ export function DataTableBulkActions<TData>({
<Button variant='destructive' onClick={handleDeleteAll}> <Button variant='destructive' onClick={handleDeleteAll}>
{t('Delete')} {t('Delete')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{' '}
</Dialog> </Dialog>
</> </>
) )
......
...@@ -17,14 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,14 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
type DescriptionDialogProps = { type DescriptionDialogProps = {
open: boolean open: boolean
...@@ -41,13 +35,15 @@ export function DescriptionDialog({ ...@@ -41,13 +35,15 @@ export function DescriptionDialog({
}: DescriptionDialogProps) { }: DescriptionDialogProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='max-w-2xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{modelName}</DialogTitle> title={modelName}
<DialogDescription>{t('Model Description')}</DialogDescription> description={t('Model Description')}
</DialogHeader> contentClassName='max-w-2xl'
contentHeight='auto'
bodyClassName='space-y-4'
>
<ScrollArea className='max-h-96'> <ScrollArea className='max-h-96'>
<div className='space-y-2 pr-4'> <div className='space-y-2 pr-4'>
<p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'> <p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'>
...@@ -55,7 +51,6 @@ export function DescriptionDialog({ ...@@ -55,7 +51,6 @@ export function DescriptionDialog({
</p> </p>
</div> </div>
</ScrollArea> </ScrollArea>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -22,15 +22,9 @@ import { Loader2 } from 'lucide-react' ...@@ -22,15 +22,9 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Dialog } from '@/components/dialog'
import { estimatePrice, extendDeployment, getDeployment } from '../../api' import { estimatePrice, extendDeployment, getDeployment } from '../../api'
import { deploymentsQueryKeys } from '../../lib' import { deploymentsQueryKeys } from '../../lib'
...@@ -164,12 +158,28 @@ export function ExtendDeploymentDialog({ ...@@ -164,12 +158,28 @@ export function ExtendDeploymentDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Extend deployment')}</DialogTitle> title={t('Extend deployment')}
</DialogHeader> contentClassName='sm:max-w-lg'
footerClassName='mt-4'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')}
</Button>
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
{isSubmitting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{t('Extend')}
</Button>
</>
}
>
{isLoadingDetails ? ( {isLoadingDetails ? (
<div className='flex items-center justify-center py-10'> <div className='flex items-center justify-center py-10'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' /> <Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
...@@ -218,19 +228,6 @@ export function ExtendDeploymentDialog({ ...@@ -218,19 +228,6 @@ export function ExtendDeploymentDialog({
</div> </div>
</div> </div>
)} )}
<DialogFooter className='mt-4'>
<Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')}
</Button>
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
{isSubmitting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{t('Extend')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -23,13 +23,6 @@ import { useTranslation } from 'react-i18next' ...@@ -23,13 +23,6 @@ import { useTranslation } from 'react-i18next'
import { useIsMobile } from '@/hooks/use-mobile' import { useIsMobile } from '@/hooks/use-mobile'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Empty, Empty,
EmptyDescription, EmptyDescription,
EmptyHeader, EmptyHeader,
...@@ -37,6 +30,7 @@ import { ...@@ -37,6 +30,7 @@ import {
EmptyTitle, EmptyTitle,
} from '@/components/ui/empty' } from '@/components/ui/empty'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { getMissingModels } from '../../api' import { getMissingModels } from '../../api'
import { DEFAULT_PAGE_SIZE } from '../../constants' import { DEFAULT_PAGE_SIZE } from '../../constants'
...@@ -115,18 +109,19 @@ export function MissingModelsDialog({ ...@@ -115,18 +109,19 @@ export function MissingModelsDialog({
const showPagination = totalItems > pageSize const showPagination = totalItems > pageSize
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent open={open}
className='flex max-h-[85vh] max-w-2xl flex-col gap-3 p-4' onOpenChange={onOpenChange}
title={t('Missing Models')}
description={t(
'Models that are being used but not configured in the system'
)}
contentClassName='flex max-h-[85vh] max-w-2xl flex-col gap-3 p-4'
headerClassName='flex-shrink-0 text-start'
contentHeight='min(74vh, 760px)'
bodyClassName='space-y-4'
initialFocus={!isMobile} initialFocus={!isMobile}
> >
<DialogHeader className='flex-shrink-0 text-start'>
<DialogTitle>{t('Missing Models')}</DialogTitle>
<DialogDescription>
{t('Models that are being used but not configured in the system')}
</DialogDescription>
</DialogHeader>
{isLoading ? ( {isLoading ? (
<div className='flex items-center justify-center py-12'> <div className='flex items-center justify-center py-12'>
<Loader2 className='h-8 w-8 animate-spin' /> <Loader2 className='h-8 w-8 animate-spin' />
...@@ -142,8 +137,7 @@ export function MissingModelsDialog({ ...@@ -142,8 +137,7 @@ export function MissingModelsDialog({
<div className='flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto'> <div className='flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto'>
<div className='flex flex-shrink-0 items-center justify-between gap-3'> <div className='flex flex-shrink-0 items-center justify-between gap-3'>
<div className='text-muted-foreground text-sm whitespace-nowrap'> <div className='text-muted-foreground text-sm whitespace-nowrap'>
{t('Showing')} {displayStart}-{displayEnd} {t('of')}{' '} {t('Showing')} {displayStart}-{displayEnd} {t('of')} {totalItems}
{totalItems}
</div> </div>
<div className='relative w-48'> <div className='relative w-48'>
<Search className='text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' /> <Search className='text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' />
...@@ -225,9 +219,7 @@ export function MissingModelsDialog({ ...@@ -225,9 +219,7 @@ export function MissingModelsDialog({
size='icon' size='icon'
className='h-8 w-8' className='h-8 w-8'
onClick={() => onClick={() =>
setCurrentPage((prev) => setCurrentPage((prev) => Math.min(totalPages, prev + 1))
Math.min(totalPages, prev + 1)
)
} }
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
aria-label={t('Next page')} aria-label={t('Next page')}
...@@ -241,7 +233,6 @@ export function MissingModelsDialog({ ...@@ -241,7 +233,6 @@ export function MissingModelsDialog({
)} )}
</div> </div>
)} )}
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -22,14 +22,8 @@ import { Loader2 } from 'lucide-react' ...@@ -22,14 +22,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
import { checkClusterNameAvailability, updateDeploymentName } from '../../api' import { checkClusterNameAvailability, updateDeploymentName } from '../../api'
import { deploymentsQueryKeys } from '../../lib' import { deploymentsQueryKeys } from '../../lib'
...@@ -111,12 +105,28 @@ export function RenameDeploymentDialog({ ...@@ -111,12 +105,28 @@ export function RenameDeploymentDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Rename deployment')}</DialogTitle> title={t('Rename deployment')}
</DialogHeader> contentClassName='sm:max-w-lg'
footerClassName='mt-4'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')}
</Button>
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
{isSubmitting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{t('Rename')}
</Button>
</>
}
>
<div className='space-y-2'> <div className='space-y-2'>
<div className='text-muted-foreground text-sm'> <div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '} {t('Deployment ID')}:{' '}
...@@ -130,19 +140,6 @@ export function RenameDeploymentDialog({ ...@@ -130,19 +140,6 @@ export function RenameDeploymentDialog({
/> />
<div className='text-muted-foreground text-xs'>{helper}</div> <div className='text-muted-foreground text-xs'>{helper}</div>
</div> </div>
<DialogFooter className='mt-4'>
<Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')}
</Button>
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
{isSubmitting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{t('Rename')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -24,16 +24,9 @@ import { toast } from 'sonner' ...@@ -24,16 +24,9 @@ import { toast } from 'sonner'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useIsMobile } from '@/hooks/use-mobile' import { useIsMobile } from '@/hooks/use-mobile'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { syncUpstream, previewUpstreamDiff } from '../../api' import { syncUpstream, previewUpstreamDiff } from '../../api'
import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants' import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants'
...@@ -125,19 +118,31 @@ export function SyncWizardDialog({ ...@@ -125,19 +118,31 @@ export function SyncWizardDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent open={open}
className='flex max-h-[90vh] w-full flex-col gap-4 p-4 sm:max-w-2xl sm:p-6' onOpenChange={onOpenChange}
title={t('Sync Upstream Models')}
description={t('Synchronize models and vendors from an upstream source')}
initialFocus={!isMobile} initialFocus={!isMobile}
contentHeight='auto'
bodyClassName='flex flex-col gap-6'
footer={
<>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isSyncing}
>
{t('Cancel')}
</Button>
<Button onClick={handleSync} disabled={isSyncing}>
{isSyncing && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
<RefreshCw className='mr-2 h-4 w-4' />
{isSyncing ? t('Syncing...') : t('Sync Now')}
</Button>
</>
}
> >
<DialogHeader className='flex-shrink-0 text-start'>
<DialogTitle>{t('Sync Upstream Models')}</DialogTitle>
<DialogDescription>
{t('Synchronize models and vendors from an upstream source')}
</DialogDescription>
</DialogHeader>
<div className='flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto'>
<div className='space-y-3'> <div className='space-y-3'>
<div> <div>
<Label className='text-base'>{t('Select Sync Source')}</Label> <Label className='text-base'>{t('Select Sync Source')}</Label>
...@@ -233,23 +238,6 @@ export function SyncWizardDialog({ ...@@ -233,23 +238,6 @@ export function SyncWizardDialog({
)} )}
</p> </p>
</div> </div>
</div>
<DialogFooter className='flex-shrink-0 gap-2 sm:justify-end'>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isSyncing}
>
{t('Cancel')}
</Button>
<Button onClick={handleSync} disabled={isSyncing}>
{isSyncing && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
<RefreshCw className='mr-2 h-4 w-4' />
{isSyncing ? 'Syncing...' : 'Sync Now'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -31,13 +31,6 @@ import { ...@@ -31,13 +31,6 @@ import {
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
import { import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
...@@ -47,6 +40,7 @@ import { ...@@ -47,6 +40,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { getDeployment, updateDeployment } from '../../api' import { getDeployment, updateDeployment } from '../../api'
import { deploymentsQueryKeys } from '../../lib' import { deploymentsQueryKeys } from '../../lib'
...@@ -64,6 +58,8 @@ const schema = z.object({ ...@@ -64,6 +58,8 @@ const schema = z.object({
type Values = z.input<typeof schema> type Values = z.input<typeof schema>
const UPDATE_CONFIG_FORM_ID = 'update-config-form'
function normalizeJsonObject(input?: string) { function normalizeJsonObject(input?: string) {
if (!input || !input.trim()) return undefined if (!input || !input.trim()) return undefined
const parsed = JSON.parse(input) const parsed = JSON.parse(input)
...@@ -212,12 +208,37 @@ export function UpdateConfigDialog({ ...@@ -212,12 +208,37 @@ export function UpdateConfigDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{title}</DialogTitle> title={title}
</DialogHeader> contentClassName='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
isLoading ? null : (
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button
type='submit'
form={UPDATE_CONFIG_FORM_ID}
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{t('Update')}
</Button>
</>
)
}
>
{isLoading ? ( {isLoading ? (
<div className='flex items-center justify-center py-10'> <div className='flex items-center justify-center py-10'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' /> <Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
...@@ -226,6 +247,7 @@ export function UpdateConfigDialog({ ...@@ -226,6 +247,7 @@ export function UpdateConfigDialog({
<div className='max-h-[calc(100dvh-8.5rem)] overflow-y-auto py-2 pr-1 sm:max-h-[72vh]'> <div className='max-h-[calc(100dvh-8.5rem)] overflow-y-auto py-2 pr-1 sm:max-h-[72vh]'>
<Form {...form}> <Form {...form}>
<form <form
id={UPDATE_CONFIG_FORM_ID}
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
autoComplete='off' autoComplete='off'
className='space-y-4' className='space-y-4'
...@@ -238,10 +260,7 @@ export function UpdateConfigDialog({ ...@@ -238,10 +260,7 @@ export function UpdateConfigDialog({
<FormItem> <FormItem>
<FormLabel>{t('Image')}</FormLabel> <FormLabel>{t('Image')}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder='ollama/ollama:latest' {...field} />
placeholder='ollama/ollama:latest'
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
...@@ -286,9 +305,7 @@ export function UpdateConfigDialog({ ...@@ -286,9 +305,7 @@ export function UpdateConfigDialog({
name='entrypoint' name='entrypoint'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>{t('Entrypoint (space separated)')}</FormLabel>
{t('Entrypoint (space separated)')}
</FormLabel>
<FormControl> <FormControl>
<Input placeholder='bash -lc' {...field} /> <Input placeholder='bash -lc' {...field} />
</FormControl> </FormControl>
...@@ -394,9 +411,7 @@ export function UpdateConfigDialog({ ...@@ -394,9 +411,7 @@ export function UpdateConfigDialog({
name='secret_env_json' name='secret_env_json'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>{t('Secret env (JSON object)')}</FormLabel>
{t('Secret env (JSON object)')}
</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
className='min-h-40 font-mono text-xs' className='min-h-40 font-mono text-xs'
...@@ -411,27 +426,10 @@ export function UpdateConfigDialog({ ...@@ -411,27 +426,10 @@ export function UpdateConfigDialog({
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
<DialogFooter className='grid grid-cols-2 gap-2 pt-2 sm:flex'>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='submit' disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{t('Update')}
</Button>
</DialogFooter>
</form> </form>
</Form> </Form>
</div> </div>
)} )}
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -37,14 +37,6 @@ import { toast } from 'sonner' ...@@ -37,14 +37,6 @@ import { toast } from 'sonner'
import { useIsMobile } from '@/hooks/use-mobile' import { useIsMobile } from '@/hooks/use-mobile'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { import {
Popover, Popover,
...@@ -67,6 +59,7 @@ import { ...@@ -67,6 +59,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { applyUpstreamOverwrite } from '../../api' import { applyUpstreamOverwrite } from '../../api'
import { modelsQueryKeys, vendorsQueryKeys } from '../../lib' import { modelsQueryKeys, vendorsQueryKeys } from '../../lib'
...@@ -453,21 +446,46 @@ export function UpstreamConflictDialog({ ...@@ -453,21 +446,46 @@ export function UpstreamConflictDialog({
} }
onOpenChange(nextOpen) onOpenChange(nextOpen)
}} }}
> title={t('Resolve Conflicts')}
<DialogContent description={t(
className='flex max-h-[90vh] w-full flex-col gap-4 p-4 sm:max-w-5xl sm:p-6' 'Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.'
)}
contentClassName='w-full sm:max-w-5xl'
contentHeight='min(72vh, 720px)'
bodyClassName='flex flex-col gap-4'
initialFocus={!isMobile} initialFocus={!isMobile}
> footerClassName='sm:justify-between'
<div className='flex min-h-0 flex-1 flex-col gap-4 overflow-hidden'> footer={
<DialogHeader className='flex-shrink-0 text-start'> <div className='flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
<DialogTitle>{t('Resolve Conflicts')}</DialogTitle> <div className='text-muted-foreground flex flex-1 items-start gap-2 text-xs'>
<DialogDescription> <Info className='h-4 w-4 flex-shrink-0' />
<span>
{t( {t(
'Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.' 'Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.'
)} )}
</DialogDescription> </span>
</DialogHeader> </div>
<div className='flex flex-col gap-2 sm:flex-row sm:justify-end'>
<Button
variant='outline'
onClick={() => {
setUpstreamConflicts([])
onOpenChange(false)
}}
>
{t('Cancel')}
</Button>
<Button
onClick={handleApplyOverwrite}
disabled={isSubmitting || !hasSelection}
>
{isSubmitting ? t('Applying...') : t('Apply Overwrite')}
</Button>
</div>
</div>
}
>
<div className='flex min-h-0 flex-1 flex-col gap-4'>
{!hasConflicts ? ( {!hasConflicts ? (
<div className='text-muted-foreground flex flex-1 items-center justify-center rounded-md border border-dashed p-8 text-center text-sm'> <div className='text-muted-foreground flex flex-1 items-center justify-center rounded-md border border-dashed p-8 text-center text-sm'>
{t('No conflict entries available.')} {t('No conflict entries available.')}
...@@ -639,36 +657,6 @@ export function UpstreamConflictDialog({ ...@@ -639,36 +657,6 @@ export function UpstreamConflictDialog({
</div> </div>
)} )}
</div> </div>
<DialogFooter className='flex-shrink-0'>
<div className='flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
<div className='text-muted-foreground flex flex-1 items-start gap-2 text-xs'>
<Info className='h-4 w-4 flex-shrink-0' />
<span>
{t(
'Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.'
)}
</span>
</div>
<div className='flex flex-col gap-2 sm:flex-row sm:justify-end'>
<Button
variant='outline'
onClick={() => {
setUpstreamConflicts([])
onOpenChange(false)
}}
>
{t('Cancel')}
</Button>
<Button
onClick={handleApplyOverwrite}
disabled={isSubmitting || !hasSelection}
>
{isSubmitting ? t('Applying...') : t('Apply Overwrite')}
</Button>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -25,14 +25,6 @@ import { useTranslation } from 'react-i18next' ...@@ -25,14 +25,6 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -43,6 +35,7 @@ import { ...@@ -43,6 +35,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { createVendor, updateVendor } from '../../api' import { createVendor, updateVendor } from '../../api'
import { vendorsQueryKeys, modelsQueryKeys } from '../../lib' import { vendorsQueryKeys, modelsQueryKeys } from '../../lib'
import { vendorFormSchema, type Vendor } from '../../types' import { vendorFormSchema, type Vendor } from '../../types'
...@@ -53,6 +46,8 @@ type VendorMutateDialogProps = { ...@@ -53,6 +46,8 @@ type VendorMutateDialogProps = {
currentVendor?: Vendor | null currentVendor?: Vendor | null
} }
const VENDOR_MUTATE_FORM_ID = 'vendor-mutate-form'
export function VendorMutateDialog({ export function VendorMutateDialog({
open, open,
onOpenChange, onOpenChange,
...@@ -118,23 +113,48 @@ export function VendorMutateDialog({ ...@@ -118,23 +113,48 @@ export function VendorMutateDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle> title={isEdit ? t('Edit Vendor') : t('Create Vendor')}
{isEdit ? t('Edit Vendor') : t('Create Vendor')} description={
</DialogTitle> isEdit
<DialogDescription>
{isEdit
? t('Update vendor information for {{name}}', { ? t('Update vendor information for {{name}}', {
name: currentVendor?.name, name: currentVendor?.name,
}) })
: t('Add a new vendor to the system')} : t('Add a new vendor to the system')
</DialogDescription> }
</DialogHeader> contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
{t('Cancel')}
</Button>
<Button
type='submit'
form={VENDOR_MUTATE_FORM_ID}
disabled={isSaving}
>
{isSaving ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{isSaving ? t('Saving...') : isEdit ? t('Update') : t('Create')}
</Button>
</>
}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'> <form
id={VENDOR_MUTATE_FORM_ID}
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-4'
>
<FormField <FormField
control={form.control} control={form.control}
name='name' name='name'
...@@ -192,24 +212,8 @@ export function VendorMutateDialog({ ...@@ -192,24 +212,8 @@ export function VendorMutateDialog({
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
{t('Cancel')}
</Button>
<Button type='submit' disabled={isSaving}>
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isSaving ? 'Saving...' : isEdit ? 'Update' : 'Create'}
</Button>
</DialogFooter>
</form> </form>
</Form> </Form>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -27,14 +27,8 @@ import { ...@@ -27,14 +27,8 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Dialog } from '@/components/dialog'
import { getDeployment, listDeploymentContainers } from '../../api' import { getDeployment, listDeploymentContainers } from '../../api'
export function ViewDetailsDialog({ export function ViewDetailsDialog({
...@@ -116,12 +110,25 @@ export function ViewDetailsDialog({ ...@@ -116,12 +110,25 @@ export function ViewDetailsDialog({
}, [details]) }, [details])
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Deployment details')}</DialogTitle> title={t('Deployment details')}
</DialogHeader> contentClassName='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
className='w-full sm:w-auto'
>
{t('Close')}
</Button>
</>
}
>
<div className='max-h-[calc(100dvh-8.5rem)] space-y-3 overflow-y-auto py-2 pr-1 sm:max-h-[72vh] sm:space-y-4'> <div className='max-h-[calc(100dvh-8.5rem)] space-y-3 overflow-y-auto py-2 pr-1 sm:max-h-[72vh] sm:space-y-4'>
<div className='flex flex-wrap items-center justify-between gap-2'> <div className='flex flex-wrap items-center justify-between gap-2'>
<div className='text-muted-foreground text-sm'> <div className='text-muted-foreground text-sm'>
...@@ -184,9 +191,7 @@ export function ViewDetailsDialog({ ...@@ -184,9 +191,7 @@ export function ViewDetailsDialog({
{t('Total GPUs')} {t('Total GPUs')}
</div> </div>
<div className='mt-1 font-medium'> <div className='mt-1 font-medium'>
{String( {String(details?.total_gpus ?? details?.hardware_qty ?? '-')}
details?.total_gpus ?? details?.hardware_qty ?? '-'
)}
</div> </div>
</div> </div>
<div className='rounded-lg border p-3'> <div className='rounded-lg border p-3'>
...@@ -231,9 +236,7 @@ export function ViewDetailsDialog({ ...@@ -231,9 +236,7 @@ export function ViewDetailsDialog({
className='flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2' className='flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2'
> >
<div className='min-w-0'> <div className='min-w-0'>
<div className='truncate font-mono text-sm'> <div className='truncate font-mono text-sm'>{id}</div>
{id}
</div>
<div className='text-muted-foreground text-xs'> <div className='text-muted-foreground text-xs'>
{status ? `${t('Status')}: ${status}` : ''} {status ? `${t('Status')}: ${status}` : ''}
</div> </div>
...@@ -268,17 +271,6 @@ export function ViewDetailsDialog({ ...@@ -268,17 +271,6 @@ export function ViewDetailsDialog({
</> </>
)} )}
</div> </div>
<DialogFooter>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
className='w-full sm:w-auto'
>
{t('Close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -22,12 +22,6 @@ import { Download, Loader2, RefreshCcw, Terminal } from 'lucide-react' ...@@ -22,12 +22,6 @@ import { Download, Loader2, RefreshCcw, Terminal } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select, Select,
SelectContent, SelectContent,
SelectGroup, SelectGroup,
...@@ -36,6 +30,7 @@ import { ...@@ -36,6 +30,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Dialog } from '@/components/dialog'
import { getDeploymentLogs, listDeploymentContainers } from '../../api' import { getDeploymentLogs, listDeploymentContainers } from '../../api'
interface ViewLogsDialogProps { interface ViewLogsDialogProps {
...@@ -142,15 +137,20 @@ export function ViewLogsDialog({ ...@@ -142,15 +137,20 @@ export function ViewLogsDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='flex h-[calc(100dvh-2rem)] flex-col max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:h-[80vh] sm:max-w-4xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle className='flex items-center gap-2'> title={
<>
<Terminal className='h-5 w-5' /> <Terminal className='h-5 w-5' />
{t('Deployment logs')} {t('Deployment logs')}
</DialogTitle> </>
</DialogHeader> }
contentClassName='flex h-[calc(100dvh-2rem)] flex-col max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:h-[80vh] sm:max-w-4xl'
titleClassName='flex items-center gap-2'
contentHeight='min(72vh, 720px)'
bodyClassName='space-y-4'
>
<div className='mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3'> <div className='mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3'>
<div className='text-muted-foreground text-sm'> <div className='text-muted-foreground text-sm'>
{t('Deployment ID')}: {deploymentId} {t('Deployment ID')}: {deploymentId}
...@@ -187,12 +187,9 @@ export function ViewLogsDialog({ ...@@ -187,12 +187,9 @@ export function ViewLogsDialog({
</div> </div>
</div> </div>
</div> </div>
<div className='mb-3 grid gap-2 sm:grid-cols-2 sm:gap-3'> <div className='mb-3 grid gap-2 sm:grid-cols-2 sm:gap-3'>
<div className='space-y-1'> <div className='space-y-1'>
<div className='text-muted-foreground text-xs'> <div className='text-muted-foreground text-xs'>{t('Container')}</div>
{t('Container')}
</div>
<Select <Select
items={[ items={[
...containers.flatMap((c) => { ...containers.flatMap((c) => {
...@@ -280,7 +277,6 @@ export function ViewLogsDialog({ ...@@ -280,7 +277,6 @@ export function ViewLogsDialog({
</Select> </Select>
</div> </div>
</div> </div>
<div <div
ref={scrollRef} ref={scrollRef}
className='flex-1 overflow-auto rounded-md border bg-black p-3 sm:p-4' className='flex-1 overflow-auto rounded-md border bg-black p-3 sm:p-4'
...@@ -315,7 +311,6 @@ export function ViewLogsDialog({ ...@@ -315,7 +311,6 @@ export function ViewLogsDialog({
</div> </div>
)} )}
</div> </div>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -32,12 +32,6 @@ import { formatQuotaWithCurrency } from '@/lib/currency' ...@@ -32,12 +32,6 @@ import { formatQuotaWithCurrency } from '@/lib/currency'
import dayjs from '@/lib/dayjs' import dayjs from '@/lib/dayjs'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { import {
Tooltip, Tooltip,
...@@ -45,6 +39,7 @@ import { ...@@ -45,6 +39,7 @@ import {
TooltipTrigger, TooltipTrigger,
TooltipProvider, TooltipProvider,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { Dialog } from '@/components/dialog'
import { Turnstile } from '@/components/turnstile' import { Turnstile } from '@/components/turnstile'
import { getCheckinStatus, performCheckin } from '../api' import { getCheckinStatus, performCheckin } from '../api'
import type { CheckinRecord } from '../types' import type { CheckinRecord } from '../types'
...@@ -253,11 +248,11 @@ export function CheckinCalendarCard({ ...@@ -253,11 +248,11 @@ export function CheckinCalendarCard({
setTurnstileWidgetKey((v) => v + 1) setTurnstileWidgetKey((v) => v + 1)
} }
}} }}
title={t('Security Check')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
> >
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Security Check')}</DialogTitle>
</DialogHeader>
<div className='text-muted-foreground text-sm'> <div className='text-muted-foreground text-sm'>
{t('Please complete the security check to continue.')} {t('Please complete the security check to continue.')}
</div> </div>
...@@ -273,7 +268,6 @@ export function CheckinCalendarCard({ ...@@ -273,7 +268,6 @@ export function CheckinCalendarCard({
}} }}
/> />
</div> </div>
</DialogContent>
</Dialog> </Dialog>
<div className='bg-card overflow-hidden rounded-2xl border'> <div className='bg-card overflow-hidden rounded-2xl border'>
......
...@@ -20,17 +20,10 @@ import { useEffect } from 'react' ...@@ -20,17 +20,10 @@ import { useEffect } from 'react'
import { RefreshCw, Loader2 } from 'lucide-react' import { RefreshCw, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button' import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
import { useAccessToken } from '../../hooks' import { useAccessToken } from '../../hooks'
// ============================================================================ // ============================================================================
...@@ -57,17 +50,41 @@ export function AccessTokenDialog({ ...@@ -57,17 +50,41 @@ export function AccessTokenDialog({
}, [open, token, generate]) }, [open, token, generate])
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Access Token')}</DialogTitle> title={t('Access Token')}
<DialogDescription> description={t(
{t(
"Your system access token for API authentication. Keep it secure and don't share it with others." "Your system access token for API authentication. Keep it secure and don't share it with others."
)} )}
</DialogDescription> contentClassName='sm:max-w-md'
</DialogHeader> contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Close')}
</Button>
<Button
type='button'
onClick={generate}
disabled={generating}
className='gap-2'
>
{generating ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<RefreshCw className='h-4 w-4' />
)}
{generating ? t('Generating...') : t('Regenerate')}
</Button>
</>
}
>
<div className='my-6 space-y-4'> <div className='my-6 space-y-4'>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='token'>{t('Token')}</Label> <Label htmlFor='token'>{t('Token')}</Label>
...@@ -94,30 +111,6 @@ export function AccessTokenDialog({ ...@@ -94,30 +111,6 @@ export function AccessTokenDialog({
</p> </p>
</div> </div>
</div> </div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Close')}
</Button>
<Button
type='button'
onClick={generate}
disabled={generating}
className='gap-2'
>
{generating ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<RefreshCw className='h-4 w-4' />
)}
{generating ? t('Generating...') : t('Regenerate')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -21,15 +21,8 @@ import { Loader2 } from 'lucide-react' ...@@ -21,15 +21,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { updateUserProfile } from '../../api' import { updateUserProfile } from '../../api'
...@@ -114,27 +107,45 @@ export function ChangePasswordDialog({ ...@@ -114,27 +107,45 @@ export function ChangePasswordDialog({
} }
} }
const formId = 'change-password-form'
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<form onSubmit={handleSubmit}> onOpenChange={onOpenChange}
<DialogHeader> title={t('Change Password')}
<DialogTitle>{t('Change Password')}</DialogTitle> description={
<DialogDescription> <>
{t('Update your password for account:')}{' '} {t('Update your password for account:')} <strong>{username}</strong>
<strong>{username}</strong> </>
</DialogDescription> }
</DialogHeader> contentClassName='sm:max-w-md'
contentHeight='auto'
<div className='my-6 space-y-4'> bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button type='submit' form={formId} disabled={loading}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Changing...') : t('Change Password')}
</Button>
</>
}
>
<form id={formId} onSubmit={handleSubmit} className='space-y-4'>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='currentPassword'>{t('Current Password')}</Label> <Label htmlFor='currentPassword'>{t('Current Password')}</Label>
<PasswordInput <PasswordInput
id='currentPassword' id='currentPassword'
value={formData.originalPassword} value={formData.originalPassword}
onChange={(e) => onChange={(e) => handleChange('originalPassword', e.target.value)}
handleChange('originalPassword', e.target.value)
}
disabled={loading} disabled={loading}
required required
autoComplete='current-password' autoComplete='current-password'
...@@ -158,38 +169,17 @@ export function ChangePasswordDialog({ ...@@ -158,38 +169,17 @@ export function ChangePasswordDialog({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='confirmPassword'> <Label htmlFor='confirmPassword'>{t('Confirm New Password')}</Label>
{t('Confirm New Password')}
</Label>
<PasswordInput <PasswordInput
id='confirmPassword' id='confirmPassword'
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => onChange={(e) => handleChange('confirmPassword', e.target.value)}
handleChange('confirmPassword', e.target.value)
}
disabled={loading} disabled={loading}
required required
autoComplete='new-password' autoComplete='new-password'
/> />
</div> </div>
</div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button type='submit' disabled={loading}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Changing...') : t('Change Password')}
</Button>
</DialogFooter>
</form> </form>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -25,16 +25,9 @@ import { useAuthStore } from '@/stores/auth-store' ...@@ -25,16 +25,9 @@ import { useAuthStore } from '@/stores/auth-store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { deleteUserAccount } from '../../api' import { deleteUserAccount } from '../../api'
// ============================================================================ // ============================================================================
...@@ -101,20 +94,44 @@ export function DeleteAccountDialog({ ...@@ -101,20 +94,44 @@ export function DeleteAccountDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle className='text-destructive flex items-center gap-2'> title={
<>
<AlertTriangle className='h-5 w-5' /> <AlertTriangle className='h-5 w-5' />
{t('Delete Account')} {t('Delete Account')}
</DialogTitle> </>
<DialogDescription> }
{t( description={t(
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.' 'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
)} )}
</DialogDescription> contentClassName='sm:max-w-md'
</DialogHeader> titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button
type='button'
variant='destructive'
onClick={handleDelete}
disabled={loading || confirmation !== username}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Deleting...') : t('Delete Account')}
</Button>
</>
}
>
<div className='my-6 space-y-4'> <div className='my-6 space-y-4'>
<Alert variant='destructive'> <Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' /> <AlertTriangle className='h-4 w-4' />
...@@ -138,27 +155,6 @@ export function DeleteAccountDialog({ ...@@ -138,27 +155,6 @@ export function DeleteAccountDialog({
/> />
</div> </div>
</div> </div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button
type='button'
variant='destructive'
onClick={handleDelete}
disabled={loading || confirmation !== username}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Deleting...') : t('Delete Account')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -22,16 +22,9 @@ import { useTranslation } from 'react-i18next' ...@@ -22,16 +22,9 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useCountdown } from '@/hooks/use-countdown' import { useCountdown } from '@/hooks/use-countdown'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { sendEmailVerification, bindEmail } from '../../api' import { sendEmailVerification, bindEmail } from '../../api'
// ============================================================================ // ============================================================================
...@@ -129,19 +122,41 @@ export function EmailBindDialog({ ...@@ -129,19 +122,41 @@ export function EmailBindDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle>{t('Bind Email')}</DialogTitle> title={t('Bind Email')}
<DialogDescription> description={
{currentEmail currentEmail
? t('Current email: {{email}}. Enter a new email to change.', { ? t('Current email: {{email}}. Enter a new email to change.', {
email: currentEmail, email: currentEmail,
}) })
: t('Bind an email address to your account.')} : t('Bind an email address to your account.')
</DialogDescription> }
</DialogHeader> contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleBind}
disabled={loading || !email || !code}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Binding...') : t('Bind Email')}
</Button>
</>
}
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='email'>{t('Email Address')}</Label> <Label htmlFor='email'>{t('Email Address')}</Label>
...@@ -181,26 +196,6 @@ export function EmailBindDialog({ ...@@ -181,26 +196,6 @@ export function EmailBindDialog({
</div> </div>
</div> </div>
</div> </div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleBind}
disabled={loading || !email || !code}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Binding...') : t('Bind Email')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { Send } from 'lucide-react' import { Send } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { import { Dialog } from '@/components/dialog'
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
// ============================================================================ // ============================================================================
// Telegram Bind Dialog Component // Telegram Bind Dialog Component
...@@ -45,15 +39,15 @@ export function TelegramBindDialog({ ...@@ -45,15 +39,15 @@ export function TelegramBindDialog({
}: TelegramBindDialogProps) { }: TelegramBindDialogProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Bind Telegram Account')}</DialogTitle> title={t('Bind Telegram Account')}
<DialogDescription> description={t('Click the button below to bind your Telegram account')}
{t('Click the button below to bind your Telegram account')} contentClassName='sm:max-w-md'
</DialogDescription> contentHeight='auto'
</DialogHeader> bodyClassName='space-y-4'
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
<Alert> <Alert>
<Send className='h-4 w-4' /> <Send className='h-4 w-4' />
...@@ -94,7 +88,6 @@ export function TelegramBindDialog({ ...@@ -94,7 +88,6 @@ export function TelegramBindDialog({
{t('The binding will complete automatically after authorization')} {t('The binding will complete automatically after authorization')}
</p> </p>
</div> </div>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -23,17 +23,10 @@ import { toast } from 'sonner' ...@@ -23,17 +23,10 @@ import { toast } from 'sonner'
import { regenerate2FABackupCodes } from '@/lib/api' import { regenerate2FABackupCodes } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button' import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
// ============================================================================ // ============================================================================
// Two-FA Backup Codes Dialog Component // Two-FA Backup Codes Dialog Component
...@@ -94,20 +87,46 @@ export function TwoFABackupDialog({ ...@@ -94,20 +87,46 @@ export function TwoFABackupDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle className='flex items-center gap-2'> title={
<>
<RefreshCw className='h-5 w-5' /> <RefreshCw className='h-5 w-5' />
{t('Regenerate Backup Codes')} {t('Regenerate Backup Codes')}
</DialogTitle> </>
<DialogDescription> }
{backupCodes.length > 0 description={
backupCodes.length > 0
? t('Your new backup codes are ready') ? t('Your new backup codes are ready')
: t('Generate new backup codes for account recovery')} : t('Generate new backup codes for account recovery')
</DialogDescription> }
</DialogHeader> contentClassName='sm:max-w-md'
titleClassName='flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
{backupCodes.length === 0 ? (
<>
<Button
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button onClick={handleRegenerate} disabled={loading || !code}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Generating...') : t('Generate New Codes')}
</Button>
</>
) : (
<Button onClick={handleDone}>{t('Done')}</Button>
)}
</>
}
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
{backupCodes.length === 0 ? ( {backupCodes.length === 0 ? (
<> <>
...@@ -168,27 +187,6 @@ export function TwoFABackupDialog({ ...@@ -168,27 +187,6 @@ export function TwoFABackupDialog({
</> </>
)} )}
</div> </div>
<DialogFooter>
{backupCodes.length === 0 ? (
<>
<Button
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button onClick={handleRegenerate} disabled={loading || !code}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Generating...') : t('Generate New Codes')}
</Button>
</>
) : (
<Button onClick={handleDone}>{t('Done')}</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -24,16 +24,9 @@ import { disable2FA } from '@/lib/api' ...@@ -24,16 +24,9 @@ import { disable2FA } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
// ============================================================================ // ============================================================================
// Two-FA Disable Dialog Component // Two-FA Disable Dialog Component
...@@ -98,20 +91,42 @@ export function TwoFADisableDialog({ ...@@ -98,20 +91,42 @@ export function TwoFADisableDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle className='text-destructive flex items-center gap-2'> title={
<>
<AlertTriangle className='h-5 w-5' /> <AlertTriangle className='h-5 w-5' />
{t('Disable Two-Factor Authentication')} {t('Disable Two-Factor Authentication')}
</DialogTitle> </>
<DialogDescription> }
{t( description={t(
'This action will permanently remove 2FA protection from your account.' 'This action will permanently remove 2FA protection from your account.'
)} )}
</DialogDescription> contentClassName='sm:max-w-md'
</DialogHeader> titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button
variant='destructive'
onClick={handleDisable}
disabled={loading || !code || !confirmed}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Disabling...') : t('Disable 2FA')}
</Button>
</>
}
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
<Alert variant='destructive'> <Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' /> <AlertTriangle className='h-4 w-4' />
...@@ -150,25 +165,6 @@ export function TwoFADisableDialog({ ...@@ -150,25 +165,6 @@ export function TwoFADisableDialog({
</Label> </Label>
</div> </div>
</div> </div>
<DialogFooter>
<Button
variant='outline'
onClick={() => handleOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button
variant='destructive'
onClick={handleDisable}
disabled={loading || !code || !confirmed}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Disabling...') : t('Disable 2FA')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -24,17 +24,10 @@ import { toast } from 'sonner' ...@@ -24,17 +24,10 @@ import { toast } from 'sonner'
import { setup2FA, enable2FA } from '@/lib/api' import { setup2FA, enable2FA } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button' import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
import type { TwoFASetupData } from '../../types' import type { TwoFASetupData } from '../../types'
// ============================================================================ // ============================================================================
...@@ -136,15 +129,51 @@ export function TwoFASetupDialog({ ...@@ -136,15 +129,51 @@ export function TwoFASetupDialog({
}, [open, setupData, initializing, handleSetup]) }, [open, setupData, initializing, handleSetup])
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-lg'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle>{t('Setup Two-Factor Authentication')}</DialogTitle> title={t('Setup Two-Factor Authentication')}
<DialogDescription> description={
{t('Step')} {step + 1} {t('of 3:')} {stepLabels[step]} <>
</DialogDescription> {t('Step')}
</DialogHeader> {step + 1}
{t('of 3:')}
{stepLabels[step]}
</>
}
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
{step > 0 && (
<Button
variant='outline'
onClick={() => setStep(step - 1)}
disabled={initializing || loading}
>
{t('Back')}
</Button>
)}
{step < 2 ? (
<Button
onClick={() => setStep(step + 1)}
disabled={initializing || !setupData}
>
{t('Next')}
</Button>
) : (
<Button
onClick={handleEnable}
disabled={initializing || loading || !code}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Enabling...') : t('Enable 2FA')}
</Button>
)}
</>
}
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
{initializing ? ( {initializing ? (
<div className='flex flex-col items-center justify-center gap-3 py-8'> <div className='flex flex-col items-center justify-center gap-3 py-8'>
...@@ -251,35 +280,6 @@ export function TwoFASetupDialog({ ...@@ -251,35 +280,6 @@ export function TwoFASetupDialog({
</> </>
)} )}
</div> </div>
<DialogFooter>
{step > 0 && (
<Button
variant='outline'
onClick={() => setStep(step - 1)}
disabled={initializing || loading}
>
{t('Back')}
</Button>
)}
{step < 2 ? (
<Button
onClick={() => setStep(step + 1)}
disabled={initializing || !setupData}
>
{t('Next')}
</Button>
) : (
<Button
onClick={handleEnable}
disabled={initializing || loading || !code}
>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Enabling...') : t('Enable 2FA')}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { QrCode } from 'lucide-react' import { QrCode } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { import { Dialog } from '@/components/dialog'
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
// ============================================================================ // ============================================================================
// WeChat Bind Dialog Component // WeChat Bind Dialog Component
...@@ -43,15 +37,15 @@ export function WeChatBindDialog({ ...@@ -43,15 +37,15 @@ export function WeChatBindDialog({
}: WeChatBindDialogProps) { }: WeChatBindDialogProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Bind WeChat Account')}</DialogTitle> title={t('Bind WeChat Account')}
<DialogDescription> description={t('Scan the QR code with WeChat to bind your account')}
{t('Scan the QR code with WeChat to bind your account')} contentClassName='sm:max-w-md'
</DialogDescription> contentHeight='auto'
</DialogHeader> bodyClassName='space-y-4'
>
<div className='space-y-4 py-4'> <div className='space-y-4 py-4'>
<Alert> <Alert>
<QrCode className='h-4 w-4' /> <QrCode className='h-4 w-4' />
...@@ -76,7 +70,6 @@ export function WeChatBindDialog({ ...@@ -76,7 +70,6 @@ export function WeChatBindDialog({
{t('After scanning, the binding will complete automatically')} {t('After scanning, the binding will complete automatically')}
</p> </p>
</div> </div>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -26,12 +26,6 @@ import { useSystemConfig } from '@/hooks/use-system-config' ...@@ -26,12 +26,6 @@ import { useSystemConfig } from '@/hooks/use-system-config'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select, Select,
SelectContent, SelectContent,
SelectGroup, SelectGroup,
...@@ -40,6 +34,7 @@ import { ...@@ -40,6 +34,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Dialog } from '@/components/dialog'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { import {
paySubscriptionStripe, paySubscriptionStripe,
...@@ -259,15 +254,20 @@ export function SubscriptionPurchaseDialog(props: Props) { ...@@ -259,15 +254,20 @@ export function SubscriptionPurchaseDialog(props: Props) {
} }
return ( return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}> <Dialog
<DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'> open={props.open}
<DialogHeader> onOpenChange={props.onOpenChange}
<DialogTitle className='flex items-center gap-2'> title={
<>
<Crown className='h-5 w-5' /> <Crown className='h-5 w-5' />
{t('Purchase Subscription')} {t('Purchase Subscription')}
</DialogTitle> </>
</DialogHeader> }
contentClassName='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'
titleClassName='flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
>
<div className='space-y-3 sm:space-y-4'> <div className='space-y-3 sm:space-y-4'>
<div className='bg-muted/50 space-y-2.5 rounded-lg border p-3 sm:space-y-3 sm:p-4'> <div className='bg-muted/50 space-y-2.5 rounded-lg border p-3 sm:space-y-3 sm:p-4'>
<div className='flex justify-between'> <div className='flex justify-between'>
...@@ -346,9 +346,7 @@ export function SubscriptionPurchaseDialog(props: Props) { ...@@ -346,9 +346,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
) : ( ) : (
insufficientBalance && ( insufficientBalance && (
<Alert variant='destructive'> <Alert variant='destructive'>
<AlertDescription> <AlertDescription>{t('Insufficient balance')}</AlertDescription>
{t('Insufficient balance')}
</AlertDescription>
</Alert> </Alert>
) )
)} )}
...@@ -412,9 +410,7 @@ export function SubscriptionPurchaseDialog(props: Props) { ...@@ -412,9 +410,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
})), })),
]} ]}
value={selectedEpayMethod} value={selectedEpayMethod}
onValueChange={(v) => onValueChange={(v) => v !== null && setSelectedEpayMethod(v)}
v !== null && setSelectedEpayMethod(v)
}
disabled={limitReached} disabled={limitReached}
> >
<SelectTrigger className='flex-1'> <SelectTrigger className='flex-1'>
...@@ -441,7 +437,6 @@ export function SubscriptionPurchaseDialog(props: Props) { ...@@ -441,7 +437,6 @@ export function SubscriptionPurchaseDialog(props: Props) {
</div> </div>
)} )}
</div> </div>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -22,14 +22,6 @@ import { zodResolver } from '@hookform/resolvers/zod' ...@@ -22,14 +22,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -50,6 +42,7 @@ import { ...@@ -50,6 +42,7 @@ import {
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { import {
SettingsForm, SettingsForm,
SettingsSwitchContent, SettingsSwitchContent,
...@@ -74,6 +67,8 @@ type ProviderFormDialogProps = { ...@@ -74,6 +67,8 @@ type ProviderFormDialogProps = {
provider?: CustomOAuthProvider | null provider?: CustomOAuthProvider | null
} }
const PROVIDER_FORM_ID = 'custom-oauth-provider-form'
export function ProviderFormDialog(props: ProviderFormDialogProps) { export function ProviderFormDialog(props: ProviderFormDialogProps) {
const { t } = useTranslation() const { t } = useTranslation()
const isEditing = !!props.provider const isEditing = !!props.provider
...@@ -174,23 +169,43 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { ...@@ -174,23 +169,43 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
const isPending = createProvider.isPending || updateProvider.isPending const isPending = createProvider.isPending || updateProvider.isPending
return ( return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}> <Dialog
<DialogContent className='max-h-[85vh] overflow-y-auto sm:max-w-2xl'> open={props.open}
<DialogHeader> onOpenChange={props.onOpenChange}
<DialogTitle> title={isEditing ? t('Edit OAuth Provider') : t('Add OAuth Provider')}
{isEditing ? t('Edit OAuth Provider') : t('Add OAuth Provider')} description={
</DialogTitle> isEditing
<DialogDescription>
{isEditing
? t('Update the configuration for this custom OAuth provider.') ? t('Update the configuration for this custom OAuth provider.')
: t( : t('Configure a new custom OAuth provider for user authentication.')
'Configure a new custom OAuth provider for user authentication.' }
)} contentClassName='max-h-[85vh] overflow-y-auto sm:max-w-2xl'
</DialogDescription> contentHeight='auto'
</DialogHeader> bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => props.onOpenChange(false)}
disabled={isPending}
>
{t('Cancel')}
</Button>
<Button type='submit' form={PROVIDER_FORM_ID} disabled={isPending}>
{isPending
? t('Saving...')
: isEditing
? t('Update Provider')
: t('Create Provider')}
</Button>
</>
}
>
<Form {...form}> <Form {...form}>
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}> <SettingsForm
id={PROVIDER_FORM_ID}
onSubmit={form.handleSubmit(onSubmit)}
>
{/* Preset Selector (only for creating) */} {/* Preset Selector (only for creating) */}
{!isEditing && <PresetSelector form={form} />} {!isEditing && <PresetSelector form={form} />}
...@@ -352,9 +367,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { ...@@ -352,9 +367,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription> <FormDescription>
{t( {t('How client credentials are sent to the token endpoint')}
'How client credentials are sent to the token endpoint'
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
...@@ -587,27 +600,8 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { ...@@ -587,27 +600,8 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
)} )}
/> />
</div> </div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => props.onOpenChange(false)}
disabled={isPending}
>
{t('Cancel')}
</Button>
<Button type='submit' disabled={isPending}>
{isPending
? t('Saving...')
: isEditing
? t('Update Provider')
: t('Create Provider')}
</Button>
</DialogFooter>
</SettingsForm> </SettingsForm>
</Form> </Form>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -37,14 +37,6 @@ import { ...@@ -37,14 +37,6 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -72,6 +64,7 @@ import { ...@@ -72,6 +64,7 @@ import {
} from '@/components/ui/table' } from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { DateTimePicker } from '@/components/datetime-picker' import { DateTimePicker } from '@/components/datetime-picker'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
...@@ -105,6 +98,8 @@ const announcementSchema = z.object({ ...@@ -105,6 +98,8 @@ const announcementSchema = z.object({
type AnnouncementFormValues = z.infer<typeof announcementSchema> type AnnouncementFormValues = z.infer<typeof announcementSchema>
const ANNOUNCEMENT_FORM_ID = 'announcement-form'
const typeOptions = [ const typeOptions = [
{ {
value: 'default', value: 'default',
...@@ -460,20 +455,36 @@ export function AnnouncementsSection({ ...@@ -460,20 +455,36 @@ export function AnnouncementsSection({
</div> </div>
</div> </div>
<Dialog open={showDialog} onOpenChange={setShowDialog}> <Dialog
<DialogContent className='max-w-2xl'> open={showDialog}
<DialogHeader> onOpenChange={setShowDialog}
<DialogTitle> title={
{editingAnnouncement editingAnnouncement ? t('Edit Announcement') : t('Add Announcement')
? t('Edit Announcement') }
: t('Add Announcement')} description={t(
</DialogTitle> 'Create or update system announcements for the dashboard'
<DialogDescription> )}
{t('Create or update system announcements for the dashboard')} contentClassName='max-w-2xl'
</DialogDescription> contentHeight='auto'
</DialogHeader> bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit' form={ANNOUNCEMENT_FORM_ID}>
{editingAnnouncement ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}> <Form {...form}>
<form <form
id={ANNOUNCEMENT_FORM_ID}
onSubmit={form.handleSubmit(handleSubmitForm)} onSubmit={form.handleSubmit(handleSubmitForm)}
className='space-y-4' className='space-y-4'
> >
...@@ -593,21 +604,8 @@ export function AnnouncementsSection({ ...@@ -593,21 +604,8 @@ export function AnnouncementsSection({
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit'>
{editingAnnouncement ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form> </form>
</Form> </Form>
</DialogContent>
</Dialog> </Dialog>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
......
...@@ -37,14 +37,6 @@ import { ...@@ -37,14 +37,6 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -70,6 +62,7 @@ import { ...@@ -70,6 +62,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
...@@ -98,6 +91,8 @@ const createApiInfoSchema = (t: (key: string) => string) => ...@@ -98,6 +91,8 @@ const createApiInfoSchema = (t: (key: string) => string) =>
type ApiInfoFormValues = z.infer<ReturnType<typeof createApiInfoSchema>> type ApiInfoFormValues = z.infer<ReturnType<typeof createApiInfoSchema>>
const API_INFO_FORM_ID = 'api-info-form'
const colorOptions = [ const colorOptions = [
{ value: 'blue', label: 'Blue' }, { value: 'blue', label: 'Blue' },
{ value: 'green', label: 'Green' }, { value: 'green', label: 'Green' },
...@@ -408,18 +403,31 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -408,18 +403,31 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
</div> </div>
</div> </div>
<Dialog open={showDialog} onOpenChange={setShowDialog}> <Dialog
<DialogContent> open={showDialog}
<DialogHeader> onOpenChange={setShowDialog}
<DialogTitle> title={editingApiInfo ? t('Edit API Shortcut') : t('Add API Shortcut')}
{editingApiInfo ? t('Edit API Shortcut') : t('Add API Shortcut')} description={t('Configure API documentation links for the dashboard')}
</DialogTitle> contentHeight='auto'
<DialogDescription> bodyClassName='space-y-4'
{t('Configure API documentation links for the dashboard')} footer={
</DialogDescription> <>
</DialogHeader> <Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit' form={API_INFO_FORM_ID}>
{editingApiInfo ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}> <Form {...form}>
<form <form
id={API_INFO_FORM_ID}
onSubmit={form.handleSubmit(handleSubmitForm)} onSubmit={form.handleSubmit(handleSubmitForm)}
className='space-y-4' className='space-y-4'
> >
...@@ -520,21 +528,8 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -520,21 +528,8 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit'>
{editingApiInfo ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form> </form>
</Form> </Form>
</DialogContent>
</Dialog> </Dialog>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
......
...@@ -23,14 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod' ...@@ -23,14 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -40,6 +32,7 @@ import { ...@@ -40,6 +32,7 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
const createChatDialogSchema = (t: (key: string) => string) => const createChatDialogSchema = (t: (key: string) => string) =>
z.object({ z.object({
...@@ -49,6 +42,8 @@ const createChatDialogSchema = (t: (key: string) => string) => ...@@ -49,6 +42,8 @@ const createChatDialogSchema = (t: (key: string) => string) =>
type ChatDialogFormValues = z.infer<ReturnType<typeof createChatDialogSchema>> type ChatDialogFormValues = z.infer<ReturnType<typeof createChatDialogSchema>>
const CHAT_DIALOG_FORM_ID = 'chat-dialog-form'
export type ChatEntryData = { export type ChatEntryData = {
name: string name: string
url: string url: string
...@@ -97,19 +92,32 @@ export function ChatDialog({ ...@@ -97,19 +92,32 @@ export function ChatDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-[500px]'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle> title={isEditMode ? t('Edit chat preset') : t('Add chat preset')}
{isEditMode ? t('Edit chat preset') : t('Add chat preset')} description={t('Configure a predefined chat link for end users.')}
</DialogTitle> contentClassName='sm:max-w-[500px]'
<DialogDescription> contentHeight='auto'
{t('Configure a predefined chat link for end users.')} bodyClassName='space-y-4'
</DialogDescription> footer={
</DialogHeader> <>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='submit' form={CHAT_DIALOG_FORM_ID}>
{isEditMode ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}> <Form {...form}>
<form <form
id={CHAT_DIALOG_FORM_ID}
onSubmit={form.handleSubmit(handleSubmit)} onSubmit={form.handleSubmit(handleSubmit)}
className='space-y-4' className='space-y-4'
> >
...@@ -149,22 +157,8 @@ export function ChatDialog({ ...@@ -149,22 +157,8 @@ export function ChatDialog({
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='submit'>
{isEditMode ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form> </form>
</Form> </Form>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -36,14 +36,6 @@ import { ...@@ -36,14 +36,6 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -62,6 +54,7 @@ import { ...@@ -62,6 +54,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -90,6 +83,8 @@ const faqSchema = z.object({ ...@@ -90,6 +83,8 @@ const faqSchema = z.object({
type FAQFormValues = z.infer<typeof faqSchema> type FAQFormValues = z.infer<typeof faqSchema>
const FAQ_FORM_ID = 'faq-form'
export function FAQSection({ enabled, data }: FAQSectionProps) { export function FAQSection({ enabled, data }: FAQSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const updateOption = useUpdateOption() const updateOption = useUpdateOption()
...@@ -348,18 +343,32 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { ...@@ -348,18 +343,32 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
</div> </div>
</div> </div>
<Dialog open={showDialog} onOpenChange={setShowDialog}> <Dialog
<DialogContent className='max-w-2xl'> open={showDialog}
<DialogHeader> onOpenChange={setShowDialog}
<DialogTitle> title={editingFaq ? t('Edit FAQ') : t('Add FAQ')}
{editingFaq ? t('Edit FAQ') : t('Add FAQ')} description={t('Create or update frequently asked questions for users')}
</DialogTitle> contentClassName='max-w-2xl'
<DialogDescription> contentHeight='auto'
{t('Create or update frequently asked questions for users')} bodyClassName='space-y-4'
</DialogDescription> footer={
</DialogHeader> <>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit' form={FAQ_FORM_ID}>
{editingFaq ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}> <Form {...form}>
<form <form
id={FAQ_FORM_ID}
onSubmit={form.handleSubmit(handleSubmitForm)} onSubmit={form.handleSubmit(handleSubmitForm)}
className='space-y-4' className='space-y-4'
> >
...@@ -398,29 +407,14 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { ...@@ -398,29 +407,14 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t( {t('Maximum 1000 characters. Supports Markdown and HTML.')}
'Maximum 1000 characters. Supports Markdown and HTML.'
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit'>
{editingFaq ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form> </form>
</Form> </Form>
</DialogContent>
</Dialog> </Dialog>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment