feat: backups page cleanup before worlds (#5844)
* feat: card alignment + fix modals * feat: change admon title in restore alert modal * fix: lint * feat: backups queue api into api-client * feat: impl backup queue api endpoints into frontend * feat: ack fix * feat: bulk actions * feat: bulk delete impl * fix: lint * fix: align error states * fix: transition group * feat: ready for qa * fix: lint * feat: qa * feat: stacked admonitions component * fix: issues with stacking * feat: hook up admonition stacking + fix app csp for staging kyros nodes * fix: logs.vue * qa: close stack on admonitions click * fix: all problems with stacked admonitions * qa: admonition cleanup and copy overhaul draft * fix: qa issues padding * fix: padding bug * feat: qa * fix: intercom in app csp bug * fix: positioning intercom * feat: loading overlay on top of console + admon consistency changes * feat: scroll indicator fade in backup delete modal + admon timestamp fix * feat: move action bar behind modal * fix: lint + i18n * fix: server ping spam on filter (cache but clear on unmount) * fix: 1 admon fade in flicker issue * chore: temp staging undo * qa: changes * fix: lint * chore: revert staging to use staging * fix: scoping
This commit is contained in:
@@ -1,72 +1,95 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'relative flex flex-col rounded-2xl border-[1px] border-solid p-4 gap-3 text-contrast',
|
||||
'relative grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-start gap-x-2 rounded-2xl border border-solid p-4 text-contrast',
|
||||
progress != null ? 'overflow-hidden pb-5' : '',
|
||||
typeClasses[type],
|
||||
]"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<slot name="icon" :icon-class="['h-6 w-6 flex-none', iconClasses[type]]">
|
||||
<component :is="getSeverityIcon(type)" :class="['h-6 w-6 flex-none', iconClasses[type]]" />
|
||||
</slot>
|
||||
<div class="col-start-2 flex min-w-0 flex-1 flex-col gap-2">
|
||||
<div
|
||||
:class="[
|
||||
'flex flex-1 gap-2',
|
||||
header || $slots.header ? 'flex-col items-start' : 'items-center',
|
||||
(dismissible || $slots['top-right-actions']) && 'pr-8',
|
||||
]"
|
||||
v-if="header || $slots.header || normalizedTimestamp"
|
||||
class="flex flex-wrap items-center gap-2 text-lg font-bold leading-6"
|
||||
>
|
||||
<div
|
||||
class="flex gap-2 items-start"
|
||||
:class="header || $slots.header ? 'w-full' : 'contents'"
|
||||
<slot name="header">{{ header }}</slot>
|
||||
<span
|
||||
v-if="normalizedTimestamp"
|
||||
v-tooltip="timestampTooltip"
|
||||
class="flex items-center gap-1.5 text-base font-medium leading-normal text-secondary"
|
||||
>
|
||||
<slot name="icon" :icon-class="['h-6 w-6 flex-none', iconClasses[type]]">
|
||||
<component
|
||||
:is="getSeverityIcon(type)"
|
||||
:class="['h-6 w-6 flex-none', iconClasses[type]]"
|
||||
/>
|
||||
</slot>
|
||||
<div v-if="header || $slots.header" class="font-semibold text-base">
|
||||
<slot name="header">{{ header }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-normal text-contrast/80" :class="!(header || $slots.header) && 'flex-1'">
|
||||
<slot>{{ body }}</slot>
|
||||
</div>
|
||||
<ClockIcon class="size-4" />
|
||||
{{ relativeTimeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="$slots['top-right-actions']" class="flex shrink-0 items-center gap-2">
|
||||
<slot name="top-right-actions" />
|
||||
<div class="font-normal text-contrast/85">
|
||||
<slot>{{ body }}</slot>
|
||||
</div>
|
||||
<div v-if="showActionsUnderneath || $slots.actions" class="mt-2">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots['top-right-actions'] || dismissible"
|
||||
class="col-start-3 row-start-1 flex shrink-0 items-center gap-2 self-start"
|
||||
>
|
||||
<slot name="top-right-actions" />
|
||||
<ButtonStyled
|
||||
v-else-if="dismissible"
|
||||
v-if="dismissible"
|
||||
circular
|
||||
type="highlight-colored-text"
|
||||
type="transparent"
|
||||
:color="buttonColors[type]"
|
||||
hover-color-fill="background"
|
||||
>
|
||||
<button aria-label="Dismiss" class="absolute top-3 right-3" @click="$emit('dismiss')">
|
||||
<XIcon class="h-4 w-4" />
|
||||
<button type="button" aria-label="Dismiss" @click="$emit('dismiss')">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="$slots.progress">
|
||||
<slot name="progress" />
|
||||
</div>
|
||||
<div v-if="showActionsUnderneath || $slots.actions">
|
||||
<slot name="actions" />
|
||||
<div
|
||||
v-if="progress != null"
|
||||
class="absolute inset-x-0 bottom-0 h-1 overflow-hidden"
|
||||
:class="progressTrackClasses[type]"
|
||||
role="progressbar"
|
||||
:aria-valuenow="waiting ? undefined : Math.round(normalizedProgress * 100)"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-r-full transition-[width] duration-200 ease-in-out"
|
||||
:class="[
|
||||
progressFillClasses[progressColor ?? type],
|
||||
{ 'admonition-progress--waiting': waiting },
|
||||
]"
|
||||
:style="waiting ? undefined : { width: `${normalizedProgress * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { ClockIcon, XIcon } from '@modrinth/assets'
|
||||
import { useNow } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFormatDateTime, useRelativeTime } from '../../composables'
|
||||
import { getSeverityIcon } from '../../utils'
|
||||
import ButtonStyled from './ButtonStyled.vue'
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: 'info' | 'warning' | 'critical' | 'success'
|
||||
header?: string
|
||||
body?: string
|
||||
showActionsUnderneath?: boolean
|
||||
dismissible?: boolean
|
||||
progress?: number
|
||||
progressColor?: 'info' | 'warning' | 'critical' | 'success' | 'blue' | 'green' | 'red'
|
||||
waiting?: boolean
|
||||
/** Accepts a Date, an ISO string, or a millisecond Unix timestamp. */
|
||||
timestamp?: Date | string | number
|
||||
}>(),
|
||||
{
|
||||
type: 'info',
|
||||
@@ -74,6 +97,10 @@ withDefaults(
|
||||
body: '',
|
||||
showActionsUnderneath: false,
|
||||
dismissible: false,
|
||||
progress: undefined,
|
||||
progressColor: undefined,
|
||||
waiting: false,
|
||||
timestamp: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -81,6 +108,34 @@ defineEmits<{
|
||||
dismiss: []
|
||||
}>()
|
||||
|
||||
const relativeTime = useRelativeTime()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
const now = useNow({ interval: 1000 })
|
||||
|
||||
const normalizedProgress = computed(() => Math.min(Math.max(props.progress ?? 0, 0), 1))
|
||||
|
||||
const normalizedTimestamp = computed(() => {
|
||||
const t = props.timestamp
|
||||
if (t == null) return null
|
||||
if (t instanceof Date) return t.toISOString()
|
||||
if (typeof t === 'number') return new Date(t).toISOString()
|
||||
return t
|
||||
})
|
||||
|
||||
const relativeTimeLabel = computed(() => {
|
||||
void now.value
|
||||
const t = normalizedTimestamp.value
|
||||
return t ? relativeTime(t) : ''
|
||||
})
|
||||
|
||||
const timestampTooltip = computed(() => {
|
||||
const t = normalizedTimestamp.value
|
||||
return t ? formatDateTime(t) : ''
|
||||
})
|
||||
|
||||
const typeClasses = {
|
||||
info: 'border-brand-blue bg-bg-blue',
|
||||
warning: 'border-brand-orange bg-bg-orange',
|
||||
@@ -95,10 +150,45 @@ const iconClasses = {
|
||||
success: 'text-brand-green',
|
||||
}
|
||||
|
||||
const buttonColors: Record<string, 'blue' | 'orange' | 'red' | 'green'> = {
|
||||
const buttonColors = {
|
||||
info: 'blue',
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
success: 'green',
|
||||
} as const
|
||||
|
||||
const progressTrackClasses = {
|
||||
info: 'bg-brand-blue/20',
|
||||
warning: 'bg-brand-orange/20',
|
||||
critical: 'bg-brand-red/20',
|
||||
success: 'bg-brand-green/20',
|
||||
}
|
||||
|
||||
const progressFillClasses = {
|
||||
info: 'bg-brand-blue',
|
||||
warning: 'bg-brand-orange',
|
||||
critical: 'bg-brand-red',
|
||||
success: 'bg-brand-green',
|
||||
blue: 'bg-brand-blue',
|
||||
green: 'bg-brand-green',
|
||||
red: 'bg-brand-red',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admonition-progress--waiting {
|
||||
animation: admonition-progress-waiting 1s linear infinite;
|
||||
position: relative;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
@keyframes admonition-progress-waiting {
|
||||
0% {
|
||||
left: -20%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user