fix(logs): 「所有人」策略保存被拒 + 快捷 LogQL namespace 空且不认默认策略
=== 一、保存策略组报「用户ID无效或重复」===
validatePolicyGroup 里 `if id == 0 || seen[id]` 直接把「所有人」哨兵当非法。
修复:
- 新增常量 AnyoneUserID = 0(与 policyGroupAccess、前端 ANYONE 对齐)
- 0 跳过用户存在性校验,但必须独占:与具体用户混选时报
「「所有人」不能与具体用户同时选择」
- 重复检查独立出来,报错带具体 ID
=== 二、快捷 LogQL 的 namespace 下拉总是空的 ===
两个叠加原因:
1. 用的是原生 <datalist>,只在输入时才提示、样式弱,看着像"下拉是空的"
2. 候选只来自 Loki 实际值 —— 而默认策略配的 default 命名空间里没有业务 Pod,
Loki 里根本没有这个 label 值,所以永远不出现;前端也不会拿策略值当默认选中
修复:
后端新增 GET /api/v1/logs/my-scope?sourceId= → {labels, unrestricted}
(services.UserAccessLabels 暴露当前用户在数据源上的授权 labels;
admin 返回 unrestricted=true 但仍带上策略 labels 供 UI 做默认值)
前端:
- logsApi.myScope()
- QuickQueryTemplates 用策略里的 namespace 作为初始选中值
- namespaceOptions = 策略值 ∪ Loki 实际值(去重,策略值优先)
- namespace/app/pod 三个 <datalist> 全部换成 SearchableValueSelect
(点击即展开、可搜索,placeholder 显示「请选择(共 N 个)」)
验证:
PUT /logs/policy-groups/1 {userIds:[0]} → 200 ✅
POST userIds:[0,2](混选) → 400「「所有人」不能与具体用户同时选择」✅
GET /logs/my-scope?sourceId=1 → {"labels":{"namespace":"default"},"unrestricted":true} ✅
bundle: my-scope 1 处、datalist 残留 0 处 ✅
镜像:lghost/lokistack:latest + v0.1 = sha256:ad659f564665
This commit is contained in:
1 parent
4dda6e5ba8
commit
cf54be3f7d
6 files changed
+131
-16
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { logsApi } from '@/api/logs'
|
||||
import { SearchableValueSelect } from './SearchableValueSelect'
|
||||
import { applyLabelFilter, extractSelector } from '../query-utils'
|
||||
|
||||
type Template = 'app' | 'error' | 'warn' | 'pod' | 'ingress'
|
||||
@@ -33,6 +33,23 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
* 因此按 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(() => {
|
||||
@@ -112,6 +129,13 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
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)
|
||||
|
||||
@@ -119,18 +143,35 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
{!compact && <h2 className="text-sm font-medium">快捷生成 LogQL</h2>}
|
||||
{!compact && <p className="text-xs text-muted-foreground">先选 namespace,再选 app,最后选副本 pod;app 候选只来自所选命名空间。</p>}
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="min-w-36 flex-1 text-xs text-muted-foreground">namespace
|
||||
<Input className="mt-1 h-8" list="quick-log-namespaces" value={namespace} onChange={event => chooseNamespace(event.target.value)} placeholder="先选命名空间" />
|
||||
</label>
|
||||
<datalist id="quick-log-namespaces">{namespaces.map(value => <option value={value} key={value} />)}</datalist>
|
||||
<label className="min-w-36 flex-1 text-xs text-muted-foreground">app
|
||||
<Input className="mt-1 h-8" list="quick-log-apps" value={app} onChange={event => chooseApp(event.target.value)} placeholder={namespace ? '搜索该命名空间下的 app' : '先选 namespace 可缩小范围'} />
|
||||
</label>
|
||||
<datalist id="quick-log-apps">{apps.map(value => <option value={value} key={value} />)}</datalist>
|
||||
<label className="min-w-36 flex-1 text-xs text-muted-foreground">副本 pod
|
||||
<Input className="mt-1 h-8" list="quick-log-pods" value={pod} onChange={event => setPod(event.target.value)} placeholder={app ? '搜索该 app 的副本' : '可选'} />
|
||||
</label>
|
||||
<datalist id="quick-log-pods">{pods.map(value => <option value={value} key={value} />)}</datalist>
|
||||
{/* 用 SearchableValueSelect(点击即展开列表 + 可搜索),
|
||||
原生 datalist 只在输入时才提示、样式弱,容易让人以为「下拉是空的」 */}
|
||||
<SearchableValueSelect
|
||||
label="命名空间"
|
||||
values={namespaceOptions}
|
||||
value={namespace}
|
||||
onChange={chooseNamespace}
|
||||
placeholder={`请选择(共 ${namespaceOptions.length} 个)`}
|
||||
allowEmpty
|
||||
loading={!namespaceOptions.length}
|
||||
/>
|
||||
<SearchableValueSelect
|
||||
label={appLabel === 'app' ? 'App' : `App(${appLabel})`}
|
||||
values={apps}
|
||||
value={app}
|
||||
onChange={chooseApp}
|
||||
placeholder={apps.length ? `请选择(共 ${apps.length} 个)` : namespace ? '该范围内无 app' : '先选命名空间可缩小范围'}
|
||||
allowEmpty
|
||||
disabled={!sourceId}
|
||||
/>
|
||||
<SearchableValueSelect
|
||||
label="副本 / Pod"
|
||||
values={pods}
|
||||
value={pod}
|
||||
onChange={setPod}
|
||||
placeholder={pods.length ? `请选择(共 ${pods.length} 个)` : app ? '该 app 下无副本' : '可选'}
|
||||
allowEmpty
|
||||
disabled={!sourceId}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('app')}>查询该范围日志</Button>
|
||||
|
||||
Reference in new issue
Block a user