Skip to content

Commit bf759ae

Browse files
author
Lasim
committed
feat(frontend): implement featured MCP servers list and browsing option
1 parent 4c6a684 commit bf759ae

File tree

3 files changed

+185
-32
lines changed

3 files changed

+185
-32
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
import { useRouter } from 'vue-router'
5+
import { Button } from '@/components/ui/button'
6+
import { Skeleton } from '@/components/ui/skeleton'
7+
import { Layers } from 'lucide-vue-next'
8+
import { McpCatalogService } from '@/services/mcpCatalogService'
9+
import type { McpServer } from '@/views/admin/mcp-server-catalog/types'
10+
import McpServerAvatar from './McpServerAvatar.vue'
11+
12+
interface Props {
13+
limit?: number
14+
}
15+
16+
const props = withDefaults(defineProps<Props>(), {
17+
limit: 8
18+
})
19+
20+
const { t } = useI18n()
21+
const router = useRouter()
22+
23+
const featuredServers = ref<McpServer[]>([])
24+
const isLoading = ref(true)
25+
const error = ref<string | null>(null)
26+
27+
const fetchFeaturedServers = async () => {
28+
try {
29+
isLoading.value = true
30+
error.value = null
31+
32+
const response = await McpCatalogService.getGlobalServersPaginated(
33+
{ featured: true },
34+
{ limit: props.limit, offset: 0 }
35+
)
36+
37+
featuredServers.value = response.items
38+
} catch (err) {
39+
console.error('Error fetching featured servers:', err)
40+
error.value = err instanceof Error ? err.message : 'Failed to load featured servers'
41+
featuredServers.value = []
42+
} finally {
43+
isLoading.value = false
44+
}
45+
}
46+
47+
const handleServerClick = (server: McpServer) => {
48+
router.push(`/mcp-server/install/${server.id}`)
49+
}
50+
51+
const handleBrowseCatalog = () => {
52+
router.push('/mcp-server/search')
53+
}
54+
55+
const truncateDescription = (description: string | null | undefined, maxLength: number = 80) => {
56+
if (!description) return 'No description available'
57+
if (description.length <= maxLength) return description
58+
return description.substring(0, maxLength) + '...'
59+
}
60+
61+
onMounted(() => {
62+
fetchFeaturedServers()
63+
})
64+
</script>
65+
66+
<template>
67+
<div class="flex flex-col items-center justify-start gap-6 p-6 border border-solid rounded-lg">
68+
<Layers class="h-5 w-5 text-muted-foreground" />
69+
70+
<p class="text-base font-medium text-center">
71+
{{ t('mcpInstallations.featuredList.title') }}
72+
</p>
73+
74+
<div v-if="isLoading" class="flex flex-col items-stretch justify-start gap-4 w-full">
75+
<div v-for="i in 5" :key="i" class="flex flex-row items-center gap-3">
76+
<Skeleton class="h-9 w-9 rounded-md" />
77+
<div class="flex flex-col gap-1 flex-1">
78+
<Skeleton class="h-4 w-32" />
79+
<Skeleton class="h-3 w-full" />
80+
</div>
81+
</div>
82+
</div>
83+
84+
<div v-else-if="error" class="text-center py-4">
85+
<p class="text-sm text-destructive">{{ error }}</p>
86+
<Button
87+
variant="outline"
88+
size="sm"
89+
@click="fetchFeaturedServers"
90+
class="mt-2"
91+
>
92+
{{ t('actions.retry') }}
93+
</Button>
94+
</div>
95+
96+
<div v-else-if="featuredServers.length === 0" class="text-center py-4">
97+
<p class="text-sm text-muted-foreground">
98+
{{ t('mcpInstallations.featured.noServers') }}
99+
</p>
100+
</div>
101+
102+
<div v-else class="flex flex-col items-stretch justify-start gap-4 w-full">
103+
<button
104+
v-for="server in featuredServers"
105+
:key="server.id"
106+
type="button"
107+
@click="handleServerClick(server)"
108+
class="flex flex-row items-start gap-3 text-left hover:bg-muted/50 rounded-md p-2 -mx-2 transition-colors"
109+
>
110+
<McpServerAvatar
111+
:icon-url="server.icon_url"
112+
:server-name="server.name"
113+
size="sm"
114+
rounded="md"
115+
class="shrink-0"
116+
/>
117+
<div class="flex flex-col items-stretch justify-start min-w-0">
118+
<p class="text-sm font-medium truncate">{{ server.name }}</p>
119+
<p class="text-xs text-muted-foreground line-clamp-2">
120+
{{ truncateDescription(server.description) }}
121+
</p>
122+
</div>
123+
</button>
124+
</div>
125+
126+
<div class="w-full border-t border-border" />
127+
128+
<Button
129+
variant="outline"
130+
@click="handleBrowseCatalog"
131+
class="w-full"
132+
>
133+
{{ t('mcpInstallations.featuredList.browseCatalog') }}
134+
</Button>
135+
</div>
136+
</template>
137+
138+
<style scoped>
139+
.line-clamp-2 {
140+
display: -webkit-box;
141+
-webkit-line-clamp: 2;
142+
-webkit-box-orient: vertical;
143+
overflow: hidden;
144+
}
145+
</style>
Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,51 @@
11
<script setup lang="ts">
22
import { useI18n } from 'vue-i18n'
3-
import { Plus, PackagePlus } from 'lucide-vue-next'
4-
import FeaturedMcpServers from './FeaturedMcpServers.vue'
5-
6-
const emit = defineEmits<{
7-
installServer: []
8-
}>()
3+
import { useRouter } from 'vue-router'
4+
import { Info } from 'lucide-vue-next'
5+
import { Button } from '@/components/ui/button'
6+
import {
7+
Empty,
8+
EmptyHeader,
9+
EmptyMedia,
10+
EmptyTitle,
11+
EmptyDescription,
12+
EmptyContent,
13+
} from '@/components/ui/empty'
14+
import McpFeaturedList from './McpFeaturedList.vue'
915
1016
const { t } = useI18n()
17+
const router = useRouter()
1118
12-
const handleInstallServer = () => {
13-
emit('installServer')
19+
const handleBrowseCatalog = () => {
20+
router.push('/mcp-server/search')
1421
}
1522
</script>
1623

1724
<template>
18-
<div class="space-y-8">
19-
<div class="pt-20">
20-
<button
21-
type="button"
22-
@click="handleInstallServer"
23-
class="relative block w-full max-w-2xl mx-auto rounded-lg border-2 border-dashed border-muted-foreground/25 p-12 text-center hover:border-muted-foreground/40 hover:bg-muted/20 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden transition-all duration-200 group"
24-
>
25-
<div class="mx-auto size-16 text-muted-foreground/60 group-hover:text-muted-foreground/80 transition-colors duration-200">
26-
<PackagePlus class="w-full h-full" stroke-width="1.25" />
27-
</div>
28-
<div class="mt-4 space-y-2">
29-
<span class="block text-sm font-semibold text-foreground group-hover:text-foreground/90 transition-colors duration-200">
25+
<div class="flex flex-col lg:flex-row items-stretch justify-start gap-4 lg:gap-8 py-6 lg:py-10">
26+
<div class="flex flex-col items-stretch justify-start gap-5 flex-1 w-full">
27+
<Empty class="border border-solid">
28+
<EmptyHeader>
29+
<EmptyMedia variant="icon">
30+
<Info class="h-5 w-5" />
31+
</EmptyMedia>
32+
<EmptyTitle>
3033
{{ t('mcpInstallations.emptyState.title') }}
31-
</span>
32-
<span class="block text-sm text-muted-foreground group-hover:text-muted-foreground/80 transition-colors duration-200">
34+
</EmptyTitle>
35+
<EmptyDescription>
3336
{{ t('mcpInstallations.emptyState.description') }}
34-
</span>
35-
<div class="mt-4 inline-flex items-center gap-1.5 text-xs text-primary font-medium group-hover:text-primary/80 transition-colors duration-200">
36-
<Plus class="h-3.5 w-3.5" />
37-
{{ t('mcpInstallations.actions.install') }}
38-
</div>
39-
</div>
40-
</button>
37+
</EmptyDescription>
38+
</EmptyHeader>
39+
<EmptyContent>
40+
<Button @click="handleBrowseCatalog">
41+
{{ t('mcpInstallations.featuredList.browseCatalog') }}
42+
</Button>
43+
</EmptyContent>
44+
</Empty>
4145
</div>
4246

43-
<!-- Featured MCP Servers -->
44-
<div class="mt-20 max-w-7xl">
45-
<FeaturedMcpServers />
47+
<div class="w-full lg:w-80 shrink-0">
48+
<McpFeaturedList />
4649
</div>
4750
</div>
4851
</template>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export default {
2525
},
2626
},
2727

28+
featuredList: {
29+
title: 'Featured MCP Servers',
30+
browseCatalog: 'Browse Catalog',
31+
},
32+
2833
catalog: {
2934
title: 'Server Catalog',
3035
description: 'Browse all available MCP servers',

0 commit comments

Comments
 (0)