fix(ui): SearchableValueSelect 支持 commitOnType,恢复域名边打边筛
设计补刀:域名筛换成可搜 Select 后丢失「键入即过滤当前页」。新增 commitOnType——键入即 onChange、列表当建议、「全部」清空;长选项 truncate+title。域名筛启用;采集器 sidecar 命名空间/工作负载长列表一并改为可搜(同刀)。
This commit is contained in:
1 parent
8c9f5a95f8
commit
305d7c5904
3 files changed
+50
-24
No files matched your search
@@ -16,6 +16,11 @@ interface SearchableValueSelectProps {
|
|||||||
allowEmpty?: boolean
|
allowEmpty?: boolean
|
||||||
/** Label for the empty option and closed-state display when value is empty. */
|
/** Label for the empty option and closed-state display when value is empty. */
|
||||||
emptyLabel?: string
|
emptyLabel?: string
|
||||||
|
/**
|
||||||
|
* Commit every keystroke via onChange (list is suggestions only).
|
||||||
|
* Used by domain filter: type to filter the current page without requiring a pick.
|
||||||
|
*/
|
||||||
|
commitOnType?: boolean
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
@@ -33,6 +38,7 @@ export function SearchableValueSelect({
|
|||||||
placeholder,
|
placeholder,
|
||||||
allowEmpty = false,
|
allowEmpty = false,
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
|
commitOnType = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
@@ -46,9 +52,9 @@ export function SearchableValueSelect({
|
|||||||
const id = useId()
|
const id = useId()
|
||||||
const emptyText = emptyLabel ?? t('select.any')
|
const emptyText = emptyLabel ?? t('select.any')
|
||||||
const choices = values.filter(item => item.toLowerCase().includes(filter.toLowerCase()))
|
const choices = values.filter(item => item.toLowerCase().includes(filter.toLowerCase()))
|
||||||
// active index accounts for optional empty row at top
|
|
||||||
const optionCount = (allowEmpty ? 1 : 0) + choices.length
|
const optionCount = (allowEmpty ? 1 : 0) + choices.length
|
||||||
const closedDisplay = value || (allowEmpty ? emptyText : '')
|
const closedDisplay = value || (allowEmpty ? emptyText : '')
|
||||||
|
const inputValue = open ? filter : closedDisplay
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const close = (event: MouseEvent) => {
|
const close = (event: MouseEvent) => {
|
||||||
@@ -60,7 +66,7 @@ export function SearchableValueSelect({
|
|||||||
|
|
||||||
function choose(next: string) {
|
function choose(next: string) {
|
||||||
onChange(next)
|
onChange(next)
|
||||||
setFilter('')
|
setFilter(commitOnType ? next : '')
|
||||||
setActive(0)
|
setActive(0)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
@@ -81,6 +87,7 @@ export function SearchableValueSelect({
|
|||||||
else {
|
else {
|
||||||
const item = choices[allowEmpty ? active - 1 : active]
|
const item = choices[allowEmpty ? active - 1 : active]
|
||||||
if (item) choose(item)
|
if (item) choose(item)
|
||||||
|
else if (commitOnType) setOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,11 +105,20 @@ export function SearchableValueSelect({
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-controls={`${id}-options`}
|
aria-controls={`${id}-options`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={open ? filter : closedDisplay}
|
value={inputValue}
|
||||||
placeholder={placeholder ?? t('select.placeholder')}
|
placeholder={placeholder ?? t('select.placeholder')}
|
||||||
className={cn('h-9', triggerClassName)}
|
className={cn('h-9', triggerClassName)}
|
||||||
onFocus={() => { setFilter(''); setOpen(true) }}
|
onFocus={() => {
|
||||||
onChange={event => { setFilter(event.target.value); setActive(0); setOpen(true) }}
|
setFilter(commitOnType ? value : '')
|
||||||
|
setOpen(true)
|
||||||
|
}}
|
||||||
|
onChange={event => {
|
||||||
|
const next = event.target.value
|
||||||
|
setFilter(next)
|
||||||
|
setActive(0)
|
||||||
|
setOpen(true)
|
||||||
|
if (commitOnType) onChange(next)
|
||||||
|
}}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
{open && (
|
{open && (
|
||||||
@@ -112,7 +128,8 @@ export function SearchableValueSelect({
|
|||||||
type="button"
|
type="button"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={!value}
|
aria-selected={!value}
|
||||||
className={cn('block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-muted', active === 0 && 'bg-primary/10 text-primary')}
|
className={cn('block w-full truncate rounded px-2 py-1.5 text-left text-sm hover:bg-muted', active === 0 && 'bg-primary/10 text-primary')}
|
||||||
|
title={emptyText}
|
||||||
onMouseDown={event => event.preventDefault()}
|
onMouseDown={event => event.preventDefault()}
|
||||||
onClick={() => choose('')}
|
onClick={() => choose('')}
|
||||||
>
|
>
|
||||||
@@ -132,8 +149,9 @@ export function SearchableValueSelect({
|
|||||||
type="button"
|
type="button"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={item === value}
|
aria-selected={item === value}
|
||||||
|
title={item}
|
||||||
className={cn(
|
className={cn(
|
||||||
'block w-full rounded px-2 py-1.5 text-left text-sm',
|
'block w-full truncate rounded px-2 py-1.5 text-left text-sm',
|
||||||
optionIndex === active ? 'bg-primary/10 text-primary' : 'hover:bg-muted',
|
optionIndex === active ? 'bg-primary/10 text-primary' : 'hover:bg-muted',
|
||||||
)}
|
)}
|
||||||
onMouseDown={event => event.preventDefault()}
|
onMouseDown={event => event.preventDefault()}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import { SearchableValueSelect } from '@/components/SearchableValueSelect'
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -367,15 +368,19 @@ export const CollectorFormDialog = memo(function CollectorFormDialog({
|
|||||||
<FormLabel>{t('form.namespace')}{isSidecarType ? ' *' : ''}</FormLabel>
|
<FormLabel>{t('form.namespace')}{isSidecarType ? ' *' : ''}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
{isSidecarType && !manualNamespace ? (
|
{isSidecarType && !manualNamespace ? (
|
||||||
<Select value={field.value} onValueChange={(value) => {
|
<SearchableValueSelect
|
||||||
field.onChange(value)
|
hideLabel
|
||||||
form.setValue('targetDeployment', '')
|
ariaLabel={t('form.namespace')}
|
||||||
}} disabled={!clusterID || namespacesLoading}>
|
values={namespaces}
|
||||||
<SelectTrigger><SelectValue placeholder={!clusterID ? t('form.nsPickCluster') : namespacesLoading ? t('form.nsLoading') : t('form.nsPick')} /></SelectTrigger>
|
value={field.value}
|
||||||
<SelectContent>
|
onChange={(value) => {
|
||||||
{namespaces.map((item) => <SelectItem key={item} value={item}>{item}</SelectItem>)}
|
field.onChange(value)
|
||||||
</SelectContent>
|
form.setValue('targetDeployment', '')
|
||||||
</Select>
|
}}
|
||||||
|
loading={namespacesLoading}
|
||||||
|
disabled={!clusterID || namespacesLoading}
|
||||||
|
placeholder={!clusterID ? t('form.nsPickCluster') : namespacesLoading ? t('form.nsLoading') : t('form.nsPick')}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input placeholder={collectorType === 'k8s-daemonset' ? 'logging' : 'default'} {...field} onChange={(event) => {
|
<Input placeholder={collectorType === 'k8s-daemonset' ? 'logging' : 'default'} {...field} onChange={(event) => {
|
||||||
field.onChange(event)
|
field.onChange(event)
|
||||||
@@ -405,14 +410,16 @@ export const CollectorFormDialog = memo(function CollectorFormDialog({
|
|||||||
{manualDeployment ? (
|
{manualDeployment ? (
|
||||||
<Input placeholder="my-app" {...field} />
|
<Input placeholder="my-app" {...field} />
|
||||||
) : (
|
) : (
|
||||||
<Select value={field.value} onValueChange={field.onChange} disabled={!namespace || workloadsLoading}>
|
<SearchableValueSelect
|
||||||
<SelectTrigger><SelectValue placeholder={!namespace ? t('form.wlPickNamespace') : workloadsLoading ? t('form.wlLoading') : t('form.wlPick')} /></SelectTrigger>
|
hideLabel
|
||||||
<SelectContent>
|
ariaLabel={t('form.targetDeployment')}
|
||||||
{workloads.map((item) => <SelectItem key={item.name} value={item.name}>
|
values={workloads.map((item) => item.name)}
|
||||||
{item.name}{t('form.wlReady', { ready: item.readyReplicas, total: item.replicas })}
|
value={field.value}
|
||||||
</SelectItem>)}
|
onChange={field.onChange}
|
||||||
</SelectContent>
|
loading={workloadsLoading}
|
||||||
</Select>
|
disabled={!namespace || workloadsLoading}
|
||||||
|
placeholder={!namespace ? t('form.wlPickNamespace') : workloadsLoading ? t('form.wlLoading') : t('form.wlPick')}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export function LogResultsPanel({ entries, searched, hasSource, pageIndex, hasNe
|
|||||||
onChange={setDomainFilter}
|
onChange={setDomainFilter}
|
||||||
allowEmpty
|
allowEmpty
|
||||||
emptyLabel={t('results.domainAll')}
|
emptyLabel={t('results.domainAll')}
|
||||||
|
commitOnType
|
||||||
placeholder={t('results.domainPlaceholder')}
|
placeholder={t('results.domainPlaceholder')}
|
||||||
className="min-w-60"
|
className="min-w-60"
|
||||||
triggerClassName="h-8"
|
triggerClassName="h-8"
|
||||||
|
|||||||
Reference in new issue
Block a user