@@ -10,6 +10,7 @@ import React, {
1010} from "react" ;
1111import {
1212 ComponentArgument ,
13+ ComponentArgumentSection ,
1314 ComponentInputType ,
1415} from "Common/Types/Dashboard/DashboardComponents/ComponentArgument" ;
1516import DashboardComponentsUtil from "Common/Utils/Dashboard/Components/Index" ;
@@ -21,8 +22,13 @@ import DashboardComponentType from "Common/Types/Dashboard/DashboardComponentTyp
2122import MetricQueryConfig from "../../Metrics/MetricQueryConfig" ;
2223import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData" ;
2324import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field" ;
24- import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes" ;
2525import MetricType from "Common/Models/DatabaseModels/MetricType" ;
26+ import CollapsibleSection from "Common/UI/Components/CollapsibleSection/CollapsibleSection" ;
27+ import Button , {
28+ ButtonSize ,
29+ ButtonStyleType ,
30+ } from "Common/UI/Components/Button/Button" ;
31+ import IconProp from "Common/Types/Icon/IconProp" ;
2632
2733export interface ComponentProps {
2834 // eslint-disable-next-line react/no-unused-prop-types
@@ -37,16 +43,32 @@ export interface ComponentProps {
3743 onFormChange : ( component : DashboardBaseComponent ) => void ;
3844}
3945
46+ interface SectionGroup {
47+ section : ComponentArgumentSection ;
48+ args : Array < ComponentArgument < DashboardBaseComponent > > ;
49+ }
50+
4051const ArgumentsForm : FunctionComponent < ComponentProps > = (
4152 props : ComponentProps ,
4253) : ReactElement => {
43- const formRef : any = useRef < FormProps < FormValues < JSONObject > > > ( null ) ;
54+ const formRefs : React . MutableRefObject <
55+ Record < string , FormProps < FormValues < JSONObject > > | null >
56+ > = useRef ( { } ) ;
4457 const [ component , setComponent ] = useState < DashboardBaseComponent > (
4558 props . component ,
4659 ) ;
4760 const [ hasFormValidationErrors , setHasFormValidationErrors ] = useState <
4861 Dictionary < boolean >
4962 > ( { } ) ;
63+ const [ multiQueryConfigs , setMultiQueryConfigs ] = useState <
64+ Array < MetricQueryConfigData >
65+ > (
66+ (
67+ ( ( props . component ?. arguments as JSONObject ) ?. [
68+ "metricQueryConfigs"
69+ ] as unknown as Array < MetricQueryConfigData > ) || [ ]
70+ ) ,
71+ ) ;
5072
5173 useEffect ( ( ) => {
5274 if ( props . onHasFormValidationErrors ) {
@@ -66,6 +88,57 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
6688 const componentArguments : Array < ComponentArgument < DashboardBaseComponent > > =
6789 DashboardComponentsUtil . getComponentSettingsArguments ( componentType ) ;
6890
91+ // Group arguments by section
92+ const groupArgumentsBySections : ( ) => Array < SectionGroup > = ( ) : Array < SectionGroup > => {
93+ const sectionMap : Map < string , SectionGroup > = new Map ( ) ;
94+ const unsectionedArgs : Array < ComponentArgument < DashboardBaseComponent > > =
95+ [ ] ;
96+
97+ for ( const arg of componentArguments ) {
98+ // Skip MetricsQueryConfigs - we render it as a custom multi-query UI
99+ if ( arg . type === ComponentInputType . MetricsQueryConfigs ) {
100+ continue ;
101+ }
102+
103+ if ( arg . section ) {
104+ const key : string = arg . section . name ;
105+ if ( ! sectionMap . has ( key ) ) {
106+ sectionMap . set ( key , {
107+ section : arg . section ,
108+ args : [ ] ,
109+ } ) ;
110+ }
111+ sectionMap . get ( key ) ! . args . push ( arg ) ;
112+ } else {
113+ unsectionedArgs . push ( arg ) ;
114+ }
115+ }
116+
117+ const groups : Array < SectionGroup > = [ ] ;
118+
119+ // Add unsectioned args as a "General" section if they exist
120+ if ( unsectionedArgs . length > 0 ) {
121+ groups . push ( {
122+ section : {
123+ name : "General" ,
124+ order : 0 ,
125+ } ,
126+ args : unsectionedArgs ,
127+ } ) ;
128+ }
129+
130+ // Sort sections by order
131+ const sortedSections : Array < SectionGroup > = Array . from (
132+ sectionMap . values ( ) ,
133+ ) . sort (
134+ ( a : SectionGroup , b : SectionGroup ) =>
135+ a . section . order - b . section . order ,
136+ ) ;
137+ groups . push ( ...sortedSections ) ;
138+
139+ return groups ;
140+ } ;
141+
69142 type GetMetricsQueryConfigFormFunction = (
70143 arg : ComponentArgument < DashboardBaseComponent > ,
71144 ) => (
@@ -105,7 +178,7 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
105178 ) => ReactElement )
106179 | undefined ;
107180
108- const getCustomElememnt : GetCustomElementFunction = (
181+ const getCustomElement : GetCustomElementFunction = (
109182 arg : ComponentArgument < DashboardBaseComponent > ,
110183 ) :
111184 | ( (
@@ -119,11 +192,17 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
119192 return undefined ;
120193 } ;
121194
122- const getForm : GetReactElementFunction = ( ) : ReactElement => {
195+ const renderSectionForm : (
196+ sectionGroup : SectionGroup ,
197+ ) => ReactElement = ( sectionGroup : SectionGroup ) : ReactElement => {
198+ const sectionKey : string = sectionGroup . section . name ;
199+
123200 return (
124201 < BasicForm
125202 hideSubmitButton = { true }
126- ref = { formRef }
203+ ref = { ( ref : FormProps < FormValues < JSONObject > > | null ) => {
204+ formRefs . current [ sectionKey ] = ref ;
205+ } }
127206 values = { {
128207 ...( component ?. arguments || { } ) ,
129208 } }
@@ -137,54 +216,197 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
137216 } ) ;
138217 } }
139218 onFormValidationErrorChanged = { ( hasError : boolean ) => {
140- if ( hasFormValidationErrors [ "id" ] !== hasError ) {
219+ if ( hasFormValidationErrors [ sectionKey ] !== hasError ) {
141220 setHasFormValidationErrors ( {
142221 ...hasFormValidationErrors ,
143- id : hasError ,
222+ [ sectionKey ] : hasError ,
144223 } ) ;
145224 }
146225 } }
147- fields = {
148- componentArguments &&
149- componentArguments . map (
150- ( arg : ComponentArgument < DashboardBaseComponent > ) => {
151- return {
152- title : `${ arg . name } ` ,
153- description : `${
154- arg . required ? "Required" : "Optional"
155- } . ${ arg . description } `,
156- field : {
157- [ arg . id ] : true ,
158- } ,
159- required : arg . required ,
160- placeholder : arg . placeholder ,
161- ...ComponentInputTypeToFormFieldType . getFormFieldTypeByComponentInputType (
162- arg . type ,
163- arg . dropdownOptions ,
164- ) ,
165- getCustomElement : getCustomElememnt ( arg ) ,
166- } ;
167- } ,
168- )
169- }
226+ fields = { sectionGroup . args . map (
227+ ( arg : ComponentArgument < DashboardBaseComponent > ) => {
228+ return {
229+ title : arg . name ,
230+ description : arg . description ,
231+ field : {
232+ [ arg . id ] : true ,
233+ } ,
234+ required : arg . required ,
235+ placeholder : arg . placeholder ,
236+ ...ComponentInputTypeToFormFieldType . getFormFieldTypeByComponentInputType (
237+ arg . type ,
238+ arg . dropdownOptions ,
239+ ) ,
240+ getCustomElement : getCustomElement ( arg ) ,
241+ } ;
242+ } ,
243+ ) }
170244 />
171245 ) ;
172246 } ;
173247
248+ // Check if this component has a MetricsQueryConfigs argument
249+ const hasMultiQueryArg : boolean = componentArguments . some (
250+ ( arg : ComponentArgument < DashboardBaseComponent > ) =>
251+ arg . type === ComponentInputType . MetricsQueryConfigs ,
252+ ) ;
253+
254+ const multiQueryArg : ComponentArgument < DashboardBaseComponent > | undefined =
255+ componentArguments . find (
256+ ( arg : ComponentArgument < DashboardBaseComponent > ) =>
257+ arg . type === ComponentInputType . MetricsQueryConfigs ,
258+ ) ;
259+
260+ const renderMultiQuerySection : ( ) => ReactElement | null =
261+ ( ) : ReactElement | null => {
262+ if ( ! hasMultiQueryArg || ! multiQueryArg ) {
263+ return null ;
264+ }
265+
266+ return (
267+ < div className = "mt-3" >
268+ < CollapsibleSection
269+ title = "Additional Queries"
270+ description = "Overlay more metric series on the same chart"
271+ variant = "bordered"
272+ defaultCollapsed = { multiQueryConfigs . length === 0 }
273+ >
274+ < div >
275+ { multiQueryConfigs . length === 0 && (
276+ < p className = "text-sm text-gray-400 mb-3" >
277+ No additional queries yet. Add one to overlay multiple metrics
278+ on the same chart.
279+ </ p >
280+ ) }
281+
282+ { multiQueryConfigs . map (
283+ ( queryConfig : MetricQueryConfigData , index : number ) => {
284+ return (
285+ < div
286+ key = { index }
287+ className = "mb-4 p-3 border border-gray-200 rounded-lg bg-gray-50"
288+ >
289+ < div className = "flex items-center justify-between mb-2" >
290+ < span className = "text-xs font-medium text-gray-500 uppercase tracking-wide" >
291+ Query { index + 2 }
292+ </ span >
293+ < Button
294+ title = "Remove"
295+ buttonSize = { ButtonSize . Small }
296+ buttonStyle = { ButtonStyleType . DANGER_OUTLINE }
297+ icon = { IconProp . Trash }
298+ onClick = { ( ) => {
299+ const updated : Array < MetricQueryConfigData > = [
300+ ...multiQueryConfigs ,
301+ ] ;
302+ updated . splice ( index , 1 ) ;
303+ setMultiQueryConfigs ( updated ) ;
304+ setComponent ( {
305+ ...component ,
306+ arguments : {
307+ ...( ( component . arguments as JSONObject ) || { } ) ,
308+ metricQueryConfigs : updated as any ,
309+ } ,
310+ } ) ;
311+ } }
312+ />
313+ </ div >
314+ < MetricQueryConfig
315+ data = { queryConfig }
316+ metricTypes = { props . metrics . metricTypes }
317+ telemetryAttributes = { props . metrics . telemetryAttributes }
318+ hideCard = { true }
319+ onChange = { ( data : MetricQueryConfigData ) => {
320+ const updated : Array < MetricQueryConfigData > = [
321+ ...multiQueryConfigs ,
322+ ] ;
323+ updated [ index ] = data ;
324+ setMultiQueryConfigs ( updated ) ;
325+ setComponent ( {
326+ ...component ,
327+ arguments : {
328+ ...( ( component . arguments as JSONObject ) || { } ) ,
329+ metricQueryConfigs : updated as any ,
330+ } ,
331+ } ) ;
332+ } }
333+ />
334+ </ div >
335+ ) ;
336+ } ,
337+ ) }
338+
339+ < Button
340+ title = "Add Query"
341+ buttonSize = { ButtonSize . Small }
342+ buttonStyle = { ButtonStyleType . OUTLINE }
343+ icon = { IconProp . Add }
344+ onClick = { ( ) => {
345+ const newQuery : MetricQueryConfigData = {
346+ metricQueryData : {
347+ filterData : { } ,
348+ groupBy : undefined ,
349+ } ,
350+ } ;
351+ const updated : Array < MetricQueryConfigData > = [
352+ ...multiQueryConfigs ,
353+ newQuery ,
354+ ] ;
355+ setMultiQueryConfigs ( updated ) ;
356+ setComponent ( {
357+ ...component ,
358+ arguments : {
359+ ...( ( component . arguments as JSONObject ) || { } ) ,
360+ metricQueryConfigs : updated as any ,
361+ } ,
362+ } ) ;
363+ } }
364+ />
365+ </ div >
366+ </ CollapsibleSection >
367+ </ div >
368+ ) ;
369+ } ;
370+
371+ const sectionGroups : Array < SectionGroup > = groupArgumentsBySections ( ) ;
372+
174373 return (
175- < div className = "mb-3 mt-3" >
176- < div className = "mt-5 mb-5" >
177- < h2 className = "text-base font-medium text-gray-500" > Arguments</ h2 >
178- < p className = "text-sm font-medium text-gray-400 mb-5" >
179- Arguments for this component
180- </ p >
181- { componentArguments && componentArguments . length === 0 && (
182- < ErrorMessage
183- message = { "This component does not take any arguments." }
184- />
185- ) }
186- { componentArguments && componentArguments . length > 0 && getForm ( ) }
187- </ div >
374+ < div className = "mb-3 mt-1" >
375+ { componentArguments && componentArguments . length === 0 && (
376+ < ErrorMessage
377+ message = { "This component does not take any arguments." }
378+ />
379+ ) }
380+ { sectionGroups . map (
381+ ( sectionGroup : SectionGroup , index : number ) => {
382+ const isFirstSection : boolean = index === 0 ;
383+ const shouldCollapse : boolean =
384+ ! isFirstSection &&
385+ ( sectionGroup . section . defaultCollapsed ?? false ) ;
386+
387+ return (
388+ < div key = { sectionGroup . section . name } className = "mt-3" >
389+ < CollapsibleSection
390+ title = { sectionGroup . section . name }
391+ description = { sectionGroup . section . description }
392+ variant = "bordered"
393+ defaultCollapsed = { shouldCollapse }
394+ >
395+ < div > { renderSectionForm ( sectionGroup ) } </ div >
396+ </ CollapsibleSection >
397+
398+ { /* Render multi-query section after the Data Source section */ }
399+ { sectionGroup . section . name === "Data Source" &&
400+ renderMultiQuerySection ( ) }
401+ </ div >
402+ ) ;
403+ } ,
404+ ) }
405+
406+ { /* If no Data Source section but has multi-query, render at end */ }
407+ { ! sectionGroups . some (
408+ ( g : SectionGroup ) => g . section . name === "Data Source" ,
409+ ) && renderMultiQuerySection ( ) }
188410 </ div >
189411 ) ;
190412} ;
0 commit comments