@@ -5,10 +5,14 @@ import { toast } from 'vue-sonner'
55import { useMcpToolsStore } from ' @/stores/mcpToolsStore'
66import { McpToolsService } from ' @/services/mcpToolsService'
77import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from ' @/components/ui/table'
8- import { Switch } from ' @/components/ui/switch'
8+ import { Checkbox } from ' @/components/ui/checkbox'
9+ import { Button } from ' @/components/ui/button'
10+ import { ButtonGroup } from ' @/components/ui/button-group'
11+ import { Badge } from ' @/components/ui/badge'
12+ import { Spinner } from ' @/components/ui/spinner'
913import { Alert , AlertDescription } from ' @/components/ui/alert'
1014import { Chart } from ' @/components/ui/chart'
11- import { AlertCircle , Package , Wrench , Coins } from ' lucide-vue-next'
15+ import { AlertCircle , Package , Wrench , Coins , CircleCheck , CircleMinus } from ' lucide-vue-next'
1216import { use } from ' echarts/core'
1317import { PieChart } from ' echarts/charts'
1418import { TooltipComponent , LegendComponent } from ' echarts/components'
@@ -37,7 +41,8 @@ const mcpToolsStore = useMcpToolsStore()
3741const tools = ref <any >(null )
3842const isLoading = ref (true )
3943const error = ref <string | null >(null )
40- const togglingToolId = ref <string | null >(null )
44+ const selectedToolIds = ref <string []>([])
45+ const isBulkToggling = ref (false )
4146
4247// Format token count with commas
4348const formatTokenCount = (count : number ) => {
@@ -67,46 +72,115 @@ const hasTools = computed(() => {
6772 return tools .value && tools .value .tools && tools .value .tools .length > 0
6873})
6974
70- // Handle tool toggle
71- async function handleToolToggle(toolId : string , toolName : string , currentDisabled : boolean ) {
72- if (! props .canEdit ) return
75+ // Check if all tools are selected
76+ const allToolsSelected = computed (() => {
77+ return hasTools .value && selectedToolIds .value .length === tools .value .tools .length
78+ })
7379
74- const newDisabledState = ! currentDisabled
75- togglingToolId .value = toolId
80+ // Check if some (but not all) tools are selected
81+ const someToolsSelected = computed (() => {
82+ return selectedToolIds .value .length > 0 && ! allToolsSelected .value
83+ })
7684
77- // Optimistic update
78- const toolIndex = tools .value .tools .findIndex ((t : { id: string }) => t .id === toolId )
79- if (toolIndex !== - 1 ) {
80- tools .value .tools [toolIndex ].is_disabled = newDisabledState
85+ // Toggle all tools selection
86+ const toggleAllTools = () => {
87+ if (allToolsSelected .value ) {
88+ selectedToolIds .value = []
89+ } else {
90+ selectedToolIds .value = tools .value .tools .map ((t : { id: string }) => t .id )
8191 }
92+ }
93+
94+ // Toggle individual tool selection
95+ const toggleToolSelection = (toolId : string ) => {
96+ const index = selectedToolIds .value .indexOf (toolId )
97+ if (index > - 1 ) {
98+ selectedToolIds .value .splice (index , 1 )
99+ } else {
100+ selectedToolIds .value .push (toolId )
101+ }
102+ }
103+
104+ // Check if tool is selected
105+ const isToolSelected = (toolId : string ) => {
106+ return selectedToolIds .value .includes (toolId )
107+ }
108+
109+ // Handle bulk enable
110+ async function handleBulkEnable() {
111+ if (selectedToolIds .value .length === 0 ) return
112+ await handleBulkToggle (false ) // false = enabled
113+ }
114+
115+ // Handle bulk disable
116+ async function handleBulkDisable() {
117+ if (selectedToolIds .value .length === 0 ) return
118+ await handleBulkToggle (true ) // true = disabled
119+ }
120+
121+ // Batch toggle selected tools
122+ async function handleBulkToggle(isDisabled : boolean ) {
123+ if (! props .canEdit || selectedToolIds .value .length === 0 ) return
124+
125+ isBulkToggling .value = true
126+
127+ // Prepare batch request
128+ const toolsToToggle = selectedToolIds .value .map (toolId => ({
129+ tool_id: toolId ,
130+ is_disabled: isDisabled
131+ }))
132+
133+ // Optimistic update
134+ const originalTools = JSON .parse (JSON .stringify (tools .value .tools ))
135+ selectedToolIds .value .forEach (toolId => {
136+ const tool = tools .value .tools .find ((t : { id: string }) => t .id === toolId )
137+ if (tool ) {
138+ tool .is_disabled = isDisabled
139+ }
140+ })
82141
83142 try {
84- const response = await McpToolsService .toggleToolStatus (
143+ const response = await McpToolsService .batchToggleTools (
85144 props .teamId ,
86145 props .installation .id ,
87- toolId ,
88- newDisabledState
146+ toolsToToggle
89147 )
90148
91- const action = newDisabledState
149+ const action = isDisabled
92150 ? t (' mcpInstallations.details.tools.toggle.disabled' )
93151 : t (' mcpInstallations.details.tools.toggle.enabled' )
94152
95- toast .success (t (' mcpInstallations.details.tools.toggle.success' , { toolName , action }), {
96- description: response .message
97- })
153+ // Show success toast
154+ if (response .total_failed === 0 ) {
155+ toast .success (
156+ t (' mcpInstallations.details.tools.bulkToggle.allSuccess' , {
157+ count: response .total_succeeded ,
158+ action
159+ })
160+ )
161+ } else if (response .total_succeeded > 0 ) {
162+ toast .warning (
163+ t (' mcpInstallations.details.tools.bulkToggle.partialSuccess' , {
164+ succeeded: response .total_succeeded ,
165+ failed: response .total_failed ,
166+ action
167+ })
168+ )
169+ }
170+
171+ // Clear selection (optimistic update already applied)
172+ selectedToolIds .value = []
173+
98174 } catch (err ) {
99175 // Revert optimistic update on error
100- if (toolIndex !== - 1 ) {
101- tools .value .tools [toolIndex ].is_disabled = currentDisabled
102- }
176+ tools .value .tools = originalTools
103177
104178 const errorMessage = err instanceof Error ? err .message : t (' mcpInstallations.details.tools.toggle.error' )
105- toast .error (t (' mcpInstallations.details.tools.toggle .errorTitle' ), {
179+ toast .error (t (' mcpInstallations.details.tools.bulkToggle .errorTitle' ), {
106180 description: errorMessage
107181 })
108182 } finally {
109- togglingToolId .value = null
183+ isBulkToggling .value = false
110184 }
111185}
112186
@@ -209,39 +283,101 @@ const pieChartOption = computed<EChartsOption>(() => {
209283 </div >
210284 </div >
211285
286+ <!-- Bulk Actions -->
287+ <div class =" flex items-center justify-end gap-2 mb-4" >
288+ <ButtonGroup aria-label =" Bulk tool actions" >
289+ <Button
290+ variant =" outline"
291+ class =" w-24"
292+ :disabled =" !props.canEdit || selectedToolIds.length === 0 || isBulkToggling"
293+ @click =" handleBulkEnable"
294+ >
295+ <Spinner v-if =" isBulkToggling" />
296+ <span v-else >{{ t('mcpInstallations.details.tools.bulkActions.enable') }}</span >
297+ </Button >
298+ <Button
299+ variant =" outline"
300+ class =" w-24"
301+ :disabled =" !props.canEdit || selectedToolIds.length === 0 || isBulkToggling"
302+ @click =" handleBulkDisable"
303+ >
304+ <Spinner v-if =" isBulkToggling" />
305+ <span v-else >{{ t('mcpInstallations.details.tools.bulkActions.disable') }}</span >
306+ </Button >
307+ </ButtonGroup >
308+ </div >
309+
212310 <!-- Tools Table -->
213311 <div class =" rounded-md border" >
214312 <Table >
215313 <TableHeader >
216314 <TableRow >
217- <TableHead class =" w-20" >{{ t('mcpInstallations.details.tools.table.columns.enabled') }}</TableHead >
315+ <TableHead class =" w-12" >
316+ <Checkbox
317+ :checked =" allToolsSelected"
318+ :indeterminate =" someToolsSelected"
319+ :disabled =" !props.canEdit"
320+ @update:checked =" toggleAllTools"
321+ />
322+ </TableHead >
323+ <TableHead >{{ t('mcpInstallations.details.tools.table.columns.status') }}</TableHead >
218324 <TableHead >{{ t('mcpInstallations.details.tools.table.columns.toolName') }}</TableHead >
219325 <TableHead >{{ t('mcpInstallations.details.tools.table.columns.description') }}</TableHead >
220326 <TableHead class =" text-right" >{{ t('mcpInstallations.details.tools.table.columns.tokenCount') }}</TableHead >
221327 </TableRow >
222328 </TableHeader >
223329 <TableBody >
224330 <TableRow v-for =" tool in tools.tools" :key =" tool.id" >
225- <TableCell class = " align-top " >
226- <Switch
227- :model-value = " ! tool.is_disabled "
228- :disabled =" !props.canEdit || togglingToolId === tool.id "
229- @update:model-value = " handleToolToggle(tool.id, tool.tool_name, tool.is_disabled )"
331+ <TableCell >
332+ <Checkbox
333+ :checked = " isToolSelected( tool.id) "
334+ :disabled =" !props.canEdit"
335+ @update:checked = " () => toggleToolSelection( tool.id )"
230336 />
231337 </TableCell >
232- <TableCell class =" text-sm font-medium align-top whitespace-nowrap" >{{ tool.tool_name }}</TableCell >
338+ <TableCell >
339+ <div
340+ class =" inline-flex items-center justify-center rounded-full border px-1.5 py-0.5 text-xs font-medium text-muted-foreground gap-1"
341+ >
342+ <CircleCheck
343+ v-if =" !tool.is_disabled"
344+ class =" size-3 fill-green-500 text-green-500 dark:fill-green-400 dark:text-green-400"
345+ />
346+ <CircleMinus
347+ v-else
348+ class =" size-3 text-muted-foreground"
349+ />
350+ <span >
351+ {{ tool.is_disabled
352+ ? t('mcpInstallations.details.tools.table.values.disabled')
353+ : t('mcpInstallations.details.tools.table.values.enabled')
354+ }}
355+ </span >
356+ </div >
357+ </TableCell >
358+ <TableCell class =" text-sm font-medium" >{{ tool.tool_name }}</TableCell >
233359 <TableCell class =" text-sm text-muted-foreground max-w-2xl" >
234360 <div class =" whitespace-normal wrap-break-word" >
235361 {{ tool.description || t('mcpInstallations.details.tools.table.values.noDescription') }}
236362 </div >
237363 </TableCell >
238- <TableCell class =" text-right align-top whitespace-nowrap text-sm font-medium" >
364+ <TableCell class =" text-right whitespace-nowrap text-sm font-medium" >
239365 {{ formatTokenCount(tool.token_count) }}
240366 </TableCell >
241367 </TableRow >
242368 </TableBody >
243369 </Table >
244370 </div >
371+
372+ <!-- Selection Counter (outside table, matching catalog layout) -->
373+ <div class =" flex items-center justify-between px-4 py-4" >
374+ <div class =" flex-1 text-sm text-muted-foreground" >
375+ {{ t('mcpInstallations.details.tools.selection.rowsSelected', {
376+ selected: selectedToolIds.length,
377+ total: tools.tools.length
378+ }) }}
379+ </div >
380+ </div >
245381 </div >
246382 </div >
247383</template >
0 commit comments