feat(datasource): enhance DataSourceSettings and DataSourceSyncLogs functionality

- Implemented polling mechanism in DataSourceSettings for real-time updates on data source synchronization status.
- Improved loading state management and user feedback during data source operations.
- Added load more functionality in DataSourceSyncLogs for better log retrieval experience.
- Updated UI elements for consistency, including icon changes and styling adjustments for better visual clarity.
This commit is contained in:
wizardchen
2026-03-30 20:10:08 +08:00
committed by lyingbug
parent 442c340e7f
commit d2d1d6d59a
3 changed files with 135 additions and 28 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
import { useI18n } from 'vue-i18n'
import {
@@ -26,17 +26,39 @@ const editingDs = ref<DataSource | null>(null)
const logsVisible = ref(false)
const logsDsId = ref('')
const logsDsName = ref('')
const pollTimer = ref<number | null>(null)
async function loadList() {
loading.value = true
function stopPolling() {
if (pollTimer.value !== null) {
window.clearTimeout(pollTimer.value)
pollTimer.value = null
}
}
function schedulePolling() {
stopPolling()
pollTimer.value = window.setTimeout(() => {
loadList(true)
}, 3000)
}
async function loadList(silent = false) {
if (!silent) loading.value = true
try {
const res = await listDataSources(props.kbId)
dataSources.value = res?.data || res || []
emit('count', dataSources.value.length)
const hasRunningSync = dataSources.value.some(ds => ds.latest_sync_log?.status === 'running')
if (hasRunningSync) {
schedulePolling()
} else {
stopPolling()
}
} catch (e: any) {
console.error(e)
} finally {
loading.value = false
if (!silent) loading.value = false
}
}
@@ -79,7 +101,7 @@ async function handleSync(ds: DataSource) {
try {
await triggerSync(ds.id)
MessagePlugin.success(t('datasource.syncTriggered'))
loadList()
await loadList(true)
} catch (e: any) {
MessagePlugin.error(e?.message || e?.error || t('datasource.syncFailed'))
}
@@ -167,12 +189,17 @@ function lastSyncStatusColor(ds: DataSource) {
}
}
function isSyncRunning(ds: DataSource) {
return ds.latest_sync_log?.status === 'running'
}
function onEditorSaved() {
editorVisible.value = false
loadList()
}
onMounted(loadList)
onBeforeUnmount(stopPolling)
</script>
<template>
@@ -203,7 +230,7 @@ onMounted(loadList)
<div v-for="ds in dataSources" :key="ds.id" class="ds-card">
<div class="ds-card-header">
<div class="ds-card-title-wrap">
<DataSourceTypeIcon :type="ds.type" :size="24" />
<DataSourceTypeIcon :type="ds.type" :size="32" />
<div class="ds-title-text">
<div class="ds-name-row">
<span class="ds-name" :title="ds.name">{{ ds.name }}</span>
@@ -216,14 +243,22 @@ onMounted(loadList)
</div>
<div class="ds-card-actions">
<t-tooltip :content="t('datasource.syncNow')">
<t-button size="small" variant="text" theme="primary" @click="handleSync(ds)">
<template #icon><t-icon name="refresh" /></template>
<t-tooltip :content="isSyncRunning(ds) ? t('datasource.logStatus.running') : t('datasource.syncNow')">
<t-button
size="small"
variant="text"
theme="primary"
:disabled="isSyncRunning(ds)"
@click="handleSync(ds)"
>
<template #icon>
<t-icon name="refresh" :class="{ 'ds-icon-spin': isSyncRunning(ds) }" />
</template>
</t-button>
</t-tooltip>
<t-tooltip :content="t('datasource.logs')">
<t-button size="small" variant="text" @click="openLogs(ds)">
<template #icon><t-icon name="history" /></template>
<template #icon><t-icon name="root-list" /></template>
</t-button>
</t-tooltip>
<t-dropdown trigger="click" :min-column-width="120">
@@ -394,8 +429,8 @@ onMounted(loadList)
position: relative;
background: var(--td-bg-color-container);
border: 1px solid var(--td-border-level-2-color);
border-radius: 12px;
padding: 20px;
border-radius: 8px;
padding: 15px 20px;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
@@ -527,6 +562,15 @@ onMounted(loadList)
.ds-pill.skipped { background: var(--td-bg-color-component); color: var(--td-text-color-placeholder); }
.ds-pill.failed { background: var(--td-error-color-1); color: var(--td-error-color); }
.ds-icon-spin {
animation: ds-spin 1s linear infinite;
}
@keyframes ds-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* --- Error alert --- */
.ds-card-error {
margin-top: 16px;
@@ -547,10 +591,10 @@ onMounted(loadList)
align-items: center;
justify-content: center;
gap: 12px;
padding: 15px;
padding: 6px 15px;
background: var(--td-bg-color-secondarycontainer);
border: 1px solid transparent;
border-radius: 12px;
border-radius: 8px;
color: var(--td-text-color-secondary);
font-size: 14px;
font-weight: 500;

View File

@@ -12,28 +12,50 @@ const { t } = useI18n()
const logs = ref<SyncLog[]>([])
const loading = ref(false)
const loadingMore = ref(false)
const hasMore = ref(false)
const expandedId = ref('')
const pageSize = 50
async function fetchLogs() {
async function fetchLogs(reset = true) {
if (!props.dataSourceId) return
loading.value = true
if (reset) {
loading.value = true
} else {
loadingMore.value = true
}
try {
const res = await getSyncLogs(props.dataSourceId, 50, 0)
logs.value = res?.data || res || []
const offset = reset ? 0 : logs.value.length
const res = await getSyncLogs(props.dataSourceId, pageSize, offset)
const items = res?.data || res || []
logs.value = reset ? items : [...logs.value, ...items]
hasMore.value = items.length === pageSize
} catch { /* ignore */ }
loading.value = false
if (reset) {
loading.value = false
} else {
loadingMore.value = false
}
}
watch(visible, (v) => {
if (!v) return
expandedId.value = ''
fetchLogs()
fetchLogs(true)
})
function toggleExpand(id: string) {
expandedId.value = expandedId.value === id ? '' : id
}
function loadMore() {
if (loading.value || loadingMore.value || !hasMore.value) return
fetchLogs(false)
}
// --- Stats ---
const stats = computed(() => {
const total = logs.value.length
@@ -154,7 +176,7 @@ const groupedLogs = computed(() => {
<div v-if="loading" style="text-align:center;padding:60px"><t-loading /></div>
<div v-else-if="logs.length === 0" class="logs-empty">
<t-icon name="history" size="40px" />
<t-icon name="root-list" size="40px" />
<p>{{ t('datasource.noLogs') }}</p>
</div>
@@ -238,6 +260,19 @@ const groupedLogs = computed(() => {
</div>
</div>
</div>
<div class="logs-load-more">
<t-button
v-if="hasMore"
variant="outline"
block
:loading="loadingMore"
@click="loadMore"
>
{{ t('common.loadMore') }}
</t-button>
<span v-else class="logs-load-more-text">{{ t('common.noMoreData') }}</span>
</div>
</div>
</template>
</t-drawer>
@@ -326,6 +361,16 @@ const groupedLogs = computed(() => {
margin-bottom: 4px;
}
.logs-load-more {
padding: 16px 0 8px;
text-align: center;
}
.logs-load-more-text {
font-size: 12px;
color: var(--td-text-color-placeholder);
}
/* --- Dot column: continuous line --- */
.tl-dot-col {
display: flex;

View File

@@ -272,7 +272,9 @@ func (s *DataSourceService) ManualSync(ctx context.Context, dsID string) (*types
return nil, err
}
if ds.Status != types.DataSourceStatusActive && ds.Status != types.DataSourceStatusError {
if ds.Status != types.DataSourceStatusActive &&
ds.Status != types.DataSourceStatusError &&
ds.Status != types.DataSourceStatusPaused {
return nil, datasource.ErrDataSourceNotActive
}
@@ -307,7 +309,9 @@ func (s *DataSourceService) ManualSync(ctx context.Context, dsID string) (*types
syncLog.FinishedAt = timePtr(time.Now().UTC())
syncLog.ErrorMessage = err.Error()
_ = s.syncLogRepo.Update(ctx, syncLog)
ds.Status = types.DataSourceStatusError
if ds.Status != types.DataSourceStatusPaused {
ds.Status = types.DataSourceStatusError
}
ds.ErrorMessage = fmt.Sprintf("Failed to enqueue sync: %v", err)
_ = s.dsRepo.Update(ctx, ds)
return nil, err
@@ -408,6 +412,8 @@ func (s *DataSourceService) ProcessSync(ctx context.Context, task *asynq.Task) e
return nil
}
wasPaused := ds.Status == types.DataSourceStatusPaused
// Get connector
connector, err := s.connectorRegistry.Get(ds.Type)
if err != nil {
@@ -416,7 +422,9 @@ func (s *DataSourceService) ProcessSync(ctx context.Context, task *asynq.Task) e
syncLog.FinishedAt = timePtr(time.Now().UTC())
syncLog.ErrorMessage = fmt.Sprintf("Connector not found: %s", ds.Type)
_ = s.syncLogRepo.Update(ctx, syncLog)
ds.Status = types.DataSourceStatusError
if !wasPaused {
ds.Status = types.DataSourceStatusError
}
ds.ErrorMessage = syncLog.ErrorMessage
_ = s.dsRepo.Update(ctx, ds)
return err
@@ -430,7 +438,9 @@ func (s *DataSourceService) ProcessSync(ctx context.Context, task *asynq.Task) e
syncLog.FinishedAt = timePtr(time.Now().UTC())
syncLog.ErrorMessage = fmt.Sprintf("Invalid configuration: %v", err)
_ = s.syncLogRepo.Update(ctx, syncLog)
ds.Status = types.DataSourceStatusError
if !wasPaused {
ds.Status = types.DataSourceStatusError
}
ds.ErrorMessage = syncLog.ErrorMessage
_ = s.dsRepo.Update(ctx, ds)
return err
@@ -458,7 +468,9 @@ func (s *DataSourceService) ProcessSync(ctx context.Context, task *asynq.Task) e
syncLog.FinishedAt = timePtr(time.Now().UTC())
syncLog.ErrorMessage = fmt.Sprintf("Fetch failed: %v", fetchErr)
_ = s.syncLogRepo.Update(ctx, syncLog)
ds.Status = types.DataSourceStatusError
if !wasPaused {
ds.Status = types.DataSourceStatusError
}
ds.ErrorMessage = syncLog.ErrorMessage
_ = s.dsRepo.Update(ctx, ds)
return fetchErr
@@ -479,7 +491,9 @@ func (s *DataSourceService) ProcessSync(ctx context.Context, task *asynq.Task) e
syncLog.FinishedAt = timePtr(time.Now().UTC())
syncLog.ErrorMessage = fmt.Sprintf("Failed to get tenant info: %v", err)
_ = s.syncLogRepo.Update(ctx, syncLog)
ds.Status = types.DataSourceStatusError
if !wasPaused {
ds.Status = types.DataSourceStatusError
}
ds.ErrorMessage = syncLog.ErrorMessage
_ = s.dsRepo.Update(ctx, ds)
return err
@@ -553,7 +567,11 @@ func (s *DataSourceService) ProcessSync(ctx context.Context, task *asynq.Task) e
}
ds.LastSyncAt = timePtr(time.Now().UTC())
ds.Status = types.DataSourceStatusActive
if wasPaused {
ds.Status = types.DataSourceStatusPaused
} else {
ds.Status = types.DataSourceStatusActive
}
ds.ErrorMessage = ""
// Store result