mirror of
https://github.com/outbackdingo/openapi-ui.git
synced 2026-01-27 18:19:50 +00:00
new logic
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
11
src/components/organisms/Search/constants.ts
Normal file
11
src/components/organisms/Search/constants.ts
Normal 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'
|
||||
42
src/components/organisms/Search/utils.ts
Normal file
42
src/components/organisms/Search/utils.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user