release: bump version to 0.65.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
ci / backend (pull_request) Has been cancelled
ci / frontend (pull_request) Has been cancelled
ci / delivery (pull_request) Has been cancelled
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
ci / backend (pull_request) Has been cancelled
ci / frontend (pull_request) Has been cancelled
ci / delivery (pull_request) Has been cancelled
This commit is contained in:
@@ -16,6 +16,13 @@ interface DataTableProps<TData> {
|
||||
data: TData[]
|
||||
getRowId?: (row: TData, index: number) => string
|
||||
getRowClassName?: (row: TData) => string | undefined
|
||||
selection?: {
|
||||
selectedRowIds: Set<string>
|
||||
onToggleAllVisible: (rowIds: string[]) => void
|
||||
onToggleRow: (rowId: string, row: TData) => void
|
||||
getCheckboxLabel?: (row: TData) => string
|
||||
isRowSelectable?: (row: TData) => boolean
|
||||
}
|
||||
loading?: boolean
|
||||
emptyText?: string
|
||||
className?: string
|
||||
@@ -28,6 +35,7 @@ export function DataTable<TData>({
|
||||
data,
|
||||
getRowId,
|
||||
getRowClassName,
|
||||
selection,
|
||||
loading = false,
|
||||
emptyText = '暂无数据',
|
||||
className = '',
|
||||
@@ -46,6 +54,13 @@ export function DataTable<TData>({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
})
|
||||
const visibleSelectableRows = selection
|
||||
? table.getRowModel().rows.filter((row) => selection.isRowSelectable?.(row.original) ?? true)
|
||||
: []
|
||||
const visibleSelectableIds = visibleSelectableRows.map((row) => row.id)
|
||||
const allVisibleSelected = visibleSelectableIds.length > 0 && visibleSelectableIds.every((rowId) => selection?.selectedRowIds.has(rowId))
|
||||
const someVisibleSelected = visibleSelectableIds.some((rowId) => selection?.selectedRowIds.has(rowId))
|
||||
const columnCount = columns.length + (selection ? 1 : 0)
|
||||
|
||||
return (
|
||||
<div className={`an-data-table ${className}`}>
|
||||
@@ -55,6 +70,20 @@ export function DataTable<TData>({
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{selection ? (
|
||||
<th className="an-data-table__selection-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="选择当前可见数据"
|
||||
checked={allVisibleSelected}
|
||||
disabled={!visibleSelectableIds.length}
|
||||
ref={(element) => {
|
||||
if (element) element.indeterminate = someVisibleSelected && !allVisibleSelected
|
||||
}}
|
||||
onChange={() => selection.onToggleAllVisible(visibleSelectableIds)}
|
||||
/>
|
||||
</th>
|
||||
) : null}
|
||||
{headerGroup.headers.map((header) => {
|
||||
const sorted = header.column.getIsSorted()
|
||||
const stickyEnd = header.column.id === 'actions' || header.column.id === 'action'
|
||||
@@ -82,7 +111,7 @@ export function DataTable<TData>({
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length}>
|
||||
<td colSpan={columnCount}>
|
||||
<div className="an-data-table__state">
|
||||
<span className="an-spinner" />
|
||||
加载中
|
||||
@@ -97,6 +126,18 @@ export function DataTable<TData>({
|
||||
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
||||
data-clickable={onRowClick ? 'true' : undefined}
|
||||
>
|
||||
{selection ? (
|
||||
<td className="an-data-table__selection-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={selection.getCheckboxLabel?.(row.original) || '选择行'}
|
||||
checked={selection.selectedRowIds.has(row.id)}
|
||||
disabled={selection.isRowSelectable ? !selection.isRowSelectable(row.original) : false}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onChange={() => selection.onToggleRow(row.id, row.original)}
|
||||
/>
|
||||
</td>
|
||||
) : null}
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} data-sticky-end={cell.column.id === 'actions' || cell.column.id === 'action' || undefined}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
@@ -106,7 +147,7 @@ export function DataTable<TData>({
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length}>
|
||||
<td colSpan={columnCount}>
|
||||
<div className="an-data-table__state">{emptyText}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -42,6 +42,14 @@ export function AdminThemeProvider({ children }: { children: ReactNode }) {
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return
|
||||
document.body.dataset.adminNextTheme = theme
|
||||
return () => {
|
||||
delete document.body.dataset.adminNextTheme
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
const value = useMemo(() => ({ mode, theme, setMode }), [mode, setMode, theme])
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FileText,
|
||||
Globe2,
|
||||
ImageUp,
|
||||
ListChecks,
|
||||
Radio,
|
||||
Redo2,
|
||||
RefreshCw,
|
||||
@@ -527,12 +528,20 @@ function ModuleTable({
|
||||
selected,
|
||||
onSelect,
|
||||
columns,
|
||||
selection,
|
||||
loading,
|
||||
}: {
|
||||
rows: TableRecord[]
|
||||
selected: TableRecord | null
|
||||
onSelect: (record: TableRecord) => void
|
||||
columns?: Array<ColumnDef<TableRecord>>
|
||||
selection?: {
|
||||
selectedRowIds: Set<string>
|
||||
onToggleAllVisible: (rowIds: string[]) => void
|
||||
onToggleRow: (rowId: string, row: TableRecord) => void
|
||||
getCheckboxLabel?: (row: TableRecord) => string
|
||||
isRowSelectable?: (row: TableRecord) => boolean
|
||||
}
|
||||
loading?: boolean
|
||||
}) {
|
||||
const tableColumns = useMemo(() => columns || defaultColumns(onSelect), [columns, onSelect])
|
||||
@@ -546,6 +555,7 @@ function ModuleTable({
|
||||
getRowId={(row) => row.__rowId}
|
||||
loading={loading}
|
||||
onRowClick={onSelect}
|
||||
selection={selection}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -2025,6 +2035,8 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
const [rowActionLoading, setRowActionLoading] = useState<Record<string, boolean>>({})
|
||||
const [collectionQueue, setCollectionQueue] = useState<CollectionQueueItem[]>([])
|
||||
const [collectionQueueOpen, setCollectionQueueOpen] = useState(false)
|
||||
const collectionQueueRef = useRef<HTMLDivElement>(null)
|
||||
const [datasourceSelectedRowIds, setDatasourceSelectedRowIds] = useState<Set<string>>(() => new Set())
|
||||
const pendingDatasourceTasksRef = useRef<Record<string, { sourceId: string; source?: string; name: string; taskId?: number | string | null; completed?: boolean }>>({})
|
||||
const completedDatasourceTasksRef = useRef<Set<string>>(new Set())
|
||||
const datasourcePollTimersRef = useRef<Record<string, number>>({})
|
||||
@@ -2038,11 +2050,8 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
const [visibleSecretFields, setVisibleSecretFields] = useState<Record<string, boolean>>({})
|
||||
const [smtpTestEmail, setSmtpTestEmail] = useState('')
|
||||
const [brandUploadFile, setBrandUploadFile] = useState<File | null>(null)
|
||||
const [batchTriggerOpen, setBatchTriggerOpen] = useState(false)
|
||||
const [resolveTarget, setResolveTarget] = useState<TableRecord | null>(null)
|
||||
const [resolutionText, setResolutionText] = useState('已处理')
|
||||
const [batchTriggerIds, setBatchTriggerIds] = useState('')
|
||||
const [batchTriggerForce, setBatchTriggerForce] = useState(false)
|
||||
const [credentialGuide, setCredentialGuide] = useState<AnyRecord | null>(null)
|
||||
const [credentialGuideOpen, setCredentialGuideOpen] = useState(false)
|
||||
const [credentialGuidePosition, setCredentialGuidePosition] = useState<{ x: number; y: number } | null>(null)
|
||||
@@ -2095,6 +2104,23 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
window.addEventListener('pointerup', stopMove, { once: true })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!collectionQueueOpen) return
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
if (collectionQueueRef.current?.contains(event.target as Node)) return
|
||||
setCollectionQueueOpen(false)
|
||||
}
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setCollectionQueueOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handlePointerDown)
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handlePointerDown)
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [collectionQueueOpen])
|
||||
|
||||
useEffect(() => {
|
||||
datasourceFiltersRef.current = datasourceFilters
|
||||
if (config === configs.datasources) {
|
||||
@@ -2105,19 +2131,19 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
useEffect(() => {
|
||||
if (config !== configs.datasources) return
|
||||
const next = datasourceFiltersFromSearch(location.search)
|
||||
setDatasourceFilters((current) => (
|
||||
current.product === next.product &&
|
||||
const current = datasourceFiltersRef.current
|
||||
const unchanged = current.product === next.product &&
|
||||
current.module === next.module &&
|
||||
current.isActive === next.isActive &&
|
||||
current.runStatus === next.runStatus &&
|
||||
current.dataStatus === next.dataStatus
|
||||
? current
|
||||
: next
|
||||
))
|
||||
if (!unchanged) setDatasourceSelectedRowIds(new Set())
|
||||
setDatasourceFilters((filters) => unchanged ? filters : next)
|
||||
}, [config, location.search])
|
||||
|
||||
const setDatasourceFilter = (key: keyof DatasourceFilters, value: string) => {
|
||||
const next = { ...datasourceFiltersRef.current, [key]: value }
|
||||
setDatasourceSelectedRowIds(new Set())
|
||||
setDatasourceFilters(next)
|
||||
const params = new URLSearchParams(location.search)
|
||||
const queryKeyByFilter: Record<keyof DatasourceFilters, string> = {
|
||||
@@ -2150,6 +2176,9 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
const load = useCallback(async (sectionKey = activeSectionKey) => {
|
||||
const section = config.sections.find((item) => item.key === sectionKey) ?? config.sections[0]
|
||||
if (!section) return
|
||||
if (config === configs.datasources && section.key === 'builtin') {
|
||||
setDatasourceSelectedRowIds(new Set())
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await (async (): Promise<SectionState> => {
|
||||
@@ -2460,6 +2489,42 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
const activeSection = useMemo(() => config.sections.find((section) => section.key === activeSectionKey) ?? config.sections[0], [activeSectionKey, config.sections])
|
||||
const activeState = useMemo(() => states.find((state) => state.section.key === activeSection?.key), [activeSection?.key, states])
|
||||
const rows = activeState?.rows ?? []
|
||||
const isDatasourceBuiltinSection = config === configs.datasources && activeSection?.key === 'builtin'
|
||||
const selectedDatasourceRows = useMemo(
|
||||
() => isDatasourceBuiltinSection ? rows.filter((row) => datasourceSelectedRowIds.has(row.__rowId)) : [],
|
||||
[datasourceSelectedRowIds, isDatasourceBuiltinSection, rows],
|
||||
)
|
||||
const selectedDatasourceIds = useMemo(
|
||||
() => selectedDatasourceRows
|
||||
.map((row) => Number(row.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0),
|
||||
[selectedDatasourceRows],
|
||||
)
|
||||
const toggleDatasourceSelection = useCallback((rowId: string) => {
|
||||
setDatasourceSelectedRowIds((current) => {
|
||||
const next = new Set(current)
|
||||
if (next.has(rowId)) {
|
||||
next.delete(rowId)
|
||||
} else {
|
||||
next.add(rowId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
const toggleAllVisibleDatasourceSelection = useCallback((rowIds: string[]) => {
|
||||
setDatasourceSelectedRowIds((current) => {
|
||||
const next = new Set(current)
|
||||
const allSelected = rowIds.length > 0 && rowIds.every((rowId) => next.has(rowId))
|
||||
rowIds.forEach((rowId) => {
|
||||
if (allSelected) {
|
||||
next.delete(rowId)
|
||||
} else {
|
||||
next.add(rowId)
|
||||
}
|
||||
})
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
const summary = sectionSummary(states)
|
||||
const collectionQueueSummary = useMemo(() => {
|
||||
const running = collectionQueue.filter((item) => item.status === 'queued' || item.status === 'running').length
|
||||
@@ -2485,6 +2550,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
|
||||
const handleSectionChange = (key: string) => {
|
||||
setActiveSectionKey(key)
|
||||
setDatasourceSelectedRowIds(new Set())
|
||||
setActiveGroupKey('')
|
||||
setHierarchyDraft('')
|
||||
setTvDraftGroup(null)
|
||||
@@ -2637,20 +2703,17 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
}
|
||||
}
|
||||
|
||||
const submitBatchTrigger = async () => {
|
||||
const sourceIds = batchTriggerIds
|
||||
.split(/[\s,,;;]+/)
|
||||
.map((item) => Number(item.trim()))
|
||||
.filter((item) => Number.isFinite(item) && item > 0)
|
||||
const triggerSelectedDatasources = async () => {
|
||||
const sourceIds = selectedDatasourceIds
|
||||
if (!sourceIds.length) {
|
||||
toast({ title: '请输入数据源 ID', description: '可以用逗号、空格或换行分隔。', tone: 'error' })
|
||||
toast({ title: '请先勾选数据源', description: '勾选内置源后,主触发按钮会只触发所选数据源。', tone: 'error' })
|
||||
return
|
||||
}
|
||||
setActionLoading(true)
|
||||
try {
|
||||
const response = await axios.post(apiPath('/datasources/trigger-batch'), {
|
||||
source_ids: sourceIds,
|
||||
force: batchTriggerForce,
|
||||
force: false,
|
||||
})
|
||||
addBatchQueueResult(response.data)
|
||||
arrayAt(response.data, 'triggered').forEach((item) => {
|
||||
@@ -2665,15 +2728,23 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
__status: '已提交',
|
||||
__metric: `${sourceIds.length} 个数据源`,
|
||||
}])
|
||||
setBatchTriggerOpen(false)
|
||||
toast({ title: '批量触发已提交', tone: 'success' })
|
||||
setDatasourceSelectedRowIds(new Set())
|
||||
toast({ title: '已触发所选数据源', description: `${sourceIds.length} 个数据源已提交。`, tone: 'success' })
|
||||
} catch (error) {
|
||||
toast({ title: '批量触发失败', description: error instanceof Error ? error.message : '接口请求失败', tone: 'error' })
|
||||
toast({ title: '触发已选失败', description: error instanceof Error ? error.message : '接口请求失败', tone: 'error' })
|
||||
} finally {
|
||||
setActionLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const triggerDatasourcePrimaryAction = async () => {
|
||||
if (selectedDatasourceIds.length) {
|
||||
await triggerSelectedDatasources()
|
||||
return
|
||||
}
|
||||
await triggerAllDatasources()
|
||||
}
|
||||
|
||||
const triggerAllDatasources = async () => {
|
||||
setActionLoading(true)
|
||||
try {
|
||||
@@ -4880,12 +4951,17 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
const renderModuleActions = () => {
|
||||
const actions: ReactNode[] = []
|
||||
if (config === configs.datasources) {
|
||||
const selectedCount = selectedDatasourceIds.length
|
||||
actions.push(
|
||||
<Button key="trigger-all" variant="primary" icon="trigger" onClick={() => void triggerAllDatasources()} loading={actionLoading} title="触发全部数据源">
|
||||
触发全部
|
||||
</Button>,
|
||||
<Button key="trigger-batch" size="icon" variant="subtle" onClick={() => setBatchTriggerOpen(true)} loading={actionLoading} title="批量触发数据源" aria-label="批量触发数据源">
|
||||
<DatabaseZap size={15} />
|
||||
<Button
|
||||
key="trigger-primary"
|
||||
variant="primary"
|
||||
icon="trigger"
|
||||
onClick={() => void triggerDatasourcePrimaryAction()}
|
||||
loading={actionLoading}
|
||||
title={selectedCount ? `触发已选 ${selectedCount} 个数据源` : '触发全部数据源'}
|
||||
>
|
||||
{selectedCount ? `触发已选 ${selectedCount}` : '触发全部'}
|
||||
</Button>,
|
||||
)
|
||||
}
|
||||
@@ -5035,8 +5111,8 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
void triggerDatasourceWithPrecheck(record)
|
||||
}
|
||||
|
||||
const renderCollectionQueue = () => {
|
||||
if (config !== configs.datasources || !collectionQueueSummary.total) return null
|
||||
const renderCollectionQueuePanel = () => {
|
||||
if (config !== configs.datasources) return null
|
||||
const groups: Array<{ key: string; title: string; items: CollectionQueueItem[] }> = [
|
||||
{ key: 'running', title: '运行中', items: collectionQueue.filter((item) => item.status === 'queued' || item.status === 'running') },
|
||||
{ key: 'failed', title: '失败', items: collectionQueue.filter((item) => item.status === 'failed') },
|
||||
@@ -5055,9 +5131,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
<span>跳过 {collectionQueueSummary.skipped}</span>
|
||||
</div>
|
||||
<div className="an-collection-queue__actions">
|
||||
<Button size="sm" variant="subtle" onClick={() => setCollectionQueueOpen((open) => !open)}>
|
||||
{collectionQueueOpen ? '收起队列' : '查看队列'}
|
||||
</Button>
|
||||
{collectionQueueSummary.running === 0 ? (
|
||||
<Button size="icon" variant="subtle" title="清空已结束队列项" aria-label="清空已结束队列项" onClick={() => setCollectionQueue((items) => items.filter((item) => item.status === 'queued' || item.status === 'running'))}>
|
||||
<X size={14} />
|
||||
@@ -5068,7 +5141,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
<div className="an-collection-queue__track" aria-hidden="true">
|
||||
<span style={{ width: `${collectionQueueSummary.progress}%` }} />
|
||||
</div>
|
||||
{collectionQueueOpen ? (
|
||||
{collectionQueueSummary.total ? (
|
||||
<div className="an-collection-queue__panel">
|
||||
{groups.map((group) => (
|
||||
<section key={group.key} className="an-collection-queue__group">
|
||||
@@ -5097,6 +5170,43 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="an-collection-queue__empty an-collection-queue__empty--panel">暂无采集任务</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderCollectionQueueAction = () => {
|
||||
if (config !== configs.datasources) return null
|
||||
const hasQueue = collectionQueueSummary.total > 0
|
||||
const queueLabel = hasQueue
|
||||
? `采集队列 ${collectionQueueSummary.progress}%,运行 ${collectionQueueSummary.running},完成 ${collectionQueueSummary.completed},失败 ${collectionQueueSummary.failed},跳过 ${collectionQueueSummary.skipped}`
|
||||
: '采集队列,暂无采集任务'
|
||||
return (
|
||||
<div className="an-collection-queue-anchor" ref={collectionQueueRef}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="subtle"
|
||||
title={queueLabel}
|
||||
aria-label={queueLabel}
|
||||
aria-expanded={collectionQueueOpen}
|
||||
onClick={() => setCollectionQueueOpen((open) => !open)}
|
||||
>
|
||||
{hasQueue ? (
|
||||
<span
|
||||
className="an-collection-queue-trigger an-collection-queue-trigger--progress"
|
||||
style={{ '--queue-progress': `${collectionQueueSummary.progress}%` } as CSSProperties}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ListChecks size={15} />
|
||||
)}
|
||||
</Button>
|
||||
{collectionQueueOpen ? (
|
||||
<div className="an-collection-queue-popover">
|
||||
{renderCollectionQueuePanel()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
@@ -5126,6 +5236,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
actions={(
|
||||
<>
|
||||
<Button size="icon" variant="subtle" onClick={() => void load()} loading={loading} title="刷新" aria-label="刷新"><RefreshCw size={15} /></Button>
|
||||
{renderCollectionQueueAction()}
|
||||
{moduleActions}
|
||||
{config.actions.map((action) => (
|
||||
<Button key={action.label} asChild size="icon" variant="subtle" title={action.label} aria-label={action.label}>
|
||||
@@ -5144,7 +5255,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
|
||||
<SectionTabs sections={config.sections} states={states} activeKey={activeSection?.key || ''} onChange={handleSectionChange} />
|
||||
{renderDatasourceFilters()}
|
||||
{renderCollectionQueue()}
|
||||
</div>
|
||||
|
||||
{isPlaygroundSection ? (
|
||||
@@ -5163,7 +5273,19 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
</div>
|
||||
<div className="an-panel-body">
|
||||
{loading || rows.length > 0 ? (
|
||||
<ModuleTable rows={rows} selected={selected} onSelect={openResourceDetail} columns={config.columns} loading={loading} />
|
||||
<ModuleTable
|
||||
rows={rows}
|
||||
selected={selected}
|
||||
onSelect={openResourceDetail}
|
||||
columns={config.columns}
|
||||
loading={loading}
|
||||
selection={isDatasourceBuiltinSection ? {
|
||||
selectedRowIds: datasourceSelectedRowIds,
|
||||
onToggleAllVisible: toggleAllVisibleDatasourceSelection,
|
||||
onToggleRow: toggleDatasourceSelection,
|
||||
getCheckboxLabel: (row) => `选择${recordTitle(row)}`,
|
||||
} : undefined}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState title={loading ? '正在加载数据' : '当前分区暂无记录'} description="切换上方分区可精准查看不同配置和接口。" />
|
||||
)}
|
||||
@@ -5291,31 +5413,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Dialog
|
||||
open={batchTriggerOpen}
|
||||
onOpenChange={setBatchTriggerOpen}
|
||||
title="批量触发数据源"
|
||||
description="输入内置数据源 ID,提交到 trigger-batch。"
|
||||
width={560}
|
||||
footer={(
|
||||
<>
|
||||
<Button variant="subtle" onClick={() => setBatchTriggerOpen(false)} disabled={actionLoading}>取消</Button>
|
||||
<Button variant="primary" onClick={submitBatchTrigger} loading={actionLoading}>触发</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="an-form">
|
||||
<label className="an-field">
|
||||
<span>数据源 ID</span>
|
||||
<Textarea value={batchTriggerIds} onChange={(event) => setBatchTriggerIds(event.target.value)} placeholder="例如:1, 2, 3" />
|
||||
</label>
|
||||
<label className="an-checkbox-row">
|
||||
<input type="checkbox" checked={batchTriggerForce} onChange={(event) => setBatchTriggerForce(event.target.checked)} />
|
||||
强制重新触发运行中的任务
|
||||
</label>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={Boolean(resolveTarget)}
|
||||
onOpenChange={(open) => {
|
||||
|
||||
@@ -365,6 +365,46 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
|
||||
transition: width 180ms ease;
|
||||
}
|
||||
|
||||
.an-collection-queue-anchor {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.an-collection-queue-trigger {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.an-collection-queue-trigger--progress::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 999px;
|
||||
background: conic-gradient(var(--an-accent) var(--queue-progress, 0%), var(--an-soft) 0);
|
||||
mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0);
|
||||
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0);
|
||||
}
|
||||
|
||||
.an-collection-queue-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 40;
|
||||
width: min(960px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.an-collection-queue-popover .an-collection-queue {
|
||||
max-height: min(560px, calc(100vh - 96px));
|
||||
}
|
||||
|
||||
.an-collection-queue-popover .an-collection-queue__panel {
|
||||
max-height: min(430px, calc(100vh - 210px));
|
||||
}
|
||||
|
||||
.an-collection-queue__panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@@ -433,6 +473,15 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.an-collection-queue__empty--panel {
|
||||
min-height: 96px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px dashed var(--an-border);
|
||||
border-radius: 8px;
|
||||
background: var(--an-surface-alt);
|
||||
}
|
||||
|
||||
.an-collection-queue__item > span {
|
||||
min-width: 38px;
|
||||
color: var(--an-muted);
|
||||
@@ -711,6 +760,20 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.an-toolbar > .tui-button {
|
||||
align-self: stretch;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.an-toolbar .an-status-pill {
|
||||
align-self: stretch;
|
||||
height: 34px;
|
||||
min-width: 0;
|
||||
padding: 0 12px;
|
||||
border-radius: 7px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.an-inline-field {
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
@@ -2378,9 +2441,11 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
|
||||
}
|
||||
|
||||
.an-toolbar .an-badge {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
align-self: stretch;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 7px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.an-data-table {
|
||||
@@ -2431,6 +2496,22 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.an-data-table__selection-cell {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
max-width: 42px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.an-data-table__selection-cell input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0;
|
||||
accent-color: var(--an-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.an-data-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -3775,6 +3856,14 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.an-collection-queue-popover {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.an-collection-queue__panel {
|
||||
grid-template-columns: 1fr;
|
||||
max-height: 46vh;
|
||||
|
||||
@@ -69,10 +69,10 @@ export function tactileButtonPreset(
|
||||
return {
|
||||
...base,
|
||||
background: 'var(--tui-primary)',
|
||||
backgroundHover: 'var(--tui-primary-hover)',
|
||||
backgroundHover: 'color-mix(in srgb, var(--tui-primary) 86%, white)',
|
||||
backgroundActive: 'var(--tui-primary-active)',
|
||||
borderColor: 'var(--tui-primary)',
|
||||
borderColorHover: 'var(--tui-primary-hover)',
|
||||
borderColor: 'color-mix(in srgb, var(--tui-primary) 64%, white)',
|
||||
borderColorHover: 'color-mix(in srgb, var(--tui-primary) 52%, white)',
|
||||
color: '#fff',
|
||||
darkBackground: 'var(--tui-primary)',
|
||||
}
|
||||
@@ -81,10 +81,10 @@ export function tactileButtonPreset(
|
||||
return {
|
||||
...base,
|
||||
background: 'var(--tui-danger)',
|
||||
backgroundHover: 'var(--tui-danger-hover)',
|
||||
backgroundHover: 'color-mix(in srgb, var(--tui-danger) 86%, white)',
|
||||
backgroundActive: 'var(--tui-danger-active)',
|
||||
borderColor: 'var(--tui-danger)',
|
||||
borderColorHover: 'var(--tui-danger-hover)',
|
||||
borderColor: 'color-mix(in srgb, var(--tui-danger) 64%, white)',
|
||||
borderColorHover: 'color-mix(in srgb, var(--tui-danger) 52%, white)',
|
||||
color: '#fff',
|
||||
darkBackground: 'var(--tui-danger)',
|
||||
}
|
||||
|
||||
@@ -37,7 +37,13 @@
|
||||
[data-theme='dark'] .tui-control-group,
|
||||
[data-theme='dark'] .tui-scrollbar,
|
||||
[data-theme='dark'] .tui-table-scroll-region,
|
||||
[data-theme='dark'] .tui-tooltip {
|
||||
[data-theme='dark'] .tui-tooltip,
|
||||
body[data-admin-next-theme='dark'] .tui-button,
|
||||
body[data-admin-next-theme='dark'] .tui-switch,
|
||||
body[data-admin-next-theme='dark'] .tui-control-group,
|
||||
body[data-admin-next-theme='dark'] .tui-scrollbar,
|
||||
body[data-admin-next-theme='dark'] .tui-table-scroll-region,
|
||||
body[data-admin-next-theme='dark'] .tui-tooltip {
|
||||
--tui-bg: #0f1724;
|
||||
--tui-surface: #172033;
|
||||
--tui-surface-soft: #202b3d;
|
||||
|
||||
Reference in New Issue
Block a user