From 2a603a6dbedbd9decadc526340e32a26504e97c0 Mon Sep 17 00:00:00 2001 From: typescreep Date: Thu, 25 Sep 2025 17:19:51 +0300 Subject: [PATCH] new logic --- src/components/organisms/Search/Search.tsx | 220 +++++++++++++++++-- src/components/organisms/Search/constants.ts | 11 + src/components/organisms/Search/utils.ts | 42 ++++ 3 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 src/components/organisms/Search/constants.ts create mode 100644 src/components/organisms/Search/utils.ts diff --git a/src/components/organisms/Search/Search.tsx b/src/components/organisms/Search/Search.tsx index d58114b..b99d973 100644 --- a/src/components/organisms/Search/Search.tsx +++ b/src/components/organisms/Search/Search.tsx @@ -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() + const [isLoading, setIsLoading] = useState(false) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [kindIndex, setKindIndex] = useState() + const [kindsWithVersion, setKindWithVersion] = useState() + + 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(FIELD_NAME, form) + const watchedName = Form.useWatch(FIELD_NAME_STRING, form) + const watchedLabels = Form.useWatch(FIELD_NAME_LABELS, form) + const watchedFields = Form.useWatch(FIELD_NAME_FIELDS, form) + const watchedTypedSelector = Form.useWatch(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 + } + + if (isLoading || !kindsWithVersion) { + return + } + + if (!kindsWithVersion) { + return + } return ( <> - setCurrentSearch(value)} /> - {currentSearch?.resources?.map(item => ( + + {watchedKinds?.map(item => ( - + ))} diff --git a/src/components/organisms/Search/constants.ts b/src/components/organisms/Search/constants.ts new file mode 100644 index 0000000..021c83e --- /dev/null +++ b/src/components/organisms/Search/constants.ts @@ -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' diff --git a/src/components/organisms/Search/utils.ts b/src/components/organisms/Search/utils.ts new file mode 100644 index 0000000..bcc977f --- /dev/null +++ b/src/components/organisms/Search/utils.ts @@ -0,0 +1,42 @@ +import { useRef } from 'react' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const useDebouncedCallback = void>(fn: T, delay = 300) => { + const timer = useRef(undefined) + return (...args: Parameters) => { + if (timer.current) window.clearTimeout(timer.current) + timer.current = window.setTimeout(() => fn(...args), delay) + } +} + +// Convert between array 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 +}