mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 08:51:58 +00:00
数据目录 F2b / B10:顶部摘要点进来只列运行中查询过的表,和摘要同一个口径——摘要说「4 张还没有目录」,清单就是那 4 张;清单上挂「只看运行中查询过的表」标签,可取消;筛选按钮的数字按同一口径算;补检查脚本的断言
This commit is contained in:
@@ -2501,6 +2501,18 @@ export const CODES_TEXT = {
|
||||
completeMarkHint: '已列出全部取值:SQL 检查会提醒码值表中没有的取值',
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 数据目录界面的补充文案(2026-10 完整性走查 R2 之后补的:清单的「只看运行中查询过的表」、剖析报告的逐条结论、
|
||||
// 码值含义待填写时的确认、保存后的影响面提示……)。新增的都放在这里,上面几组的已有写法不动。
|
||||
// ===========================================================================
|
||||
|
||||
export const CATALOG_UI_TEXT = {
|
||||
// ---- 表清单:从顶部摘要点进来时只看运行中查询过的表,口径和摘要一致
|
||||
usedOnly: '只看运行中查询过的表',
|
||||
usedOnlyHint: '使用次数大于 0、还在表结构里的表,和顶部摘要的口径一致',
|
||||
usedOnlyClear: '取消「只看运行中查询过的表」',
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 基于数据目录的 SQL 检查(后端 data/sqlcheck.py)
|
||||
//
|
||||
|
||||
@@ -65,6 +65,8 @@ export function CatalogPage() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [filter, setFilter] = useState<ListFilter>('all')
|
||||
const [sort, setSort] = useState<ListSort>('usage')
|
||||
/** 只看运行中查询过的表:从顶部摘要点进来时打开,筛选结果和摘要的数字一致 */
|
||||
const [usedOnly, setUsedOnly] = useState(false)
|
||||
const [selected, setSelected] = useState<Set<string>>(() => new Set())
|
||||
const [drafting, setDrafting] = useState(false)
|
||||
/** 起草写进了这些表:详情据此重新载入(编辑中的不动,保存时服务端会用版本号拦下) */
|
||||
@@ -89,8 +91,8 @@ export function CatalogPage() {
|
||||
sourceRef.current = sourceId
|
||||
|
||||
const rows = useMemo(() => data?.tables ?? [], [data])
|
||||
const visible = useMemo(() => visibleRows(rows, query, filter, sort), [rows, query, filter, sort])
|
||||
const counts = useMemo(() => filterCounts(rows, query), [rows, query])
|
||||
const visible = useMemo(() => visibleRows(rows, query, filter, sort, usedOnly), [rows, query, filter, sort, usedOnly])
|
||||
const counts = useMemo(() => filterCounts(rows, query, usedOnly), [rows, query, usedOnly])
|
||||
const base = `/data/catalog/${encodeURIComponent(sourceId)}`
|
||||
const openTable = useCallback((t: string) => navigate(`${base}/${encodeURIComponent(t)}`), [navigate, base])
|
||||
const back = source?.origin === 'upload' ? '/data/tables' : '/data/databases'
|
||||
@@ -191,17 +193,19 @@ export function CatalogPage() {
|
||||
|
||||
const usage = useMemo(() => usageSummary(rows), [rows])
|
||||
/**
|
||||
* 顶部摘要点进来:清掉搜索词,切到对应的筛选,按使用次数排——用到最多的排在最前。窄屏一次只显示一栏,
|
||||
* 顶部摘要点进来:清掉搜索词,切到对应的筛选、只看运行中查询过的表(和摘要同一个口径:摘要说「1 张还没有目录」,
|
||||
* 点进去就是那 1 张,不是全部没有目录的表),按使用次数排——用到最多的排在最前。窄屏一次只显示一栏,
|
||||
* 正在看表详情时回到清单才看得到筛选结果;宽屏两栏都在,正在看的表不动
|
||||
*/
|
||||
const focusUsed = useCallback((f: ListFilter) => {
|
||||
setQuery('')
|
||||
setFilter(f)
|
||||
setUsedOnly(true)
|
||||
setSort('usage')
|
||||
if (table && typeof matchMedia === 'function' && matchMedia('(max-width: 1023px)').matches) navigate(base)
|
||||
}, [table, navigate, base])
|
||||
|
||||
const filtered = !!query.trim() || filter !== 'all'
|
||||
const filtered = !!query.trim() || filter !== 'all' || usedOnly
|
||||
const empty = rows.length > 0 && rows.every((r) => progressOf(r.counts) === 'none')
|
||||
const hasTables = rows.length > 0
|
||||
const draftButton = (primary = false) => (
|
||||
@@ -270,6 +274,8 @@ export function CatalogPage() {
|
||||
onQuery={setQuery}
|
||||
filter={filter}
|
||||
onFilter={setFilter}
|
||||
usedOnly={usedOnly}
|
||||
onUsedOnly={setUsedOnly}
|
||||
sort={sort}
|
||||
onSort={setSort}
|
||||
selected={selected}
|
||||
@@ -312,7 +318,7 @@ export function CatalogPage() {
|
||||
<SchemaPartialNotice total={data.schema_total ?? 0} explored={rows.filter((r) => r.in_schema).length}
|
||||
onGoSource={() => navigate(back)} />
|
||||
)}
|
||||
{hasTables && usage.used > 0 && !empty && <UsageSummary {...usage} active={filter} onFocus={focusUsed} />}
|
||||
{hasTables && usage.used > 0 && !empty && <UsageSummary {...usage} active={usedOnly ? filter : null} onFocus={focusUsed} />}
|
||||
{body}
|
||||
{drafting && data && (
|
||||
<DraftDialog
|
||||
@@ -382,7 +388,8 @@ function UsageSummary({ used, pending, none, active, onFocus }: {
|
||||
used: number
|
||||
pending: number
|
||||
none: number
|
||||
active: ListFilter
|
||||
/** 清单正按摘要的哪一段筛(只看运行中查询过的表时);没有按摘要筛为 null */
|
||||
active: ListFilter | null
|
||||
onFocus: (f: ListFilter) => void
|
||||
}) {
|
||||
const part = (f: 'pending' | 'none', text: string, hint: string) => (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { memo, useCallback, useEffect, useRef } from 'react'
|
||||
import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { AlertTriangle, ScanSearch, Search, Sparkles, X } from 'lucide-react'
|
||||
import { AlertTriangle, History, ScanSearch, Search, Sparkles, X } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import type { CatalogTableRow } from '../../types'
|
||||
import { useRadioGroup } from '../../components/ui'
|
||||
import { formatNumber } from '../../lib/format'
|
||||
import { CATALOG_KIND_LABEL, CATALOG_TEXT as CT, PROFILE_TEXT as PT } from '../../lib/terms'
|
||||
import { CATALOG_KIND_LABEL, CATALOG_TEXT as CT, CATALOG_UI_TEXT as UT, PROFILE_TEXT as PT } from '../../lib/terms'
|
||||
import { CountBadges } from './parts'
|
||||
import { LIST_FILTERS, LIST_SORTS, progressOf } from './model'
|
||||
import type { ListFilter, ListSort } from './model'
|
||||
@@ -17,7 +17,8 @@ import type { ListFilter, ListSort } from './model'
|
||||
// ===========================================================================
|
||||
|
||||
export function TableIndex({
|
||||
rows, total, counts, query, onQuery, filter, onFilter, sort, onSort, selected, onSelected, active, onOpen, onDraft, onProfile,
|
||||
rows, total, counts, query, onQuery, filter, onFilter, usedOnly = false, onUsedOnly, sort, onSort, selected, onSelected, active, onOpen,
|
||||
onDraft, onProfile,
|
||||
}: {
|
||||
/** 筛选、排序后的行 */
|
||||
rows: CatalogTableRow[]
|
||||
@@ -28,6 +29,9 @@ export function TableIndex({
|
||||
onQuery: (q: string) => void
|
||||
filter: ListFilter
|
||||
onFilter: (f: ListFilter) => void
|
||||
/** 只看运行中查询过的表(从顶部摘要点进来):筛选组下面挂一个能取消的标签 */
|
||||
usedOnly?: boolean
|
||||
onUsedOnly?: (v: boolean) => void
|
||||
sort: ListSort
|
||||
onSort: (s: ListSort) => void
|
||||
selected: Set<string>
|
||||
@@ -131,6 +135,21 @@ export function TableIndex({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{usedOnly && (
|
||||
<div className="flex" data-catalog-used-only="">
|
||||
<span className="inline-flex min-w-0 items-center gap-1 rounded-md border border-[var(--accent)] bg-accent-soft py-0.5 pl-2 pr-0.5 text-2xs text-fg"
|
||||
title={UT.usedOnlyHint}>
|
||||
<History size={10} className="shrink-0 text-[var(--accent)]" aria-hidden />
|
||||
<span className="truncate">{UT.usedOnly}</span>
|
||||
{onUsedOnly && (
|
||||
<button type="button" className="rounded p-0.5 text-faint outline-none hover:bg-hover hover:text-fg focus-visible:ring-2 focus-visible:ring-[var(--accent)]"
|
||||
onClick={() => onUsedOnly(false)} aria-label={UT.usedOnlyClear} title={UT.usedOnlyClear} data-catalog-used-only-clear="">
|
||||
<X size={10} aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-2xs text-faint">
|
||||
<input ref={allRef} type="checkbox" checked={all} onChange={toggleAll} disabled={!rows.length}
|
||||
aria-label={CT.selectAll} title={CT.selectAll} data-catalog-select-all="" />
|
||||
|
||||
@@ -109,13 +109,16 @@ export function matchesQuery(r: CatalogTableRow, q: string): boolean {
|
||||
return [r.table_name, r.qualified, r.label ?? ''].some((s) => s.toLowerCase().includes(needle))
|
||||
}
|
||||
|
||||
/** 运行中查询过的表:使用次数大于 0、还在表结构里。顶部摘要和「只看运行中查询过的表」用同一个口径 */
|
||||
export const isUsed = (r: CatalogTableRow): boolean => r.in_schema && r.usage > 0
|
||||
|
||||
/**
|
||||
* 筛选并排序。默认保持服务端的顺序(使用次数多的在前,次数相同按表结构里的顺序);表结构里已经没有的表
|
||||
* 不论怎么排都在最后
|
||||
* 不论怎么排都在最后。used:只看运行中查询过的表(从顶部摘要点进来时带上,和摘要的数字对得上)
|
||||
*/
|
||||
export function visibleRows(rows: CatalogTableRow[], q: string, filter: ListFilter, sort: ListSort): CatalogTableRow[] {
|
||||
export function visibleRows(rows: CatalogTableRow[], q: string, filter: ListFilter, sort: ListSort, used = false): CatalogTableRow[] {
|
||||
const order = new Map(rows.map((r, i) => [r.table_name, i]))
|
||||
const picked = rows.filter((r) => matchesQuery(r, q) && (filter === 'all' || progressOf(r.counts) === filter))
|
||||
const picked = rows.filter((r) => matchesQuery(r, q) && (!used || isUsed(r)) && (filter === 'all' || progressOf(r.counts) === filter))
|
||||
const idx = (r: CatalogTableRow) => order.get(r.table_name) ?? 0
|
||||
return picked.sort((a, b) => {
|
||||
if (a.in_schema !== b.in_schema) return a.in_schema ? -1 : 1
|
||||
@@ -132,7 +135,7 @@ export function visibleRows(rows: CatalogTableRow[], q: string, filter: ListFilt
|
||||
export function usageSummary(rows: CatalogTableRow[]): { used: number; pending: number; none: number } {
|
||||
const out = { used: 0, pending: 0, none: 0 }
|
||||
for (const r of rows) {
|
||||
if (!r.in_schema || r.usage <= 0) continue
|
||||
if (!isUsed(r)) continue
|
||||
out.used++
|
||||
const p = progressOf(r.counts)
|
||||
if (p === 'pending') out.pending++
|
||||
@@ -141,11 +144,11 @@ export function usageSummary(rows: CatalogTableRow[]): { used: number; pending:
|
||||
return out
|
||||
}
|
||||
|
||||
/** 各筛选项下有几张表(筛选按钮上的数字,按搜索词算) */
|
||||
export function filterCounts(rows: CatalogTableRow[], q: string): Record<ListFilter, number> {
|
||||
/** 各筛选项下有几张表(筛选按钮上的数字,按搜索词和「只看运行中查询过的表」算) */
|
||||
export function filterCounts(rows: CatalogTableRow[], q: string, used = false): Record<ListFilter, number> {
|
||||
const out: Record<ListFilter, number> = { all: 0, pending: 0, done: 0, none: 0 }
|
||||
for (const r of rows) {
|
||||
if (!matchesQuery(r, q)) continue
|
||||
if (!matchesQuery(r, q) || (used && !isUsed(r))) continue
|
||||
out.all++
|
||||
out[progressOf(r.counts)]++
|
||||
}
|
||||
|
||||
@@ -620,11 +620,24 @@ await section('数据目录 · 用到但没确认(顶部摘要)', async () =
|
||||
check('……排序是按使用次数', await page.locator('[data-catalog-sort]').inputValue() === 'usage')
|
||||
const shown = await rowNames(page)
|
||||
const expectTop = used.filter((t) => progress(t) === 'pending').sort((a, b) => b.usage - a.usage).map((t) => t.name)
|
||||
check('……查询过的、有推断项的表排在最前', shown.slice(0, expectTop.length).join(',') === expectTop.join(','),
|
||||
`${shown.slice(0, expectTop.length).join(',')} ≠ ${expectTop.join(',')}`)
|
||||
check('……只列查询过的、有推断项的表,按使用次数排', shown.join(',') === expectTop.join(','), `${shown.join(',')} ≠ ${expectTop.join(',')}`)
|
||||
await bar.locator('[data-usage-focus="none"]').click()
|
||||
check('点「还没有目录」:切到「没有目录」筛选', (await page.locator('[data-catalog-filter]').getAttribute('data-catalog-filter')) === 'none'
|
||||
&& (await rowNames(page))[0] === used.filter((t) => progress(t) === 'none').sort((a, b) => b.usage - a.usage)[0].name)
|
||||
// 摘要说「N 张还没有目录」,点进去就是那 N 张:只看运行中查询过的表,不是全部没有目录的表
|
||||
const expectNone = used.filter((t) => progress(t) === 'none').sort((a, b) => b.usage - a.usage).map((t) => t.name)
|
||||
const noneShown = await rowNames(page)
|
||||
check('……只列运行中查询过、还没有目录的表,张数和摘要一致', noneShown.join(',') === expectNone.join(',') && noneShown.length === none,
|
||||
`${noneShown.length} 张:${noneShown.slice(0, 6).join(',')}`)
|
||||
check('……清单上写明「只看运行中查询过的表」', (await page.locator('[data-catalog-used-only]').innerText().catch(() => '')).includes('只看运行中查询过的表'))
|
||||
const usedCounts = await page.locator('[data-catalog-filter] [data-filter]').evaluateAll((els) => els.map((e) => e.innerText.replace(/\s+/g, '')))
|
||||
check('……筛选按钮上的数字按同一个口径算', usedCounts.includes(`没有目录${none}`) && usedCounts.includes(`全部${used.length}`), usedCounts.join('|'))
|
||||
check('……摘要上点中的那一段标为按下', await bar.locator('[data-usage-focus="none"]').getAttribute('aria-pressed') === 'true')
|
||||
await page.locator('[data-catalog-used-only-clear]').click()
|
||||
const allNone = state.tables[S1].filter((t) => progress(t) === 'none').length
|
||||
check('取消「只看运行中查询过的表」:回到全部没有目录的表,摘要不再标按下', (await rowNames(page)).length === allNone
|
||||
&& await page.locator('[data-catalog-used-only]').count() === 0
|
||||
&& await bar.locator('[data-usage-focus="none"]').getAttribute('aria-pressed') === 'false', String((await rowNames(page)).length))
|
||||
check('摘要只读、不发请求', writes(sent, /./).length === 0)
|
||||
// 都审完了:只写一句,不给按钮
|
||||
for (const t of state.tables[S1]) if (t.notes) t.notes = applyPut({}, { label: { value: t.notes.label?.value ?? t.name } })
|
||||
|
||||
Reference in New Issue
Block a user