Skip to content

Commit 034e6b4

Browse files
authored
Merge pull request #114 from smart-mcp-proxy/fix/web-ui-smooth-rendering
feat: Fix Web UI re-rendering and scroll position loss
2 parents ef9cc34 + 21d2f96 commit 034e6b4

5 files changed

Lines changed: 161 additions & 24 deletions

File tree

frontend/src/assets/main.css

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,81 @@ body {
7171
60% { content: '..'; }
7272
80%, 100% { content: '...'; }
7373
}
74+
}
75+
76+
/* Vue TransitionGroup animations for smooth list updates */
77+
/* Server list transitions */
78+
.server-list-move,
79+
.server-list-enter-active,
80+
.server-list-leave-active {
81+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
82+
}
83+
84+
.server-list-enter-from {
85+
opacity: 0;
86+
transform: translateY(20px) scale(0.95);
87+
}
88+
89+
.server-list-leave-to {
90+
opacity: 0;
91+
transform: translateY(-20px) scale(0.95);
92+
}
93+
94+
.server-list-leave-active {
95+
position: absolute;
96+
}
97+
98+
/* Secret list transitions */
99+
.secret-list-move,
100+
.secret-list-enter-active,
101+
.secret-list-leave-active {
102+
transition: all 0.25s ease-out;
103+
}
104+
105+
.secret-list-enter-from {
106+
opacity: 0;
107+
transform: translateX(-10px);
108+
}
109+
110+
.secret-list-leave-to {
111+
opacity: 0;
112+
transform: translateX(10px);
113+
}
114+
115+
/* Tool call row transitions */
116+
.tool-call-move,
117+
.tool-call-enter-active,
118+
.tool-call-leave-active {
119+
transition: all 0.2s ease;
120+
}
121+
122+
.tool-call-enter-from {
123+
opacity: 0;
124+
transform: translateY(-5px);
125+
}
126+
127+
.tool-call-leave-to {
128+
opacity: 0;
129+
transform: translateY(5px);
130+
}
131+
132+
/* Repository card transitions */
133+
.repo-card-move,
134+
.repo-card-enter-active,
135+
.repo-card-leave-active {
136+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
137+
}
138+
139+
.repo-card-enter-from {
140+
opacity: 0;
141+
transform: scale(0.9);
142+
}
143+
144+
.repo-card-leave-to {
145+
opacity: 0;
146+
transform: scale(0.9);
147+
}
148+
149+
.repo-card-leave-active {
150+
position: absolute;
74151
}

