perf(web): improve dialog sizing and footer layout
- migrate frontend dialogs to the shared footer API so actions stay separated from scrollable body content. - tune dialog dimensions for model analytics, prefill groups, billing history, channel model sync, and related workflows. - update channel terminology and dialog action translations across supported locales.
This commit is contained in:
+36
-37
@@ -20,12 +20,7 @@ import { useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { formatTimestampToDate } from '@/lib/format'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { getAffinityUsageCache } from './api'
|
||||
|
||||
function formatRate(hit: number, total: number): string {
|
||||
@@ -135,38 +130,42 @@ export function CacheStatsDialog(props: Props) {
|
||||
}, [stats, props.target, t])
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent className='sm:max-w-lg'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Channel Affinity: Upstream Cache Hit')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t(
|
||||
'Hit criteria: If cached tokens exist in usage, it counts as a hit.'
|
||||
)}
|
||||
</p>
|
||||
{loading ? (
|
||||
<div className='text-muted-foreground py-8 text-center text-sm'>
|
||||
{t('Loading...')}
|
||||
</div>
|
||||
) : rows.length > 0 ? (
|
||||
<div className='space-y-2'>
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row.key}
|
||||
className='flex justify-between border-b pb-1 text-sm'
|
||||
>
|
||||
<span className='text-muted-foreground'>{row.key}</span>
|
||||
<span className='font-medium'>{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-muted-foreground py-8 text-center text-sm'>
|
||||
{t('No data available')}
|
||||
</div>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
title={t('Channel Affinity: Upstream Cache Hit')}
|
||||
contentClassName='sm:max-w-lg'
|
||||
contentHeight='auto'
|
||||
bodyClassName='space-y-4'
|
||||
>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t(
|
||||
'Hit criteria: If cached tokens exist in usage, it counts as a hit.'
|
||||
)}
|
||||
</DialogContent>
|
||||
</p>
|
||||
{loading ? (
|
||||
<div className='text-muted-foreground py-8 text-center text-sm'>
|
||||
{t('Loading...')}
|
||||
</div>
|
||||
) : rows.length > 0 ? (
|
||||
<div className='space-y-2'>
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row.key}
|
||||
className='flex justify-between gap-4 border-b pb-1 text-sm'
|
||||
>
|
||||
<span className='text-muted-foreground'>{row.key}</span>
|
||||
<span className='text-right font-medium break-all'>
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-muted-foreground py-8 text-center text-sm'>
|
||||
{t('No data available')}
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
+42
-5
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
||||
import { Edit, FileText, Plus, RefreshCw, Trash2, X } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { StatusBadge, StatusBadgeList } from '@/components/status-badge'
|
||||
import { SettingsSwitchField } from '../../components/settings-form-layout'
|
||||
import { SettingsPageActionsPortal } from '../../components/settings-page-context'
|
||||
@@ -82,6 +82,43 @@ function RuleBadgeList(props: { items: string[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ChannelAffinityConfirmDialog(props: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
title: ReactNode
|
||||
desc: ReactNode
|
||||
handleConfirm: () => void
|
||||
destructive?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
title={props.title}
|
||||
contentClassName='sm:max-w-md'
|
||||
contentHeight='auto'
|
||||
bodyClassName='flex items-start'
|
||||
footer={
|
||||
<>
|
||||
<Button variant='outline' onClick={() => props.onOpenChange(false)}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={props.destructive ? 'destructive' : 'default'}
|
||||
onClick={props.handleConfirm}
|
||||
>
|
||||
{t('Continue')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className='text-muted-foreground text-sm'>{props.desc}</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function serializeRules(rules: AffinityRule[]): string {
|
||||
return JSON.stringify(rules.map(({ id: _, ...rest }) => rest))
|
||||
}
|
||||
@@ -641,7 +678,7 @@ export function ChannelAffinitySection(props: Props) {
|
||||
templateKey={ruleTemplateKey}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
<ChannelAffinityConfirmDialog
|
||||
open={clearAllDialogOpen}
|
||||
onOpenChange={setClearAllDialogOpen}
|
||||
title={t('Confirm clearing all channel affinity cache')}
|
||||
@@ -653,7 +690,7 @@ export function ChannelAffinitySection(props: Props) {
|
||||
/>
|
||||
|
||||
{clearRuleName !== null && (
|
||||
<ConfirmDialog
|
||||
<ChannelAffinityConfirmDialog
|
||||
open
|
||||
onOpenChange={(v) => !v && setClearRuleName(null)}
|
||||
title={t('Confirm clearing cache for this rule')}
|
||||
@@ -663,7 +700,7 @@ export function ChannelAffinitySection(props: Props) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
<ChannelAffinityConfirmDialog
|
||||
open={fillTemplateDialogOpen}
|
||||
onOpenChange={setFillTemplateDialogOpen}
|
||||
title={t('Fill Codex CLI / Claude CLI Templates')}
|
||||
|
||||
+214
-216
@@ -27,13 +27,6 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
@@ -46,6 +39,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { SettingsSwitchField } from '../../components/settings-form-layout'
|
||||
import { RULE_TEMPLATES } from './constants'
|
||||
import type { AffinityRule, KeySource } from './types'
|
||||
@@ -69,6 +63,8 @@ const CONTEXT_KEY_PRESETS = [
|
||||
'specific_channel_id',
|
||||
]
|
||||
|
||||
const RULE_FORM_ID = 'channel-affinity-rule-form'
|
||||
|
||||
interface RuleFormValues {
|
||||
name: string
|
||||
model_regex_text: string
|
||||
@@ -230,228 +226,230 @@ export function RuleEditorDialog(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent className='max-h-[85vh] max-w-2xl overflow-y-auto'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? t('Edit Rule') : t('Add Rule')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
title={isEdit ? t('Edit Rule') : t('Add Rule')}
|
||||
contentClassName='max-w-2xl'
|
||||
contentHeight='auto'
|
||||
bodyClassName='pr-2'
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => props.onOpenChange(false)}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button type='submit' form={RULE_FORM_ID}>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form
|
||||
id={RULE_FORM_ID}
|
||||
onSubmit={form.handleSubmit(handleSave)}
|
||||
className='min-w-0 space-y-4 overflow-x-clip'
|
||||
>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Name')} *</Label>
|
||||
<Input
|
||||
placeholder='prefer-by-conversation-id'
|
||||
{...form.register('name', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form onSubmit={form.handleSubmit(handleSave)} className='space-y-4'>
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Name')} *</Label>
|
||||
<Input
|
||||
placeholder='prefer-by-conversation-id'
|
||||
{...form.register('name', { required: true })}
|
||||
<Label>{t('Model Regex (one per line)')} *</Label>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder={'^gpt-4o.*$\n^claude-3.*$'}
|
||||
{...form.register('model_regex_text', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Model Regex (one per line)')} *</Label>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder={'^gpt-4o.*$\n^claude-3.*$'}
|
||||
{...form.register('model_regex_text', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Path Regex (one per line)')}</Label>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder='/v1/chat/completions'
|
||||
{...form.register('path_regex_text')}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Path Regex (one per line)')}</Label>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder='/v1/chat/completions'
|
||||
{...form.register('path_regex_text')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('skip_retry_on_failure')}
|
||||
onCheckedChange={(v) => form.setValue('skip_retry_on_failure', v)}
|
||||
label={t('Skip retry on failure')}
|
||||
/>
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('skip_retry_on_failure')}
|
||||
onCheckedChange={(v) => form.setValue('skip_retry_on_failure', v)}
|
||||
label={t('Skip retry on failure')}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
{/* Key Sources */}
|
||||
<div>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<Label>{t('Key Sources')}</Label>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
setKeySources((prev) => [
|
||||
...prev,
|
||||
{ type: 'gjson', path: '' },
|
||||
])
|
||||
}
|
||||
>
|
||||
<Plus className='mr-1 h-3 w-3' />
|
||||
{t('Add')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className='text-muted-foreground mb-2 text-xs'>
|
||||
{t('Common Keys')}: {CONTEXT_KEY_PRESETS.join(', ')}
|
||||
</p>
|
||||
<div className='space-y-2'>
|
||||
{keySources.map((src, idx) => (
|
||||
<div key={idx} className='flex items-center gap-2'>
|
||||
<Select
|
||||
items={[
|
||||
...KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t })),
|
||||
]}
|
||||
value={src.type}
|
||||
onValueChange={(v) => {
|
||||
if (v === null) return
|
||||
const next = [...keySources]
|
||||
next[idx] = normalizeKeySource({
|
||||
...src,
|
||||
type: v as KeySource['type'],
|
||||
})
|
||||
setKeySources(next)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className='w-[160px]'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
<SelectGroup>
|
||||
{KEY_SOURCE_TYPES.map((t) => (
|
||||
<SelectItem key={t} value={t}>
|
||||
{t}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
className='flex-1'
|
||||
placeholder={
|
||||
src.type === 'gjson'
|
||||
? 'metadata.conversation_id'
|
||||
: 'user_id'
|
||||
}
|
||||
value={
|
||||
src.type === 'gjson' ? src.path || '' : src.key || ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
const next = [...keySources]
|
||||
if (src.type === 'gjson') {
|
||||
next[idx] = { ...src, path: e.target.value }
|
||||
} else {
|
||||
next[idx] = { ...src, key: e.target.value }
|
||||
}
|
||||
setKeySources(next)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() =>
|
||||
setKeySources((prev) => prev.filter((_, i) => i !== idx))
|
||||
}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Advanced */}
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger
|
||||
render={
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
className='w-full justify-start'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{advancedOpen ? '▼' : '▶'} {t('Advanced Settings')}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className='space-y-3 pt-2'>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('User-Agent include (one per line)')}</Label>
|
||||
<Textarea
|
||||
rows={3}
|
||||
placeholder='curl PostmanRuntime'
|
||||
{...form.register('user_agent_include_text')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Value Regex')}</Label>
|
||||
<Input
|
||||
placeholder='^[-0-9A-Za-z._:]{1,128}$'
|
||||
{...form.register('value_regex')}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('TTL (seconds, 0 = default)')}</Label>
|
||||
<Input
|
||||
type='number'
|
||||
min={0}
|
||||
{...form.register('ttl_seconds')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Parameter Override Template (JSON)')}</Label>
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder='{"operations": [...]}'
|
||||
{...form.register('param_override_template_json')}
|
||||
className='font-mono text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-3 sm:grid-cols-3'>
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('include_using_group')}
|
||||
onCheckedChange={(v) =>
|
||||
form.setValue('include_using_group', v)
|
||||
}
|
||||
label={t('Include Group')}
|
||||
className='border-b-0 py-0'
|
||||
/>
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('include_model_name')}
|
||||
onCheckedChange={(v) =>
|
||||
form.setValue('include_model_name', v)
|
||||
}
|
||||
label={t('Include Model')}
|
||||
className='border-b-0 py-0'
|
||||
/>
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('include_rule_name')}
|
||||
onCheckedChange={(v) => form.setValue('include_rule_name', v)}
|
||||
label={t('Include Rule Name')}
|
||||
className='border-b-0 py-0'
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<DialogFooter>
|
||||
{/* Key Sources */}
|
||||
<div>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<Label>{t('Key Sources')}</Label>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => props.onOpenChange(false)}
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
setKeySources((prev) => [...prev, { type: 'gjson', path: '' }])
|
||||
}
|
||||
>
|
||||
{t('Cancel')}
|
||||
<Plus className='mr-1 h-3 w-3' />
|
||||
{t('Add')}
|
||||
</Button>
|
||||
<Button type='submit'>{t('Save')}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</div>
|
||||
<p className='text-muted-foreground mb-2 text-xs'>
|
||||
{t('Common Keys')}: {CONTEXT_KEY_PRESETS.join(', ')}
|
||||
</p>
|
||||
<div className='space-y-2'>
|
||||
{keySources.map((src, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className='flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center'
|
||||
>
|
||||
<Select
|
||||
items={[
|
||||
...KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t })),
|
||||
]}
|
||||
value={src.type}
|
||||
onValueChange={(v) => {
|
||||
if (v === null) return
|
||||
const next = [...keySources]
|
||||
next[idx] = normalizeKeySource({
|
||||
...src,
|
||||
type: v as KeySource['type'],
|
||||
})
|
||||
setKeySources(next)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className='w-full sm:w-[160px]'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
<SelectGroup>
|
||||
{KEY_SOURCE_TYPES.map((t) => (
|
||||
<SelectItem key={t} value={t}>
|
||||
{t}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
className='min-w-0 flex-1'
|
||||
placeholder={
|
||||
src.type === 'gjson'
|
||||
? 'metadata.conversation_id'
|
||||
: 'user_id'
|
||||
}
|
||||
value={src.type === 'gjson' ? src.path || '' : src.key || ''}
|
||||
onChange={(e) => {
|
||||
const next = [...keySources]
|
||||
if (src.type === 'gjson') {
|
||||
next[idx] = { ...src, path: e.target.value }
|
||||
} else {
|
||||
next[idx] = { ...src, key: e.target.value }
|
||||
}
|
||||
setKeySources(next)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() =>
|
||||
setKeySources((prev) => prev.filter((_, i) => i !== idx))
|
||||
}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Advanced */}
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger
|
||||
render={
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
className='w-full justify-start'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{advancedOpen ? '▼' : '▶'} {t('Advanced Settings')}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className='space-y-3 pt-2'>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('User-Agent include (one per line)')}</Label>
|
||||
<Textarea
|
||||
rows={3}
|
||||
placeholder='curl PostmanRuntime'
|
||||
{...form.register('user_agent_include_text')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Value Regex')}</Label>
|
||||
<Input
|
||||
placeholder='^[-0-9A-Za-z._:]{1,128}$'
|
||||
{...form.register('value_regex')}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('TTL (seconds, 0 = default)')}</Label>
|
||||
<Input
|
||||
type='number'
|
||||
min={0}
|
||||
{...form.register('ttl_seconds')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-1.5'>
|
||||
<Label>{t('Parameter Override Template (JSON)')}</Label>
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder='{"operations": [...]}'
|
||||
{...form.register('param_override_template_json')}
|
||||
className='font-mono text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-3 sm:grid-cols-3'>
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('include_using_group')}
|
||||
onCheckedChange={(v) => form.setValue('include_using_group', v)}
|
||||
label={t('Include Group')}
|
||||
className='border-b-0 py-0'
|
||||
/>
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('include_model_name')}
|
||||
onCheckedChange={(v) => form.setValue('include_model_name', v)}
|
||||
label={t('Include Model')}
|
||||
className='border-b-0 py-0'
|
||||
/>
|
||||
<SettingsSwitchField
|
||||
checked={form.watch('include_rule_name')}
|
||||
onCheckedChange={(v) => form.setValue('include_rule_name', v)}
|
||||
label={t('Include Rule Name')}
|
||||
className='border-b-0 py-0'
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</form>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user