Skip to content

Commit 9e7bd6a

Browse files
authored
Merge pull request #2283 from topcoder-platform/sys-admin-challenge-ingestion-ui
PM-5999 - fix filter by track in sys admin/challenge bulk ingestion
2 parents cd3b531 + 93244b0 commit 9e7bd6a

4 files changed

Lines changed: 131 additions & 51 deletions

File tree

src/apps/admin/src/ai/topscout-rag/IndexedChallengesPanel.tsx

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
Button,
1717
IconOutline,
1818
InputSelect,
19-
InputSelectOption,
2019
InputWrapper,
2120
Table,
2221
TableColumn,
@@ -32,28 +31,13 @@ import {
3231
IndexedChallenge,
3332
} from '../../lib/services/rag-index.service'
3433

34+
import { INDEXED_TRACK_OPTIONS, INDEXED_TYPE_OPTIONS } from './ingest-options'
3535
import styles from './IndexedChallengesPanel.module.scss'
3636

3737
const stopPropagation: MouseEventHandler = ev => ev.stopPropagation()
3838

3939
const PER_PAGE = 10
4040

41-
const TRACK_OPTIONS: InputSelectOption[] = [
42-
{ label: 'All tracks', value: '' },
43-
{ label: 'Development', value: 'Development' },
44-
{ label: 'Design', value: 'Design' },
45-
{ label: 'Data Science', value: 'Data Science' },
46-
{ label: 'Quality Assurance', value: 'Quality Assurance' },
47-
]
48-
49-
const TYPE_OPTIONS: InputSelectOption[] = [
50-
{ label: 'All types', value: '' },
51-
{ label: 'Challenge', value: 'Challenge' },
52-
{ label: 'First2Finish', value: 'First2Finish' },
53-
{ label: 'Marathon Match', value: 'Marathon Match' },
54-
{ label: 'Task', value: 'Task' },
55-
]
56-
5741
interface Filters {
5842
search: string
5943
projectId: string
@@ -390,15 +374,15 @@ export const IndexedChallengesPanel: FC<IndexedChallengesPanelProps> = props =>
390374
<InputSelect
391375
name='filterTrack'
392376
label=''
393-
options={TRACK_OPTIONS}
377+
options={INDEXED_TRACK_OPTIONS}
394378
value={filters.track}
395379
onChange={handleFilterChange('track')}
396380
tabIndex={0}
397381
/>
398382
<InputSelect
399383
name='filterType'
400384
label=''
401-
options={TYPE_OPTIONS}
385+
options={INDEXED_TYPE_OPTIONS}
402386
value={filters.type}
403387
onChange={handleFilterChange('type')}
404388
tabIndex={0}

src/apps/admin/src/ai/topscout-rag/IngestChallengesPanel.tsx

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,18 @@ import {
1313
ingestChallengeInRag,
1414
WorkflowPollTimeoutError,
1515
} from '~/libs/shared'
16-
import { Button, InputSelect, InputSelectOption } from '~/libs/ui'
16+
import { Button, InputSelect } from '~/libs/ui'
1717
import FormToggleSwitch from '~/libs/ui/lib/components/form/form-groups/form-toggle-switch'
1818

1919
import { Field, TextField } from './FormFields'
20+
import {
21+
BULK_STATUS_OPTIONS,
22+
BULK_TRACK_OPTIONS,
23+
BULK_TYPE_OPTIONS,
24+
} from './ingest-options'
2025
import { IngestionRun, IngestionRunSummary } from './IngestionRunSummary'
2126
import styles from './IngestChallengesPanel.module.scss'
2227

23-
/**
24-
* tc-ai-api treats tracks and types as free-form strings (its own config
25-
* documents them as informational, not enums), so these are conveniences for
26-
* the common cases — never validation.
27-
*/
28-
const TRACK_OPTIONS: InputSelectOption[] = [
29-
{ label: 'Any track', value: '' },
30-
{ label: 'Development', value: 'Development' },
31-
{ label: 'Design', value: 'Design' },
32-
{ label: 'Data Science', value: 'Data Science' },
33-
{ label: 'Quality Assurance', value: 'Quality Assurance' },
34-
]
35-
36-
const TYPE_OPTIONS: InputSelectOption[] = [
37-
{ label: 'Any type', value: '' },
38-
{ label: 'Challenge', value: 'Challenge' },
39-
{ label: 'First2Finish', value: 'First2Finish' },
40-
{ label: 'Marathon Match', value: 'Marathon Match' },
41-
{ label: 'Task', value: 'Task' },
42-
]
43-
44-
/** Mirrors the workflow's own default status set. */
45-
const STATUS_OPTIONS: InputSelectOption[] = [
46-
{ label: 'Active + Completed', value: '' },
47-
{ label: 'Active only', value: 'ACTIVE' },
48-
{ label: 'Completed only', value: 'COMPLETED' },
49-
]
50-
5128
interface BulkFormState {
5229
projectId: string
5330
track: string
@@ -221,7 +198,7 @@ export const IngestChallengesPanel: FC<IngestChallengesPanelProps> = props => {
221198
<InputSelect
222199
name='track'
223200
label=''
224-
options={TRACK_OPTIONS}
201+
options={BULK_TRACK_OPTIONS}
225202
value={bulk.track}
226203
onChange={handleBulkChange('track')}
227204
disabled={bulkDisabled}
@@ -232,7 +209,7 @@ export const IngestChallengesPanel: FC<IngestChallengesPanelProps> = props => {
232209
<InputSelect
233210
name='type'
234211
label=''
235-
options={TYPE_OPTIONS}
212+
options={BULK_TYPE_OPTIONS}
236213
value={bulk.type}
237214
onChange={handleBulkChange('type')}
238215
disabled={bulkDisabled}
@@ -243,7 +220,7 @@ export const IngestChallengesPanel: FC<IngestChallengesPanelProps> = props => {
243220
<InputSelect
244221
name='status'
245222
label=''
246-
options={STATUS_OPTIONS}
223+
options={BULK_STATUS_OPTIONS}
247224
value={bulk.status}
248225
onChange={handleBulkChange('status')}
249226
disabled={bulkDisabled}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
import {
3+
BULK_TRACK_OPTIONS,
4+
INDEXED_TRACK_OPTIONS,
5+
} from './ingest-options'
6+
7+
jest.mock('~/libs/ui', () => ({}), { virtual: true })
8+
9+
describe('ingest-options', () => {
10+
describe('BULK_TRACK_OPTIONS', () => {
11+
// The v6 Challenges API resolves `tracks` against challengeTrack.abbreviation,
12+
// and an unmatched value silently widens the filter to every track rather
13+
// than erroring — so these are pinned rather than left to drift.
14+
it.each([
15+
['Development', 'Dev'],
16+
['Data Science', 'DS'],
17+
['Design', 'Des'],
18+
['Quality Assurance', 'QA'],
19+
])('sends the %s abbreviation %s to the search API', (label, abbreviation) => {
20+
expect(BULK_TRACK_OPTIONS.find(option => option.label === label)?.value)
21+
.toBe(abbreviation)
22+
})
23+
24+
it('keeps an unfiltered option that sends nothing', () => {
25+
expect(BULK_TRACK_OPTIONS[0])
26+
.toEqual({ label: 'Any track', value: '' })
27+
})
28+
})
29+
30+
describe('INDEXED_TRACK_OPTIONS', () => {
31+
// These match stored chunk metadata, which holds the track's full name.
32+
it.each([
33+
'Development',
34+
'Data Science',
35+
'Design',
36+
'Quality Assurance',
37+
])('filters stored metadata by the full name %s', name => {
38+
expect(INDEXED_TRACK_OPTIONS.find(option => option.label === name)?.value)
39+
.toBe(name)
40+
})
41+
})
42+
43+
it('deliberately uses different values for the two surfaces', () => {
44+
// Guards against someone "aligning" the two lists: one talks to the
45+
// search API (abbreviations), the other to the vector index (names).
46+
const bulk = BULK_TRACK_OPTIONS.map(option => option.value)
47+
const indexed = INDEXED_TRACK_OPTIONS.map(option => option.value)
48+
49+
expect(bulk).not.toEqual(indexed)
50+
})
51+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { InputSelectOption } from '~/libs/ui'
2+
3+
/**
4+
* Track/type vocabularies for this page.
5+
*
6+
* These deliberately differ between the two panels, and the difference is not
7+
* cosmetic:
8+
*
9+
* - **Ingest** filters reach the v6 Challenges API (bulk ingestion paginates
10+
* `searchChallengesTool`), which resolves `tracks`/`types` against the
11+
* `challengeTrack`/`challengeType` tables' **abbreviation** column. Sending
12+
* "Development" there matches no row, so the filter silently widens to
13+
* "every track" instead of erroring.
14+
* - **Indexed Challenges** filters query stored chunk metadata, where
15+
* ingestion recorded the track's full **name** (`toFreeFormName(challenge.track)`
16+
* in challenge-ingestion-workflow.ts). Sending "Dev" there matches nothing.
17+
*
18+
* So: abbreviations going out to the search API, full names coming back from
19+
* the index. Do not "align" the two lists.
20+
*/
21+
export const BULK_TRACK_OPTIONS: InputSelectOption[] = [
22+
{ label: 'Any track', value: '' },
23+
{ label: 'Development', value: 'Dev' },
24+
{ label: 'Design', value: 'Des' },
25+
{ label: 'Data Science', value: 'DS' },
26+
{ label: 'Quality Assurance', value: 'QA' },
27+
]
28+
29+
/**
30+
* Challenge types are resolved by abbreviation too, but unlike tracks the
31+
* abbreviations here are unconfirmed — they are rows in `challengeType`, not
32+
* constants in any repo. These values are the type *names*; if bulk ingestion
33+
* ignores a type filter, this list is the first place to look.
34+
*/
35+
export const BULK_TYPE_OPTIONS: InputSelectOption[] = [
36+
{ label: 'Any type', value: '' },
37+
{ label: 'Challenge', value: 'Challenge' },
38+
{ label: 'First2Finish', value: 'First2Finish' },
39+
{ label: 'Marathon Match', value: 'Marathon Match' },
40+
{ label: 'Task', value: 'Task' },
41+
]
42+
43+
/** Mirrors the workflow's own default status set. */
44+
export const BULK_STATUS_OPTIONS: InputSelectOption[] = [
45+
{ label: 'Active + Completed', value: '' },
46+
{ label: 'Active only', value: 'ACTIVE' },
47+
{ label: 'Completed only', value: 'COMPLETED' },
48+
]
49+
50+
/**
51+
* Indexed-challenge filters match stored metadata, so these carry the track's
52+
* full name — see the note on BULK_TRACK_OPTIONS.
53+
*/
54+
export const INDEXED_TRACK_OPTIONS: InputSelectOption[] = [
55+
{ label: 'All tracks', value: '' },
56+
{ label: 'Development', value: 'Development' },
57+
{ label: 'Design', value: 'Design' },
58+
{ label: 'Data Science', value: 'Data Science' },
59+
{ label: 'Quality Assurance', value: 'Quality Assurance' },
60+
]
61+
62+
export const INDEXED_TYPE_OPTIONS: InputSelectOption[] = [
63+
{ label: 'All types', value: '' },
64+
{ label: 'Challenge', value: 'Challenge' },
65+
{ label: 'First2Finish', value: 'First2Finish' },
66+
{ label: 'Marathon Match', value: 'Marathon Match' },
67+
{ label: 'Task', value: 'Task' },
68+
]

0 commit comments

Comments
 (0)