Skip to content

Commit 7356c2b

Browse files
author
Lasim
committed
feat(frontend): add icon_url field to MCP server forms and views
1 parent 3f418cf commit 7356c2b

File tree

12 files changed

+212
-64
lines changed

12 files changed

+212
-64
lines changed

services/frontend/src/components/admin/mcp-catalog/McpServerEditFormWizard.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ const formData = ref<McpServerFormData>({
309309
tags: [],
310310
featured: false,
311311
auto_install_new_default_team: false,
312-
website_url: ''
312+
website_url: '',
313+
icon_url: ''
313314
},
314315
repository: {
315316
repository_url: '',

services/frontend/src/components/admin/mcp-catalog/ReviewStep.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ const formatJson = (jsonString: string) => {
207207
</dd>
208208
</div>
209209

210+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
211+
<dt class="text-sm/6 font-medium text-gray-900">Icon URL</dt>
212+
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0">
213+
{{ getBasicData().icon_url || t('mcpCatalog.form.review.values.notSpecified') }}
214+
</dd>
215+
</div>
216+
210217
<div v-if="getBasicData().tags && getBasicData().tags.length > 0" class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
211218
<dt class="text-sm/6 font-medium text-gray-900">{{ t('mcpCatalog.form.review.fields.tags') }}</dt>
212219
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0">

services/frontend/src/components/admin/mcp-catalog/steps/BasicInfoStepEdit.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ const defaultData: BasicInfoFormData = {
6868
tags: [],
6969
featured: false,
7070
auto_install_new_default_team: false,
71-
website_url: ''
71+
website_url: '',
72+
icon_url: ''
7273
}
7374
7475
// Storage-first reactive data - using ref instead of computed for better reactivity
@@ -343,6 +344,20 @@ onUnmounted(() => {
343344
/>
344345
</SharedFormField>
345346

347+
<!-- Icon URL -->
348+
<SharedFormField
349+
label="Icon URL"
350+
description="URL to the server icon image (auto-generated from GitHub avatar if not provided)"
351+
>
352+
<Input
353+
id="icon_url"
354+
:model-value="localData.icon_url"
355+
@update:model-value="(value) => updateField('icon_url', String(value))"
356+
placeholder="https://example.com/icon.png"
357+
type="url"
358+
/>
359+
</SharedFormField>
360+
346361
<!-- Tags -->
347362
<SharedFormField
348363
:label="t('mcpCatalog.form.basic.tags.label')"

services/frontend/src/components/mcp-server/McpInstallationsList.vue

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ import {
1717
Trash2,
1818
AlertTriangle,
1919
ChevronRight,
20-
Github,
2120
} from 'lucide-vue-next'
2221
import type { McpInstallation } from '@/types/mcp-installations'
2322
import { McpInstallationService } from '@/services/mcpInstallationService'
2423
import { TeamService } from '@/services/teamService'
2524
import CategoryDisplay from '@/components/mcp-server/CategoryDisplay.vue'
25+
import McpServerAvatar from '@/components/mcp-server/McpServerAvatar.vue'
2626
import { useEventBus } from '@/composables/useEventBus'
2727
2828
interface Props {
@@ -227,17 +227,28 @@ onUnmounted(() => {
227227
:id="index === sortedInstallations.length - 1 ? 'last-server-item' : undefined"
228228
class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6"
229229
>
230-
<div class="flex min-w-0 gap-x-4">
231-
<div class="min-w-0 flex-auto">
232-
<p class="text-sm/6 font-semibold text-gray-900 mb-2">
233-
<a @click="handleViewInstallation(installation.id)" class="cursor-pointer hover:text-blue-600 transition-colors">
234-
{{ installation.installation_name }}
235-
</a>
236-
</p>
237-
238-
<dl class="mt-1 grid grid-cols-1 gap-x-4 gap-y-1 text-xs/5 text-gray-500 sm:grid-cols-5">
230+
<div class="flex min-w-0 gap-x-4 flex-col flex-1">
231+
<!-- MCP Server Name -->
232+
<p class="text-sm/6 font-semibold text-gray-900 mb-2">
233+
<a @click="handleViewInstallation(installation.id)" class="cursor-pointer hover:text-blue-600 transition-colors">
234+
{{ installation.installation_name }}
235+
</a>
236+
</p>
237+
238+
<!-- Icon + Details Grid -->
239+
<div class="flex gap-x-4 items-center w-full">
240+
<!-- MCP Server Icon -->
241+
<McpServerAvatar
242+
:icon-url="installation.server.icon_url"
243+
:server-name="installation.installation_name"
244+
size="sm"
245+
rounded="md"
246+
class="shrink-0"
247+
/>
248+
249+
<dl class="flex-1 grid grid-cols-1 gap-x-8 gap-y-1 text-xs/5 text-gray-500 sm:grid-cols-4">
239250
<div>
240-
<dt class="font-medium text-gray-700">{{ t('mcpInstallations.table.columns.installationMethod') }}</dt>
251+
<dt class="font-medium text-gray-700">Satellite</dt>
241252
<dd>{{ installation.installation_type }}</dd>
242253
</div>
243254
<div>
@@ -259,23 +270,6 @@ onUnmounted(() => {
259270
<dt class="font-medium text-gray-700">{{ t('mcpInstallations.table.columns.installed') }}</dt>
260271
<dd>{{ formatDate(installation.created_at) }}</dd>
261272
</div>
262-
<div>
263-
<dt class="font-medium text-gray-700">{{ t('mcpInstallations.table.columns.repository') }}</dt>
264-
<dd v-if="installation.server.repository_url">
265-
<a
266-
:href="installation.server.repository_url"
267-
target="_blank"
268-
rel="noopener noreferrer"
269-
class="inline-flex items-center gap-1 text-gray-600 hover:text-gray-900 transition-colors"
270-
>
271-
<Github class="h-3 w-3" />
272-
{{ t('mcpInstallations.table.values.github') }}
273-
</a>
274-
</dd>
275-
<dd v-else class="text-gray-400">
276-
{{ t('mcpInstallations.table.values.noRepository') }}
277-
</dd>
278-
</div>
279273
</dl>
280274
</div>
281275
</div>
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
3+
4+
interface Props {
5+
iconUrl?: string | null
6+
serverName?: string
7+
size?: 'sm' | 'md' | 'lg' | 'xl' | number
8+
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full'
9+
}
10+
11+
const props = withDefaults(defineProps<Props>(), {
12+
size: 'md',
13+
rounded: 'md'
14+
})
15+
16+
const imageError = ref(false)
17+
18+
const sizeClasses = computed(() => {
19+
if (typeof props.size === 'number') {
20+
return ''
21+
}
22+
23+
const sizeMap = {
24+
sm: 'h-8 w-8',
25+
md: 'h-12 w-12',
26+
lg: 'h-16 w-16',
27+
xl: 'h-20 w-20'
28+
}
29+
30+
return sizeMap[props.size]
31+
})
32+
33+
const customSize = computed(() => {
34+
if (typeof props.size === 'number') {
35+
return {
36+
width: `${props.size}px`,
37+
height: `${props.size}px`
38+
}
39+
}
40+
return undefined
41+
})
42+
43+
const roundedClasses = computed(() => {
44+
const roundedMap = {
45+
none: 'rounded-none',
46+
sm: 'rounded-sm',
47+
md: 'rounded-md',
48+
lg: 'rounded-lg',
49+
full: 'rounded-full'
50+
}
51+
52+
return roundedMap[props.rounded]
53+
})
54+
55+
const fontSizeClasses = computed(() => {
56+
if (typeof props.size === 'number') {
57+
// For custom pixel sizes, font size is handled by customFontSize style
58+
return ''
59+
}
60+
61+
const fontSizeMap = {
62+
sm: 'text-sm',
63+
md: 'text-xl',
64+
lg: 'text-3xl',
65+
xl: 'text-4xl'
66+
}
67+
68+
return fontSizeMap[props.size]
69+
})
70+
71+
const customFontSize = computed(() => {
72+
if (typeof props.size === 'number') {
73+
const fontSize = Math.floor(props.size * 0.5)
74+
return {
75+
fontSize: `${fontSize}px`
76+
}
77+
}
78+
return undefined
79+
})
80+
81+
const showImage = computed(() => {
82+
return props.iconUrl && !imageError.value
83+
})
84+
85+
const firstLetter = computed(() => {
86+
if (!props.serverName) return '?'
87+
return props.serverName.charAt(0).toUpperCase()
88+
})
89+
90+
const handleImageError = () => {
91+
imageError.value = true
92+
}
93+
</script>
94+
95+
<template>
96+
<!-- Show image if iconUrl exists and hasn't errored -->
97+
<img
98+
v-if="showImage"
99+
:src="iconUrl!"
100+
:alt="serverName ? `${serverName} avatar` : 'MCP server avatar'"
101+
:class="[sizeClasses, roundedClasses, 'shrink-0']"
102+
:style="customSize"
103+
@error="handleImageError"
104+
/>
105+
106+
<!-- Show letter badge fallback if no iconUrl or image error -->
107+
<div
108+
v-else
109+
:class="[
110+
sizeClasses,
111+
roundedClasses,
112+
fontSizeClasses,
113+
'shrink-0',
114+
'bg-teal-700',
115+
'text-white',
116+
'font-bold',
117+
'flex',
118+
'items-center',
119+
'justify-center',
120+
'select-none'
121+
]"
122+
:style="{ ...customSize, ...customFontSize }"
123+
:aria-label="serverName ? `${serverName} avatar` : 'MCP server avatar'"
124+
>
125+
{{ firstLetter }}
126+
</div>
127+
</template>

services/frontend/src/components/mcp-server/McpServerSquareCard.vue

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@/components/ui/hover-card'
1111
import { Github, Star } from 'lucide-vue-next'
1212
import type { McpServer } from '@/views/admin/mcp-server-catalog/types'
13+
import McpServerAvatar from './McpServerAvatar.vue'
1314
1415
interface Props {
1516
server: McpServer
@@ -51,11 +52,6 @@ const getServerDescription = (server: McpServer) => {
5152
return server.description || 'No description available'
5253
}
5354
54-
const getGitHubAvatarUrl = (server: McpServer) => {
55-
if (!server.github_account_id) return null
56-
return `https://avatars.githubusercontent.com/u/${server.github_account_id}?v=4&s=64`
57-
}
58-
5955
const getRuntimeBadgeClass = (runtime: string | null | undefined) => {
6056
if (!runtime) return 'bg-gray-100 text-gray-800'
6157
@@ -94,12 +90,11 @@ const truncateServerName = (name: string, maxLength: number = 30) => {
9490
@click="handleServerClick"
9591
:title="`View ${server.name} details`"
9692
>
97-
<img
98-
v-if="getGitHubAvatarUrl(server)"
99-
:src="getGitHubAvatarUrl(server)!"
100-
:alt="`${server.name} GitHub avatar`"
101-
class="h-8 w-8 rounded-md flex-shrink-0"
102-
@error="($event.target as HTMLImageElement).style.display = 'none'"
93+
<McpServerAvatar
94+
:icon-url="server.icon_url"
95+
:server-name="server.name"
96+
size="sm"
97+
rounded="md"
10398
/>
10499
<span class="truncate">{{ truncateServerName(server.name) }}</span>
105100
</dt>

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computed } from 'vue'
33
import { useI18n } from 'vue-i18n'
44
import { Badge } from '@/components/ui/badge'
55
import { Github, ExternalLink, Calendar, Tag } from 'lucide-vue-next'
6+
import McpServerAvatar from '@/components/mcp-server/McpServerAvatar.vue'
67
import type { McpInstallation } from '@/types/mcp-installations'
78
89
interface Props {
@@ -58,9 +59,17 @@ const formatDate = (dateString: string) => {
5859

5960
<template>
6061
<div v-if="installation && server">
61-
<div class="px-4 sm:px-0">
62-
<h3 class="text-base/7 font-semibold text-gray-900">{{ t('mcpInstallations.details.installationDetails.title') }}</h3>
63-
<p class="mt-1 max-w-2xl text-sm/6 text-gray-500">{{ t('mcpInstallations.details.installationDetails.description') }}</p>
62+
<div class="px-4 sm:px-0 flex items-center gap-4">
63+
<McpServerAvatar
64+
:icon-url="server.icon_url"
65+
:server-name="server.name"
66+
size="md"
67+
rounded="lg"
68+
/>
69+
<div>
70+
<h3 class="text-base/7 font-semibold text-gray-900">{{ t('mcpInstallations.details.installationDetails.title') }}</h3>
71+
<p class="mt-1 max-w-2xl text-sm/6 text-gray-500">{{ t('mcpInstallations.details.installationDetails.description') }}</p>
72+
</div>
6473
</div>
6574
<div class="mt-6 border-t border-gray-100">
6675
<dl class="divide-y divide-gray-100">

services/frontend/src/components/mcp-server/wizard/McpServerInstallWizard.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import McpServerSelectionStep from './McpServerSelectionStep.vue'
1515
import EnvironmentVariablesStep from './EnvironmentVariablesStep.vue'
1616
import OAuthAuthorizationStep from './OAuthAuthorizationStep.vue'
1717
import SatelliteSelectionStep from './SatelliteSelectionStep.vue'
18+
import McpServerAvatar from '../McpServerAvatar.vue'
1819
1920
// Props
2021
interface Props {
@@ -655,11 +656,11 @@ onUnmounted(() => {
655656
<div class="px-6 md:flex md:items-center md:justify-between md:space-x-6 lg:space-x-8">
656657
<!-- Avatar Image -->
657658
<div class="flex-shrink-0 mb-4 md:mb-0">
658-
<img
659-
v-if="formData.server.server_data.github_account_id"
660-
:src="`https://avatars.githubusercontent.com/u/${formData.server.server_data.github_account_id}?v=4&s=64`"
661-
:alt="`${formData.server.server_data.name} GitHub avatar`"
662-
class="h-18 w-18 rounded-lg"
659+
<McpServerAvatar
660+
:icon-url="formData.server.server_data.icon_url"
661+
:server-name="formData.server.server_data.name"
662+
:size="72"
663+
rounded="lg"
663664
/>
664665
</div>
665666

services/frontend/src/types/mcp-installations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface McpServer {
88
status: 'active' | 'deprecated' | 'maintenance'
99
author_name?: string | null
1010
website_url?: string | null
11+
icon_url?: string | null
1112
repository_url?: string | null
1213
repository_source?: string | null
1314
repository_id?: string | null

services/frontend/src/views/admin/mcp-server-catalog/edit/[id].vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ const convertServerToFormData = (server: McpServer, readmeBase64: string = ''):
168168
tags: parsedTags,
169169
featured: Boolean(server.featured),
170170
auto_install_new_default_team: Boolean(server.auto_install_new_default_team),
171-
website_url: server.website_url || ''
171+
website_url: server.website_url || '',
172+
icon_url: server.icon_url || ''
172173
},
173174
repository: {
174175
repository_url: server.repository_url || '',
@@ -289,6 +290,7 @@ const handleSubmit = async (formData: McpServerFormData) => {
289290
tags: formData.basic.tags.length > 0 ? formData.basic.tags : undefined,
290291
featured: formData.basic.featured,
291292
auto_install_new_default_team: formData.basic.auto_install_new_default_team,
293+
icon_url: formData.basic.icon_url || undefined,
292294
293295
// Repository (use finalRepositoryData which merges storage + formData)
294296
repository_url: finalRepositoryData.repository_url || undefined,

0 commit comments

Comments
 (0)