Skip to content

Commit 4dace87

Browse files
author
Lasim
committed
feat(frontend): implement bulk toggle for tool enable/disable
1 parent 2b65488 commit 4dace87

File tree

3 files changed

+238
-33
lines changed

3 files changed

+238
-33
lines changed

services/frontend/src/components/mcp-server/installation/ToolsTab.vue

Lines changed: 168 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import { toast } from 'vue-sonner'
55
import { useMcpToolsStore } from '@/stores/mcpToolsStore'
66
import { McpToolsService } from '@/services/mcpToolsService'
77
import { 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'
913
import { Alert, AlertDescription } from '@/components/ui/alert'
1014
import { 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'
1216
import { use } from 'echarts/core'
1317
import { PieChart } from 'echarts/charts'
1418
import { TooltipComponent, LegendComponent } from 'echarts/components'
@@ -37,7 +41,8 @@ const mcpToolsStore = useMcpToolsStore()
3741
const tools = ref<any>(null)
3842
const isLoading = ref(true)
3943
const 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
4348
const 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>

services/frontend/src/i18n/locales/en/mcp-installations.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,19 +631,34 @@ export default {
631631
enabled: 'Enabled',
632632
toolName: 'Tool Name',
633633
description: 'Description',
634+
status: 'Status',
634635
tokenCount: 'Token Count'
635636
},
636637
values: {
637-
noDescription: 'No description provided'
638+
noDescription: 'No description provided',
639+
enabled: 'Enabled',
640+
disabled: 'Disabled'
638641
}
639642
},
643+
bulkActions: {
644+
enable: 'Enable',
645+
disable: 'Disable'
646+
},
640647
toggle: {
641648
success: 'Tool "{toolName}" {action}',
642649
enabled: 'enabled',
643650
disabled: 'disabled',
644651
errorTitle: 'Failed to update tool',
645652
error: 'An error occurred while updating the tool status'
646653
},
654+
bulkToggle: {
655+
allSuccess: '{count} tool(s) {action} successfully',
656+
partialSuccess: '{succeeded} tool(s) {action} successfully, {failed} failed',
657+
errorTitle: 'Bulk Toggle Failed'
658+
},
659+
selection: {
660+
rowsSelected: '{selected} of {total} row(s) selected.'
661+
},
647662
summary: {
648663
totalTools: 'Total Tools',
649664
totalTokens: 'Total Tokens',

services/frontend/src/services/mcpToolsService.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,28 @@ export interface ToggleToolResponse {
2323
message: string
2424
}
2525

26+
export interface BatchToggleToolRequest {
27+
tool_id: string
28+
is_disabled: boolean
29+
}
30+
31+
export interface BatchToggleToolResult {
32+
tool_id: string
33+
tool_name?: string
34+
is_disabled?: boolean
35+
status: 'success' | 'failed' | 'skipped'
36+
message: string
37+
}
38+
39+
export interface BatchToggleToolResponse {
40+
total_requested: number
41+
total_succeeded: number
42+
total_failed: number
43+
total_skipped: number
44+
command_ids?: string[]
45+
results: BatchToggleToolResult[]
46+
}
47+
2648
export interface InstallationToolsResponse {
2749
installation_id: string
2850
installation_name: string
@@ -122,6 +144,38 @@ export class McpToolsService {
122144
return data.data || data
123145
}
124146

147+
/**
148+
* Batch toggle tool enabled/disabled status for multiple tools
149+
*/
150+
static async batchToggleTools(
151+
teamId: string,
152+
installationId: string,
153+
tools: BatchToggleToolRequest[]
154+
): Promise<BatchToggleToolResponse> {
155+
const response = await fetch(
156+
`${this.baseUrl}/api/teams/${teamId}/mcp/installations/${installationId}/tools`,
157+
{
158+
method: 'PATCH',
159+
credentials: 'include',
160+
headers: {
161+
'Content-Type': 'application/json',
162+
},
163+
body: JSON.stringify({ tools }),
164+
}
165+
)
166+
167+
if (!response.ok) {
168+
const errorData = await response.json().catch(() => ({}))
169+
if (response.status === 403) {
170+
throw new Error('You don\'t have permission to manage tools')
171+
}
172+
throw new Error(errorData.error || `Failed to batch toggle tools: ${response.status}`)
173+
}
174+
175+
const data = await response.json()
176+
return data.data || data
177+
}
178+
125179
/**
126180
* Get team-wide tools summary (future phase)
127181
* Note: Backend endpoint not yet implemented

0 commit comments

Comments
 (0)