Skip to content

Commit dd23390

Browse files
authored
Merge pull request #1188 from c-bata/add-smart-filtering-form-component2
Add `useSmartFilteringForm` hook for the composition support
2 parents 982344f + 9cbf67d commit dd23390

File tree

3 files changed

+119
-139
lines changed

3 files changed

+119
-139
lines changed

optuna_dashboard/ts/components/TrialList.tsx

Lines changed: 20 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import CheckBoxIcon from "@mui/icons-material/CheckBox"
22
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
3-
import ClearIcon from "@mui/icons-material/Clear"
43
import FilterListIcon from "@mui/icons-material/FilterList"
54
import StopCircleIcon from "@mui/icons-material/StopCircle"
65

76
import {
87
Box,
98
Button,
10-
CircularProgress,
119
FormControl,
1210
IconButton,
13-
InputAdornment,
1411
InputLabel,
1512
Menu,
1613
MenuItem,
1714
Select,
18-
TextField,
1915
Typography,
2016
useTheme,
2117
} from "@mui/material"
@@ -27,18 +23,18 @@ import ListItemButton from "@mui/material/ListItemButton"
2723
import ListItemText from "@mui/material/ListItemText"
2824
import ListSubheader from "@mui/material/ListSubheader"
2925
import * as Optuna from "@optuna/types"
30-
import React, { FC, ReactNode, useCallback, useMemo, useState } from "react"
26+
import React, { FC, ReactNode, useMemo, useState } from "react"
3127

3228
import ListItemIcon from "@mui/material/ListItemIcon"
3329
import { useVirtualizer } from "@tanstack/react-virtual"
3430
import { useAtom } from "jotai"
3531
import { useNavigate } from "react-router-dom"
36-
import { FormWidgets, StudyDetail, Trial } from "ts/types/optuna"
3732
import { actionCreator } from "../action"
3833
import { useConstants } from "../constantsProvider"
3934
import { useArtifactIsAvailable, useLLMIsAvailable } from "../hooks/useAPIMeta"
40-
import { useTrialFilterQuery } from "../hooks/useTrialFilterQuery"
35+
import { useSmartFilteringForm } from "../hooks/useSmartFilteringForm"
4136
import { trialListDurationTimeUnitState } from "../state"
37+
import { FormWidgets, StudyDetail, Trial } from "../types/optuna"
4238
import { useQuery } from "../urlQuery"
4339
import { ArtifactCards } from "./Artifact/ArtifactCards"
4440
import { TrialNote } from "./Note"
@@ -416,21 +412,22 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
416412
const [llmFilteredTrials, setLlmFilteredTrials] = useState<
417413
Trial[] | undefined
418414
>()
419-
const [trialFilterQuery, setTrialFilterQuery] = useState<string>("")
420-
const handleClearFilter = useCallback(() => {
421-
setTrialFilterQuery("")
422-
setLlmFilteredTrials(undefined)
423-
}, [])
424-
const [trialFilter, renderIframe, isTrialFilterProcessing] =
425-
useTrialFilterQuery({
426-
nRetry: 5,
427-
onDenied: handleClearFilter,
428-
onFailed: (errorMsg: string) => {
429-
console.error("Failed to filter trials:", errorMsg)
430-
handleClearFilter()
431-
},
432-
})
433415
const llmEnabled = useLLMIsAvailable()
416+
const [renderSmartFilteringForm] = useSmartFilteringForm(
417+
(trialFilterQuery, trialFilter) => {
418+
if (trialFilterQuery !== "") {
419+
trialFilter(allTrials, trialFilterQuery)
420+
.then((filtered) => {
421+
setLlmFilteredTrials(filtered)
422+
})
423+
.catch(() => {
424+
setLlmFilteredTrials(allTrials) // Fallback to all trials on error
425+
})
426+
} else {
427+
setLlmFilteredTrials(allTrials)
428+
}
429+
}
430+
)
434431

