@@ -31,7 +31,7 @@ import DeleteIcon from 'mdi-material-ui/DeleteOutline';
3131import AddIcon from 'mdi-material-ui/Plus' ;
3232import CloseIcon from 'mdi-material-ui/Close' ;
3333import { produce } from 'immer' ;
34- import { generateQueryNames , useDataQueriesContext } from '@perses-dev/plugin-system' ;
34+ import { generateQueryNames , useDataQueriesContext , defaultQueryName } from '@perses-dev/plugin-system' ;
3535import {
3636 DEFAULT_AREA_OPACITY ,
3737 LINE_STYLE_CONFIG ,
@@ -42,35 +42,70 @@ import {
4242} from './time-series-chart-model' ;
4343
4444const DEFAULT_COLOR_VALUE = '#555' ;
45- const NO_INDEX_AVAILABLE = '-1' ; // invalid array index value used to represent the fact that no query index is available
45+ const NO_QUERY_AVAILABLE = '-1' ; // sentinel value used to represent the fact that no query is available
46+
47+ /**
48+ * Resolves the effective query name from a settings entry.
49+ * Falls back to the deprecated `queryIndex` field when `queryName` is absent.
50+ */
51+ function resolveQueryName ( querySettings : QuerySettingsOptions , queryNames : string [ ] ) : string {
52+ if ( querySettings . queryName !== undefined ) {
53+ return querySettings . queryName ;
54+ }
55+ if ( querySettings . queryIndex !== undefined ) {
56+ return queryNames [ querySettings . queryIndex ] ?? defaultQueryName ( querySettings . queryIndex ) ;
57+ }
58+ return '' ;
59+ }
60+
61+ /**
62+ * Normalizes a settings entry so it always persists via `queryName`.
63+ * Converts the deprecated `queryIndex` to `queryName` and removes the old field.
64+ */
65+ function normalizeToQueryName ( querySettings : QuerySettingsOptions , queryNames : string [ ] ) : QuerySettingsOptions {
66+ if ( querySettings . queryIndex === undefined ) {
67+ return querySettings ;
68+ }
69+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
70+ const { queryIndex, ...rest } = querySettings ;
71+ return { ...rest , queryName : rest . queryName ?? queryNames [ queryIndex ] ?? defaultQueryName ( queryIndex ) } ;
72+ }
4673
4774export function QuerySettingsEditor ( props : TimeSeriesChartOptionsEditorProps ) : ReactElement {
4875 const { onChange, value } = props ;
4976 const querySettingsList = value . querySettings ;
5077
51- const handleQuerySettingsChange = ( newQuerySettings : QuerySettingsOptions [ ] ) : void => {
52- onChange (
53- produce ( value , ( draft : TimeSeriesChartOptions ) => {
54- draft . querySettings = newQuerySettings ;
55- } )
56- ) ;
57- } ;
58- // Every time a new query settings input is added, we want to focus the recently added input
78+ // Hooks first — order must be stable across renders
5979 const recentlyAddedInputRef = useRef < HTMLInputElement | null > ( null ) ;
6080 const focusRef = useRef ( false ) ;
81+
82+ const { queryDefinitions } = useDataQueriesContext ( ) ;
83+ const queryNames : string [ ] = useMemo ( ( ) => generateQueryNames ( queryDefinitions ) , [ queryDefinitions ] ) ;
84+
85+ // Every time a new query settings input is added, focus it
6186 useEffect ( ( ) => {
6287 if ( ! recentlyAddedInputRef . current || ! focusRef . current ) return ;
6388 recentlyAddedInputRef . current ?. focus ( ) ;
6489 focusRef . current = false ;
6590 } , [ querySettingsList ?. length ] ) ;
6691
67- const handleQueryIndexChange = ( e : React . ChangeEvent < HTMLInputElement > , i : number ) : void => {
92+ // All writes go through here. Any entry still using the deprecated `queryIndex` is
93+ // normalized to `queryName` at save time — no side-effect migration needed.
94+ const handleQuerySettingsChange = ( newQuerySettings : QuerySettingsOptions [ ] ) : void => {
95+ onChange (
96+ produce ( value , ( draft : TimeSeriesChartOptions ) => {
97+ draft . querySettings = newQuerySettings . map ( ( qs ) => normalizeToQueryName ( qs , queryNames ) ) ;
98+ } )
99+ ) ;
100+ } ;
101+
102+ const handleQueryNameChange = ( e : React . ChangeEvent < HTMLInputElement > , i : number ) : void => {
68103 if ( querySettingsList !== undefined ) {
69104 handleQuerySettingsChange (
70105 produce ( querySettingsList , ( draft ) => {
71106 const querySettings = draft ?. [ i ] ;
72107 if ( querySettings ) {
73- querySettings . queryIndex = e . target . value ;
108+ querySettings . queryName = e . target . value ;
74109 }
75110 } )
76111 ) ;
@@ -223,24 +258,20 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R
223258 } ) ;
224259 } ;
225260
226- const { queryDefinitions } = useDataQueriesContext ( ) ;
227-
228- const queryIndexes : string [ ] = useMemo ( ( ) => generateQueryNames ( queryDefinitions ) , [ queryDefinitions ] ) ;
229-
230- // Compute the list of query indexes for which query settings are not already defined.
231- // This is to avoid already-booked indexes to still be selectable in the dropdown(s)
232- const availableQueryIndexes = useMemo ( ( ) => {
233- return queryIndexes . filter ( ( name ) => {
234- return ! querySettingsList ?. some ( ( qs ) => qs . queryIndex === name ) ;
261+ // Compute the list of query names for which query settings are not already defined.
262+ // This is to avoid already-booked queries to still be selectable in the dropdown(s)
263+ const availableQueryNames = useMemo ( ( ) => {
264+ return queryNames . filter ( ( name ) => {
265+ return ! querySettingsList ?. some ( ( qs ) => resolveQueryName ( qs , queryNames ) === name ) ;
235266 } ) ;
236- } , [ queryIndexes , querySettingsList ] ) ;
267+ } , [ queryNames , querySettingsList ] ) ;
237268
238- const firstAvailableQueryIndex = useMemo ( ( ) => {
239- return availableQueryIndexes [ 0 ] ?? NO_INDEX_AVAILABLE ;
240- } , [ availableQueryIndexes ] ) ;
269+ const firstAvailableQueryName = useMemo ( ( ) => {
270+ return availableQueryNames [ 0 ] ?? NO_QUERY_AVAILABLE ;
271+ } , [ availableQueryNames ] ) ;
241272
242273 const defaultQuerySettings : QuerySettingsOptions = {
243- queryIndex : firstAvailableQueryIndex ,
274+ queryName : firstAvailableQueryName ,
244275 } ;
245276
246277 const addQuerySettingsInput = ( ) : void => {
@@ -267,10 +298,11 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R
267298 < QuerySettingsInput
268299 inputRef = { i === querySettingsList . length - 1 ? recentlyAddedInputRef : undefined }
269300 key = { i }
301+ queryName = { resolveQueryName ( querySettings , queryNames ) }
270302 querySettings = { querySettings }
271- availableQueryIndexes = { availableQueryIndexes }
272- onQueryIndexChange = { ( e ) => {
273- handleQueryIndexChange ( e , i ) ;
303+ availableQueryNames = { availableQueryNames }
304+ onQueryNameChange = { ( e ) => {
305+ handleQueryNameChange ( e , i ) ;
274306 } }
275307 onColorModeChange = { ( e ) => handleColorModeChange ( e , i ) }
276308 onColorValueChange = { ( color ) => handleColorValueChange ( color , i ) }
@@ -291,7 +323,7 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R
291323 />
292324 ) )
293325 ) }
294- { queryDefinitions . length > 0 && firstAvailableQueryIndex !== NO_INDEX_AVAILABLE && (
326+ { queryDefinitions . length > 0 && firstAvailableQueryName !== NO_QUERY_AVAILABLE && (
295327 < Button variant = "contained" startIcon = { < AddIcon /> } sx = { { marginTop : 1 } } onClick = { addQuerySettingsInput } >
296328 Add Query Settings
297329 </ Button >
@@ -302,8 +334,9 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R
302334
303335interface QuerySettingsInputProps {
304336 querySettings : QuerySettingsOptions ;
305- availableQueryIndexes : string [ ] ;
306- onQueryIndexChange : ( e : React . ChangeEvent < HTMLInputElement > ) => void ;
337+ queryName : string ;
338+ availableQueryNames : string [ ] ;
339+ onQueryNameChange : ( e : React . ChangeEvent < HTMLInputElement > ) => void ;
307340 onColorModeChange : ( e : React . ChangeEvent < HTMLInputElement > ) => void ;
308341 onColorValueChange : ( colorValue : string ) => void ;
309342 onLineStyleChange : ( lineStyle : string ) => void ;
@@ -323,9 +356,10 @@ interface QuerySettingsInputProps {
323356}
324357
325358function QuerySettingsInput ( {
326- querySettings : { queryIndex, colorMode, colorValue, lineStyle, areaOpacity, format } ,
327- availableQueryIndexes,
328- onQueryIndexChange,
359+ querySettings : { colorMode, colorValue, lineStyle, areaOpacity, format } ,
360+ queryName,
361+ availableQueryNames,
362+ onQueryNameChange,
329363 onColorModeChange,
330364 onColorValueChange,
331365 onLineStyleChange,
@@ -342,8 +376,8 @@ function QuerySettingsInput({
342376 onRemoveFormat,
343377 onFormatChange,
344378} : QuerySettingsInputProps ) : ReactElement {
345- // current query index should also be selectable
346- const selectableQueryIndex = availableQueryIndexes . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
379+ // current query name should also be selectable
380+ const selectableQueryNames = availableQueryNames . slice ( ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
347381
348382 // State for dropdown menu
349383 const [ anchorEl , setAnchorEl ] = useState < null | HTMLElement > ( null ) ;
@@ -394,15 +428,15 @@ function QuerySettingsInput({
394428 < TextField
395429 select
396430 inputRef = { inputRef }
397- value = { queryIndex }
431+ value = { queryName }
398432 label = "Query"
399- onChange = { onQueryIndexChange }
433+ onChange = { onQueryNameChange }
400434 sx = { { minWidth : '75px' } }
401435 >
402- < MenuItem value = { queryIndex } > { queryIndex } </ MenuItem >
403- { selectableQueryIndex . map ( ( qi ) => (
404- < MenuItem key = { `query-${ qi } ` } value = { qi } >
405- { qi }
436+ < MenuItem value = { queryName } > { queryName } </ MenuItem >
437+ { selectableQueryNames . map ( ( qn ) => (
438+ < MenuItem key = { `query-${ qn } ` } value = { qn } >
439+ { qn }
406440 </ MenuItem >
407441 ) ) }
408442 </ TextField >
@@ -415,7 +449,7 @@ function QuerySettingsInput({
415449 < MenuItem value = "fixed" > Fixed</ MenuItem >
416450 </ TextField >
417451 < OptionsColorPicker
418- label = { queryIndex }
452+ label = { queryName }
419453 color = { colorValue || DEFAULT_COLOR_VALUE }
420454 onColorChange = { onColorValueChange }
421455 />
@@ -507,7 +541,7 @@ function QuerySettingsInput({
507541 </ Stack >
508542 { /* Delete Button for this query settings */ }
509543 < Box sx = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' } } >
510- < IconButton aria-label = { `delete settings for query '${ queryIndex } '` } onClick = { onDelete } >
544+ < IconButton aria-label = { `delete settings for query '${ queryName } '` } onClick = { onDelete } >
511545 < DeleteIcon />
512546 </ IconButton >
513547 </ Box >
0 commit comments