Skip to content

Commit 8f5bd9f

Browse files
author
Lasim
committed
feat(frontend): add spinner component and replace loading indicators
1 parent 2070153 commit 8f5bd9f

37 files changed

+341
-262
lines changed

services/frontend/src/components/admin/UserActionsGroup.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
33
import { useI18n } from 'vue-i18n'
44
import { toast } from 'vue-sonner'
55
import { Button } from '@/components/ui/button'
6+
import { Spinner } from '@/components/ui/spinner'
67
import {
78
AlertDialog,
89
AlertDialogAction,
@@ -88,12 +89,11 @@ const handlePasswordReset = async () => {
8889
<AlertDialogTrigger as-child>
8990
<Button
9091
variant="outline"
91-
:disabled="!canResetPassword"
92-
:loading="isLoading"
93-
:loading-text="t('adminUsers.userDetail.actions.forceResetPassword')"
92+
:disabled="!canResetPassword || isLoading"
9493
:title="!canResetPassword ? t('adminUsers.userDetail.actions.resetPasswordDisabled') : undefined"
9594
class="justify-start"
9695
>
96+
<Spinner v-if="isLoading" class="mr-2" />
9797
{{ t('adminUsers.userDetail.actions.forceResetPassword') }}
9898
</Button>
9999
</AlertDialogTrigger>
@@ -115,11 +115,11 @@ const handlePasswordReset = async () => {
115115
{{ t('adminUsers.userDetail.actions.resetPasswordConfirm.cancel') }}
116116
</AlertDialogCancel>
117117
<AlertDialogAction as-child>
118-
<Button
119-
@click="handlePasswordReset"
120-
:loading="isLoading"
121-
:loading-text="t('adminUsers.userDetail.actions.resetPasswordConfirm.confirm')"
118+
<Button
119+
@click="handlePasswordReset"
120+
:disabled="isLoading"
122121
>
122+
<Spinner v-if="isLoading" class="mr-2" />
123123
{{ t('adminUsers.userDetail.actions.resetPasswordConfirm.confirm') }}
124124
</Button>
125125
</AlertDialogAction>

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ref, computed } from 'vue'
44
import { useI18n } from 'vue-i18n'
55
import { Button } from '@/components/ui/button'
6+
import { Spinner } from '@/components/ui/spinner'
67
import { Alert, AlertDescription } from '@/components/ui/alert'
78
import { ProgressBars } from '@/components/ui/progress-bars'
89
import { FileText, Github, Settings } from 'lucide-vue-next'
@@ -554,11 +555,10 @@ const submitForm = async () => {
554555
<Button
555556
v-if="currentStep === 0"
556557
@click="handleGitHubStepNext"
557-
:disabled="!canProceedFromGitHub"
558-
:loading="isFetchingGitHub"
559-
:loading-text="t('mcpCatalog.form.navigation.fetching')"
558+
:disabled="!canProceedFromGitHub || isFetchingGitHub"
560559
class="min-w-[120px]"
561560
>
561+
<Spinner v-if="isFetchingGitHub" class="mr-2" />
562562
{{ t('mcpCatalog.form.navigation.next') }}
563563
</Button>
564564

@@ -575,10 +575,9 @@ const submitForm = async () => {
575575
<Button
576576
v-else
577577
@click="submitForm"
578-
:disabled="!canSubmit"
579-
:loading="isSubmitting"
580-
:loading-text="t('mcpCatalog.form.navigation.creating')"
578+
:disabled="!canSubmit || isSubmitting"
581579
>
580+
<Spinner v-if="isSubmitting" class="mr-2" />
582581
{{ t('mcpCatalog.form.navigation.submit') }}
583582
</Button>
584583
</div>

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

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
1616
import { useI18n } from 'vue-i18n'
1717
import { Button } from '@/components/ui/button'
18+
import { Spinner } from '@/components/ui/spinner'
1819
import { Alert, AlertDescription } from '@/components/ui/alert'
1920
import { ProgressBars } from '@/components/ui/progress-bars'
2021
import { FileText, GitBranch, Code, Settings, CheckCircle, BookOpen } from 'lucide-vue-next'
@@ -39,13 +40,15 @@ interface Props {
3940
submitButtonText?: string
4041
cancelButtonText?: string
4142
serverId?: string
43+
isSubmitting?: boolean
4244
}
4345
4446
const props = withDefaults(defineProps<Props>(), {
4547
mode: 'create',
4648
submitButtonText: '',
4749
cancelButtonText: '',
48-
serverId: ''
50+
serverId: '',
51+
isSubmitting: false
4952
})
5053
5154
// Emits
@@ -141,7 +144,7 @@ const progressPercentage = computed(() => {
141144
142145
// Progress title based on current step
143146
const progressTitle = computed(() => {
144-
if (isSubmitting.value) {
147+
if (props.isSubmitting || internalIsSubmitting.value) {
145148
return props.mode === 'edit'
146149
? t('mcpCatalog.form.navigation.updating')
147150
: t('mcpCatalog.form.navigation.creating')
@@ -163,14 +166,14 @@ const progressTitle = computed(() => {
163166
// Progress variant based on state
164167
const progressVariant = computed(() => {
165168
if (repositoryFetchError.value || submitError.value) return 'destructive'
166-
if (isSubmitting.value) return 'default' // Keep default while submitting
169+
if (props.isSubmitting || internalIsSubmitting.value) return 'default' // Keep default while submitting
167170
// Only show success after actual completion (would need to be handled by parent component)
168171
return 'default'
169172
})
170173
171174
// State
172175
const currentStep = ref(0)
173-
const isSubmitting = ref(false)
176+
const internalIsSubmitting = ref(false)
174177
const submitError = ref<string | null>(null)
175178
const isFetchingRepository = ref(false)
176179
const repositoryFetchError = ref<string | null>(null)
@@ -653,10 +656,20 @@ watch(
653656
}
654657
)
655658
659+
// Reset internal submitting state when parent signals completion
660+
watch(
661+
() => props.isSubmitting,
662+
(newValue) => {
663+
if (!newValue) {
664+
internalIsSubmitting.value = false
665+
}
666+
}
667+
)
668+
656669
// Form submission with fresh data from actual storage keys
657670
const submitForm = async () => {
658671
try {
659-
isSubmitting.value = true
672+
internalIsSubmitting.value = true
660673
submitError.value = null
661674
662675
// Get fresh data from ALL storage keys being used by components
@@ -745,9 +758,9 @@ const submitForm = async () => {
745758
} catch (error) {
746759
console.error('Form submission error:', error)
747760
submitError.value = error instanceof Error ? error.message : 'Failed to submit form'
748-
} finally {
749-
isSubmitting.value = false
761+
internalIsSubmitting.value = false
750762
}
763+
// Note: Don't reset internalIsSubmitting in finally - parent controls final state via prop
751764
}
752765
753766
// Lifecycle
@@ -771,7 +784,7 @@ onMounted(() => {
771784
772785
onUnmounted(() => {
773786
// Save current state as draft when component unmounts (unless submitting)
774-
if (!isSubmitting.value) {
787+
if (!props.isSubmitting && !internalIsSubmitting.value) {
775788
saveDraft()
776789
}
777790
})
@@ -860,11 +873,10 @@ onUnmounted(() => {
860873
<Button
861874
v-if="currentStep === 0"
862875
@click="handleRepositoryStepNext"
863-
:disabled="!canProceedFromRepository"
864-
:loading="isFetchingRepository"
865-
:loading-text="t('mcpCatalog.form.navigation.fetching')"
876+
:disabled="!canProceedFromRepository || isFetchingRepository"
866877
class="min-w-[120px]"
867878
>
879+
<Spinner v-if="isFetchingRepository" class="mr-2" />
868880
{{ t('mcpCatalog.form.navigation.next') }}
869881
</Button>
870882

@@ -880,9 +892,9 @@ onUnmounted(() => {
880892
<Button
881893
v-else
882894
@click="submitForm"
883-
:loading="isSubmitting"
884-
:loading-text="props.mode === 'edit' ? t('mcpCatalog.form.navigation.updating') : t('mcpCatalog.form.navigation.creating')"
895+
:disabled="props.isSubmitting || internalIsSubmitting"
885896
>
897+
<Spinner v-if="props.isSubmitting || internalIsSubmitting" class="mr-2" />
886898
{{ props.mode === 'edit' ? t('mcpCatalog.form.navigation.update') : t('mcpCatalog.form.navigation.submit') }}
887899
</Button>
888900
</div>

services/frontend/src/components/admin/mcp-categories/CategoryModal.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@/components/ui/alert-dialog'
1313
import { Input } from '@/components/ui/input'
1414
import { Button } from '@/components/ui/button'
15+
import { Spinner } from '@/components/ui/spinner'
1516
import { Label } from '@/components/ui/label'
1617
import IconPicker from '@/components/ui/icon-picker.vue'
1718
import { McpCategoriesService, type McpCategory, type CreateMcpCategoryRequest } from '@/services/mcpCategoriesService'
@@ -295,10 +296,9 @@ const handleSortOrderChange = () => {
295296
</Button>
296297
<Button
297298
type="submit"
298-
:disabled="!isFormValid"
299-
:loading="isSubmitting"
300-
:loading-text="t('mcpCategories.modal.saving')"
299+
:disabled="!isFormValid || isSubmitting"
301300
>
301+
<Spinner v-if="isSubmitting" class="mr-2" />
302302
{{ isEditing ? t('mcpCategories.modal.update') : t('mcpCategories.modal.create') }}
303303
</Button>
304304
</AlertDialogFooter>

services/frontend/src/components/admin/satellites/CreateTokenModal.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ref, computed, nextTick } from 'vue'
33
import { useI18n } from 'vue-i18n'
44
import { toast } from 'vue-sonner'
55
import { Button } from '@/components/ui/button'
6+
import { Spinner } from '@/components/ui/spinner'
67
import { Input } from '@/components/ui/input'
78
import { Label } from '@/components/ui/label'
89
import { Textarea } from '@/components/ui/textarea'
@@ -274,9 +275,8 @@ const selectedTeamName = computed(() => {
274275
<Button
275276
@click="handleCreateToken"
276277
:disabled="isCreating || (!canCreateGlobal && !canCreateTeam)"
277-
:loading="isCreating"
278-
:loading-text="t('satellites.pairing.createToken.buttons.creating')"
279278
>
279+
<Spinner v-if="isCreating" class="mr-2" />
280280
{{ t('satellites.pairing.createToken.buttons.create') }}
281281
</Button>
282282
</DialogFooter>

services/frontend/src/components/client-config/CommandActionRenderer.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { CommandAction } from '@/services/satelliteConfigService'
44
import { Input } from '@/components/ui/input'
55
import { Textarea } from '@/components/ui/textarea'
66
import { Button } from '@/components/ui/button'
7+
import { Spinner } from '@/components/ui/spinner'
78
import { useI18n } from 'vue-i18n'
89
910
interface Props {
@@ -50,9 +51,9 @@ async function handleCopy() {
5051
<div v-if="showCopyButton" class="flex justify-end">
5152
<Button
5253
@click="handleCopy"
53-
:loading="isCopying"
54-
loading-text="Copying..."
54+
:disabled="isCopying"
5555
>
56+
<Spinner v-if="isCopying" class="mr-2" />
5657
{{ t('satelliteConfig.button.copy') }}
5758
</Button>
5859
</div>

services/frontend/src/components/client-config/JsonActionRenderer.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { JsonAction } from '@/services/satelliteConfigService'
44
import { Input } from '@/components/ui/input'
55
import { Textarea } from '@/components/ui/textarea'
66
import { Button } from '@/components/ui/button'
7+
import { Spinner } from '@/components/ui/spinner'
78
import { useI18n } from 'vue-i18n'
89
910
interface Props {
@@ -61,9 +62,9 @@ async function handleCopy() {
6162
<div v-if="showCopyButton" class="flex justify-end">
6263
<Button
6364
@click="handleCopy"
64-
:loading="isCopying"
65-
loading-text="Copying..."
65+
:disabled="isCopying"
6666
>
67+
<Spinner v-if="isCopying" class="mr-2" />
6768
{{ t('satelliteConfig.button.copy') }}
6869
</Button>
6970
</div>

services/frontend/src/components/client-config/TextActionRenderer.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ref } from 'vue'
33
import type { TextAction } from '@/services/satelliteConfigService'
44
import { Textarea } from '@/components/ui/textarea'
55
import { Button } from '@/components/ui/button'
6+
import { Spinner } from '@/components/ui/spinner'
67
import { useI18n } from 'vue-i18n'
78
89
interface Props {
@@ -42,9 +43,9 @@ async function handleCopy() {
4243
<div v-if="showCopyButton" class="flex justify-end">
4344
<Button
4445
@click="handleCopy"
45-
:loading="isCopying"
46-
loading-text="Copying..."
46+
:disabled="isCopying"
4747
>
48+
<Spinner v-if="isCopying" class="mr-2" />
4849
{{ t('satelliteConfig.button.copy') }}
4950
</Button>
5051
</div>

services/frontend/src/components/credentials/AddCredentialDialog.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { ref, computed, onMounted, watch } from 'vue'
33
import { useI18n } from 'vue-i18n'
44
import { Button } from '@/components/ui/button'
5+
import { Spinner } from '@/components/ui/spinner'
56
import { Card } from '@/components/ui/card'
67
import { Input } from '@/components/ui/input'
78
import { Label } from '@/components/ui/label'
@@ -392,11 +393,10 @@ onMounted(() => {
392393
</Button>
393394
<Button
394395
type="submit"
395-
:disabled="!isFormValid"
396-
:loading="isSaving"
397-
:loading-text="t('credentials.form.buttons.saving')"
396+
:disabled="!isFormValid || isSaving"
398397
class="min-w-[120px]"
399398
>
399+
<Spinner v-if="isSaving" class="mr-2" />
400400
{{ t('credentials.form.buttons.save') }}
401401
</Button>
402402
</div>

services/frontend/src/components/gateway-config/ClientConfigurationModal.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
SelectValue,
1818
} from '@/components/ui/select'
1919
import { Button } from '@/components/ui/button'
20+
import { Spinner } from '@/components/ui/spinner'
2021
import { GatewayConfigService, type ClientConfigResponse, type ClientInfo, type ConfigAction } from '@/services/satelliteConfigService'
2122
import { toast } from 'vue-sonner'
2223
import LinkActionRenderer from '@/components/client-config/LinkActionRenderer.vue'
@@ -239,10 +240,9 @@ function handleLinkClick(action: { url: string; name?: string }) {
239240
<Button
240241
v-if="hasCopyableContent"
241242
@click="handleCopyAndClose"
242-
:loading="isCopying"
243-
loading-text="Copying..."
244-
:disabled="!copyableContent || isLoading || isLoadingClients"
243+
:disabled="isCopying || !copyableContent || isLoading || isLoadingClients"
245244
>
245+
<Spinner v-if="isCopying" class="mr-2" />
246246
{{ t('satelliteConfig.button.copyAndClose') }}
247247
</Button>
248248
</AlertDialogFooter>

0 commit comments

Comments
 (0)