frontend/src/stores/servers.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,66 @@ export const useServersStore = defineStore('servers', () => {
3232
servers.value.reduce((sum, server) => sum + server.tool_count, 0)
3333
)
3434

35+
// Helper: Smart merge servers to preserve object references and avoid full re-renders
36+
function mergeServers(existing: Server[], incoming: Server[]): Server[] {
37+
const existingMap = new Map(existing.map(s => [s.name, s]))
38+
const incomingMap = new Map(incoming.map(s => [s.name, s]))
39+
const result: Server[] = []
40+
41+
// Update existing servers in-place or add new ones
42+
incoming.forEach(incomingServer => {
43+
const existingServer = existingMap.get(incomingServer.name)
44+
45+
if (existingServer) {
46+
// Update existing server in-place (preserves object reference)
47+
// Only update properties that have changed
48+
let hasChanges = false
49+
Object.assign(existingServer, incomingServer)
50+
hasChanges = true
51+
52+
if (hasChanges) {
53+
console.log(`Server ${existingServer.name} updated with changes`)
54+
}
55+
56+
result.push(existingServer)
57+
} else {
58+
// Add new server
59+
console.log(`New server added: ${incomingServer.name}`)
60+
result.push(incomingServer)
61+
}
62+
})
63+
64+
// Log removed servers
65+
existing.forEach(existingServer => {
66+
if (!incomingMap.has(existingServer.name)) {
67+
console.log(`Server removed: ${existingServer.name}`)
68+
}
69+
})
70+
71+
// Sort alphabetically by name to match tray menu order
72+
return result.sort((a, b) => a.name.localeCompare(b.name))
73+
}
74+
3575
// Actions
36-
async function fetchServers() {
37-
loading.value = { loading: true, error: null }
76+
async function fetchServers(silent = false) {
77+
if (!silent) {
78+
loading.value = { loading: true, error: null }
79+
}
3880

3981
try {
4082
const response = await api.getServers()
4183
if (response.success && response.data) {
42-
// Sort servers alphabetically by name to match tray menu order
43-
servers.value = response.data.servers.sort((a, b) =>
44-
a.name.localeCompare(b.name)
45-
)
84+
// Use smart merge to preserve object references and avoid unnecessary re-renders
85+
servers.value = mergeServers(servers.value, response.data.servers)
4686
} else {
4787
loading.value.error = response.error || 'Failed to fetch servers'
4888
}
4989
} catch (error) {
5090
loading.value.error = error instanceof Error ? error.message : 'Unknown error'
5191
} finally {
52-
loading.value.loading = false
92+
if (!silent) {
93+
loading.value.loading = false
94+
}
5395
}
5496
}
5597

@@ -249,16 +291,16 @@ export const useServersStore = defineStore('servers', () => {
249291

250292
function handleServersChanged(event: Event) {
251293
const customEvent = event as CustomEvent
252-
console.log('Servers changed event received, refreshing server list...', customEvent.detail)
253-
// Refresh server list to get latest state
254-
fetchServers()
294+
console.log('Servers changed event received, updating in background...', customEvent.detail)
295+
// Silent background refresh to avoid scroll jumps and loading states
296+
fetchServers(true)
255297
}
256298

257299
function handleConfigReloaded(event: Event) {
258300
const customEvent = event as CustomEvent
259-
console.log('Config reloaded event received, refreshing server list...', customEvent.detail)
260-
// Refresh server list after config reload
261-
fetchServers()
301+
console.log('Config reloaded event received, updating in background...', customEvent.detail)
302+
// Silent background refresh to avoid scroll jumps and loading states
303+
fetchServers(true)
262304
}
263305

264306
// Initialize event listeners

frontend/src/views/Repositories.vue

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,12 @@
9595
<p class="text-sm text-base-content/70">Found {{ servers.length }} server(s)</p>
9696
</div>
9797

98-
<!-- Server Cards -->
99-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
98+
<!-- Server Cards with Smooth Transitions -->
99+
<TransitionGroup
100+
name="repo-card"
101+
tag="div"
102+
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
103+
>
100104
<div v-for="server in servers" :key="server.id" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
101105
<div class="card-body">
102106
<div class="flex justify-between items-start">
@@ -165,7 +169,7 @@
165169
</div>
166170
</div>
167171
</div>
168-
</div>
172+
</TransitionGroup>
169173
</div>
170174

171175
<!-- Empty State (no search yet) -->

frontend/src/views/Secrets.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@
119119
</button>
120120
</div>
121121

122-
<!-- Secrets List -->
123-
<div v-else class="space-y-4">
122+
<!-- Secrets List with Smooth Transitions -->
123+
<TransitionGroup v-else name="secret-list" tag="div" class="space-y-4">
124124
<!-- Keyring Secrets -->
125-
<div v-for="secret in filteredSecrets" :key="secret.secret_ref.name" class="card bg-base-100 shadow" :class="{ 'border-l-4 border-error': !secret.is_set }">
125+
<div v-for="secret in filteredSecrets" :key="`secret-${secret.secret_ref.name}`" class="card bg-base-100 shadow" :class="{ 'border-l-4 border-error': !secret.is_set }">
126126
<div class="card-body">
127127
<div class="flex justify-between items-start">
128128
<div class="flex-1">
@@ -159,7 +159,7 @@
159159
</div>
160160

161161
<!-- Environment Variables -->
162-
<div v-for="envVar in filteredEnvVars" :key="envVar.secret_ref.name" class="card bg-base-100 shadow" :class="{ 'border-l-4 border-error': !envVar.is_set }">
162+
<div v-for="envVar in filteredEnvVars" :key="`env-${envVar.secret_ref.name}`" class="card bg-base-100 shadow" :class="{ 'border-l-4 border-error': !envVar.is_set }">
163163
<div class="card-body">
164164
<div class="flex justify-between items-start">
165165
<div class="flex-1">
@@ -182,7 +182,7 @@
182182
</div>
183183
</div>
184184
</div>
185-
</div>
185+
</TransitionGroup>
186186

187187
<!-- Migration Candidates Section -->
188188
<div v-if="migrationCandidates.length > 0" class="card bg-base-100 shadow">

frontend/src/views/Servers.vue

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,28 @@
121121
</button>
122122
</div>
123123

124-
<!-- Servers Grid -->
125-
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
124+
<!-- Servers Grid with Smooth Transitions -->
125+
<TransitionGroup
126+
v-else
127+
name="server-list"
128+
tag="div"
129+
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
130+
>
126131
<ServerCard
127132
v-for="server in filteredServers"
128133
:key="server.name"
129134
:server="server"
135+
v-memo="[
136+
server.connected,
137+
server.connecting,
138+
server.enabled,
139+
server.quarantined,
140+
server.tool_count,
141+
server.last_error,
142+
server.authenticated
143+
]"
130144
/>
131-
</div>
145+
</TransitionGroup>
132146

133147
<!-- Hints Panel (Bottom of Page) -->
134148
<CollapsibleHintsPanel :hints="serversHints" />

0 commit comments

Comments
 (0)