Skip to content

Commit b1115bf

Browse files
committed
feat(frontend): implement optimistic deletion for MCP servers
1 parent aa34334 commit b1115bf

File tree

4 files changed

+72
-21
lines changed

4 files changed

+72
-21
lines changed

services/frontend/src/components/admin/mcp-server-catalog/McpServerCatalogDetailHeader.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ButtonGroup } from '@/components/ui/button-group'
77
import { McpServerCatalogDetailPageHeading } from '@/components/admin/mcp-server-catalog'
88
import { McpCatalogService } from '@/services/mcpCatalogService'
99
import McpServerDeleteDialog from '@/components/mcp-server/McpServerDeleteDialog.vue'
10+
import { useEventBus } from '@/composables/useEventBus'
1011
import type { McpServer } from '@/views/admin/mcp-server-catalog/types'
1112
1213
interface Props {
@@ -17,6 +18,7 @@ interface Props {
1718
1819
const props = defineProps<Props>()
1920
const router = useRouter()
21+
const eventBus = useEventBus()
2022
2123
const isDeleting = ref(false)
2224
const showDeleteDialog = ref(false)
@@ -28,9 +30,14 @@ const deleteServer = async () => {
2830
const serverName = props.server?.name || 'Unknown Server'
2931
await McpCatalogService.deleteGlobalServer(props.serverId)
3032
33+
// Store deleted server ID in localStorage for optimistic filtering
34+
const deletedIds = eventBus.getState<string[]>('deleted_server_ids', []) || []
35+
deletedIds.push(props.serverId)
36+
eventBus.setState('deleted_server_ids', deletedIds)
37+
3138
router.push({
3239
path: '/admin/mcp-server-catalog',
33-
query: { deletionQueued: serverName }
40+
query: { deletionQueued: serverName, deletedId: props.serverId }
3441
})
3542
} catch (err) {
3643
const errorMessage = err instanceof Error ? err.message : 'Failed to delete server'

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,8 @@ export default {
870870
source: {
871871
all: 'All',
872872
official_registry: 'Official Registry',
873-
manual: 'Manual'
873+
manual: 'Manual',
874+
github: 'GitHub'
874875
},
875876
status: {
876877
label: 'Status',

services/frontend/src/services/mcpCatalogService.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,9 @@ export class McpCatalogService {
358358

359359
/**
360360
* Delete a global MCP server (admin only)
361+
* Returns the deletion job information for optimistic UI updates
361362
*/
362-
static async deleteGlobalServer(serverId: string): Promise<void> {
363+
static async deleteGlobalServer(serverId: string): Promise<{ id: string; name: string; job_id: string; status: 'queued' }> {
363364
const response = await fetch(`${this.baseUrl}/api/mcp/servers/global/${serverId}`, {
364365
method: 'DELETE',
365366
credentials: 'include',
@@ -369,6 +370,9 @@ export class McpCatalogService {
369370
const errorData = await response.json().catch(() => ({}))
370371
throw new Error(errorData.message || `Failed to delete MCP server: ${response.status}`)
371372
}
373+
374+
const data = await response.json()
375+
return data.data
372376
}
373377

374378
/**

services/frontend/src/views/admin/mcp-server-catalog/index.vue

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, onMounted, onUnmounted, watch } from 'vue'
2+
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
33
import { useI18n } from 'vue-i18n'
44
import { useRouter, useRoute } from 'vue-router'
55
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
@@ -41,6 +41,7 @@ const servers = ref<McpServer[]>([])
4141
const isLoading = ref(true)
4242
const isSearching = ref(false)
4343
const error = ref<string | null>(null)
44+
const deletedServerIds = ref<Set<string>>(new Set())
4445
4546
// Search and filter state
4647
const searchQuery = ref('')
@@ -97,6 +98,11 @@ const pagination = ref<PaginationMeta>({
9798
// Selection state
9899
const selectedServerIds = ref<string[]>([])
99100
101+
// Filter out servers that are queued for deletion
102+
const visibleServers = computed(() => {
103+
return servers.value.filter(server => !deletedServerIds.value.has(server.id))
104+
})
105+
100106
// Visible filters state (lifted from child to preserve across loading)
101107
const visibleFilters = ref<Set<string>>(new Set())
102108
@@ -163,15 +169,21 @@ const handleDeleteServer = async (serverId: string): Promise<void> => {
163169
164170
await McpCatalogService.deleteGlobalServer(serverId)
165171
166-
toast.success(t('mcpCatalog.messages.deletionQueued', { name: serverName }))
172+
// Optimistically add to deleted IDs
173+
deletedServerIds.value.add(serverId)
167174
168-
// Refresh the table
169-
if (hasTextSearch()) {
170-
await searchServers()
171-
} else {
172-
await fetchServers()
173-
}
175+
// Store in EventBus for persistence
176+
const deletedIds = Array.from(deletedServerIds.value)
177+
eventBus.setState('deleted_server_ids', deletedIds)
178+
179+
// Clear selection if deleted server was selected
180+
selectedServerIds.value = selectedServerIds.value.filter(id => id !== serverId)
181+
182+
toast.success(t('mcpCatalog.messages.deletionQueued', { name: serverName }))
174183
} catch (err) {
184+
// Remove from deleted IDs on error
185+
deletedServerIds.value.delete(serverId)
186+
175187
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
176188
toast.error(t('mcpCatalog.messages.deleteError', { error: errorMessage }))
177189
throw err // Re-throw so the dialog knows deletion failed
@@ -182,6 +194,18 @@ const handleBulkDeleteServers = async (serverIds: string[]): Promise<void> => {
182194
try {
183195
const result = await McpCatalogService.bulkDeleteGlobalServers(serverIds)
184196
197+
// Add queued servers to deleted IDs
198+
const queuedServerIds = new Set(result.jobs.map(job => job.server_id))
199+
queuedServerIds.forEach(id => deletedServerIds.value.add(id))
200+
201+
// Update storage
202+
const deletedIds = Array.from(deletedServerIds.value)
203+
eventBus.setState('deleted_server_ids', deletedIds)
204+
205+
// Update pagination count
206+
totalItems.value = Math.max(0, totalItems.value - result.total_queued)
207+
selectedServerIds.value = []
208+
185209
if (result.total_skipped > 0) {
186210
toast.success(t('mcpCatalog.bulkDelete.partialSuccess', {
187211
queued: result.total_queued,
@@ -192,15 +216,6 @@ const handleBulkDeleteServers = async (serverIds: string[]): Promise<void> => {
192216
queued: result.total_queued
193217
}))
194218
}
195-
196-
// Optimistically remove deleted servers from the UI
197-
// (they're queued for deletion in background jobs, not immediately deleted from DB)
198-
const queuedServerIds = new Set(result.jobs.map(job => job.server_id))
199-
servers.value = servers.value.filter(server => !queuedServerIds.has(server.id))
200-
totalItems.value = Math.max(0, totalItems.value - result.total_queued)
201-
202-
// Clear selection
203-
selectedServerIds.value = []
204219
} catch (err) {
205220
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
206221
toast.error(t('mcpCatalog.bulkDelete.error', { error: errorMessage }))
@@ -541,6 +556,16 @@ const fetchLanguages = async () => {
541556
onMounted(async () => {
542557
setBreadcrumbs([{ label: t('mcpCatalog.title') }])
543558
559+
// Load deleted IDs from storage on mount
560+
const storedDeletedIds = eventBus.getState<string[]>('deleted_server_ids', []) || []
561+
deletedServerIds.value = new Set(storedDeletedIds)
562+
563+
// Check for deletion query params
564+
const deletedId = route.query.deletedId as string
565+
if (deletedId) {
566+
deletedServerIds.value.add(deletedId)
567+
}
568+
544569
await Promise.all([
545570
fetchServers(),
546571
fetchRuntimes(),
@@ -572,12 +597,26 @@ onMounted(async () => {
572597
573598
// Listen for server creation from add page
574599
eventBus.on('mcp-server-created', handleServerCreated)
600+
601+
// Listen for MCP_SERVER_DELETED event to clean up
602+
eventBus.on('mcp-server-deleted', (data: { serverId?: string; server?: { id: string } }) => {
603+
// Remove from deleted IDs when actually deleted
604+
const serverId = data.serverId || data.server?.id
605+
if (serverId) {
606+
deletedServerIds.value.delete(serverId)
607+
608+
// Update storage
609+
const deletedIds = Array.from(deletedServerIds.value)
610+
eventBus.setState('deleted_server_ids', deletedIds)
611+
}
612+
})
575613
})
576614
577615
onUnmounted(() => {
578616
// Clean up event listeners
579617
eventBus.off('mcp-catalog-updated')
580618
eventBus.off('mcp-server-created', handleServerCreated)
619+
eventBus.off('mcp-server-deleted')
581620
})
582621
</script>
583622

@@ -614,7 +653,7 @@ onUnmounted(() => {
614653
<!-- Servers Table Component -->
615654
<McpServerTableColumns
616655
:is-loading="isLoading"
617-
:servers="servers"
656+
:servers="visibleServers"
618657
:selected-source="selectedSource"
619658
:search-query="searchQuery"
620659
:is-searching="isSearching"

0 commit comments

Comments
 (0)