feat: shared loading state + cleanup loading state management (#5835)
* feat: implement shared loading bar component and polished loading states across the app * feat: align loading states + ensureQueryData changes * fix: lint + bugs * fix: skeleton for manage servers page * fix: merge conflict fix
This commit is contained in:
@@ -27,122 +27,122 @@
|
||||
</div>
|
||||
|
||||
<div v-else key="content" class="contents">
|
||||
<BackupCreateModal ref="createBackupModal" :backups="backupsData ?? []" />
|
||||
<BackupRenameModal ref="renameBackupModal" :backups="backupsData ?? []" />
|
||||
<BackupRestoreModal ref="restoreBackupModal" />
|
||||
<BackupDeleteModal ref="deleteBackupModal" @delete="deleteBackup" />
|
||||
<ReadyTransition :pending="backupsReadyPending">
|
||||
<BackupCreateModal ref="createBackupModal" :backups="backupsData ?? []" />
|
||||
<BackupRenameModal ref="renameBackupModal" :backups="backupsData ?? []" />
|
||||
<BackupRestoreModal ref="restoreBackupModal" />
|
||||
<BackupDeleteModal ref="deleteBackupModal" @delete="deleteBackup" />
|
||||
|
||||
<div v-if="backupsData?.length" class="mb-2 flex items-center align-middle justify-between">
|
||||
<span class="text-2xl font-semibold text-contrast">Backups</span>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="backupCreationDisabled"
|
||||
:disabled="!!backupCreationDisabled"
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="size-5" />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="backupsData?.length" class="mb-2 flex items-center align-middle justify-between">
|
||||
<span class="text-2xl font-semibold text-contrast">Backups</span>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="backupCreationDisabled"
|
||||
:disabled="!!backupCreationDisabled"
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="size-5" />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-1.5">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div
|
||||
v-if="groupedBackups.length === 0"
|
||||
key="empty"
|
||||
class="mt-6 flex flex-col items-center justify-center gap-2 text-center text-secondary"
|
||||
>
|
||||
<template v-if="!backupsData">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Loading backups...
|
||||
</template>
|
||||
<template v-else>
|
||||
<EmptyState
|
||||
type="empty-inbox"
|
||||
heading="No backups yet"
|
||||
description="Create your first backup"
|
||||
<template v-if="backupsData">
|
||||
<div class="flex w-full flex-col gap-1.5">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div
|
||||
v-if="groupedBackups.length === 0"
|
||||
key="empty"
|
||||
class="mt-6 flex flex-col items-center justify-center gap-2 text-center text-secondary"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="backupCreationDisabled"
|
||||
:disabled="!!backupCreationDisabled"
|
||||
class="w-min mx-auto"
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="size-5" />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<EmptyState
|
||||
type="empty-inbox"
|
||||
heading="No backups yet"
|
||||
description="Create your first backup"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="backupCreationDisabled"
|
||||
:disabled="!!backupCreationDisabled"
|
||||
class="w-min mx-auto"
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="size-5" />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</div>
|
||||
|
||||
<div v-else key="list" class="flex flex-col gap-1.5">
|
||||
<template v-for="group in groupedBackups" :key="group.label">
|
||||
<div class="flex items-center gap-2">
|
||||
<component :is="group.icon" v-if="group.icon" class="size-6 text-secondary" />
|
||||
<span class="text-lg font-semibold text-secondary">{{ group.label }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex w-5 justify-center">
|
||||
<div class="h-full w-px bg-surface-5" />
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="div" class="flex flex-1 flex-col gap-3 py-3">
|
||||
<BackupItem
|
||||
v-for="backup in group.backups"
|
||||
:key="`backup-${backup.id}`"
|
||||
:backup="backup"
|
||||
:restore-disabled="backupRestoreDisabled"
|
||||
:kyros-url="server.node?.instance"
|
||||
:jwt="server.node?.token"
|
||||
:show-copy-id-action="showCopyIdAction"
|
||||
:show-debug-info="showDebugInfo"
|
||||
@download="() => triggerDownloadAnimation()"
|
||||
@rename="() => renameBackupModal?.show(backup)"
|
||||
@restore="() => restoreBackupModal?.show(backup)"
|
||||
@delete="
|
||||
(skipConfirmation?: boolean) =>
|
||||
skipConfirmation
|
||||
? deleteBackup(backup)
|
||||
: deleteBackupModal?.show(backup)
|
||||
"
|
||||
@retry="() => retryBackup(backup.id)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else key="list" class="flex flex-col gap-1.5">
|
||||
<template v-for="group in groupedBackups" :key="group.label">
|
||||
<div class="flex items-center gap-2">
|
||||
<component :is="group.icon" v-if="group.icon" class="size-6 text-secondary" />
|
||||
<span class="text-lg font-semibold text-secondary">{{ group.label }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex w-5 justify-center">
|
||||
<div class="h-full w-px bg-surface-5" />
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="div" class="flex flex-1 flex-col gap-3 py-3">
|
||||
<BackupItem
|
||||
v-for="backup in group.backups"
|
||||
:key="`backup-${backup.id}`"
|
||||
:backup="backup"
|
||||
:restore-disabled="backupRestoreDisabled"
|
||||
:kyros-url="server.node?.instance"
|
||||
:jwt="server.node?.token"
|
||||
:show-copy-id-action="showCopyIdAction"
|
||||
:show-debug-info="showDebugInfo"
|
||||
@download="() => triggerDownloadAnimation()"
|
||||
@rename="() => renameBackupModal?.show(backup)"
|
||||
@restore="() => restoreBackupModal?.show(backup)"
|
||||
@delete="
|
||||
(skipConfirmation?: boolean) =>
|
||||
skipConfirmation ? deleteBackup(backup) : deleteBackupModal?.show(backup)
|
||||
"
|
||||
@retry="() => retryBackup(backup.id)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="over-the-top-download-animation"
|
||||
:class="{ 'animation-hidden': !overTheTopDownloadAnimation }"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-1 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight"
|
||||
>
|
||||
<DownloadIcon class="h-20 w-20 text-contrast" />
|
||||
<div
|
||||
class="over-the-top-download-animation"
|
||||
:class="{ 'animation-hidden': !overTheTopDownloadAnimation }"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-1 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight"
|
||||
>
|
||||
<DownloadIcon class="h-20 w-20 text-contrast" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ReadyTransition>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { CalendarIcon, DownloadIcon, IssuesIcon, PlusIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { CalendarIcon, DownloadIcon, IssuesIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import dayjs from 'dayjs'
|
||||
import type { Component } from 'vue'
|
||||
@@ -151,11 +151,13 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import EmptyState from '#ui/components/base/EmptyState.vue'
|
||||
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
|
||||
import BackupCreateModal from '#ui/components/servers/backups/BackupCreateModal.vue'
|
||||
import BackupDeleteModal from '#ui/components/servers/backups/BackupDeleteModal.vue'
|
||||
import BackupItem from '#ui/components/servers/backups/BackupItem.vue'
|
||||
import BackupRenameModal from '#ui/components/servers/backups/BackupRenameModal.vue'
|
||||
import BackupRestoreModal from '#ui/components/servers/backups/BackupRestoreModal.vue'
|
||||
import { useReadyState } from '#ui/composables'
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
@@ -184,13 +186,17 @@ defineEmits(['onDownload'])
|
||||
const backupsQueryKey = ['backups', 'list', serverId]
|
||||
const {
|
||||
data: backupsData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: backupsQueryKey,
|
||||
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
})
|
||||
|
||||
const backupsReadyPending = useReadyState({ isLoading, data: backupsData })
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (backupId: string) =>
|
||||
client.archon.backups_v1.delete(serverId, worldId.value!, backupId),
|
||||
|
||||
Reference in New Issue
Block a user