mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user