Skip to content

Commit f3d3e70

Browse files
author
Lasim
committed
feat(all): implement breadcrumbs and several frontend and UI improvements
1 parent b9624a8 commit f3d3e70

File tree

117 files changed

+1288
-992
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+1288
-992
lines changed

services/backend/api-spec.json

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18065,9 +18065,17 @@
1806518065
"description": "Website URL"
1806618066
},
1806718067
"icon_url": {
18068-
"type": "string",
18069-
"format": "uri",
18070-
"description": "Icon/logo URL"
18068+
"oneOf": [
18069+
{
18070+
"type": "string",
18071+
"maxLength": 0
18072+
},
18073+
{
18074+
"type": "string",
18075+
"format": "uri"
18076+
}
18077+
],
18078+
"description": "Icon/logo URL (empty string or valid URI)"
1807118079
},
1807218080
"github_account_id": {
1807318081
"type": "string",
@@ -18708,9 +18716,17 @@
1870818716
"description": "Website URL"
1870918717
},
1871018718
"icon_url": {
18711-
"type": "string",
18712-
"format": "uri",
18713-
"description": "Icon/logo URL"
18719+
"oneOf": [
18720+
{
18721+
"type": "string",
18722+
"maxLength": 0
18723+
},
18724+
{
18725+
"type": "string",
18726+
"format": "uri"
18727+
}
18728+
],
18729+
"description": "Icon/logo URL (empty string or valid URI)"
1871418730
},
1871518731
"github_account_id": {
1871618732
"type": "string",

services/backend/api-spec.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12698,9 +12698,12 @@ paths:
1269812698
format: uri
1269912699
description: Website URL
1270012700
icon_url:
12701-
type: string
12702-
format: uri
12703-
description: Icon/logo URL
12701+
oneOf:
12702+
- type: string
12703+
maxLength: 0
12704+
- type: string
12705+
format: uri
12706+
description: Icon/logo URL (empty string or valid URI)
1270412707
github_account_id:
1270512708
type: string
1270612709
description: GitHub Account ID (owner.id from GitHub API)
@@ -13180,9 +13183,12 @@ paths:
1318013183
format: uri
1318113184
description: Website URL
1318213185
icon_url:
13183-
type: string
13184-
format: uri
13185-
description: Icon/logo URL
13186+
oneOf:
13187+
- type: string
13188+
maxLength: 0
13189+
- type: string
13190+
format: uri
13191+
description: Icon/logo URL (empty string or valid URI)
1318613192
github_account_id:
1318713193
type: string
1318813194
description: GitHub Account ID (owner.id from GitHub API)

services/backend/src/routes/mcp/servers/schemas.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ const SERVER_FIELDS = {
5555
repository_subfolder: { type: 'string', description: 'Subfolder path for monorepos' },
5656
git_branch: { type: 'string', description: 'Git branch (defaults to main)' },
5757
website_url: { type: 'string', format: 'uri', description: 'Website URL' },
58-
icon_url: { type: 'string', format: 'uri', description: 'Icon/logo URL' },
58+
icon_url: {
59+
oneOf: [
60+
{ type: 'string', maxLength: 0 },
61+
{ type: 'string', format: 'uri' }
62+
],
63+
description: 'Icon/logo URL (empty string or valid URI)'
64+
},
5965
language: { type: 'string', minLength: 1, description: 'Programming language is required' },
6066
runtime: { type: 'string', minLength: 1, description: 'Runtime environment is required' },
6167
transport_type: { type: 'string', enum: ['stdio', 'http', 'sse'], description: 'MCP transport type' },
@@ -582,9 +588,11 @@ export const CREATE_GLOBAL_SERVER_REQUEST_SCHEMA = {
582588
description: 'Website URL'
583589
},
584590
icon_url: {
585-
type: 'string',
586-
format: 'uri',
587-
description: 'Icon/logo URL'
591+
oneOf: [
592+
{ type: 'string', maxLength: 0 },
593+
{ type: 'string', format: 'uri' }
594+
],
595+
description: 'Icon/logo URL (empty string or valid URI)'
588596
},
589597
github_account_id: {
590598
type: 'string',

services/backend/src/utils/banner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const displayStartupBanner = (port: number, logger: FastifyBaseLogger): v
1515
║ \x1b[38;5;93m██████╔╝███████╗██║ ███████╗╚██████╔╝ ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗\x1b[38;5;51m
1616
║ \x1b[38;5;93m╚═════╝ ╚══════╝╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝\x1b[38;5;51m
1717
18-
║ \x1b[38;5;82mDeployStack CI/CD Backend \x1b[38;5;196mv${version}\x1b[38;5;51m
18+
║ \x1b[38;5;82mDeployStack Backend \x1b[38;5;196mv${version}\x1b[38;5;51m
1919
║ \x1b[38;5;82mRunning on port \x1b[38;5;196m${port}\x1b[38;5;51m
2020
║ \x1b[38;5;82mEnvironment: \x1b[38;5;196m${process.env.NODE_ENV || 'development'}\x1b[38;5;51m
2121

services/backend/tests/unit/utils/banner.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('banner.ts', () => {
8484
const bannerOutput = logCall[1] as string;
8585
// Check for parts of the ASCII art
8686
expect(bannerOutput).toContain('██████╗ ███████╗██████╗ ██╗ ██████╗ ██╗ ██╗███████╗████████╗ █████╗ ██████╗██╗ ██╗');
87-
expect(bannerOutput).toContain('DeployStack CI/CD Backend');
87+
expect(bannerOutput).toContain('DeployStack Backend');
8888
});
8989

9090
it('should include ANSI color codes', () => {
@@ -177,7 +177,7 @@ describe('banner.ts', () => {
177177
expect(bannerOutput).toContain('╔═══'); // Top border
178178
expect(bannerOutput).toContain('╚═══'); // Bottom border
179179
expect(bannerOutput).toContain('║'); // Side borders
180-
expect(bannerOutput).toContain('DeployStack CI/CD Backend');
180+
expect(bannerOutput).toContain('DeployStack Backend');
181181
expect(bannerOutput).toContain('Running on port');
182182
expect(bannerOutput).toContain('Environment:');
183183
});

services/frontend/src/components/DashboardLayout.vue

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,30 @@
11
<script setup lang="ts">
22
import { computed } from 'vue'
33
import type { StyleValue } from 'vue'
4-
import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar'
4+
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
55
import { Separator } from '@/components/ui/separator'
66
import AppSidebar from '@/components/AppSidebar.vue'
7-
8-
interface Props {
9-
title: string
10-
}
11-
const props = defineProps<Props>()
7+
import SiteHeader from '@/components/SiteHeader.vue'
128
139
// TODO: Implement cookie-based persistence for defaultOpen if needed, like in the shadcn/ui example.
1410
// For now, defaulting to true.
1511
const defaultOpen = true
1612
1713
// Define sidebar width using custom values
18-
// Override the collapsed width to 30rem for testing
1914
const sidebarStyle = computed(() => ({
2015
'--sidebar-width': '16rem',
2116
'--sidebar-width-mobile': '18rem',
2217
'--sidebar-width-icon': '4rem',
2318
} as StyleValue))
24-
25-
// A simple ref for the SiteHeader, can be expanded later
26-
// For now, just using the title prop.
2719
</script>
2820

2921
<template>
3022
<SidebarProvider :default-open="defaultOpen" :style="sidebarStyle">
3123
<AppSidebar variant="inset" />
3224
<SidebarInset class="px-5">
33-
<!-- SiteHeader equivalent -->
34-
<header class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
35-
<div class="flex items-center gap-2 px-4">
36-
<SidebarTrigger class="-ml-1" />
37-
<h1 class="text-lg font-semibold md:text-xl">{{ props.title }}</h1>
38-
</div>
39-
</header>
25+
<SiteHeader />
4026

41-
<Separator class="my-6 max-w-7xl" />
27+
<Separator class="mb-6 max-w-7xl" />
4228

4329
<!-- Content area -->
4430
<div class="flex flex-1 flex-col gap-4 py-4 pt-0 max-w-7xl">
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
import { RouterLink } from 'vue-router'
3+
import { SidebarTrigger } from '@/components/ui/sidebar'
4+
import { Separator } from '@/components/ui/separator'
5+
import {
6+
Breadcrumb,
7+
BreadcrumbItem,
8+
BreadcrumbLink,
9+
BreadcrumbList,
10+
BreadcrumbPage,
11+
BreadcrumbSeparator,
12+
} from '@/components/ui/breadcrumb'
13+
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
14+
15+
const { breadcrumbs } = useBreadcrumbs()
16+
</script>
17+
18+
<template>
19+
<header class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
20+
<div class="flex items-center gap-2 px-4">
21+
<SidebarTrigger class="-ml-1" />
22+
<Separator orientation="vertical" class="mr-2! h-4! bg-muted-foreground/50" />
23+
<Breadcrumb>
24+
<BreadcrumbList>
25+
<template v-for="(item, index) in breadcrumbs" :key="index">
26+
<BreadcrumbItem :class="{ 'hidden md:block': index < breadcrumbs.length - 1 }">
27+
<BreadcrumbLink v-if="item.href" as-child>
28+
<RouterLink :to="item.href">
29+
{{ item.label }}
30+
</RouterLink>
31+
</BreadcrumbLink>
32+
<BreadcrumbPage v-else>
33+
{{ item.label }}
34+
</BreadcrumbPage>
35+
</BreadcrumbItem>
36+
<BreadcrumbSeparator
37+
v-if="index < breadcrumbs.length - 1"
38+
class="hidden md:block"
39+
/>
40+
</template>
41+
</BreadcrumbList>
42+
</Breadcrumb>
43+
</div>
44+
</header>
45+
</template>

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ interface McpServerAddFormData {
6666
auto_install_new_default_team: boolean
6767
transport_type: string
6868
website_url: string
69+
icon_url: string
6970
}
7071
}
7172
@@ -192,7 +193,8 @@ const formData = ref<McpServerAddFormData>({
192193
featured: false,
193194
auto_install_new_default_team: false,
194195
transport_type: 'auto',
195-
website_url: ''
196+
website_url: '',
197+
icon_url: ''
196198
}
197199
})
198200
@@ -374,6 +376,7 @@ const autoPopulateFromGitHub = (repositoryData: any) => {
374376
license: repositoryData.license?.spdx_id || repositoryData.license || '',
375377
tags: repositoryData.topics || repositoryData.tags || [],
376378
website_url: repositoryData.homepage || '',
379+
icon_url: repositoryData.owner?.avatar_url || repositoryData.icon_url || '',
377380
// Keep existing values for these properties
378381
featured: formData.value.basic.featured,
379382
auto_install_new_default_team: formData.value.basic.auto_install_new_default_team,
@@ -437,6 +440,7 @@ const submitForm = async () => {
437440
tags: formData.value.basic.tags,
438441
featured: formData.value.basic.featured,
439442
auto_install_new_default_team: formData.value.basic.auto_install_new_default_team,
443+
icon_url: formData.value.basic.icon_url,
440444
441445
// New Configuration Schema (ADR-007)
442446
configuration_schema: formData.value.configuration_schema,

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ import {
1818
SelectContent,
1919
SelectItem,
2020
SelectTrigger,
21-
SelectValue,
2221
} from '@/components/ui/select'
2322
import { Badge } from '@/components/ui/badge'
2423
import { Button } from '@/components/ui/button'
2524
import { Alert, AlertDescription } from '@/components/ui/alert'
2625
import { Switch } from '@/components/ui/switch'
2726
import { X, Plus, CheckCircle } from 'lucide-vue-next'
27+
import { DynamicIcon } from '@/components/ui/dynamic-icon'
2828
import type { BasicInfoFormData } from '@/views/admin/mcp-server-catalog/types'
2929
import { useCategories } from '@/composables/admin/mcp-catalog/useCategories'
3030
import { useTagManager } from '@/composables/admin/mcp-catalog/useTagManager'
@@ -66,6 +66,11 @@ const isAutoPopulated = computed(() => {
6666
return props.formData?.github?.auto_populated || false
6767
})
6868
69+
// Get selected category for displaying icon in trigger
70+
const selectedCategory = computed(() => {
71+
return categories.value.find(c => c.id === localData.value.category_id)
72+
})
73+
6974
// Update field helper
7075
const updateField = <K extends keyof BasicInfoFormData>(field: K, value: BasicInfoFormData[K]) => {
7176
const newData = {
@@ -125,19 +130,31 @@ loadCategories()
125130
:disabled="categoriesLoading"
126131
>
127132
<SelectTrigger>
128-
<SelectValue
129-
:placeholder="categoriesLoading
130-
? 'Loading categories...'
131-
: t('mcpCatalog.form.basic.category.placeholder')"
132-
/>
133+
<div class="flex items-center gap-2">
134+
<DynamicIcon
135+
v-if="selectedCategory?.icon"
136+
:name="selectedCategory.icon"
137+
class="h-4 w-4 shrink-0 text-muted-foreground"
138+
/>
139+
<span class="truncate">
140+
{{ selectedCategory?.name || (categoriesLoading ? 'Loading categories...' : t('mcpCatalog.form.basic.category.placeholder')) }}
141+
</span>
142+
</div>
133143
</SelectTrigger>
134144
<SelectContent>
135145
<SelectItem
136146
v-for="category in categories"
137147
:key="category.id"
138148
:value="category.id"
139149
>
140-
{{ category.name }}
150+
<div class="flex items-center gap-2">
151+
<DynamicIcon
152+
v-if="category.icon"
153+
:name="category.icon"
154+
class="h-4 w-4 shrink-0 text-muted-foreground"
155+
/>
156+
<span>{{ category.name }}</span>
157+
</div>
141158
</SelectItem>
142159
</SelectContent>
143160
</Select>
@@ -273,6 +290,20 @@ loadCategories()
273290
/>
274291
</SharedFormField>
275292

293+
<!-- Icon URL -->
294+
<SharedFormField
295+
label="Icon URL"
296+
description="URL to the server icon image (auto-generated from GitHub avatar if not provided)"
297+
>
298+
<Input
299+
id="icon_url"
300+
:model-value="localData.icon_url"
301+
@update:model-value="(value) => updateField('icon_url', String(value))"
302+
placeholder="https://example.com/icon.png"
303+
type="url"
304+
/>
305+
</SharedFormField>
306+
276307
<!-- Tags -->
277308
<SharedFormField
278309
:label="t('mcpCatalog.form.basic.tags.label')"

0 commit comments

Comments
 (0)