diff --git a/frontend/src/canvas/Inspector.tsx b/frontend/src/canvas/Inspector.tsx index d0a099e8..e67938bc 100644 --- a/frontend/src/canvas/Inspector.tsx +++ b/frontend/src/canvas/Inspector.tsx @@ -15,7 +15,15 @@ import { formatShortcut } from '../lib/keys' import { INPUT_FIELD_TEXT, JUDGE_FIELD_LABEL, JUDGE_ON_UNSUPPORTED_HINT, JUDGE_ON_UNSUPPORTED_LABEL, JUDGE_SETTING_TEXT, MERGE_TEXT, CATALOG_UI_TEXT, NODE_TYPE_LABEL, PROVIDER_REF_TEXT, SUBGRAPH_UPGRADE_HELP, judgeByModelText, upgradeNewerText } from '../lib/terms' import { TemplateText } from './TemplateText' import { MERGE_ALIAS, isQueryNode, mergeAlias } from './mergeInputs' -import type { NodeType, Provider, ValidationIssue, WorkflowVersion } from '../types' +import { + ITEM_ID, TWO_QUERY_SKIP, allowsHeadline, allowsParent, allowsTwoQueries, itemsOf, nextItemId, paramSpecs, paramText, + paramValue, withMode, withOp, type ParamSpec, +} from './analysisItems' +import { + ANALYSIS_MODES, ANALYSIS_MODE_LABEL, ANALYSIS_OPS, ANALYSIS_OP_LABEL, ANALYSIS_SETTING_KEYS, ANALYSIS_SETTING_LABEL, + ANALYSIS_TEXT, isAnalysisOp, settingRangeText, settingValueProblem, type AnalysisSettingKey, +} from '../lib/termsAnalysis' +import type { AnalysisItem, NodeType, Provider, ValidationIssue, WorkflowVersion } from '../types' /** 检查器里一条落到字段上的问题 */ type FieldIssue = ValidationIssue & { at: FieldRef | null } @@ -151,6 +159,34 @@ function NodeRefsInput({ nodeId, type, value, onChange }: { ) } +/** 上游能当查询输入的节点(选了数据库查询工具的「调用工具」节点、合并查询):合并查询、分析卡共用 */ +function useUpstreamQueries(nodeId: string): { value: string; label: string; hint?: string }[] { + const nodes = useStudio((s) => s.nodes) + const edges = useStudio((s) => s.edges) + return useMemo(() => { + const parents = new Map() + for (const e of edges) parents.set(e.target, [...(parents.get(e.target) ?? []), e.source]) + const seen = new Set() + const stack = [...(parents.get(nodeId) ?? [])] + while (stack.length) { + const id = stack.pop()! + if (seen.has(id) || id === nodeId) continue + seen.add(id) + stack.push(...(parents.get(id) ?? [])) + } + return nodes + .filter((n) => seen.has(n.id) && isQueryNode(n)) + .map((n) => { + const label = String(n.data?.label || n.id) + const tool = String((n.data?.config as Record | undefined)?.tool ?? '') + return { + value: n.id, label: label === n.id ? label : `${label}(${n.id})`, + hint: n.data?.nodeType === 'merge' ? NODE_TYPE_LABEL.merge : tool, + } + }) + }, [nodes, edges, nodeId]) +} + interface MergeRow { alias: string; node: string } /** config.inputs({别名: 节点 id})→ 行。对象的键顺序就是行的顺序 */ @@ -178,28 +214,7 @@ function MergeInputList({ nodeId, value, issues, onChange }: { const nodes = useStudio((s) => s.nodes) const edges = useStudio((s) => s.edges) const uid = useId() - const options = useMemo(() => { - const parents = new Map() - for (const e of edges) parents.set(e.target, [...(parents.get(e.target) ?? []), e.source]) - const seen = new Set() - const stack = [...(parents.get(nodeId) ?? [])] - while (stack.length) { - const id = stack.pop()! - if (seen.has(id) || id === nodeId) continue - seen.add(id) - stack.push(...(parents.get(id) ?? [])) - } - return nodes - .filter((n) => seen.has(n.id) && isQueryNode(n)) - .map((n) => { - const label = String(n.data?.label || n.id) - const tool = String((n.data?.config as Record | undefined)?.tool ?? '') - return { - value: n.id, label: label === n.id ? label : `${label}(${n.id})`, - hint: n.data?.nodeType === 'merge' ? NODE_TYPE_LABEL.merge : tool, - } - }) - }, [nodes, edges, nodeId]) + const options = useUpstreamQueries(nodeId) const external = useMemo(() => mergeRowsOf(value), [value]) const [rows, setRows] = useState(external) @@ -295,6 +310,319 @@ function MergeInputList({ nodeId, value, issues, onChange }: { ) } +const AT = ANALYSIS_TEXT.node + +/** 选一个上游查询节点。已经选了、后来删掉或挪到下游的节点照样列出来并说明,好让人看见并改掉 */ +function QueryPick({ id, value, options, onChange }: { + id: string; value: string; options: { value: string; label: string }[]; onChange: (v: string) => void +}) { + const known = options.some((o) => o.value === value) + return ( + + ) +} + +/** + * 分析卡的分析项(engine/schema.analysis_issues 的配置):每一项一个标识、一种分析方法(有模式的再选模式)、要分析的上游 + * 查询节点(或基期、本期两份查询),再按方法摆参数格(analysisItems.paramSpecs);可加、比率的变化拆解另有总数核对、下钻。 + * 阈值不在这里写(统一写在阈值覆盖里)。服务端按 items[i].<键> 报的问题落在那一格下面,定位按 [data-item] [data-sub] 落进来 + */ +function AnalysisItemList({ nodeId, value, issues, onChange }: { + nodeId: string; value: any; issues: FieldIssue[]; onChange: (v: any) => void +}) { + const uid = useId() + const options = useUpstreamQueries(nodeId) + const items = itemsOf(value) + const commit = (next: AnalysisItem[]) => onChange(next) + const update = (i: number, next: AnalysisItem) => commit(items.map((it, j) => (j === i ? next : it))) + const setParam = (i: number, key: string, v: unknown) => { + const params = { ...(items[i].params ?? {}) } + if (v === undefined) delete params[key] + else params[key] = v + update(i, { ...items[i], params }) + } + const add = () => commit([...items, { id: nextItemId(items), params: {} }]) + return ( +
+ {items.map((item, i) => { + const own = issues.filter((x) => x.at?.index === i) + const at = (sub: string) => own.filter((x) => x.at?.sub === sub || x.at?.sub?.startsWith(`${sub}.`)) + const lines = (sub: string) => at(sub).map((x, k) => ) + const op = isAnalysisOp(item.op) ? item.op : null + const modes = op ? ANALYSIS_MODES[op] : undefined + const params = (item.params && typeof item.params === 'object' ? item.params : {}) as Record + const two = !!op && allowsTwoQueries(op) && ('base_query' in params || 'target_query' in params) + const id = String(item.id ?? '') + const dup = !!id && items.some((other, j) => j !== i && String(other.id ?? '') === id) + const local = !id || !ITEM_ID.test(id) ? AT.idInvalid : dup ? AT.idDuplicate(id) : '' + const tone = own.some((x) => x.level === 'error') || local ? 'error' : own.length ? 'warning' : undefined + const before = items.slice(0, i).filter((it) => it.op === 'decompose' && it.id) + const parent = params.parent && typeof params.parent === 'object' ? params.parent as { of?: string; key?: unknown } : null + const field = (spec: ParamSpec) => { + const fid = `${uid}-${i}-p-${spec.key}` + const label = AT.param[spec.key] ?? spec.key + const text = paramText(spec.kind, params[spec.key]) + const set = (t: string) => setParam(i, spec.key, paramValue(spec.kind, t)) + let control + if (spec.kind === 'basis' || spec.kind === 'grain') { + const opts = spec.kind === 'basis' ? AT.basis : AT.grain + control = ( + + ) + } else if (spec.kind === 'pairs') { + control =