Skip to content

Commit a1122ed

Browse files
committed
feat: Organize component arguments into sections and enhance dashboard components with new features
1 parent 72a796c commit a1122ed

12 files changed

Lines changed: 527 additions & 127 deletions

File tree

App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/ArgumentsForm.tsx

Lines changed: 265 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, {
1010
} from "react";
1111
import {
1212
ComponentArgument,
13+
ComponentArgumentSection,
1314
ComponentInputType,
1415
} from "Common/Types/Dashboard/DashboardComponents/ComponentArgument";
1516
import DashboardComponentsUtil from "Common/Utils/Dashboard/Components/Index";
@@ -21,8 +22,13 @@ import DashboardComponentType from "Common/Types/Dashboard/DashboardComponentTyp
2122
import MetricQueryConfig from "../../Metrics/MetricQueryConfig";
2223
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
2324
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
24-
import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes";
2525
import 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

2733
export 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+
4051
const 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
};

App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/ComponentInputTypeToFormFieldType.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ export default class ComponentInputTypeToFormFieldType {
5353
};
5454
}
5555

56+
if (componentInputType === ComponentInputType.MetricsQueryConfigs) {
57+
return {
58+
fieldType: FormFieldSchemaType.CustomComponent,
59+
};
60+
}
61+
5662
if (componentInputType === ComponentInputType.Dropdown) {
5763
return {
5864
fieldType: FormFieldSchemaType.Dropdown,

0 commit comments

Comments
 (0)