fix(logs): 域名筛选与残余原生 select 统一为可搜索/Radix Select
通病:/logs 域名筛选仍用 Input+datalist(与已修的快捷模板同类);策略组数据源、数据源弹窗集群/鉴权仍为原生 select。改为 SearchableValueSelect / ui/select,并顺带理顺策略组列表空标签与用户勾选交互。采集器编辑里 app 预设 datalist 仍保留(自由输入场景)。
This commit is contained in:
1 parent
016d3ecdfd
commit
8c9f5a95f8
5 files changed
+77
-18
No files matched your search
@@ -146,6 +146,7 @@ export default {
|
|||||||
loadingQuery: 'Querying Loki',
|
loadingQuery: 'Querying Loki',
|
||||||
loadingWait: 'Waiting for {{seconds}}s. Wide-range queries may take a while.',
|
loadingWait: 'Waiting for {{seconds}}s. Wide-range queries may take a while.',
|
||||||
pagingHint: 'Logs are paged newest to oldest; charts show the current page, and the top timeline covers the whole query range.',
|
pagingHint: 'Logs are paged newest to oldest; charts show the current page, and the top timeline covers the whole query range.',
|
||||||
|
domainAll: 'All domains',
|
||||||
domainFilter: 'Domain filter',
|
domainFilter: 'Domain filter',
|
||||||
domainPlaceholder: 'Type or pick a domain',
|
domainPlaceholder: 'Type or pick a domain',
|
||||||
domainQuery: 'Query in Loki',
|
domainQuery: 'Query in Loki',
|
||||||
@@ -256,6 +257,7 @@ export default {
|
|||||||
loadFailed: 'Failed to load context',
|
loadFailed: 'Failed to load context',
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
|
labelsUnlimited: 'Unlimited · all namespaces',
|
||||||
loadFailed: 'Failed to load query policies',
|
loadFailed: 'Failed to load query policies',
|
||||||
validate: 'Fill in the policy group name, choose a data source, and select "Anyone" or at least one user',
|
validate: 'Fill in the policy group name, choose a data source, and select "Anyone" or at least one user',
|
||||||
saveFailed: 'Failed to save policy group',
|
saveFailed: 'Failed to save policy group',
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export default {
|
|||||||
loadingQuery: '正在查询 Loki',
|
loadingQuery: '正在查询 Loki',
|
||||||
loadingWait: '已等待 {{seconds}} 秒。大范围查询可能需要较长时间。',
|
loadingWait: '已等待 {{seconds}} 秒。大范围查询可能需要较长时间。',
|
||||||
pagingHint: '日志按时间从新到旧翻页;图表展示当前页,顶部时间线展示整个查询范围。',
|
pagingHint: '日志按时间从新到旧翻页;图表展示当前页,顶部时间线展示整个查询范围。',
|
||||||
|
domainAll: '全部域名',
|
||||||
domainFilter: '域名筛选',
|
domainFilter: '域名筛选',
|
||||||
domainPlaceholder: '输入或选择域名',
|
domainPlaceholder: '输入或选择域名',
|
||||||
domainQuery: '在 Loki 中查询',
|
domainQuery: '在 Loki 中查询',
|
||||||
@@ -249,6 +250,7 @@ export default {
|
|||||||
loadFailed: '读取上下文失败',
|
loadFailed: '读取上下文失败',
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
|
labelsUnlimited: '不限 · 可查询全部命名空间',
|
||||||
loadFailed: '加载查询策略失败',
|
loadFailed: '加载查询策略失败',
|
||||||
validate: '请填写策略组名称,选择数据源,并选择「所有人」或至少一位用户',
|
validate: '请填写策略组名称,选择数据源,并选择「所有人」或至少一位用户',
|
||||||
saveFailed: '保存策略组失败',
|
saveFailed: '保存策略组失败',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { SearchableValueSelect } from '@/components/SearchableValueSelect'
|
||||||
import type { LogEntry } from '@/api/logs'
|
import type { LogEntry } from '@/api/logs'
|
||||||
import { parseIngressAccessLog } from '@/lib/ingress-access-log'
|
import { parseIngressAccessLog } from '@/lib/ingress-access-log'
|
||||||
import { applyLabelFilter } from '../query-utils'
|
import { applyLabelFilter } from '../query-utils'
|
||||||
@@ -50,9 +50,19 @@ export function LogResultsPanel({ entries, searched, hasSource, pageIndex, hasNe
|
|||||||
</div>}
|
</div>}
|
||||||
{pageIndex > 0 && <p className="mb-2 text-xs text-muted-foreground">{t('results.pagingHint')}</p>}
|
{pageIndex > 0 && <p className="mb-2 text-xs text-muted-foreground">{t('results.pagingHint')}</p>}
|
||||||
{domains.length > 0 && <div className="mb-3 flex flex-wrap items-center gap-2 text-xs">
|
{domains.length > 0 && <div className="mb-3 flex flex-wrap items-center gap-2 text-xs">
|
||||||
<label htmlFor="log-domain-filter" className="shrink-0 text-muted-foreground">{t('results.domainFilter')}</label>
|
<span className="shrink-0 text-muted-foreground">{t('results.domainFilter')}</span>
|
||||||
<Input id="log-domain-filter" list="log-domain-options" type="search" className="h-8 w-60" value={domainFilter} onChange={event => setDomainFilter(event.target.value)} placeholder={t('results.domainPlaceholder')} />
|
<SearchableValueSelect
|
||||||
<datalist id="log-domain-options">{domains.map(domain => <option key={domain} value={domain} />)}</datalist>
|
hideLabel
|
||||||
|
ariaLabel={t('results.domainFilter')}
|
||||||
|
values={domains}
|
||||||
|
value={domainFilter}
|
||||||
|
onChange={setDomainFilter}
|
||||||
|
allowEmpty
|
||||||
|
emptyLabel={t('results.domainAll')}
|
||||||
|
placeholder={t('results.domainPlaceholder')}
|
||||||
|
className="min-w-60"
|
||||||
|
triggerClassName="h-8"
|
||||||
|
/>
|
||||||
<Button type="button" size="sm" variant="outline" disabled={!domainFilter.trim() || loading} onClick={() => onApplyDomain(domainFilter.trim())}>{t('results.domainQuery')}</Button>
|
<Button type="button" size="sm" variant="outline" disabled={!domainFilter.trim() || loading} onClick={() => onApplyDomain(domainFilter.trim())}>{t('results.domainQuery')}</Button>
|
||||||
<span className="text-muted-foreground">{t('results.domainHint', { shown: visibleEntries.length, total: entries.length })}</span>
|
<span className="text-muted-foreground">{t('results.domainHint', { shown: visibleEntries.length, total: entries.length })}</span>
|
||||||
</div>}
|
</div>}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Plus, UsersRound } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
import { ErrorState } from '@/components/ErrorState'
|
import { ErrorState } from '@/components/ErrorState'
|
||||||
@@ -84,20 +87,49 @@ export function PolicyGroupsPage({ sources }: { sources: LogSource[] }) {
|
|||||||
|
|
||||||
return <div className="grid gap-6 xl:grid-cols-[minmax(240px,0.85fr)_minmax(0,2fr)]">
|
return <div className="grid gap-6 xl:grid-cols-[minmax(240px,0.85fr)_minmax(0,2fr)]">
|
||||||
<aside className="rounded-xl border bg-card p-5 space-y-4">
|
<aside className="rounded-xl border bg-card p-5 space-y-4">
|
||||||
<div className="flex items-center justify-between gap-2"><h2 className="font-semibold">{t('access.listTitle')}</h2><Button size="sm" onClick={() => setForm(emptyForm(sources[0]?.id ?? 0))}>{t('access.create')}</Button></div>
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<h2 className="font-semibold">{t('access.listTitle')} <span className="text-sm font-normal text-muted-foreground">({groups.length})</span></h2>
|
||||||
|
<Button size="sm" onClick={() => setForm(emptyForm(sources[0]?.id ?? 0))}><Plus className="mr-1 size-4" />{t('access.create')}</Button>
|
||||||
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{t('access.listHint')}</p>
|
<p className="text-sm text-muted-foreground">{t('access.listHint')}</p>
|
||||||
{loading && <p className="text-sm text-muted-foreground">{t('access.loading')}</p>}
|
{loading && <p className="text-sm text-muted-foreground">{t('access.loading')}</p>}
|
||||||
{error && <ErrorState message={error} onRetry={() => void reload()} />}
|
{error && <ErrorState message={error} onRetry={() => void reload()} />}
|
||||||
{!loading && !error && groups.length === 0 && <p className="text-sm text-muted-foreground">{t('access.empty')}</p>}
|
{!loading && !error && groups.length === 0 && <p className="text-sm text-muted-foreground">{t('access.empty')}</p>}
|
||||||
<div className="space-y-2">{groups.map(group => <button key={group.id} type="button" onClick={() => setForm({ ...group, labels: { ...group.labels }, userIds: [...group.userIds] })} className={`w-full rounded-lg border p-3 text-left transition-colors ${form.id === group.id ? 'border-primary bg-primary/5' : 'hover:bg-muted'}`}>
|
<div className="space-y-2">{groups.map(group => <button key={group.id} type="button" onClick={() => setForm({ ...group, labels: { ...group.labels }, userIds: [...group.userIds] })} className={`w-full rounded-lg border p-3 text-left transition-colors ${form.id === group.id ? 'border-primary bg-primary/5' : 'hover:bg-muted'}`}>
|
||||||
<strong className="block text-sm">{group.name}</strong><span className="block mt-1 text-xs text-muted-foreground">{sourceName(group.sourceId)} · {group.userIds.includes(ANYONE) ? t('access.anyone') : t('access.userCount', { count: group.userIds.length })}</span><span className="block mt-1 truncate font-mono text-xs text-muted-foreground">{JSON.stringify(group.labels)}</span>
|
<strong className="block text-sm">{group.name}</strong><span className="block mt-1 text-xs text-muted-foreground">{sourceName(group.sourceId)} · {group.userIds.includes(ANYONE) ? t('access.anyone') : t('access.userCount', { count: group.userIds.length })}</span>{Object.keys(group.labels).length > 0 ? (
|
||||||
|
<span className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{Object.entries(group.labels).map(([key, value]) => <span key={key} className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">{key}={value}</span>)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="mt-1 block text-xs text-muted-foreground">{t('access.labelsUnlimited')}</span>
|
||||||
|
)}
|
||||||
</button>)}</div>
|
</button>)}</div>
|
||||||
</aside>
|
</aside>
|
||||||
<section className="rounded-xl border bg-card p-5 space-y-6">
|
<section className="rounded-xl border bg-card p-5 space-y-6">
|
||||||
<div className="flex items-start justify-between gap-3"><div><h2 className="font-semibold">{form.id ? t('access.editTitle') : t('access.createTitle')}</h2><p className="mt-1 text-sm text-muted-foreground">{t('access.formHint')}</p></div>{form.id && <Button variant="outline" size="sm" className="text-destructive" onClick={() => setDeleteTarget(groups.find(group => group.id === form.id) ?? null)}>{t('access.remove')}</Button>}</div>
|
<div className="flex items-start justify-between gap-3"><div><h2 className="font-semibold">{form.id ? t('access.editTitle') : t('access.createTitle')}</h2><p className="mt-1 text-sm text-muted-foreground">{t('access.formHint')}</p></div>{form.id && <Button variant="outline" size="sm" className="text-destructive" onClick={() => setDeleteTarget(groups.find(group => group.id === form.id) ?? null)}>{t('access.remove')}</Button>}</div>
|
||||||
<div className="grid gap-4 sm:grid-cols-2"><label className="text-sm font-medium">{t('access.nameLabel')}<Input className="mt-1" value={form.name} onChange={event => setForm(current => ({ ...current, name: event.target.value }))} placeholder={t('access.namePlaceholder')} /></label><label className="text-sm font-medium">{t('access.sourceLabel')}<select className="mt-1 block h-9 w-full rounded-md border bg-background px-3" value={form.sourceId} onChange={event => setForm(current => ({ ...current, sourceId: Number(event.target.value), labels: {}, userIds: [] }))}>{sources.length === 0 && <option value={0}>{t('access.sourceEmpty')}</option>}{sources.map(source => <option key={source.id} value={source.id}>{source.name}</option>)}</select></label></div>
|
<div className="grid gap-4 sm:grid-cols-2"><label className="text-sm font-medium">{t('access.nameLabel')}<Input className="mt-1" value={form.name} onChange={event => setForm(current => ({ ...current, name: event.target.value }))} placeholder={t('access.namePlaceholder')} /></label><label className="text-sm font-medium">{t('access.sourceLabel')}
|
||||||
|
<Select
|
||||||
|
value={form.sourceId ? String(form.sourceId) : undefined}
|
||||||
|
onValueChange={value => setForm(current => ({ ...current, sourceId: Number(value), labels: {}, userIds: [] }))}
|
||||||
|
disabled={sources.length === 0}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1 h-9 w-full">
|
||||||
|
<SelectValue placeholder={t('access.sourceEmpty')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{sources.map(source => <SelectItem key={source.id} value={String(source.id)}>{source.name}</SelectItem>)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</label></div>
|
||||||
<div><h3 className="mb-2 text-sm font-medium">{t('access.scopeTitle')}</h3><AccessRuleEditor sourceId={form.sourceId} username={form.name || t('access.ownerFallback')} labels={form.labels} onChange={labels => setForm(current => ({ ...current, labels }))} /><p className="mt-2 text-xs text-muted-foreground">{t('access.scopeHint')}</p></div>
|
<div><h3 className="mb-2 text-sm font-medium">{t('access.scopeTitle')}</h3><AccessRuleEditor sourceId={form.sourceId} username={form.name || t('access.ownerFallback')} labels={form.labels} onChange={labels => setForm(current => ({ ...current, labels }))} /><p className="mt-2 text-xs text-muted-foreground">{t('access.scopeHint')}</p></div>
|
||||||
<div className="space-y-3"><div className="flex items-center justify-between gap-2"><h3 className="text-sm font-medium">{t('access.usersTitle')} <span className="text-muted-foreground">{form.userIds.includes(ANYONE) ? t('access.selectedAnyone') : t('access.selectedCount', { count: form.userIds.length })}</span></h3><div className="flex gap-2"><Button size="sm" variant={form.userIds.includes(ANYONE) ? 'default' : 'outline'} onClick={() => setForm(current => current.userIds.includes(ANYONE) ? { ...current, userIds: [] } : { ...current, userIds: [ANYONE] })}>{form.userIds.includes(ANYONE) ? t('access.anyoneActive') : t('access.anyoneToggle')}</Button><Button size="sm" variant="outline" disabled={form.userIds.includes(ANYONE)} onClick={() => setForm(current => ({ ...current, userIds: Array.from(new Set([...current.userIds, ...visibleUsers.filter(user => !assignedElsewhere.has(user.id)).map(user => user.id)])) }))}>{t('access.selectFiltered')}</Button><Button size="sm" variant="ghost" onClick={() => setForm(current => ({ ...current, userIds: [] }))}>{t('access.clear')}</Button></div></div><p className="text-xs text-muted-foreground">{t('access.usersHint')}</p><Input aria-label={t('access.searchAria')} value={filter} onChange={event => setFilter(event.target.value)} placeholder={t('access.searchPlaceholder')} /><div className="max-h-60 overflow-auto rounded-md border p-2 grid gap-1 sm:grid-cols-2 lg:grid-cols-3">{visibleUsers.map(user => <label key={user.id} className={`flex items-center gap-2 rounded px-2 py-1.5 text-sm ${assignedElsewhere.has(user.id) ? 'text-muted-foreground' : 'hover:bg-muted'}`}><input type="checkbox" disabled={assignedElsewhere.has(user.id) || form.userIds.includes(ANYONE)} checked={form.userIds.includes(user.id)} onChange={event => setForm(current => ({ ...current, userIds: event.target.checked ? [...current.userIds, user.id] : current.userIds.filter(id => id !== user.id) }))} /><span className="truncate">{user.username}</span>{assignedElsewhere.has(user.id) && <span className="text-xs">{t('access.grouped')}</span>}</label>)}{visibleUsers.length === 0 && <p className="p-2 text-sm text-muted-foreground">{t('access.noMatch')}</p>}</div></div>
|
<div className="space-y-3"><div className="flex items-center justify-between gap-2"><h3 className="text-sm font-medium">{t('access.usersTitle')} <span className="text-muted-foreground">{form.userIds.includes(ANYONE) ? t('access.selectedAnyone') : t('access.selectedCount', { count: form.userIds.length })}</span></h3><div className="flex gap-2"><Button size="sm" variant={form.userIds.includes(ANYONE) ? 'default' : 'outline'} onClick={() => setForm(current => current.userIds.includes(ANYONE) ? { ...current, userIds: [] } : { ...current, userIds: [ANYONE] })}><UsersRound className="mr-1 size-3.5" />{form.userIds.includes(ANYONE) ? t('access.anyoneActive') : t('access.anyoneToggle')}</Button><Button size="sm" variant="outline" disabled={form.userIds.includes(ANYONE)} onClick={() => setForm(current => ({ ...current, userIds: Array.from(new Set([...current.userIds, ...visibleUsers.filter(user => !assignedElsewhere.has(user.id)).map(user => user.id)])) }))}>{t('access.selectFiltered')}</Button><Button size="sm" variant="ghost" onClick={() => setForm(current => ({ ...current, userIds: [] }))}>{t('access.clear')}</Button></div></div><p className="text-xs text-muted-foreground">{t('access.usersHint')}</p><Input aria-label={t('access.searchAria')} value={filter} onChange={event => setFilter(event.target.value)} placeholder={t('access.searchPlaceholder')} /><div className={`grid max-h-60 grid-cols-1 gap-1 overflow-auto rounded-md border p-2 sm:grid-cols-2 lg:grid-cols-3 ${form.userIds.includes(ANYONE) ? 'pointer-events-none opacity-50' : ''}`}>{visibleUsers.map(user => (
|
||||||
|
<label key={user.id} htmlFor={`policy-user-${user.id}`} className={`flex items-center gap-2 rounded px-2 py-1.5 text-sm ${assignedElsewhere.has(user.id) ? 'text-muted-foreground' : 'hover:bg-muted'}`}>
|
||||||
|
<Checkbox id={`policy-user-${user.id}`} disabled={assignedElsewhere.has(user.id)} checked={form.userIds.includes(user.id)} onCheckedChange={checked => setForm(current => ({ ...current, userIds: checked ? [...current.userIds, user.id] : current.userIds.filter(id => id !== user.id) }))} />
|
||||||
|
<span className="truncate">{user.username}</span>
|
||||||
|
{user.nickname && <span className="truncate text-xs text-muted-foreground">{user.nickname}</span>}
|
||||||
|
{assignedElsewhere.has(user.id) && <span className="ml-auto shrink-0 text-xs">{t('access.grouped')}</span>}
|
||||||
|
</label>
|
||||||
|
))}{visibleUsers.length === 0 && <p className="p-2 text-sm text-muted-foreground">{t('access.noMatch')}</p>}</div></div>
|
||||||
<div className="flex justify-end"><Button disabled={saving || !sources.length} onClick={() => void save()}>{saving ? t('access.saving') : t('access.save')}</Button></div>
|
<div className="flex justify-end"><Button disabled={saving || !sources.length} onClick={() => void save()}>{saving ? t('access.saving') : t('access.save')}</Button></div>
|
||||||
</section>
|
</section>
|
||||||
<ConfirmDialog open={deleteTarget !== null} onOpenChange={open => { if (!open) setDeleteTarget(null) }} name={deleteTarget?.name} title={t('access.deleteTitle')} description={t('access.deleteDesc', { name: deleteTarget?.name ?? '' })} loading={saving} onConfirm={() => void remove()} />
|
<ConfirmDialog open={deleteTarget !== null} onOpenChange={open => { if (!open) setDeleteTarget(null) }} name={deleteTarget?.name} title={t('access.deleteTitle')} description={t('access.deleteDesc', { name: deleteTarget?.name ?? '' })} loading={saving} onConfirm={() => void remove()} />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { toast } from 'sonner'
|
|||||||
import { Database, Globe, Loader2, Lock, Pencil, Plus, Server, Trash2, Zap } from 'lucide-react'
|
import { Database, Globe, Loader2, Lock, Pencil, Plus, Server, Trash2, Zap } from 'lucide-react'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
@@ -180,10 +181,17 @@ export const SourcesManager = memo(function SourcesManager({ sources, onSaved }:
|
|||||||
<>
|
<>
|
||||||
<label className={labelClass}>
|
<label className={labelClass}>
|
||||||
{t('source.clusterLabel')}
|
{t('source.clusterLabel')}
|
||||||
<select className="mt-1 block h-9 w-full rounded-md border bg-background px-3 text-sm" value={form.clusterID ?? ''} onChange={event => setForm({ ...form, clusterID: Number(event.target.value) })}>
|
<Select
|
||||||
<option value="">{t('source.clusterPlaceholder')}</option>
|
value={form.clusterID ? String(form.clusterID) : undefined}
|
||||||
{clusters.map(cluster => <option key={cluster.id} value={cluster.id}>{cluster.name}</option>)}
|
onValueChange={value => setForm({ ...form, clusterID: Number(value) })}
|
||||||
</select>
|
>
|
||||||
|
<SelectTrigger className="mt-1 h-9 w-full">
|
||||||
|
<SelectValue placeholder={t('source.clusterPlaceholder')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{clusters.map(cluster => <SelectItem key={cluster.id} value={String(cluster.id)}>{cluster.name}</SelectItem>)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</label>
|
</label>
|
||||||
<label className={labelClass}>
|
<label className={labelClass}>
|
||||||
{t('source.namespaceLabel')}
|
{t('source.namespaceLabel')}
|
||||||
@@ -201,12 +209,17 @@ export const SourcesManager = memo(function SourcesManager({ sources, onSaved }:
|
|||||||
)}
|
)}
|
||||||
<label className={labelClass}>
|
<label className={labelClass}>
|
||||||
{t('source.authLabel')}
|
{t('source.authLabel')}
|
||||||
<select className="mt-1 block h-9 w-full rounded-md border bg-background px-3 text-sm" value={form.authType ?? 'none'} onChange={event => setForm({ ...form, authType: event.target.value })}>
|
<Select value={form.authType ?? 'none'} onValueChange={value => setForm({ ...form, authType: value })}>
|
||||||
<option value="none">{t('source.authNone')}</option>
|
<SelectTrigger className="mt-1 h-9 w-full">
|
||||||
<option value="basic">{t('source.authBasic')}</option>
|
<SelectValue />
|
||||||
<option value="bearer">{t('source.authBearer')}</option>
|
</SelectTrigger>
|
||||||
<option value="headers">{t('source.authHeaders')}</option>
|
<SelectContent>
|
||||||
</select>
|
<SelectItem value="none">{t('source.authNone')}</SelectItem>
|
||||||
|
<SelectItem value="basic">{t('source.authBasic')}</SelectItem>
|
||||||
|
<SelectItem value="bearer">{t('source.authBearer')}</SelectItem>
|
||||||
|
<SelectItem value="headers">{t('source.authHeaders')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</label>
|
</label>
|
||||||
{form.authType === 'basic' && (
|
{form.authType === 'basic' && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in new issue
Block a user