import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { logsApi } from '@/api/logs' import { SearchableValueSelect } from './SearchableValueSelect' import { applyLabelFilter, extractSelector } from '../query-utils' type Template = 'app' | 'error' | 'warn' | 'pod' | 'ingress' /** Ingress 访问日志所在命名空间候选(按顺序取第一个实际存在的) */ const INGRESS_NAMESPACE_CANDIDATES = ['ingress-nginx', 'ingress'] function selectorOf(labels: Record) { const entries = Object.entries(labels).filter(([, value]) => value.trim()) if (entries.length === 0) return '' return `{${entries.map(([key, value]) => `${key}=${JSON.stringify(value.trim())}`).join(',')}}` } export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }: { sourceId: number query: string onQueryChange: (query: string) => void compact?: boolean }) { const { t } = useTranslation('logs') const [namespace, setNamespace] = useState('') const [app, setApp] = useState('') const [pod, setPod] = useState('') const [namespaces, setNamespaces] = useState([]) const [apps, setApps] = useState([]) const [pods, setPods] = useState([]) /** * 实际用来标识「应用」的标签名。 * * Kubernetes 惯例是 app 标签,但很多集群(如本环境)Deployment 模板没打 app, * 只有 Kuboard 注入的 k8s.kuboard.cn/name,此时 Loki 里 app 只剩零星几个值。 * Promtail 会从容器名生成 service_name / container,覆盖度更高, * 因此按 app → service_name → container 逐级回退。 */ const [appLabel, setAppLabel] = useState('app') /** 授权策略里配置的 namespace(如默认策略的 default),用作默认选中值 */ const [scopeNamespace, setScopeNamespace] = useState('') // 授权策略的 namespace:作为默认选中值(管理员在「查询授权」里配好后开箱即用) useEffect(() => { if (!sourceId) return let active = true logsApi.myScope(sourceId) .then(scope => { if (!active) return const ns = (scope?.labels?.namespace ?? '').trim() setScopeNamespace(ns) if (ns) setNamespace(current => current || ns) }) .catch(() => { /* 无权限或无策略时静默 */ }) return () => { active = false } }, [sourceId]) // namespace 列表只加载一次 useEffect(() => { if (!sourceId) return let active = true logsApi.labelValues(sourceId, 'namespace') // 后端历史版本可能返回 null(Loki 无值时 data:null),这里兜底成数组, // 否则 setState(null) 后 datalist 的 .map() 会抛 // "Cannot read properties of null (reading 'map')" .then(next => { if (active) setNamespaces(Array.isArray(next) ? next : []) }) .catch(() => { if (active) setNamespaces([]) }) return () => { active = false } }, [sourceId]) // app 候选:选了 namespace 就只在该命名空间内搜索 useEffect(() => { if (!sourceId) return let active = true const selector = selectorOf({ namespace }) const fetchLabel = (name: string) => logsApi.labelValues(sourceId, name, selector) .then(list => (Array.isArray(list) ? list : [])) .catch(() => [] as string[]) // 逐级回退:app → service_name → container fetchLabel('app') .then(list => { if (list.length > 0) return { label: 'app', list } return fetchLabel('service_name').then(s => s.length > 0 ? { label: 'service_name', list: s } : fetchLabel('container').then(c => ({ label: 'container', list: c })), ) }) .then(({ label, list }) => { if (!active) return setAppLabel(label) setApps(list) }) .catch(() => { if (active) { setAppLabel('app'); setApps([]) } }) return () => { active = false } }, [sourceId, namespace]) // pod 候选:按 namespace + app 收窄 useEffect(() => { if (!sourceId) return let active = true // 用实际生效的 appLabel 收窄;否则在没打 app 标签的集群上筛不出东西 const selector = selectorOf(appLabel === 'app' ? { namespace, app } : { namespace, [appLabel]: app }) logsApi.labelValues(sourceId, 'pod', selector) .then(next => { if (active) setPods(Array.isArray(next) ? next : []) }) .catch(() => { if (active) setPods([]) }) return () => { active = false } }, [sourceId, namespace, app, appLabel]) function chooseNamespace(value: string) { setNamespace(value) // 换命名空间后旧的 app / pod 条件失效 setApp('') setPod('') } function chooseApp(value: string) { setApp(value) setPod('') } function generate(template: Template) { // Ingress 访问日志与当前选中的业务命名空间无关: // ingress controller 的 Pod 只在它自己的命名空间(ingress-nginx / ingress)里, // 所以这里固定用该命名空间,并且不叠加 app / pod 过滤(那些属于业务服务)。 // 原实现 `namespace.trim() || 'ingress-nginx'` 会导致选了 monitor 就变成 // {namespace="monitor"} |= "HTTP/",一条都查不到。 if (template === 'ingress') { const ingressNs = INGRESS_NAMESPACE_CANDIDATES.find((n) => namespaces.includes(n)) ?? INGRESS_NAMESPACE_CANDIDATES[0] onQueryChange(`{namespace="${ingressNs}"} |= "HTTP/"`) return } let next = extractSelector(query) || '{}' if (namespace.trim()) next = applyLabelFilter(next, 'namespace', namespace.trim()) if (app.trim()) next = applyLabelFilter(next, appLabel, app.trim()) if (pod.trim()) next = applyLabelFilter(next, 'pod', pod.trim()) if (next === '{}') return if (template === 'error') next += ' |= "error"' if (template === 'warn') next += ' |= "warn"' onQueryChange(next) } // 候选 = 策略里配的 namespace ∪ Loki 实际有的 namespace // (default 这类「策略配了但没日志」的命名空间也要能选到,否则用户以为策略没生效) const namespaceOptions = useMemo( () => (scopeNamespace && !namespaces.includes(scopeNamespace) ? [scopeNamespace, ...namespaces] : namespaces), [namespaces, scopeNamespace], ) const hasSelector = extractSelector(query).replace(/[{}\s]/g, '').length > 0 const hasScope = Boolean(app.trim() || namespace.trim() || pod.trim() || hasSelector) return
{!compact &&

{t('templates.title')}

} {!compact &&

{t('templates.hint')}

}
{/* 用 SearchableValueSelect(点击即展开列表 + 可搜索), 原生 datalist 只在输入时才提示、样式弱,容易让人以为「下拉是空的」 */}
{!hasScope && {t('templates.scopeHint')}}
}