Skip to content

Commit 4fcbaee

Browse files
authored
Merge pull request #168 from PRO-Robotech/feature/dev
search by fields
2 parents a0df7c5 + a20f39a commit 4fcbaee

File tree

7 files changed

+137
-26
lines changed

7 files changed

+137
-26
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prorobotech/openapi-k8s-toolkit",
3-
"version": "0.0.1-alpha.120",
3+
"version": "0.0.1-alpha.121",
44
"description": "ProRobotech OpenAPI k8s tools",
55
"main": "dist/openapi-k8s-toolkit.cjs.js",
66
"module": "dist/openapi-k8s-toolkit.es.js",

src/api/getApiResource.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const getApiResources = async <T>({
88
typeName,
99
specificName,
1010
labels,
11+
fields,
1112
limit,
1213
}: {
1314
clusterName: string
@@ -17,6 +18,7 @@ export const getApiResources = async <T>({
1718
typeName: string
1819
specificName?: string
1920
labels?: string[]
21+
fields?: string[]
2022
limit: string | null
2123
}): Promise<AxiosResponse<T>> => {
2224
const params = new URLSearchParams()
@@ -26,6 +28,9 @@ export const getApiResources = async <T>({
2628
if (labels && labels.length > 0) {
2729
params.set('labelSelector', labels.join(','))
2830
}
31+
if (fields && fields.length > 0) {
32+
params.set('fieldSelector', fields.join(','))
33+
}
2934
const searchParams = params.toString()
3035
return axios.get(
3136
`/api/clusters/${clusterName}/k8s/apis/${apiGroup}/${apiVersion}${

src/api/getBuiltinResource.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ export const getBuiltinResources = async <T>({
66
typeName,
77
specificName,
88
labels,
9+
fields,
910
limit,
1011
}: {
1112
clusterName: string
1213
namespace?: string
1314
typeName: string
1415
specificName?: string
1516
labels?: string[]
17+
fields?: string[]
1618
limit: string | null
1719
}): Promise<AxiosResponse<T>> => {
1820
const params = new URLSearchParams()
@@ -22,6 +24,9 @@ export const getBuiltinResources = async <T>({
2224
if (labels && labels.length > 0) {
2325
params.set('labelSelector', labels.join(','))
2426
}
27+
if (fields && fields.length > 0) {
28+
params.set('fieldSelector', fields.join(','))
29+
}
2530
const searchParams = params.toString()
2631
return axios.get(
2732
`/api/clusters/${clusterName}/k8s/api/v1${namespace ? `/namespaces/${namespace}` : ''}/${typeName}${

src/components/molecules/Search/Search.tsx

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,17 @@ import { Styled } from './styled'
1313

1414
type TSearchProps = {
1515
cluster: string
16-
updateCurrentSearch: ({ resources, name, labels }: { resources?: string[]; name?: string; labels?: string[] }) => void
16+
updateCurrentSearch: ({
17+
resources,
18+
name,
19+
labels,
20+
fields,
21+
}: {
22+
resources?: string[]
23+
name?: string
24+
labels?: string[]
25+
fields?: string[]
26+
}) => void
1727
}
1828

1929
export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
@@ -23,17 +33,20 @@ export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
2333

2434
const FIELD_NAME = 'kinds'
2535
const FIELD_NAME_STRING = 'name'
26-
const FIELD_NAME_MULTIPLE = 'labels'
36+
const FIELD_NAME_LABELS = 'labels'
37+
const FIELD_NAME_FIELDS = 'fields'
2738

2839
const TYPE_SELECTOR = 'TYPE_SELECTOR'
2940

3041
const QUERY_KEY = 'kinds' // the query param name
3142
const NAME_QUERY_KEY = 'name'
3243
const LABELS_QUERY_KEY = 'labels'
44+
const FIELDS_QUERY_KEY = 'fields'
3345

3446
const watchedKinds = Form.useWatch<string[] | undefined>(FIELD_NAME, form)
3547
const watchedName = Form.useWatch<string | undefined>(FIELD_NAME_STRING, form)
36-
const watchedMultiple = Form.useWatch<string[] | undefined>(FIELD_NAME_MULTIPLE, form)
48+
const watchedLabels = Form.useWatch<string[] | undefined>(FIELD_NAME_LABELS, form)
49+
const watchedFields = Form.useWatch<string[] | undefined>(FIELD_NAME_FIELDS, form)
3750
const watchedTypedSelector = Form.useWatch<string | undefined>(TYPE_SELECTOR, form)
3851

3952
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -63,26 +76,35 @@ export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
6376

6477
// labels
6578
const fromLabels = getArrayParam(searchParams, LABELS_QUERY_KEY)
66-
const currentLabels = form.getFieldValue(FIELD_NAME_MULTIPLE) as string[] | undefined
79+
const currentLabels = form.getFieldValue(FIELD_NAME_LABELS) as string[] | undefined
6780
const labelsDiffer =
6881
(fromLabels.length || 0) !== (currentLabels?.length || 0) || fromLabels.some((v, i) => v !== currentLabels?.[i])
6982

83+
// labels
84+
const fromFields = getArrayParam(searchParams, FIELDS_QUERY_KEY)
85+
const currentFields = form.getFieldValue(FIELD_NAME_FIELDS) as string[] | undefined
86+
const fieldsDiffer =
87+
(fromFields.length || 0) !== (currentFields?.length || 0) || fromFields.some((v, i) => v !== currentFields?.[i])
88+
7089
// decide type from params
7190
const currentType = form.getFieldValue(TYPE_SELECTOR)
7291
let inferredType: string | undefined
7392
if (fromName) {
7493
inferredType = 'name'
7594
} else if (fromLabels.length > 0) {
7695
inferredType = 'labels'
96+
} else if (fromFields.length > 0) {
97+
inferredType = 'fields'
7798
}
7899
const typeDiffer = inferredType !== currentType
79100

80101
// Only update the form if URL differs from form (prevents loops)
81-
if (kindsDiffer || nameDiffer || labelsDiffer) {
102+
if (kindsDiffer || nameDiffer || labelsDiffer || fieldsDiffer) {
82103
form.setFieldsValue({
83104
[FIELD_NAME]: kindsDiffer ? fromKinds : currentKinds,
84105
[FIELD_NAME_STRING]: nameDiffer ? fromName : currentName,
85-
[FIELD_NAME_MULTIPLE]: labelsDiffer ? fromLabels : currentLabels,
106+
[FIELD_NAME_LABELS]: labelsDiffer ? fromLabels : currentLabels,
107+
[FIELD_NAME_FIELDS]: fieldsDiffer ? fromFields : currentFields,
86108
[TYPE_SELECTOR]: typeDiffer ? inferredType : currentType,
87109
})
88110
}
@@ -105,6 +127,11 @@ export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
105127
setSearchParams(next, { replace: true })
106128
}, 250)
107129

130+
const debouncedPushFields = useDebouncedCallback((values: string[]) => {
131+
const next = setArrayParam(searchParams, FIELDS_QUERY_KEY, values)
132+
setSearchParams(next, { replace: true })
133+
}, 250)
134+
108135
useEffect(() => {
109136
debouncedPush(watchedKinds || [])
110137
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -116,23 +143,34 @@ export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
116143
}, [watchedName])
117144

118145
useEffect(() => {
119-
debouncedPushLabels(watchedMultiple || [])
146+
debouncedPushLabels(watchedLabels || [])
120147
// eslint-disable-next-line react-hooks/exhaustive-deps
121-
}, [watchedMultiple])
148+
}, [watchedLabels])
149+
150+
useEffect(() => {
151+
debouncedPushFields(watchedFields || [])
152+
// eslint-disable-next-line react-hooks/exhaustive-deps
153+
}, [watchedFields])
122154

123155
useEffect(() => {
124156
if (watchedTypedSelector === 'name') {
125157
// Clear labels when switching to "name"
126-
const cur = form.getFieldValue(FIELD_NAME_MULTIPLE) as string[] | undefined
127-
if (cur?.length) {
128-
form.setFieldsValue({ [FIELD_NAME_MULTIPLE]: [] })
129-
}
158+
// const cur = form.getFieldValue(FIELD_NAME_LABELS) as string[] | undefined
159+
// if (cur?.length) {
160+
form.setFieldsValue({ [FIELD_NAME_LABELS]: [], [FIELD_NAME_FIELDS]: [] })
161+
// }
130162
} else if (watchedTypedSelector === 'labels') {
131163
// Clear name when switching to "labels"
132-
const cur = (form.getFieldValue(FIELD_NAME_STRING) as string | undefined) ?? ''
133-
if (cur) {
134-
form.setFieldsValue({ [FIELD_NAME_STRING]: '' })
135-
}
164+
// const cur = (form.getFieldValue(FIELD_NAME_STRING) as string | undefined) ?? ''
165+
// if (cur) {
166+
form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_FIELDS]: [] })
167+
// }
168+
} else if (watchedTypedSelector === 'fields') {
169+
// Clear name when switching to "labels"
170+
// const cur = (form.getFieldValue(FIELD_NAME_STRING) as string | undefined) ?? ''
171+
// if (cur) {
172+
form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_LABELS]: [] })
173+
// }
136174
}
137175
// Optional: if undefined (e.g., initial), choose a default behavior:
138176
// else { form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_MULTIPLE]: [] }) }
@@ -197,20 +235,25 @@ export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
197235
options={[
198236
{ label: 'Name', value: 'name' },
199237
{ label: 'Labels', value: 'labels' },
238+
{ label: 'Fields', value: 'fields' },
200239
]}
201240
defaultValue="name"
202241
filterOption={filterSelectOptions}
203242
showSearch
204243
/>
205244
</Form.Item>
206-
<Styled.HideableContainer $isHidden={watchedTypedSelector === 'labels'}>
245+
<Styled.HideableContainer $isHidden={watchedTypedSelector === 'labels' || watchedTypedSelector === 'fields'}>
207246
<Form.Item name={FIELD_NAME_STRING} label="Name">
208247
<Input allowClear />
209248
</Form.Item>
210249
</Styled.HideableContainer>
211-
<Styled.HideableContainer $isHidden={watchedTypedSelector === 'name' || watchedTypedSelector === undefined}>
250+
<Styled.HideableContainer
251+
$isHidden={
252+
watchedTypedSelector === 'name' || watchedTypedSelector === 'fields' || watchedTypedSelector === undefined
253+
}
254+
>
212255
<Form.Item
213-
name={FIELD_NAME_MULTIPLE}
256+
name={FIELD_NAME_LABELS}
214257
label="Labels"
215258
validateTrigger="onBlur"
216259
rules={[
@@ -239,11 +282,51 @@ export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
239282
/>
240283
</Form.Item>
241284
</Styled.HideableContainer>
285+
<Styled.HideableContainer
286+
$isHidden={
287+
watchedTypedSelector === 'name' || watchedTypedSelector === 'labels' || watchedTypedSelector === undefined
288+
}
289+
>
290+
<Form.Item
291+
name={FIELD_NAME_FIELDS}
292+
label="Fields"
293+
validateTrigger="onBlur"
294+
rules={[
295+
() => ({
296+
validator(_, value) {
297+
if (
298+
Array.isArray(value) &&
299+
value.every(str => typeof str === 'string' && str.includes('=') && !str.startsWith('='))
300+
) {
301+
return Promise.resolve()
302+
}
303+
return Promise.reject(new Error('Please enter key=value style'))
304+
},
305+
}),
306+
]}
307+
>
308+
<Select
309+
mode="tags"
310+
allowClear
311+
placeholder="Select"
312+
// dropdownStyle={{ display: 'none' }}
313+
tokenSeparators={[',', ' ', ' ']}
314+
suffixIcon={null}
315+
filterOption={filterSelectOptions}
316+
tagRender={tagRender}
317+
/>
318+
</Form.Item>
319+
</Styled.HideableContainer>
242320
<Form.Item label="Search">
243321
<Button
244322
type="primary"
245323
onClick={() =>
246-
updateCurrentSearch({ resources: watchedKinds, name: watchedName, labels: watchedMultiple })
324+
updateCurrentSearch({
325+
resources: watchedKinds,
326+
name: watchedName,
327+
labels: watchedLabels,
328+
fields: watchedFields,
329+
})
247330
}
248331
>
249332
Search
@@ -254,7 +337,8 @@ export const Search: FC<TSearchProps> = ({ cluster, updateCurrentSearch }) => {
254337
{/* Example of "watching" the value for display or side-effects */}
255338
<div>Current: {(watchedKinds || []).join(', ')}</div>
256339
<div>Current name: {watchedName}</div>
257-
<div>Current labels: {(watchedMultiple || []).join(', ')}</div>
340+
<div>Current labels: {(watchedLabels || []).join(', ')}</div>
341+
<div>Current fields: {(watchedFields || []).join(', ')}</div>
258342
</div>
259343
)
260344
}

src/hooks/useApiResource.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const useApiResources = ({
1010
typeName,
1111
specificName,
1212
labels,
13+
fields,
1314
limit,
1415
refetchInterval,
1516
isEnabled,
@@ -21,12 +22,24 @@ export const useApiResources = ({
2122
typeName: string
2223
specificName?: string
2324
labels?: string[]
25+
fields?: string[]
2426
limit: string | null
2527
refetchInterval?: number | false
2628
isEnabled?: boolean
2729
}) => {
2830
return useQuery({
29-
queryKey: ['useApiResources', clusterName, namespace, apiGroup, apiVersion, typeName, specificName, labels, limit],
31+
queryKey: [
32+
'useApiResources',
33+
clusterName,
34+
namespace,
35+
apiGroup,
36+
apiVersion,
37+
typeName,
38+
specificName,
39+
labels,
40+
fields,
41+
limit,
42+
],
3043
queryFn: async () => {
3144
const response = await getApiResources<TApiResources>({
3245
clusterName,
@@ -36,6 +49,7 @@ export const useApiResources = ({
3649
typeName,
3750
specificName,
3851
labels,
52+
fields,
3953
limit,
4054
})
4155
// Deep clone the data (to avoid mutating the original response)

src/hooks/useBuiltinResource.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const useBuiltinResources = ({
88
typeName,
99
specificName,
1010
labels,
11+
fields,
1112
limit,
1213
refetchInterval,
1314
isEnabled,
@@ -17,19 +18,21 @@ export const useBuiltinResources = ({
1718
typeName: string
1819
specificName?: string
1920
labels?: string[]
21+
fields?: string[]
2022
limit: string | null
2123
refetchInterval?: number | false
2224
isEnabled?: boolean
2325
}) => {
2426
return useQuery({
25-
queryKey: ['useBuiltinResourceType', clusterName, namespace, typeName, specificName, labels, limit],
27+
queryKey: ['useBuiltinResourceType', clusterName, namespace, typeName, specificName, labels, fields, limit],
2628
queryFn: async () => {
2729
const response = await getBuiltinResources<TBuiltinResources>({
2830
clusterName,
2931
namespace,
3032
typeName,
3133
specificName,
3234
labels,
35+
fields,
3336
limit,
3437
})
3538
// Deep clone the data (to avoid mutating the original response)

0 commit comments

Comments
 (0)