diff --git a/.env b/.env index 295e21a..1844f4b 100644 --- a/.env +++ b/.env @@ -31,3 +31,5 @@ VITE_REMOVE_BACKLINK=true VITE_REMOVE_BACKLINK_TEXT=true VITE_DOCS_URL=https://in-cloud.io/docs/tech-docs/introduction/ + +VITE_SEARCH_TABLE_CUSTOMIZATION_PREFIX=stock- diff --git a/.env.options.dist b/.env.options.dist index 5d586f8..a04925e 100644 --- a/.env.options.dist +++ b/.env.options.dist @@ -33,3 +33,5 @@ REMOVE_BACKLINK= REMOVE_BACKLINK_TEXT= DOCS_URL= + +SEARCH_TABLE_CUSTOMIZATION_PREFIX= diff --git a/README.md b/README.md index 48452ce..e9e09cd 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,4 @@ This app can be configured through environment variables. | `REMOVE_BACKLINK` | `boolean` | Remove backlink arrow from right-side navigation | | `REMOVE_BACKLINK_TEXT` | `boolean` | Remove backlink text from right-side navigation | | `DOCS_URL` | `string` | URL to navigate from question mark | +| `SEARCH_TABLE_CUSTOMIZATION_PREFIX` | `string` | Search tables Customization id prefix | diff --git a/package-lock.json b/package-lock.json index e6c683a..a105c68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@ant-design/icons": "5.6.0", "@monaco-editor/react": "4.6.0", "@originjs/vite-plugin-federation": "1.3.6", - "@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.123", + "@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.124", "@readme/openapi-parser": "4.0.0", "@reduxjs/toolkit": "2.2.5", "@tanstack/react-query": "5.62.2", @@ -2804,9 +2804,9 @@ } }, "node_modules/@prorobotech/openapi-k8s-toolkit": { - "version": "0.0.1-alpha.123", - "resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.123.tgz", - "integrity": "sha512-eTge/8JaNPxlSaOluIdSLSCw3I67b4SUzKNQnevrrErofxnUR16MFEJVRiN+M6StatpVlCleQyfcyYG/0St0Lw==", + "version": "0.0.1-alpha.124", + "resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.124.tgz", + "integrity": "sha512-/y3lgZKivdK/ra/7/n2HU4LTI7Z/u4GdQ+vwXhUj6839YDqL/cfMSBiZbjATo5U7sC8A1SNKGiuYH++nkxO13Q==", "license": "MIT", "dependencies": { "@monaco-editor/react": "4.6.0", diff --git a/package.json b/package.json index e099a4d..07d1a9f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@ant-design/icons": "5.6.0", "@monaco-editor/react": "4.6.0", "@originjs/vite-plugin-federation": "1.3.6", - "@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.123", + "@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.124", "@readme/openapi-parser": "4.0.0", "@reduxjs/toolkit": "2.2.5", "@tanstack/react-query": "5.62.2", diff --git a/server/index.ts b/server/index.ts index d54d03c..f95b1ec 100644 --- a/server/index.ts +++ b/server/index.ts @@ -68,6 +68,11 @@ const REMOVE_BACKLINK_TEXT = const DOCS_URL = process.env.LOCAL === 'true' ? options?.DOCS_URL : process.env.DOCS_URL +const SEARCH_TABLE_CUSTOMIZATION_PREFIX = + process.env.LOCAL === 'true' + ? options?.SEARCH_TABLE_CUSTOMIZATION_PREFIX + : process.env.SEARCH_TABLE_CUSTOMIZATION_PREFIX + const healthcheck = require('express-healthcheck') const promBundle = require('express-prom-bundle') @@ -192,6 +197,7 @@ app.get(`${basePrefix ? basePrefix : ''}/env.js`, (_, res) => { LOGOUT_URL: ${JSON.stringify(LOGOUT_URL) || '"check envs"'}, LOGIN_USERNAME_FIELD: ${JSON.stringify(LOGIN_USERNAME_FIELD) || '"check envs"'}, DOCS_URL: ${JSON.stringify(DOCS_URL) || '"/docs"'}, + SEARCH_TABLE_CUSTOMIZATION_PREFIX: ${JSON.stringify(SEARCH_TABLE_CUSTOMIZATION_PREFIX) || '"search-"'}, REMOVE_BACKLINK: ${!!REMOVE_BACKLINK ? JSON.stringify(REMOVE_BACKLINK).toLowerCase() : '"false"'}, REMOVE_BACKLINK_TEXT: ${!!REMOVE_BACKLINK_TEXT ? JSON.stringify(REMOVE_BACKLINK_TEXT).toLowerCase() : '"false"'} } diff --git a/src/components/molecules/TableApiBuiltin/TableApiBuiltin.tsx b/src/components/molecules/TableApiBuiltin/TableApiBuiltin.tsx index 987fb40..00e3bb6 100644 --- a/src/components/molecules/TableApiBuiltin/TableApiBuiltin.tsx +++ b/src/components/molecules/TableApiBuiltin/TableApiBuiltin.tsx @@ -35,7 +35,6 @@ type TTableApiBuiltinProps = { apiGroup?: string // api apiVersion?: string // api typeName: string - specificName?: string labels?: string[] fields?: string[] limit: string | null @@ -51,7 +50,6 @@ export const TableApiBuiltin: FC = ({ apiGroup, apiVersion, typeName, - specificName, labels, fields, limit, @@ -159,7 +157,6 @@ export const TableApiBuiltin: FC = ({ clusterName: cluster, namespace, typeName, - specificName, labels, fields, limit, @@ -176,7 +173,6 @@ export const TableApiBuiltin: FC = ({ apiGroup: apiGroup || '', apiVersion: apiVersion || '', typeName, - specificName, labels, fields, limit, @@ -239,7 +235,7 @@ export const TableApiBuiltin: FC = ({ cluster={cluster} theme={theme} baseprefix={inside ? `${baseprefix}/inside` : baseprefix} - dataItems={getDataItems({ resourceType, dataBuiltin, dataApi, isSingle: !!specificName })} + dataItems={getDataItems({ resourceType, dataBuiltin, dataApi })} dataForControls={{ cluster, syntheticProject: params.syntheticProject, diff --git a/src/components/molecules/TableApiBuiltin/utils.ts b/src/components/molecules/TableApiBuiltin/utils.ts index a523a9c..77a18f3 100644 --- a/src/components/molecules/TableApiBuiltin/utils.ts +++ b/src/components/molecules/TableApiBuiltin/utils.ts @@ -4,22 +4,11 @@ export const getDataItems = ({ resourceType, dataBuiltin, dataApi, - isSingle, }: { resourceType: 'builtin' | 'api' dataBuiltin?: TBuiltinResources dataApi?: TApiResources - isSingle?: boolean }): TJSON[] => { - if (isSingle) { - if (resourceType === 'builtin') { - return dataBuiltin ? [dataBuiltin] : [] - } - - if (resourceType === 'api') { - return dataApi ? [dataApi] : [] - } - } return resourceType === 'builtin' ? dataBuiltin?.items || [] : dataApi?.items || [] } diff --git a/src/components/organisms/HeaderSecond/HeaderSecond.tsx b/src/components/organisms/HeaderSecond/HeaderSecond.tsx index 1d0d561..10b588f 100644 --- a/src/components/organisms/HeaderSecond/HeaderSecond.tsx +++ b/src/components/organisms/HeaderSecond/HeaderSecond.tsx @@ -8,9 +8,10 @@ import { Styled } from './styled' type THeaderProps = { inside?: boolean + isSearch?: boolean } -export const HeaderSecond: FC = ({ inside }) => { +export const HeaderSecond: FC = ({ inside, isSearch }) => { // const { projectName, instanceName, clusterName, entryType, namespace, syntheticProject } = useParams() const { projectName, instanceName, clusterName, namespace, syntheticProject } = useParams() const { token } = theme.useToken() @@ -24,14 +25,14 @@ export const HeaderSecond: FC = ({ inside }) => { {inside ? : } {inside && } - {!inside && BASE_USE_NAMESPACE_NAV !== 'true' && ( + {!inside && !isSearch && BASE_USE_NAMESPACE_NAV !== 'true' && ( )} - {!inside && BASE_USE_NAMESPACE_NAV === 'true' && ( + {!inside && (isSearch || BASE_USE_NAMESPACE_NAV === 'true') && ( )} diff --git a/src/components/organisms/HeaderSecond/organisms/SelectorNamespace/SelectorNamespace.tsx b/src/components/organisms/HeaderSecond/organisms/SelectorNamespace/SelectorNamespace.tsx index 41e2a40..c3bf2a1 100644 --- a/src/components/organisms/HeaderSecond/organisms/SelectorNamespace/SelectorNamespace.tsx +++ b/src/components/organisms/HeaderSecond/organisms/SelectorNamespace/SelectorNamespace.tsx @@ -1,9 +1,12 @@ import React, { FC, useState } from 'react' import { Flex, Typography } from 'antd' -import { useNavigate } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import { useDirectUnknownResource } from '@prorobotech/openapi-k8s-toolkit' +import { useSelector } from 'react-redux' +import type { RootState } from 'store/store' import { useNavSelectorInside } from 'hooks/useNavSelectorInside' import { useMountEffect } from 'hooks/useMountEffect' +import { useIsSearchPage } from 'hooks/useIsSearchPage' import { EntrySelect } from 'components/atoms' import { BASE_API_GROUP, @@ -19,6 +22,9 @@ type TSelectorNamespaceProps = { export const SelectorNamespace: FC = ({ clusterName, namespace }) => { const navigate = useNavigate() + const location = useLocation() + + const baseprefix = useSelector((state: RootState) => state.baseprefix.baseprefix) const [selectedClusterName, setSelectedClusterName] = useState(clusterName) const [selectedNamespace, setSelectedNamespace] = useState(namespace) @@ -34,7 +40,45 @@ export const SelectorNamespace: FC = ({ clusterName, na isEnabled: clusterName !== undefined, }) + const isSearchPage = useIsSearchPage(baseprefix || '') + const handleNamepsaceChange = (value?: string) => { + if (isSearchPage) { + const { pathname, search, hash } = location + const segs = pathname.split('/') + + // Assume pattern: /prefix/:clusterName/:namespace?/:syntheticProject?/search/* + // Find the "search" segment index + const searchIdx = segs.indexOf('search') + const clusterIdx = segs.indexOf(selectedClusterName || '') + if (clusterIdx === -1) { + return + } // bail if we can't find the cluster + + const nsIdx = clusterIdx + 1 // where namespace would live if present + const spIdx = clusterIdx + 2 // where syntheticProject would live if present + const nsExists = nsIdx < searchIdx // true if something occupies ns slot + const spExists = spIdx < searchIdx // true if something occupies sp slot + + if (value && value !== 'all') { + setSelectedNamespace(value) + + if (nsExists) { + // replace namespace in place + segs[nsIdx] = value + } else { + // insert namespace before "search" (or before syntheticProject if present) + const insertAt = spExists ? spIdx : searchIdx + segs.splice(insertAt, 0, value) + } + } else if (nsExists) { + segs.splice(nsIdx, 1) // removes namespace; syntheticProject (if any) shifts left + } + // if ns didn't exist, nothing to clear + + navigate(segs.join('/') + search + hash, { replace: true }) + return + } if (value && value !== 'all') { setSelectedNamespace(value) const changeUrl = diff --git a/src/components/organisms/Search/Search.tsx b/src/components/organisms/Search/Search.tsx index 1527e5e..863dbd3 100644 --- a/src/components/organisms/Search/Search.tsx +++ b/src/components/organisms/Search/Search.tsx @@ -1,5 +1,5 @@ /* eslint-disable max-lines-per-function */ -import React, { FC, Fragment, useState, useEffect } from 'react' +import React, { FC, Fragment, useState, useEffect, useLayoutEffect, useRef } from 'react' import { useLocation, useSearchParams } from 'react-router-dom' import { Search as PackageSearch, @@ -9,7 +9,6 @@ import { TKindWithVersion, getKinds, getSortedKinds, - // kindByGvr, } from '@prorobotech/openapi-k8s-toolkit' import { ConfigProvider, theme as antdtheme, Form, Spin, Alert } from 'antd' import { useSelector } from 'react-redux' @@ -20,13 +19,22 @@ import { FIELD_NAME_STRING, FIELD_NAME_LABELS, FIELD_NAME_FIELDS, - TYPE_SELECTOR, QUERY_KEY, NAME_QUERY_KEY, LABELS_QUERY_KEY, FIELDS_QUERY_KEY, + TYPE_SELECTOR, + TYPE_QUERY_KEY, } from './constants' -import { useDebouncedCallback, getArrayParam, setArrayParam, getStringParam, setStringParam } from './utils' +import { + useDebouncedCallback, + getArrayParam, + setArrayParam, + getStringParam, + setStringParam, + getTypeParam, + setTypeParam, +} from './utils' import { SearchEntry } from './molecules' import { Styled } from './styled' @@ -67,9 +75,7 @@ export const Search: FC = () => { useEffect(() => { setIsLoading(true) setError(undefined) - getKinds({ - clusterName: cluster, - }) + getKinds({ clusterName: cluster }) .then(data => { setKindIndex(data) setKindWithVersion(getSortedKinds(data)) @@ -88,59 +94,79 @@ export const Search: FC = () => { 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 + // —— hydration control to prevent “push empties” on first render —— + const isHydratingRef = useRef(true) + + // First, synchronously hydrate form from URL so watchers aren’t undefined on paint + useLayoutEffect(() => { + const fromKinds = getArrayParam(searchParams, QUERY_KEY) + const fromName = getStringParam(searchParams, NAME_QUERY_KEY) + const fromLabels = getArrayParam(searchParams, LABELS_QUERY_KEY) + const fromFields = getArrayParam(searchParams, FIELDS_QUERY_KEY) + + const explicitType = getTypeParam(searchParams, TYPE_QUERY_KEY) + const inferredFromValues = + (fromFields.length > 0 && 'fields') || (fromLabels.length > 0 && 'labels') || (fromName ? 'name' : undefined) + const nextType = explicitType ?? inferredFromValues + + form.setFieldsValue({ + [FIELD_NAME]: fromKinds, + [FIELD_NAME_STRING]: fromName, + [FIELD_NAME_LABELS]: fromLabels, + [FIELD_NAME_FIELDS]: fromFields, + [TYPE_SELECTOR]: nextType, + }) + + isHydratingRef.current = false + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // keep form in sync if URL changes later (back/forward/external edits) —— useEffect(() => { + if (isHydratingRef.current) return + 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 currentName = form.getFieldValue(FIELD_NAME_STRING) const nameDiffer = (fromName || '') !== (currentName || '') - // labels const fromLabels = getArrayParam(searchParams, LABELS_QUERY_KEY) - const currentLabels = form.getFieldValue(FIELD_NAME_LABELS) as string[] | undefined + const currentLabels = form.getFieldValue(FIELD_NAME_LABELS) 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 currentFields = form.getFieldValue(FIELD_NAME_FIELDS) const fieldsDiffer = (fromFields.length || 0) !== (currentFields?.length || 0) || fromFields.some((v, i) => v !== currentFields?.[i]) - // decide type from params + const explicitType = getTypeParam(searchParams, TYPE_QUERY_KEY) 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 + const inferredFromValues = + (fromFields.length > 0 && 'fields') || (fromLabels.length > 0 && 'labels') || (fromName ? 'name' : undefined) + const nextType = explicitType ?? currentType ?? inferredFromValues + const typeDiffer = nextType !== currentType - // Only update the form if URL differs from form (prevents loops) - if (kindsDiffer || nameDiffer || labelsDiffer || fieldsDiffer) { + if (kindsDiffer || nameDiffer || labelsDiffer || fieldsDiffer || typeDiffer) { 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, + ...(typeDiffer ? { [TYPE_SELECTOR]: nextType } : {}), }) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.search]) // react to back/forward, external URL edits + }, [location.search]) - // Watch field changes to push to URL (debounced) - const debouncedPush = useDebouncedCallback((values: string[]) => { + // debounced URL pushers (guarded against hydration & undefined) —— + const debouncedPushKinds = useDebouncedCallback((values: string[]) => { const next = setArrayParam(searchParams, QUERY_KEY, values) - setSearchParams(next, { replace: true }) // replace to keep history cleaner + setSearchParams(next, { replace: true }) }, 250) const debouncedPushName = useDebouncedCallback((value: string) => { @@ -159,47 +185,43 @@ export const Search: FC = () => { }, 250) useEffect(() => { - debouncedPush(watchedKinds || []) + if (isHydratingRef.current || watchedKinds === undefined) { + return + } + debouncedPushKinds(watchedKinds) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedKinds]) useEffect(() => { - debouncedPushName((watchedName || '').trim()) + if (isHydratingRef.current || watchedName === undefined) { + return + } + debouncedPushName(watchedName.trim()) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedName]) useEffect(() => { + if (isHydratingRef.current || watchedLabels === undefined) { + return + } debouncedPushLabels(watchedLabels || []) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedLabels]) useEffect(() => { + if (isHydratingRef.current || watchedFields === undefined) { + return + } 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]: [] }) - // } + if (isHydratingRef.current || watchedTypedSelector === undefined) { + return } - // Optional: if undefined (e.g., initial), choose a default behavior: - // else { form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_MULTIPLE]: [] }) } + const next = setTypeParam(searchParams, TYPE_QUERY_KEY, watchedTypedSelector) + setSearchParams(next, { replace: true }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedTypedSelector]) @@ -240,22 +262,24 @@ export const Search: FC = () => { }, }} > - {watchedKinds?.map(item => ( - - - - - ))} + {watchedKinds?.map(item => { + const fields = [...(watchedFields || []), ...(watchedName ? [`metadata.name=${watchedName}`] : [])] + return ( + + + + + ) + })} diff --git a/src/components/organisms/Search/constants.ts b/src/components/organisms/Search/constants.ts index 021c83e..af6cc45 100644 --- a/src/components/organisms/Search/constants.ts +++ b/src/components/organisms/Search/constants.ts @@ -3,9 +3,10 @@ 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' + +export const TYPE_SELECTOR = 'TYPE_SELECTOR' +export const TYPE_QUERY_KEY = 'type' diff --git a/src/components/organisms/Search/molecules/SearchEntry/SearchEntry.tsx b/src/components/organisms/Search/molecules/SearchEntry/SearchEntry.tsx index 3312adc..c4e766d 100644 --- a/src/components/organisms/Search/molecules/SearchEntry/SearchEntry.tsx +++ b/src/components/organisms/Search/molecules/SearchEntry/SearchEntry.tsx @@ -3,6 +3,7 @@ import { useParams, useSearchParams } from 'react-router-dom' import { TKindWithVersion, kindByGvr, + namespacedByGvr, getUppercase, hslFromString, Spacer, @@ -19,7 +20,6 @@ import { Styled } from './styled' type TSearchEntryProps = { resource: string - name?: string labels?: string[] fields?: string[] form: FormInstance @@ -29,15 +29,7 @@ type TSearchEntryProps = { kindsWithVersion: TKindWithVersion[] } -export const SearchEntry: FC = ({ - resource, - name, - labels, - fields, - form, - constants, - kindsWithVersion, -}) => { +export const SearchEntry: FC = ({ resource, labels, fields, form, constants, kindsWithVersion }) => { const { namespace, syntheticProject } = useParams() const [searchParams] = useSearchParams() const { token } = antdtheme.useToken() @@ -54,6 +46,8 @@ export const SearchEntry: FC = ({ const abbr = getUppercase(kindName && kindName.length ? kindName : 'Loading') const bgColor = kindName && kindName.length ? hslFromString(abbr, theme) : '' + const isNamespaceResource = namespacedByGvr(kindsWithVersion)(resource) + const tableCustomizationIdPrefix = getTableCustomizationIdPrefix({ instance: !!syntheticProject, project: BASE_USE_NAMESPACE_NAV !== 'true' && !!namespace, @@ -92,11 +86,10 @@ export const SearchEntry: FC = ({ {typeName && ( 0 ? 'api' : 'builtin'} - namespace={namespace} + namespace={isNamespaceResource ? namespace : undefined} apiGroup={apiGroup.length > 0 ? apiGroup : undefined} apiVersion={apiGroup.length > 0 ? apiVersion : undefined} typeName={typeName} - specificName={name?.length ? name : undefined} labels={labels?.length ? labels : undefined} fields={fields?.length ? fields : undefined} limit={searchParams.get('limit')} diff --git a/src/components/organisms/Search/utils.ts b/src/components/organisms/Search/utils.ts index bcc977f..01d2034 100644 --- a/src/components/organisms/Search/utils.ts +++ b/src/components/organisms/Search/utils.ts @@ -40,3 +40,20 @@ export const setStringParam = (sp: URLSearchParams, key: string, value: string | else next.set(key, v) return next } + +export const getTypeParam = (sp: URLSearchParams, key: string): 'name' | 'labels' | 'fields' | undefined => { + const v = sp.get(key)?.trim() + if (v === 'name' || v === 'labels' || v === 'fields') return v + return undefined +} + +export const setTypeParam = (sp: URLSearchParams, key: string, value: string | undefined | null) => { + const next = new URLSearchParams(sp) + const v = (value ?? '').trim() + if (v !== 'name' && v !== 'labels' && v !== 'fields') { + next.delete(key) + } else { + next.set(key, v) + } + return next +} diff --git a/src/constants/customizationApiGroupAndVersion.ts b/src/constants/customizationApiGroupAndVersion.ts index 2cf6fc3..b261e26 100644 --- a/src/constants/customizationApiGroupAndVersion.ts +++ b/src/constants/customizationApiGroupAndVersion.ts @@ -66,6 +66,10 @@ export const DOCS_URL = import.meta.env.DEV ? window._env_.DOCS_URL || import.meta.env.VITE_DOCS_URL : window._env_.DOCS_URL +export const SEARCH_TABLE_CUSTOMIZATION_PREFIX = import.meta.env.DEV + ? window._env_.SEARCH_TABLE_CUSTOMIZATION_PREFIX || import.meta.env.VITE_SEARCH_TABLE_CUSTOMIZATION_PREFIX + : window._env_.SEARCH_TABLE_CUSTOMIZATION_PREFIX + export const BASE_REMOVE_BACKLINK = import.meta.env.DEV ? window._env_.REMOVE_BACKLINK === 'true' || import.meta.env.VITE_REMOVE_BACKLINK?.toString().toLowerCase() === 'true' : window._env_.REMOVE_BACKLINK === 'true' diff --git a/src/hooks/useIsSearchPage/index.ts b/src/hooks/useIsSearchPage/index.ts new file mode 100644 index 0000000..6b84b05 --- /dev/null +++ b/src/hooks/useIsSearchPage/index.ts @@ -0,0 +1 @@ +export * from './useIsSearchPage' diff --git a/src/hooks/useIsSearchPage/useIsSearchPage.ts b/src/hooks/useIsSearchPage/useIsSearchPage.ts new file mode 100644 index 0000000..a7c3ad9 --- /dev/null +++ b/src/hooks/useIsSearchPage/useIsSearchPage.ts @@ -0,0 +1,9 @@ +import { useLocation, matchPath } from 'react-router-dom' + +export const useIsSearchPage = (prefix: string) => { + const { pathname } = useLocation() + const base = `/${prefix}`.replace(/\/{2,}/g, '/').replace(/\/$/, '') + const pattern = `${base}/:clusterName/:namespace?/:syntheticProject?/search/*` + + return Boolean(matchPath({ path: pattern }, pathname)) +} diff --git a/src/pages/SearchPage/SearchPage.tsx b/src/pages/SearchPage/SearchPage.tsx index 922f312..57d96d4 100644 --- a/src/pages/SearchPage/SearchPage.tsx +++ b/src/pages/SearchPage/SearchPage.tsx @@ -16,16 +16,17 @@ export const SearchPage: FC = ({ forcedTheme }) => { const possibleProject = syntheticProject && namespace ? syntheticProject : namespace const possibleInstance = syntheticProject && namespace ? namespace : undefined - const sidebarId = `${getSidebarIdPrefix({ instance: !!syntheticProject, project: !!namespace })}seach-page` + const sidebarId = `${getSidebarIdPrefix({ instance: !!syntheticProject, project: !!namespace })}search-page` const breadcrumbsId = `${getBreadcrumbsIdPrefix({ instance: !!syntheticProject, project: !!namespace, - })}seach-page` + })}search-page` return ( = ({ children, withNoCluster, forcedTheme, inside, sidebar }) => { +export const BaseTemplate: FC = ({ + children, + withNoCluster, + forcedTheme, + inside, + isSearch, + sidebar, +}) => { const navigate = useNavigate() const { clusterName } = useParams() const { useToken } = antdtheme @@ -106,7 +114,7 @@ export const BaseTemplate: FC = ({ children, withNoCluster, - + {clusterListQuery.error && ( )} diff --git a/src/utils/getTableCustomizationIdPrefix.ts b/src/utils/getTableCustomizationIdPrefix.ts index 4dc6106..336c0d9 100644 --- a/src/utils/getTableCustomizationIdPrefix.ts +++ b/src/utils/getTableCustomizationIdPrefix.ts @@ -1,3 +1,5 @@ +import { SEARCH_TABLE_CUSTOMIZATION_PREFIX } from 'constants/customizationApiGroupAndVersion' + export const getTableCustomizationIdPrefix = ({ project, instance, @@ -16,7 +18,7 @@ export const getTableCustomizationIdPrefix = ({ if (inside) { result = 'inside-' } else if (search) { - result = 'search-' + result = SEARCH_TABLE_CUSTOMIZATION_PREFIX } else { result = 'stock-' }