Files
nocodb/packages/nc-gui/components/general/ProgressPanel.vue
2026-01-14 18:24:21 +07:00

145 lines
3.8 KiB
Vue

<script lang="ts" setup>
import type { Card as AntCard } from 'ant-design-vue'
import { JobStatus, iconMap } from '#imports'
const progress = ref<Record<string, any>[]>([])
const logRef = ref<typeof AntCard>()
const autoScroll = ref(true)
const scrollToBottom = () => {
const container: HTMLDivElement = logRef.value?.$el?.firstElementChild
if (!container) return
container.scrollTop = container.scrollHeight
}
const pushProgress = (message: string, status: JobStatus | 'progress' | 'warning') => {
if (!message?.trim()) return
progress.value.push({
msg: message,
status: status === JobStatus.FAILED ? JobStatus.FAILED : message.startsWith('WARNING: ') ? 'warning' : status,
})
if (autoScroll.value) {
nextTick(scrollToBottom)
}
}
const onUserScroll = (e: Event) => {
const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLDivElement
// If user is not at the bottom, disable auto-scroll
autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10
}
function downloadLogs(filename: string) {
let text = ''
for (const { msg } of progress.value) {
text += `${msg}\n`
}
const element = document.createElement('a')
element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`)
element.setAttribute('download', filename)
element.style.display = 'none'
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
}
const progressEnd = computed(() =>
[JobStatus.FAILED, JobStatus.COMPLETED].includes(progress.value[progress.value.length - 1]?.status),
)
useEventListener(() => logRef.value?.$el?.firstElementChild, 'scroll', onUserScroll, {
passive: true,
})
defineExpose({
pushProgress,
})
onMounted(() => {
scrollToBottom()
})
</script>
<template>
<a-card
ref="logRef"
:bordered="false"
:body-style="{
'overflow': 'auto',
'width': '100%',
'height': '100%',
'backgroundColor': '#101015',
'borderRadius': '0.5rem',
'padding': '16px !important',
'scrollbar-color': 'var(--scrollbar-thumb) var(--scrollbar-track)',
'scrollbar-width': 'thin',
'--scrollbar-thumb': '#E7E7E9',
'--scrollbar-track': 'transparent',
}"
>
<div v-for="({ msg, status }, i) in progress" :key="i">
<div v-if="status === JobStatus.FAILED" class="flex items-center">
<component :is="iconMap.closeCircle" class="text-red-500" />
<span class="text-red-500 ml-2">{{ msg }}</span>
</div>
<div v-else-if="status === 'warning'" class="flex items-center">
<component :is="iconMap.warning" class="text-yellow-500" />
<span class="text-yellow-500 ml-2">{{ msg }}</span>
</div>
<div v-else class="flex items-center">
<MdiCurrencyUsd class="text-green-500" />
<span class="text-green-500 ml-2">{{ msg }}</span>
</div>
</div>
<div v-if="!progressEnd" class="flex items-center">
<component :is="iconMap.loading" class="text-green-500 animate-spin" />
<span class="text-green-500 ml-2">Loading...</span>
</div>
<NcButton
v-if="progressEnd"
class="!absolute z-1 right-2 bottom-2 opacity-75 hover:opacity-100 !rounded-md !w-8 !h-8"
size="small"
type="secondary"
@click="downloadLogs('logs.txt')"
>
<nc-tooltip>
<template #title>Download Logs</template>
<component :is="iconMap.download" />
</nc-tooltip>
</NcButton>
</a-card>
</template>
<style lang="scss" scoped>
.nc-progress-panel {
@apply p-6 flex-1 flex justify-center;
}
</style>
<style lang="scss">
.nc-modal-create-source {
.nc-modal {
@apply !p-0;
height: min(calc(100vh - 100px), 1024px);
max-height: min(calc(100vh - 100px), 1024px) !important;
}
}
.nc-dropdown-ext-db-type {
@apply !z-1000;
}
</style>