mirror of
https://github.com/outbackdingo/openapi-ui.git
synced 2026-01-27 18:19:50 +00:00
2
.env
2
.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-
|
||||
|
||||
@@ -33,3 +33,5 @@ REMOVE_BACKLINK=
|
||||
REMOVE_BACKLINK_TEXT=
|
||||
|
||||
DOCS_URL=
|
||||
|
||||
SEARCH_TABLE_CUSTOMIZATION_PREFIX=
|
||||
|
||||
@@ -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 |
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"'}
|
||||
}
|
||||
|
||||
@@ -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<TTableApiBuiltinProps> = ({
|
||||
apiGroup,
|
||||
apiVersion,
|
||||
typeName,
|
||||
specificName,
|
||||
labels,
|
||||
fields,
|
||||
limit,
|
||||
@@ -159,7 +157,6 @@ export const TableApiBuiltin: FC<TTableApiBuiltinProps> = ({
|
||||
clusterName: cluster,
|
||||
namespace,
|
||||
typeName,
|
||||
specificName,
|
||||
labels,
|
||||
fields,
|
||||
limit,
|
||||
@@ -176,7 +173,6 @@ export const TableApiBuiltin: FC<TTableApiBuiltinProps> = ({
|
||||
apiGroup: apiGroup || '',
|
||||
apiVersion: apiVersion || '',
|
||||
typeName,
|
||||
specificName,
|
||||
labels,
|
||||
fields,
|
||||
limit,
|
||||
@@ -239,7 +235,7 @@ export const TableApiBuiltin: FC<TTableApiBuiltinProps> = ({
|
||||
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,
|
||||
|
||||
@@ -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 || []
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ import { Styled } from './styled'
|
||||
|
||||
type THeaderProps = {
|
||||
inside?: boolean
|
||||
isSearch?: boolean
|
||||
}
|
||||
|
||||
export const HeaderSecond: FC<THeaderProps> = ({ inside }) => {
|
||||
export const HeaderSecond: FC<THeaderProps> = ({ 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<THeaderProps> = ({ inside }) => {
|
||||
<Flex gap={18}>
|
||||
{inside ? <SelectorClusterInside clusterName={clusterName} /> : <SelectorCluster clusterName={clusterName} />}
|
||||
{inside && <SelectorInside clusterName={clusterName} namespace={namespace} />}
|
||||
{!inside && BASE_USE_NAMESPACE_NAV !== 'true' && (
|
||||
{!inside && !isSearch && BASE_USE_NAMESPACE_NAV !== 'true' && (
|
||||
<Selector
|
||||
clusterName={clusterName}
|
||||
projectName={projectName || possibleProject}
|
||||
instanceName={instanceName || possibleInstance}
|
||||
/>
|
||||
)}
|
||||
{!inside && BASE_USE_NAMESPACE_NAV === 'true' && (
|
||||
{!inside && (isSearch || BASE_USE_NAMESPACE_NAV === 'true') && (
|
||||
<SelectorNamespace clusterName={clusterName} namespace={namespace} />
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -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<TSelectorNamespaceProps> = ({ 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<TSelectorNamespaceProps> = ({ 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 =
|
||||
|
||||
@@ -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<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
|
||||
// —— 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 => (
|
||||
<Fragment key={item}>
|
||||
<Spacer $space={20} $samespace />
|
||||
<SearchEntry
|
||||
kindsWithVersion={kindsWithVersion}
|
||||
form={form}
|
||||
constants={{
|
||||
FIELD_NAME,
|
||||
}}
|
||||
resource={item}
|
||||
name={watchedName}
|
||||
labels={watchedLabels}
|
||||
fields={watchedFields}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
{watchedKinds?.map(item => {
|
||||
const fields = [...(watchedFields || []), ...(watchedName ? [`metadata.name=${watchedName}`] : [])]
|
||||
return (
|
||||
<Fragment key={item}>
|
||||
<Spacer $space={20} $samespace />
|
||||
<SearchEntry
|
||||
kindsWithVersion={kindsWithVersion}
|
||||
form={form}
|
||||
constants={{
|
||||
FIELD_NAME,
|
||||
}}
|
||||
resource={item}
|
||||
labels={watchedLabels}
|
||||
fields={fields.length ? fields : undefined}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</ConfigProvider>
|
||||
<Spacer $space={20} $samespace />
|
||||
</Styled.OverflowContainer>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<TSearchEntryProps> = ({
|
||||
resource,
|
||||
name,
|
||||
labels,
|
||||
fields,
|
||||
form,
|
||||
constants,
|
||||
kindsWithVersion,
|
||||
}) => {
|
||||
export const SearchEntry: FC<TSearchEntryProps> = ({ 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<TSearchEntryProps> = ({
|
||||
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<TSearchEntryProps> = ({
|
||||
{typeName && (
|
||||
<TableApiBuiltin
|
||||
resourceType={apiGroup.length > 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')}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
1
src/hooks/useIsSearchPage/index.ts
Normal file
1
src/hooks/useIsSearchPage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './useIsSearchPage'
|
||||
9
src/hooks/useIsSearchPage/useIsSearchPage.ts
Normal file
9
src/hooks/useIsSearchPage/useIsSearchPage.ts
Normal file
@@ -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))
|
||||
}
|
||||
@@ -16,16 +16,17 @@ export const SearchPage: FC<TSearchPageProps> = ({ 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 (
|
||||
<BaseTemplate
|
||||
forcedTheme={forcedTheme}
|
||||
inside={false}
|
||||
isSearch
|
||||
sidebar={
|
||||
<ManageableSidebar
|
||||
instanceName={possibleInstance}
|
||||
|
||||
@@ -24,10 +24,18 @@ type TBaseTemplateProps = {
|
||||
children?: ReactNode | undefined
|
||||
forcedTheme?: 'dark' | 'light'
|
||||
inside?: boolean
|
||||
isSearch?: boolean
|
||||
sidebar?: ReactNode
|
||||
}
|
||||
|
||||
export const BaseTemplate: FC<TBaseTemplateProps> = ({ children, withNoCluster, forcedTheme, inside, sidebar }) => {
|
||||
export const BaseTemplate: FC<TBaseTemplateProps> = ({
|
||||
children,
|
||||
withNoCluster,
|
||||
forcedTheme,
|
||||
inside,
|
||||
isSearch,
|
||||
sidebar,
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { clusterName } = useParams()
|
||||
const { useToken } = antdtheme
|
||||
@@ -106,7 +114,7 @@ export const BaseTemplate: FC<TBaseTemplateProps> = ({ children, withNoCluster,
|
||||
</Col>
|
||||
<FlexCol flex="auto">
|
||||
<DefaultLayout.ContentPadding $isFederation={isFederation}>
|
||||
<HeaderSecond inside={inside} />
|
||||
<HeaderSecond inside={inside} isSearch={isSearch} />
|
||||
{clusterListQuery.error && (
|
||||
<Alert message={`Cluster List Error: ${clusterListQuery.error?.message} `} type="error" />
|
||||
)}
|
||||
|
||||
@@ -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-'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user