Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions web/src/modules/Jobs/Helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,36 @@ export type ApiParams = {
cluster?: string
command?: string
status?: string[]
tags?: string
page?: string
}

export type TagPair = {
key: string
value: string
}

export const serializeTags = (pairs: TagPair[]): string =>
pairs
.filter((p) => p.key.trim() && p.value.trim())
.map((p) => `${p.key.trim()}:${p.value.trim()}`)
.join(',')

export const parseTags = (value: string): TagPair[] =>
value
.split(',')
.map((raw) => raw.trim())
.reduce<TagPair[]>((acc, tag) => {
const idx = tag.indexOf(':')
if (idx > 0) {
acc.push({
key: tag.slice(0, idx).trim(),
value: tag.slice(idx + 1).trim(),
})
}
return acc
}, [])

export type JobType = {
id: string
name: string
Expand Down
48 changes: 45 additions & 3 deletions web/src/modules/Jobs/Jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ import {
import { BreadcrumbContext } from '@/common/BreadCrumbsProvider/context'
import { fetchJobs, getJobStatus } from '@/app/api/jobs/jobs'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { ApiParams, JobType, useJobConfig } from './Helper'
import {
ApiParams,
JobType,
TagPair,
parseTags,
serializeTags,
useJobConfig,
} from './Helper'
import { useQueryState } from 'nuqs'
import { FilterStatesType } from '@patterninc/react-ui'
import {
Expand All @@ -24,6 +31,7 @@ import {
sortData,
} from '@/common/Services'
import { AutoRefreshContext } from '@/common/AutoRefreshProvider/context'
import TagFilter from './TagFilter'

type FilterType = {
id: string
Expand All @@ -33,6 +41,7 @@ type FilterType = {
clusterId: string
commandId: string
status: string[]
tags: TagPair[]
}

const Jobs = (): React.JSX.Element => {
Expand All @@ -56,6 +65,11 @@ const Jobs = (): React.JSX.Element => {
parse: (value) => (value ? value.split(',') : []),
serialize: (value) => value?.join(',') ?? '',
})
const [tags, setTags] = useQueryState<TagPair[]>('tags', {
defaultValue: [],
parse: (value) => (value ? parseTags(value) : []),
serialize: (value) => serializeTags(value),
})

const [filter, setFilter] = useState<FilterType>({
id: jobId,
Expand All @@ -65,6 +79,7 @@ const Jobs = (): React.JSX.Element => {
clusterId: clusterId,
commandId: commandId,
status: status,
tags: tags,
})

useEffect(() => {
Expand All @@ -81,8 +96,10 @@ const Jobs = (): React.JSX.Element => {
if (clusterId) params.cluster = clusterId
if (commandId) params.command = commandId
if (status.length > 0) params.status = status
const serializedTags = serializeTags(tags)
if (serializedTags) params.tags = serializedTags
return params
}, [jobId, name, user, version, clusterId, commandId, status])
}, [jobId, name, user, version, clusterId, commandId, status, tags])

// Fetch Jobs with filters applied
const { data, isLoading, fetchNextPage, hasNextPage, isSuccess } =
Expand Down Expand Up @@ -130,6 +147,7 @@ const Jobs = (): React.JSX.Element => {
setClusterId(filter.clusterId)
setCommandId(filter.commandId)
setStatus(filter.status)
setTags(filter.tags)

if (filter?.user) queryParams.append('user', filter.user)
if (filter?.name) queryParams.append('name', filter.name)
Expand All @@ -140,6 +158,8 @@ const Jobs = (): React.JSX.Element => {
if (filter?.status && filter.status.length > 0) {
queryParams.append('status', filter.status.join(','))
}
const serializedTags = serializeTags(filter.tags)
if (serializedTags) queryParams.append('tags', serializedTags)
updateBreadcrumbs({
name: 'Jobs',
link: `/jobs?${queryParams.toString()}`,
Expand All @@ -151,13 +171,15 @@ const Jobs = (): React.JSX.Element => {
filter.id,
filter.name,
filter.status,
filter.tags,
filter.user,
filter.version,
setClusterId,
setCommandId,
setJobId,
setName,
setStatus,
setTags,
setUser,
setVersion,
updateBreadcrumbs,
Expand Down Expand Up @@ -264,6 +286,7 @@ const Jobs = (): React.JSX.Element => {
setClusterId(null)
setCommandId(null)
setStatus(null)
setTags(null)
setFilter({
id: '',
name: '',
Expand All @@ -272,6 +295,7 @@ const Jobs = (): React.JSX.Element => {
clusterId: '',
commandId: '',
status: [],
tags: [],
})
}, [
setJobId,
Expand All @@ -281,6 +305,7 @@ const Jobs = (): React.JSX.Element => {
setClusterId,
setCommandId,
setStatus,
setTags,
])

// Compute applied filter count
Expand All @@ -293,8 +318,18 @@ const Jobs = (): React.JSX.Element => {
commandId,
clusterId,
status.length > 0,
tags.length > 0,
].filter(Boolean).length
}, [jobId, name, user, version, commandId, clusterId, status.length])
}, [
jobId,
name,
user,
version,
commandId,
clusterId,
status.length,
tags.length,
])

const updateFormField = useCallback(
(...params: unknown[]) => {
Expand All @@ -308,6 +343,10 @@ const Jobs = (): React.JSX.Element => {
[setFilter],
)

const updateTags = useCallback((newTags: TagPair[]) => {
setFilter((prevFilter) => ({ ...prevFilter, tags: newTags }))
}, [])

return (
<div className='pt-4'>
<StandardTable
Expand Down Expand Up @@ -339,6 +378,9 @@ const Jobs = (): React.JSX.Element => {
resetCallout: resetFilters,
cancelCallout: () => {},
onChangeCallout: updateFormField,
children: () => (
<TagFilter tags={filter.tags} onChange={updateTags} />
),
},
}}
/>
Expand Down
65 changes: 65 additions & 0 deletions web/src/modules/Jobs/TagFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react'
import { Button, FormLabel, Icon, TextInput } from '@patterninc/react-ui'
import { TagPair } from './Helper'

type TagFilterProps = {
tags: TagPair[]
onChange: (tags: TagPair[]) => void
}

const emptyPair: TagPair = { key: '', value: '' }

const TagFilter = ({ tags, onChange }: TagFilterProps): React.JSX.Element => {
const rows = tags.length > 0 ? tags : [emptyPair]

const updateRow = (index: number, field: keyof TagPair, value: string) => {
onChange(
rows.map((row, i) => (i === index ? { ...row, [field]: value } : row)),
)
}

const addRow = () => onChange([...rows, { ...emptyPair }])

const removeRow = (index: number) => {
onChange(rows.filter((_, i) => i !== index))
}

return (
<div className='flex flex-col gap-2'>
<FormLabel label='Tags' />
{rows.map((row, index) => (
<div key={index} className='flex items-center gap-2'>
<div className='flex-1'>
<TextInput
fullWidth
value={row.key}
placeholder='Key'
stateName={`tag-key-${index}`}
callout={(_, value) => updateRow(index, 'key', value as string)}
/>
</div>
<span>:</span>
<div className='flex-1'>
<TextInput
fullWidth
value={row.value}
placeholder='Value'
stateName={`tag-value-${index}`}
callout={(_, value) => updateRow(index, 'value', value as string)}
/>
</div>
<Button as='unstyled' onClick={() => removeRow(index)}>
<Icon icon='trash' color='dark-red' />
</Button>
</div>
))}
<div>
<Button as='button' styleType='tertiary' onClick={addRow}>
Add tag
</Button>
</div>
</div>
)
}

export default TagFilter
Loading