Skip to content

Commit a12903e

Browse files
author
Lasim
committed
refactor: implement ProgressBars component for multi-step progress visualization
1 parent d3ceff2 commit a12903e

4 files changed

Lines changed: 542 additions & 41 deletions

File tree

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

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@
33
import { ref, computed, onMounted } from 'vue'
44
import { useI18n } from 'vue-i18n'
55
import { Button } from '@/components/ui/button'
6-
import {
7-
Breadcrumb,
8-
BreadcrumbItem,
9-
BreadcrumbLink,
10-
BreadcrumbList,
11-
BreadcrumbPage,
12-
BreadcrumbSeparator,
13-
} from '@/components/ui/breadcrumb'
6+
import { ProgressBars } from '@/components/ui/progress-bars'
147
import { Alert, AlertDescription } from '@/components/ui/alert'
158
import { Server, Settings, Cloud, Loader2 } from 'lucide-vue-next'
169
import { McpInstallationService } from '@/services/mcpInstallationService'
@@ -66,6 +59,38 @@ const steps = [
6659
}
6760
]
6861
62+
// Progress bar steps - convert steps to ProgressBars format
63+
const progressSteps = computed(() => {
64+
return steps.map((step, index) => {
65+
let status: 'completed' | 'current' | 'pending' = 'pending'
66+
67+
if (index < currentStep.value) {
68+
status = 'completed'
69+
} else if (index === currentStep.value) {
70+
status = 'current'
71+
}
72+
73+
return {
74+
id: step.key,
75+
label: step.label,
76+
status,
77+
clickable: index < currentStep.value // Only allow clicking on completed steps
78+
}
79+
})
80+
})
81+
82+
// Calculate progress percentage
83+
const progressPercentage = computed(() => {
84+
return (currentStep.value / (steps.length - 1)) * 100
85+
})
86+
87+
// Handle step click from progress bar
88+
const handleStepClick = (step: any, index: number) => {
89+
if (index < currentStep.value) {
90+
goToStep(index)
91+
}
92+
}
93+
6994
// State
7095
const currentStep = ref(0)
7196
const isSubmitting = ref(false)
@@ -257,39 +282,15 @@ onMounted(async () => {
257282

258283
<template>
259284
<div class="space-y-6">
260-
<!-- Breadcrumb Navigation -->
261-
<Breadcrumb>
262-
<BreadcrumbList>
263-
<template v-for="(step, index) in steps" :key="index">
264-
<BreadcrumbItem>
265-
<!-- Current Step -->
266-
<BreadcrumbPage v-if="index === currentStep" class="flex items-center gap-2">
267-
<component :is="step.icon" class="h-4 w-4" />
268-
<span>{{ step.label }}</span>
269-
</BreadcrumbPage>
270-
271-
<!-- Completed Steps (clickable) -->
272-
<BreadcrumbLink
273-
v-else-if="index < currentStep"
274-
@click="goToStep(index)"
275-
class="flex items-center gap-2 cursor-pointer hover:text-foreground"
276-
>
277-
<component :is="step.icon" class="h-4 w-4" />
278-
<span>{{ step.label }}</span>
279-
</BreadcrumbLink>
280-
281-
<!-- Future Steps (disabled) -->
282-
<span v-else class="flex items-center gap-2 text-muted-foreground">
283-
<component :is="step.icon" class="h-4 w-4" />
284-
<span>{{ step.label }}</span>
285-
</span>
286-
</BreadcrumbItem>
287-
288-
<!-- Separator -->
289-
<BreadcrumbSeparator v-if="index < steps.length - 1" />
290-
</template>
291-
</BreadcrumbList>
292-
</Breadcrumb>
285+
<!-- Progress Navigation -->
286+
<ProgressBars
287+
:steps="progressSteps"
288+
:progress="progressPercentage"
289+
:title="t('mcpInstallations.wizard.title')"
290+
interactive
291+
styled
292+
@step-click="handleStepClick"
293+
/>
293294

294295
<!-- Error Message -->
295296
<Alert v-if="submitError" variant="destructive">
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
<!--
2+
@component ProgressBars
3+
@description A multi-step progress indicator component that shows current progress through a series of steps with labels and completion states.
4+
5+
@example
6+
<ProgressBars
7+
:steps="[
8+
{ id: 'copy', label: 'Copying files', status: 'completed' },
9+
{ id: 'migrate', label: 'Migrating database', status: 'current' },
10+
{ id: 'compile', label: 'Compiling assets', status: 'pending' },
11+
{ id: 'deploy', label: 'Deployed', status: 'pending' }
12+
]"
13+
:progress="37.5"
14+
title="Migrating MySQL database..."
15+
variant="default"
16+
/>
17+
18+
@props
19+
- steps: Array of step objects with id, label, and status
20+
- progress: Current progress percentage (0-100)
21+
- title: Main title/description of the process
22+
- variant: Visual style variant ('default' | 'success' | 'warning' | 'destructive')
23+
- size: Size variant ('sm' | 'md' | 'lg')
24+
- showSteps: Whether to show step labels below progress bar
25+
- hideTitle: Whether to hide the title (for screen readers only)
26+
27+
@emits
28+
- stepClick: Emitted when a step is clicked (if interactive)
29+
30+
@accessibility
31+
- Uses proper ARIA attributes for progress indication
32+
- Screen reader friendly with sr-only labels
33+
- Supports keyboard navigation for interactive steps
34+
-->
35+
36+
<script setup lang="ts">
37+
import { computed } from 'vue'
38+
import { cva, type VariantProps } from 'class-variance-authority'
39+
import { cn } from '@/lib/utils'
40+
41+
export interface ProgressStep {
42+
id: string
43+
label: string
44+
status: 'completed' | 'current' | 'pending' | 'error'
45+
clickable?: boolean
46+
}
47+
48+
const progressBarsVariants = cva(
49+
'w-full',
50+
{
51+
variants: {
52+
variant: {
53+
default: '',
54+
success: '',
55+
warning: '',
56+
destructive: ''
57+
},
58+
size: {
59+
sm: '',
60+
md: '',
61+
lg: ''
62+
}
63+
},
64+
defaultVariants: {
65+
variant: 'default',
66+
size: 'md'
67+
}
68+
}
69+
)
70+
71+
const progressBarVariants = cva(
72+
'overflow-hidden rounded-full transition-all duration-300 ease-in-out',
73+
{
74+
variants: {
75+
variant: {
76+
default: 'bg-gray-200',
77+
success: 'bg-green-100',
78+
warning: 'bg-yellow-100',
79+
destructive: 'bg-red-100'
80+
},
81+
size: {
82+
sm: 'h-1',
83+
md: 'h-2',
84+
lg: 'h-3'
85+
}
86+
},
87+
defaultVariants: {
88+
variant: 'default',
89+
size: 'md'
90+
}
91+
}
92+
)
93+
94+
const progressFillVariants = cva(
95+
'h-full rounded-full transition-all duration-500 ease-out',
96+
{
97+
variants: {
98+
variant: {
99+
default: 'bg-primary',
100+
success: 'bg-green-600',
101+
warning: 'bg-yellow-600',
102+
destructive: 'bg-destructive'
103+
}
104+
},
105+
defaultVariants: {
106+
variant: 'default'
107+
}
108+
}
109+
)
110+
111+
interface Props {
112+
steps: ProgressStep[]
113+
progress: number
114+
title?: string
115+
variant?: VariantProps<typeof progressBarsVariants>['variant']
116+
size?: VariantProps<typeof progressBarsVariants>['size']
117+
showSteps?: boolean
118+
hideTitle?: boolean
119+
interactive?: boolean
120+
styled?: boolean // New prop for styled container
121+
}
122+
123+
const props = withDefaults(defineProps<Props>(), {
124+
title: '',
125+
variant: 'default',
126+
size: 'md',
127+
showSteps: true,
128+
hideTitle: false,
129+
interactive: false,
130+
styled: false
131+
})
132+
133+
const emit = defineEmits<{
134+
stepClick: [step: ProgressStep, index: number]
135+
}>()
136+
137+
const stepVariants = cva(
138+
'text-sm font-medium transition-colors duration-200',
139+
{
140+
variants: {
141+
status: {
142+
completed: 'text-primary',
143+
current: 'text-primary',
144+
pending: 'text-gray-600',
145+
error: 'text-destructive'
146+
},
147+
clickable: {
148+
true: 'cursor-pointer hover:text-primary/80',
149+
false: ''
150+
}
151+
},
152+
defaultVariants: {
153+
status: 'pending',
154+
clickable: false
155+
}
156+
}
157+
)
158+
159+
const clampedProgress = computed(() => Math.max(0, Math.min(100, props.progress)))
160+
161+
const gridCols = computed(() => {
162+
const stepCount = props.steps.length
163+
if (stepCount <= 2) return 'grid-cols-2'
164+
if (stepCount <= 3) return 'grid-cols-3'
165+
if (stepCount <= 4) return 'grid-cols-4'
166+
if (stepCount <= 5) return 'grid-cols-5'
167+
if (stepCount <= 6) return 'grid-cols-6'
168+
return 'grid-cols-6' // Max 6 columns for readability
169+
})
170+
171+
function handleStepClick(step: ProgressStep, index: number) {
172+
if (props.interactive && step.clickable) {
173+
emit('stepClick', step, index)
174+
}
175+
}
176+
177+
function getStepAlignment(index: number) {
178+
const totalSteps = props.steps.length
179+
if (totalSteps <= 1) return 'text-center'
180+
if (index === 0) return 'text-left'
181+
if (index === totalSteps - 1) return 'text-right'
182+
return 'text-center'
183+
}
184+
</script>
185+
186+
<template>
187+
<div
188+
:class="[
189+
cn(progressBarsVariants({ variant, size })),
190+
styled ? 'rounded-lg bg-muted/50 px-4 py-6 sm:px-6' : ''
191+
]"
192+
>
193+
<!-- Title -->
194+
<div v-if="title" :class="styled ? 'mb-6' : 'mb-4'">
195+
<h4 v-if="hideTitle" class="sr-only">{{ title }}</h4>
196+
<p v-else class="text-sm font-medium text-foreground">{{ title }}</p>
197+
</div>
198+
199+
<!-- Progress Bar -->
200+
<div class="space-y-4">
201+
<div
202+
role="progressbar"
203+
:aria-valuenow="clampedProgress"
204+
aria-valuemin="0"
205+
aria-valuemax="100"
206+
:aria-label="title || 'Progress'"
207+
:class="cn(progressBarVariants({ variant, size }))"
208+
>
209+
<div
210+
:class="cn(progressFillVariants({ variant }))"
211+
:style="{ width: `${clampedProgress}%` }"
212+
/>
213+
</div>
214+
215+
<!-- Steps -->
216+
<div
217+
v-if="showSteps && steps.length > 0"
218+
:class="[
219+
'hidden sm:grid gap-2 text-sm font-medium',
220+
gridCols
221+
]"
222+
>
223+
<button
224+
v-for="(step, index) in steps"
225+
:key="step.id"
226+
:type="interactive && step.clickable ? 'button' : undefined"
227+
:disabled="!interactive || !step.clickable"
228+
:class="[
229+
cn(stepVariants({
230+
status: step.status,
231+
clickable: interactive && step.clickable
232+
})),
233+
getStepAlignment(index)
234+
]"
235+
@click="handleStepClick(step, index)"
236+
>
237+
{{ step.label }}
238+
</button>
239+
</div>
240+
241+
<!-- Mobile Steps (Vertical List) -->
242+
<div v-if="showSteps && steps.length > 0" class="sm:hidden space-y-2">
243+
<div
244+
v-for="(step, index) in steps"
245+
:key="`mobile-${step.id}`"
246+
class="flex items-center justify-between"
247+
>
248+
<span :class="cn(stepVariants({ status: step.status }))">
249+
{{ step.label }}
250+
</span>
251+
<div class="flex items-center gap-2">
252+
<!-- Status Icon -->
253+
<div
254+
:class="[
255+
'w-2 h-2 rounded-full',
256+
step.status === 'completed' ? 'bg-primary' : '',
257+
step.status === 'current' ? 'bg-primary animate-pulse' : '',
258+
step.status === 'pending' ? 'bg-red-500' : '',
259+
step.status === 'error' ? 'bg-destructive' : ''
260+
]"
261+
/>
262+
</div>
263+
</div>
264+
</div>
265+
</div>
266+
</div>
267+
</template>

0 commit comments

Comments
 (0)