435432
const allTrials = useMemo(() => {
436433
return studyDetail?.trials ?? []
@@ -484,64 +481,13 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
484481
sx={{
485482
height: theme.spacing(8),
486483
p: theme.spacing(1),
484+
gap: theme.spacing(1),
487485
display: "flex",
488486
flexDirection: "row",
489487
alignItems: "center",
490488
}}
491489
>
492-
<TextField
493-
id="trial-filter-query"
494-
variant="outlined"
495-
placeholder="Enter filter query (e.g., trial number < 10)"
496-
fullWidth
497-
size="small"
498-
value={trialFilterQuery}
499-
onChange={(e) => setTrialFilterQuery(e.target.value)}
500-
slotProps={{
501-
input: {
502-
endAdornment: trialFilterQuery && (
503-
<InputAdornment position="end">
504-
<IconButton
505-
aria-label="clear filter"
506-
onClick={handleClearFilter}
507-
edge="end"
508-
size="small"
509-
disabled={isTrialFilterProcessing}
510-
>
511-
<ClearIcon />
512-
</IconButton>
513-
</InputAdornment>
514-
),
515-
},
516-
}}
517-
/>
518-
<Button
519-
variant="contained"
520-
startIcon={
521-
isTrialFilterProcessing ? (
522-
<CircularProgress size={16} />
523-
) : (
524-
<FilterListIcon />
525-
)
526-
}
527-
disabled={isTrialFilterProcessing}
528-
onClick={() => {
529-
if (trialFilterQuery !== "" && !isTrialFilterProcessing) {
530-
trialFilter(allTrials, trialFilterQuery)
531-
.then((filtered) => {
532-
setLlmFilteredTrials(filtered)
533-
})
534-
.catch(() => {
535-
setLlmFilteredTrials(allTrials) // Fallback to all trials on error
536-
})
537-
} else {
538-
setLlmFilteredTrials(allTrials)
539-
}
540-
}}
541-
sx={{ marginLeft: theme.spacing(2), minWidth: "120px" }}
542-
>
543-
Filter
544-
</Button>
490+
{renderSmartFilteringForm()}
545491
</Box>
546492
<Divider />
547493
</>
@@ -746,7 +692,6 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
746692
</Box>
747693
</Box>
748694
</Box>
749-
{renderIframe()}
750695
</Box>
751696
)
752697
}

optuna_dashboard/ts/components/TrialSelection.tsx

Lines changed: 20 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
import DownloadIcon from "@mui/icons-material/Download"
2-
import FilterListIcon from "@mui/icons-material/FilterList"
32
import {
43
Alert,
54
Box,
65
Button,
76
Card,
87
CardContent,
9-
CircularProgress,
108
FormControl,
119
FormControlLabel,
1210
Switch,
1311
useTheme,
1412
} from "@mui/material"
1513
import { PlotParallelCoordinate, TrialTable } from "@optuna/react"
16-
import React, { FC, useState, useCallback } from "react"
14+
import React, { FC, useState } from "react"
1715
import { Link } from "react-router-dom"
1816
import { useConstants } from "../constantsProvider"
1917
import { studyDetailToStudy } from "../graphUtil"
2018
import { useLLMIsAvailable } from "../hooks/useAPIMeta"
21-
import { useTrialFilterQuery } from "../hooks/useTrialFilterQuery"
19+
import { useSmartFilteringForm } from "../hooks/useSmartFilteringForm"
2220
import { StudyDetail, Trial } from "../types/optuna"
2321
import { SelectedTrialArtifactCards } from "./Artifact/SelectedTrialArtifactCards"
24-
import { DebouncedInputTextField } from "./Debounce"
2522
import { GraphHistory } from "./GraphHistory"
2623
import { GraphParetoFront } from "./GraphParetoFront"
2724

