diff --git a/app/components/report-builder-side-panel.tsx b/app/components/report-builder-side-panel.tsx
index 310de25d..aad2223f 100644
--- a/app/components/report-builder-side-panel.tsx
+++ b/app/components/report-builder-side-panel.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
-import { Button } from "@carbon/react";
+import { Button, ComboBox, Theme } from "@carbon/react";
import { DeleteOutlined } from "@mui/icons-material";
import {
REPORT_WINDOW_PRESETS,
@@ -63,6 +63,54 @@ function isExternalCostQuery(query: Report["query"]): query is ExternalCostRepor
return query.layer === "externalCost";
}
+type GroupingOption = { label: string; value: string };
+
+function GroupingComboBox({
+ index,
+ grouping,
+ onChange,
+}: {
+ index: number;
+ grouping: string;
+ onChange: (value: string) => void;
+}) {
+ const presetMatch = ALLOCATION_GROUPING_OPTIONS.find((o) => o.value === grouping);
+ const selectedItem: GroupingOption =
+ presetMatch ?? { label: grouping, value: grouping };
+ const items: GroupingOption[] = presetMatch
+ ? ALLOCATION_GROUPING_OPTIONS
+ : [...ALLOCATION_GROUPING_OPTIONS, selectedItem];
+
+ return (
+
+ item?.label ?? ""}
+ selectedItem={selectedItem}
+ allowCustomValue
+ onChange={({
+ selectedItem: picked,
+ inputValue,
+ }: {
+ selectedItem?: GroupingOption | null;
+ inputValue?: string | null;
+ }) => {
+ if (picked) {
+ onChange(picked.value);
+ return;
+ }
+ const typed = (inputValue ?? "").trim();
+ if (!typed || typed === grouping) return;
+ onChange(typed.includes(":") ? typed : `label:${typed}`);
+ }}
+ />
+
+ );
+}
+
function toFilterOptionsByLayer(layer: ReportLayer) {
switch (layer) {
case "cloudCost":
@@ -414,19 +462,13 @@ export default function ReportBuilderSidePanel({
{groupings.map((grouping, index) => (
-
+
+ setGroupingAt(index, value)}
+ />
+