new logic

This commit is contained in:
typescreep
2025-09-25 17:19:51 +03:00
parent cfa180aca7
commit 2a603a6dbe
3 changed files with 256 additions and 17 deletions

View File

@@ -1,31 +1,217 @@
/* eslint-disable max-lines-per-function */
import React, { FC, Fragment, useState } from 'react'
import { Search as PackageSearch, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import React, { FC, Fragment, useState, useEffect } from 'react'
import { useLocation, useSearchParams } from 'react-router-dom'
import {
Search as PackageSearch,
Spacer,
TRequestError,
TKindIndex,
TKindWithVersion,
getKinds,
getSortedKinds,
// kindByGvr,
} from '@prorobotech/openapi-k8s-toolkit'
import { Form, Spin, Alert } from 'antd'
import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
import {
FIELD_NAME,
FIELD_NAME_STRING,
FIELD_NAME_LABELS,
FIELD_NAME_FIELDS,
TYPE_SELECTOR,
QUERY_KEY,
NAME_QUERY_KEY,
LABELS_QUERY_KEY,
FIELDS_QUERY_KEY,
} from './constants'
import { useDebouncedCallback, getArrayParam, setArrayParam, getStringParam, setStringParam } from './utils'
import { SearchEntry } from './molecules'
export const Search: FC = () => {
const cluster = useSelector((state: RootState) => state.cluster.cluster)
const [searchParams, setSearchParams] = useSearchParams()
const location = useLocation()
const [currentSearch, setCurrentSearch] = useState<{
resources?: string[]
name?: string
labels?: string[]
fields?: string[]
}>()
const cluster = useSelector((state: RootState) => state.cluster.cluster)
const theme = useSelector((state: RootState) => state.openapiTheme.theme)
const [form] = Form.useForm()
const [error, setError] = useState<TRequestError | undefined>()
const [isLoading, setIsLoading] = useState<boolean>(false)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [kindIndex, setKindIndex] = useState<TKindIndex>()
const [kindsWithVersion, setKindWithVersion] = useState<TKindWithVersion[]>()
useEffect(() => {
setIsLoading(true)
setError(undefined)
getKinds({
clusterName: cluster,
})
.then(data => {
setKindIndex(data)
setKindWithVersion(getSortedKinds(data))
setIsLoading(false)
setError(undefined)
})
.catch(error => {
setIsLoading(false)
setError(error)
})
}, [cluster])
const watchedKinds = Form.useWatch<string[] | undefined>(FIELD_NAME, form)
const watchedName = Form.useWatch<string | undefined>(FIELD_NAME_STRING, form)
const watchedLabels = Form.useWatch<string[] | undefined>(FIELD_NAME_LABELS, form)
const watchedFields = Form.useWatch<string[] | undefined>(FIELD_NAME_FIELDS, form)
const watchedTypedSelector = Form.useWatch<string | undefined>(TYPE_SELECTOR, form)
// Apply current values from search params on mount / when URL changes
useEffect(() => {
const fromKinds = getArrayParam(searchParams, QUERY_KEY)
const currentKinds = form.getFieldValue(FIELD_NAME)
const kindsDiffer =
(fromKinds.length || 0) !== (currentKinds?.length || 0) || fromKinds.some((v, i) => v !== currentKinds?.[i])
// name
const fromName = getStringParam(searchParams, NAME_QUERY_KEY)
const currentName = form.getFieldValue(FIELD_NAME_STRING) as string | undefined
const nameDiffer = (fromName || '') !== (currentName || '')
// labels
const fromLabels = getArrayParam(searchParams, LABELS_QUERY_KEY)
const currentLabels = form.getFieldValue(FIELD_NAME_LABELS) as string[] | undefined
const labelsDiffer =
(fromLabels.length || 0) !== (currentLabels?.length || 0) || fromLabels.some((v, i) => v !== currentLabels?.[i])
// labels
const fromFields = getArrayParam(searchParams, FIELDS_QUERY_KEY)
const currentFields = form.getFieldValue(FIELD_NAME_FIELDS) as string[] | undefined
const fieldsDiffer =
(fromFields.length || 0) !== (currentFields?.length || 0) || fromFields.some((v, i) => v !== currentFields?.[i])
// decide type from params
const currentType = form.getFieldValue(TYPE_SELECTOR)
let inferredType: string | undefined
if (fromName) {
inferredType = 'name'
} else if (fromLabels.length > 0) {
inferredType = 'labels'
} else if (fromFields.length > 0) {
inferredType = 'fields'
}
const typeDiffer = inferredType !== currentType
// Only update the form if URL differs from form (prevents loops)
if (kindsDiffer || nameDiffer || labelsDiffer || fieldsDiffer) {
form.setFieldsValue({
[FIELD_NAME]: kindsDiffer ? fromKinds : currentKinds,
[FIELD_NAME_STRING]: nameDiffer ? fromName : currentName,
[FIELD_NAME_LABELS]: labelsDiffer ? fromLabels : currentLabels,
[FIELD_NAME_FIELDS]: fieldsDiffer ? fromFields : currentFields,
[TYPE_SELECTOR]: typeDiffer ? inferredType : currentType,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.search]) // react to back/forward, external URL edits
// Watch field changes to push to URL (debounced)
const debouncedPush = useDebouncedCallback((values: string[]) => {
const next = setArrayParam(searchParams, QUERY_KEY, values)
setSearchParams(next, { replace: true }) // replace to keep history cleaner
}, 250)
const debouncedPushName = useDebouncedCallback((value: string) => {
const next = setStringParam(searchParams, NAME_QUERY_KEY, value)
setSearchParams(next, { replace: true })
}, 250)
const debouncedPushLabels = useDebouncedCallback((values: string[]) => {
const next = setArrayParam(searchParams, LABELS_QUERY_KEY, values)
setSearchParams(next, { replace: true })
}, 250)
const debouncedPushFields = useDebouncedCallback((values: string[]) => {
const next = setArrayParam(searchParams, FIELDS_QUERY_KEY, values)
setSearchParams(next, { replace: true })
}, 250)
useEffect(() => {
debouncedPush(watchedKinds || [])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [watchedKinds])
useEffect(() => {
debouncedPushName((watchedName || '').trim())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [watchedName])
useEffect(() => {
debouncedPushLabels(watchedLabels || [])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [watchedLabels])
useEffect(() => {
debouncedPushFields(watchedFields || [])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [watchedFields])
useEffect(() => {
if (watchedTypedSelector === 'name') {
// Clear labels when switching to "name"
// const cur = form.getFieldValue(FIELD_NAME_LABELS) as string[] | undefined
// if (cur?.length) {
form.setFieldsValue({ [FIELD_NAME_LABELS]: [], [FIELD_NAME_FIELDS]: [] })
// }
} else if (watchedTypedSelector === 'labels') {
// Clear name when switching to "labels"
// const cur = (form.getFieldValue(FIELD_NAME_STRING) as string | undefined) ?? ''
// if (cur) {
form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_FIELDS]: [] })
// }
} else if (watchedTypedSelector === 'fields') {
// Clear name when switching to "labels"
// const cur = (form.getFieldValue(FIELD_NAME_STRING) as string | undefined) ?? ''
// if (cur) {
form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_LABELS]: [] })
// }
}
// Optional: if undefined (e.g., initial), choose a default behavior:
// else { form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_MULTIPLE]: [] }) }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [watchedTypedSelector])
if (error) {
return <Alert type="error" message="Error while loading kinds" description={error?.response?.data?.message} />
}
if (isLoading || !kindsWithVersion) {
return <Spin />
}
if (!kindsWithVersion) {
return <Alert type="error" message="Error while loading kinds" description="Empty" />
}
return (
<>
<PackageSearch cluster={cluster} updateCurrentSearch={value => setCurrentSearch(value)} />
{currentSearch?.resources?.map(item => (
<PackageSearch
cluster={cluster}
theme={theme}
form={form}
constants={{
FIELD_NAME,
FIELD_NAME_STRING,
FIELD_NAME_LABELS,
FIELD_NAME_FIELDS,
TYPE_SELECTOR,
}}
kindsWithVersion={kindsWithVersion}
/>
{watchedKinds?.map(item => (
<Fragment key={item}>
<SearchEntry
resource={item}
name={currentSearch.name}
labels={currentSearch.labels}
fields={currentSearch.fields}
/>
<SearchEntry resource={item} name={watchedName} labels={watchedLabels} fields={watchedFields} />
<Spacer $space={50} $samespace />
</Fragment>
))}

View File

@@ -0,0 +1,11 @@
export const FIELD_NAME = 'kinds'
export const FIELD_NAME_STRING = 'name'
export const FIELD_NAME_LABELS = 'labels'
export const FIELD_NAME_FIELDS = 'fields'
export const TYPE_SELECTOR = 'TYPE_SELECTOR'
export const QUERY_KEY = 'kinds' // the query param name
export const NAME_QUERY_KEY = 'name'
export const LABELS_QUERY_KEY = 'labels'
export const FIELDS_QUERY_KEY = 'fields'

View File

@@ -0,0 +1,42 @@
import { useRef } from 'react'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useDebouncedCallback = <T extends (...args: any[]) => void>(fn: T, delay = 300) => {
const timer = useRef<number | undefined>(undefined)
return (...args: Parameters<T>) => {
if (timer.current) window.clearTimeout(timer.current)
timer.current = window.setTimeout(() => fn(...args), delay)
}
}
// Convert between array<string> and a single comma-separated query param.
export const getArrayParam = (sp: URLSearchParams, key: string): string[] => {
const raw = sp.get(key)
if (!raw) return []
return raw
.split(',')
.map(s => s.trim())
.filter(Boolean)
}
export const setArrayParam = (sp: URLSearchParams, key: string, values: string[] | undefined | null) => {
const next = new URLSearchParams(sp) // preserve other params
if (!values || values.length === 0) {
next.delete(key)
} else {
next.set(key, values.join(','))
}
return next
}
export const getStringParam = (sp: URLSearchParams, key: string): string => {
return sp.get(key) ?? ''
}
export const setStringParam = (sp: URLSearchParams, key: string, value: string | undefined | null) => {
const next = new URLSearchParams(sp) // preserve other params
const v = (value ?? '').trim()
if (!v) next.delete(key)
else next.set(key, v)
return next
}