@@ -36,7 +33,6 @@ export const TrialSelection: FC<{ studyDetail: StudyDetail }> = ({
3633
const [includeDominatedTrials, setIncludeDominatedTrials] =
3734
useState<boolean>(true)
3835
const [showArtifacts, setShowArtifacts] = useState<boolean>(false)
39-
const [filterQuery, setFilterQuery] = useState("")
4036
const [filteredTrials, setFilteredTrials] = useState<Trial[] | undefined>(
4137
undefined
4238
)
@@ -57,38 +53,24 @@ export const TrialSelection: FC<{ studyDetail: StudyDetail }> = ({
5753
}
5854
setIncludeDominatedTrials(!includeDominatedTrials)
5955
}
60-
const handleClearFilter = useCallback(() => {
61-
setFilterQuery("")
62-
setFilteredTrials(undefined)
63-
}, [])
64-
const [trialFilter, render, isTrialFilterProcessing] = useTrialFilterQuery({
65-
nRetry: 5,
66-
onDenied: handleClearFilter,
67-
onFailed: (errorMsg: string): void => {
68-
console.error(errorMsg)
69-
handleClearFilter()
70-
},
71-
})
72-
73-
const handleFilter = useCallback(async () => {
74-
if (isTrialFilterProcessing) {
75-
return
76-
}
77-
78-
if (!filterQuery.trim()) {
79-
setFilteredTrials(undefined)
80-
return
81-
}
82-
83-
try {
84-
const result = await trialFilter(studyDetail.trials, filterQuery)
85-
setFilteredTrials(result)
86-
} catch (error) {
87-
// eslint-disable-next-line no-empty
88-
// Error handling is delegated to onDenied/onFailed callbacks to avoid
89-
// emmiting error logs when user denied the execution.
56+
const [renderSmartFilteringForm] = useSmartFilteringForm(
57+
(trialFilterQuery, trialFilter) => {
58+
if (!trialFilterQuery.trim()) {
59+
setFilteredTrials(undefined)
60+
return
61+
}
62+
trialFilter(studyDetail.trials, trialFilterQuery)
63+
.then((filtered) => {
64+
setFilteredTrials(filtered)
65+
})
66+
.catch(() => {
67+
// eslint-disable-next-line no-empty
68+
// Error handling is delegated to onDenied/onFailed callbacks to avoid
69+
// emmiting error logs when user denied the execution.
70+
setFilteredTrials(undefined) // Fallback to all trials on error
71+
})
9072
}
91-
}, [filterQuery, trialFilter, studyDetail.trials])
73+
)
9274

9375
const study = studyDetailToStudy(studyDetail)
9476
const linkURL = (studyId: number, trialNumber: number) => {
@@ -127,32 +109,7 @@ export const TrialSelection: FC<{ studyDetail: StudyDetail }> = ({
127109
alignItems: "center",
128110
}}
129111
>
130-
<DebouncedInputTextField
131-
onChange={(val) => setFilterQuery(val)}
132-
delay={500}
133-
textFieldProps={{
134-
placeholder: "Enter filter query (e.g., trial number < 10)",
135-
fullWidth: true,
136-
size: "small",
137-
disabled: isTrialFilterProcessing,
138-
type: "search",
139-
}}
140-
/>
141-
<Button
142-
variant="contained"
143-
startIcon={
144-
isTrialFilterProcessing ? (
145-
<CircularProgress size={16} />
146-
) : (
147-
<FilterListIcon />
148-
)
149-
}
150-
onClick={handleFilter}
151-
disabled={isTrialFilterProcessing}
152-
sx={{ minWidth: "120px", flexShrink: 0 }}
153-
>
154-
Filter
155-
</Button>
112+
{renderSmartFilteringForm()}
156113
</Box>
157114
)}
158115
<FormControlLabel
@@ -272,7 +229,6 @@ export const TrialSelection: FC<{ studyDetail: StudyDetail }> = ({
272229
</Card>
273230
</Box>
274231
)}
275-
{render()}
276232
</Box>
277233
)
278234
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import FilterListIcon from "@mui/icons-material/FilterList"
2+
import { Button, CircularProgress, TextField } from "@mui/material"
3+
import React, { ReactNode, useState, useRef } from "react"
4+
import { Trial } from "../types/optuna"
5+
import { useTrialFilterQuery } from "./useTrialFilterQuery"
6+
7+
export const useSmartFilteringForm = (
8+
handleFilter: (
9+
trialFilterQuery: string,
10+
trialFilter: (trials: Trial[], filterQueryStr: string) => Promise<Trial[]>
11+
) => void
12+
): [() => ReactNode] => {
13+
const [isComposing, setIsComposing] = useState(false)
14+
const inputRef = useRef<HTMLInputElement>(null)
15+
const handleClearFilter = () => {
16+
if (inputRef.current) {
17+
inputRef.current.value = ""
18+
}
19+
}
20+
const [trialFilter, renderIframe, isProcessing] = useTrialFilterQuery({
21+
nRetry: 5,
22+
onDenied: handleClearFilter,
23+
onFailed: (errorMsg: string) => {
24+
console.error("Failed to filter trials:", errorMsg)
25+
handleClearFilter()
26+
},
27+
})
28+
const handleSubmit = () => {
29+
if (isProcessing) {
30+
return
31+
}
32+
handleFilter(inputRef.current?.value ?? "", trialFilter)
33+
}
34+
35+
const render = () => {
36+
return (
37+
<>
38+
<TextField
39+
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
40+
if (e.key === "Enter" && !isComposing) {
41+
e.preventDefault()
42+
handleSubmit()
43+
}
44+
}}
45+
placeholder="Enter filter query (e.g., trial number < 10)"
46+
fullWidth={true}
47+
size="small"
48+
disabled={isProcessing}
49+
type="search"
50+
onCompositionStart={() => {
51+
setIsComposing(true)
52+
}}
53+
onCompositionEnd={() => {
54+
setIsComposing(false)
55+
}}
56+
inputRef={inputRef}
57+
/>
58+
<Button
59+
variant="contained"
60+
startIcon={
61+
isProcessing ? <CircularProgress size={16} /> : <FilterListIcon />
62+
}
63+
onClick={() => {
64+
if (isProcessing) {
65+
return
66+
}
67+
handleSubmit()
68+
}}
69+
disabled={isProcessing}
70+
sx={{ minWidth: "120px", flexShrink: 0 }}
71+
>
72+
Filter
73+
</Button>
74+
{renderIframe()}
75+
</>
76+
)
77+
}
78+
return [render]
79+
}

0 commit comments

Comments
 (0)