@@ -5,7 +5,7 @@ import { z } from 'zod';
55import { useForm } from 'react-hook-form' ;
66import { zodResolver } from '@hookform/resolvers/zod' ;
77import { useVirtualizer } from '@tanstack/react-virtual' ;
8- import { X , RefreshCw , Search , ChevronLeft , ChevronRight , PanelLeft , Plus , Trash2 , Eye , EyeOff , Copy , Play , Info } from 'lucide-react' ;
8+ import { X , RefreshCw , Search , ChevronLeft , ChevronRight , PanelLeft , Plus , Trash2 , Eye , EyeOff , Copy , Play , Info , Ban } from 'lucide-react' ;
99import { useTranslation } from 'react-i18next' ;
1010import { toast } from 'sonner' ;
1111import { Badge } from '@/components/ui/badge' ;
@@ -34,6 +34,8 @@ import {
3434 useAllChannelNames ,
3535 useAllChannelTags ,
3636 useChannelDisabledAPIKeys ,
37+ useDisableChannelAPIKey ,
38+ useEnableChannelAPIKey ,
3739 useSyncChannelModels ,
3840} from '../data/channels' ;
3941import { claudecodeOAuthExchange , claudecodeOAuthStart } from '../data/claudecode' ;
@@ -356,6 +358,7 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
356358 const [ selectedKeysToRemove , setSelectedKeysToRemove ] = useState < Set < string > > ( new Set ( ) ) ;
357359 const [ confirmRemoveSelectedOpen , setConfirmRemoveSelectedOpen ] = useState ( false ) ;
358360 const [ confirmRemoveKey , setConfirmRemoveKey ] = useState < string | null > ( null ) ;
361+ const [ confirmDisableKey , setConfirmDisableKey ] = useState < string | null > ( null ) ;
359362 const [ showOpenCodeGoAuthCookie , setShowOpenCodeGoAuthCookie ] = useState ( false ) ;
360363 const [ authMode , setAuthMode ] = useState < 'official' | 'auth-json' | 'third-party' > ( 'official' ) ;
361364 const [ codexAuthJSONText , setCodexAuthJSONText ] = useState ( '' ) ;
@@ -515,6 +518,7 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
515518 setSelectedKeysToRemove ( new Set ( ) ) ;
516519 setConfirmRemoveSelectedOpen ( false ) ;
517520 setConfirmRemoveKey ( null ) ;
521+ setConfirmDisableKey ( null ) ;
518522 setShowOpenCodeGoAuthCookie ( false ) ;
519523 setPatternError ( null ) ;
520524 setFetchedModels ( [ ] ) ;
@@ -718,12 +722,21 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
718722 const apiKeysCount = useMemo ( ( ) => ( apiKeys || [ ] ) . filter ( ( k ) => k . trim ( ) . length > 0 ) . length , [ apiKeys ] ) ;
719723 const isSubmitting = createChannel . isPending || duplicateChannel . isPending || updateChannel . isPending ;
720724
721- const { data : disabledKeys = [ ] } = useChannelDisabledAPIKeys ( currentRow ?. id || '' , {
725+ const { data : disabledKeys = [ ] , isFetching : isFetchingDisabledKeys } = useChannelDisabledAPIKeys ( currentRow ?. id || '' , {
722726 enabled : isEdit && ! ! currentRow ?. id && showApiKeysPanel ,
723727 } ) ;
724728
725729 const disabledKeySet = useMemo ( ( ) => new Set ( disabledKeys . map ( ( dk ) => dk . key ) ) , [ disabledKeys ] ) ;
726730
731+ // Keys that exist in the backend (used to hide disable/enable button for unsaved new keys)
732+ const savedAPIKeySet = useMemo (
733+ ( ) => new Set ( ( currentRow ?. credentials ?. apiKeys || [ ] ) . filter ( ( k ) => k . trim ( ) . length > 0 ) ) ,
734+ [ currentRow ?. credentials ?. apiKeys ]
735+ ) ;
736+
737+ const disableAPIKey = useDisableChannelAPIKey ( ) ;
738+ const enableAPIKey = useEnableChannelAPIKey ( ) ;
739+
727740 useEffect ( ( ) => {
728741 if ( ! open || ! isDuplicate || ! duplicateFromRow ) return ;
729742 if ( ! allChannelNamesLoaded ) return ;
@@ -1565,6 +1578,7 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
15651578 setSelectedKeysToRemove ( new Set ( ) ) ;
15661579 setConfirmRemoveSelectedOpen ( false ) ;
15671580 setConfirmRemoveKey ( null ) ;
1581+ setConfirmDisableKey ( null ) ;
15681582 } , [ ] ) ;
15691583
15701584 const removeApiKeys = useCallback (
@@ -1665,6 +1679,7 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
16651679 setSelectedKeysToRemove ( new Set ( ) ) ;
16661680 setConfirmRemoveSelectedOpen ( false ) ;
16671681 setConfirmRemoveKey ( null ) ;
1682+ setConfirmDisableKey ( null ) ;
16681683 setShowApiKey ( false ) ;
16691684 setShowOpenCodeGoAuthCookie ( false ) ;
16701685 // Reset proxy state
@@ -2947,6 +2962,7 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
29472962 { ( ( ) => {
29482963 const validKeys = ( apiKeys || [ ] ) . map ( ( k ) => k . trim ( ) ) . filter ( ( k ) => k . length > 0 ) ;
29492964 const isLastKey = validKeys . length <= 1 ;
2965+ const enabledKeysCount = validKeys . filter ( ( k ) => savedAPIKeySet . has ( k ) && ! disabledKeySet . has ( k ) ) . length ;
29502966 return validKeys
29512967 . filter ( ( k ) => {
29522968 if ( ! apiKeysSearch . trim ( ) ) return true ;
@@ -2956,6 +2972,7 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
29562972 . map ( ( key ) => {
29572973 const isSelected = selectedKeysToRemove . has ( key ) ;
29582974 const isDisabled = disabledKeySet . has ( key ) ;
2975+ const isSavedKey = savedAPIKeySet . has ( key ) ;
29592976 const masked = key . length > 8 ? `${ key . slice ( 0 , 4 ) } ****${ key . slice ( - 4 ) } ` : `****${ key . slice ( - 4 ) } ` ;
29602977
29612978 return (
@@ -2993,50 +3010,133 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
29933010 </ div >
29943011 </ div >
29953012
2996- { isLastKey ? (
2997- < Tooltip >
2998- < TooltipTrigger asChild >
2999- < span className = 'inline-flex' >
3000- < Button
3001- type = 'button'
3002- variant = 'ghost'
3003- size = 'sm'
3004- className = 'text-muted-foreground h-7 w-7 p-0'
3005- disabled
3006- >
3007- < Trash2 className = 'h-4 w-4' />
3008- </ Button >
3009- </ span >
3010- </ TooltipTrigger >
3011- < TooltipContent >
3012- < p > { t ( 'channels.dialogs.fields.apiKey.mustKeepOne' ) } </ p >
3013- </ TooltipContent >
3014- </ Tooltip >
3015- ) : (
3016- < Popover
3017- open = { confirmRemoveKey === key }
3018- onOpenChange = { ( isOpen ) => setConfirmRemoveKey ( isOpen ? key : null ) }
3019- >
3020- < PopoverTrigger asChild >
3021- < Button type = 'button' variant = 'ghost' size = 'sm' className = 'text-destructive h-7 w-7 p-0' >
3022- < Trash2 className = 'h-4 w-4' />
3023- </ Button >
3024- </ PopoverTrigger >
3025- < PopoverContent className = 'w-72' >
3026- < div className = 'flex flex-col gap-3' >
3027- < p className = 'text-sm' > { t ( 'channels.dialogs.fields.apiKey.confirmRemoveSingle' ) } </ p >
3028- < div className = 'flex justify-end gap-2' >
3029- < Button size = 'sm' variant = 'outline' onClick = { ( ) => setConfirmRemoveKey ( null ) } >
3030- { t ( 'common.buttons.cancel' ) }
3013+ < div className = 'flex items-center gap-1' >
3014+ { /* Disable / Enable button — only for keys saved in backend */ }
3015+ { isSavedKey &&
3016+ ( isDisabled ? (
3017+ < Popover
3018+ open = { confirmDisableKey === key }
3019+ onOpenChange = { ( isOpen ) => setConfirmDisableKey ( isOpen ? key : null ) }
3020+ >
3021+ < PopoverTrigger asChild >
3022+ < Button type = 'button' variant = 'ghost' size = 'sm' className = 'h-7 w-7 p-0' >
3023+ < RefreshCw className = 'h-4 w-4' />
30313024 </ Button >
3032- < Button size = 'sm' variant = 'destructive' onClick = { ( ) => removeApiKeys ( [ key ] ) } >
3033- { t ( 'common.buttons.confirm' ) }
3025+ </ PopoverTrigger >
3026+ < PopoverContent className = 'w-72' >
3027+ < div className = 'flex flex-col gap-3' >
3028+ < p className = 'text-sm' > { t ( 'channels.dialogs.fields.apiKey.confirmEnable' ) } </ p >
3029+ < div className = 'flex justify-end gap-2' >
3030+ < Button size = 'sm' variant = 'outline' onClick = { ( ) => setConfirmDisableKey ( null ) } >
3031+ { t ( 'common.buttons.cancel' ) }
3032+ </ Button >
3033+ < Button
3034+ size = 'sm'
3035+ onClick = { async ( ) => {
3036+ if ( ! currentRow ?. id ) return ;
3037+ await enableAPIKey . mutateAsync ( { channelID : currentRow . id , key } ) ;
3038+ setConfirmDisableKey ( null ) ;
3039+ } }
3040+ >
3041+ { t ( 'common.buttons.confirm' ) }
3042+ </ Button >
3043+ </ div >
3044+ </ div >
3045+ </ PopoverContent >
3046+ </ Popover >
3047+ ) : enabledKeysCount <= 1 ? (
3048+ < Tooltip >
3049+ < TooltipTrigger asChild >
3050+ < span className = 'inline-flex' >
3051+ < Button type = 'button' variant = 'ghost' size = 'sm' className = 'text-muted-foreground h-7 w-7 p-0' disabled >
3052+ < Ban className = 'h-4 w-4' />
3053+ </ Button >
3054+ </ span >
3055+ </ TooltipTrigger >
3056+ < TooltipContent >
3057+ < p > { t ( 'channels.dialogs.fields.apiKey.mustKeepOneEnabled' ) } </ p >
3058+ </ TooltipContent >
3059+ </ Tooltip >
3060+ ) : (
3061+ < Popover
3062+ open = { confirmDisableKey === key }
3063+ onOpenChange = { ( isOpen ) => setConfirmDisableKey ( isOpen ? key : null ) }
3064+ >
3065+ < PopoverTrigger asChild >
3066+ < Button type = 'button' variant = 'ghost' size = 'sm' className = 'text-orange-500 h-7 w-7 p-0' disabled = { disableAPIKey . isPending || isFetchingDisabledKeys } >
3067+ < Ban className = 'h-4 w-4' />
30343068 </ Button >
3069+ </ PopoverTrigger >
3070+ < PopoverContent className = 'w-72' >
3071+ < div className = 'flex flex-col gap-3' >
3072+ < p className = 'text-sm' > { t ( 'channels.dialogs.fields.apiKey.confirmDisable' ) } </ p >
3073+ < div className = 'flex justify-end gap-2' >
3074+ < Button size = 'sm' variant = 'outline' onClick = { ( ) => setConfirmDisableKey ( null ) } >
3075+ { t ( 'common.buttons.cancel' ) }
3076+ </ Button >
3077+ < Button
3078+ size = 'sm'
3079+ disabled = { disableAPIKey . isPending || isFetchingDisabledKeys }
3080+ onClick = { async ( ) => {
3081+ if ( ! currentRow ?. id ) return ;
3082+ await disableAPIKey . mutateAsync ( { channelID : currentRow . id , key } ) ;
3083+ setConfirmDisableKey ( null ) ;
3084+ } }
3085+ >
3086+ { t ( 'common.buttons.confirm' ) }
3087+ </ Button >
3088+ </ div >
3089+ </ div >
3090+ </ PopoverContent >
3091+ </ Popover >
3092+ ) ) }
3093+
3094+ { /* Delete button */ }
3095+ { isLastKey ? (
3096+ < Tooltip >
3097+ < TooltipTrigger asChild >
3098+ < span className = 'inline-flex' >
3099+ < Button
3100+ type = 'button'
3101+ variant = 'ghost'
3102+ size = 'sm'
3103+ className = 'text-muted-foreground h-7 w-7 p-0'
3104+ disabled
3105+ >
3106+ < Trash2 className = 'h-4 w-4' />
3107+ </ Button >
3108+ </ span >
3109+ </ TooltipTrigger >
3110+ < TooltipContent >
3111+ < p > { t ( 'channels.dialogs.fields.apiKey.mustKeepOne' ) } </ p >
3112+ </ TooltipContent >
3113+ </ Tooltip >
3114+ ) : (
3115+ < Popover
3116+ open = { confirmRemoveKey === key }
3117+ onOpenChange = { ( isOpen ) => setConfirmRemoveKey ( isOpen ? key : null ) }
3118+ >
3119+ < PopoverTrigger asChild >
3120+ < Button type = 'button' variant = 'ghost' size = 'sm' className = 'text-destructive h-7 w-7 p-0' >
3121+ < Trash2 className = 'h-4 w-4' />
3122+ </ Button >
3123+ </ PopoverTrigger >
3124+ < PopoverContent className = 'w-72' >
3125+ < div className = 'flex flex-col gap-3' >
3126+ < p className = 'text-sm' > { t ( 'channels.dialogs.fields.apiKey.confirmRemoveSingle' ) } </ p >
3127+ < div className = 'flex justify-end gap-2' >
3128+ < Button size = 'sm' variant = 'outline' onClick = { ( ) => setConfirmRemoveKey ( null ) } >
3129+ { t ( 'common.buttons.cancel' ) }
3130+ </ Button >
3131+ < Button size = 'sm' variant = 'destructive' onClick = { ( ) => removeApiKeys ( [ key ] ) } >
3132+ { t ( 'common.buttons.confirm' ) }
3133+ </ Button >
3134+ </ div >
30353135 </ div >
3036- </ div >
3037- </ PopoverContent >
3038- </ Popover >
3039- ) }
3136+ </ PopoverContent >
3137+ </ Popover >
3138+ ) }
3139+ </ div >
30403140 </ div >
30413141 ) ;
30423142 } ) ;
@@ -3079,6 +3179,7 @@ export function ChannelsActionDialog({ currentRow, duplicateFromRow, open, onOpe
30793179 setSelectedKeysToRemove ( new Set ( ) ) ;
30803180 setConfirmRemoveSelectedOpen ( false ) ;
30813181 setConfirmRemoveKey ( null ) ;
3182+ setConfirmDisableKey ( null ) ;
30823183 } }
30833184 disabled = { selectedKeysToRemove . size === 0 }
30843185 >
0 commit comments