{
isDirty={isDirty}
isValid={isValid}
// @ts-ignore
- onAdd={() => setInnerConfig((existing) => [...existing, { type: "" }])}
- onApply={() => onApply(innerConfig)}
- onClear={() => onApply([])}
+ onAdd={() =>
+ // Insert BEFORE a trailing "result" rather than appending blindly.
+ // "result" is the leaf level - there is nothing beneath a result to
+ // subdivide - so the validator below requires it to be last. Appending
+ // after it produced the one arrangement that is always invalid, which
+ // greyed out Apply with the explanation hidden in a title tooltip.
+ setInnerConfig((existing) => {
+ const resultIndex = existing.findIndex((c) => c.type === "result");
+ return resultIndex === -1
+ ? [...existing, { type: "", __rowId: `row-${nextRowId++}` } as any]
+ : [
+ ...existing.slice(0, resultIndex),
+ { type: "", __rowId: `row-${nextRowId++}` } as any,
+ ...existing.slice(resultIndex),
+ ];
+ })
+ }
+ onApply={() => onApply(stripRowIds(innerConfig))}
+ onClear={() => {
+ // Show the DEFAULT immediately, then clear the saved grouping.
+ //
+ // Two traps here, both hit in earlier attempts:
+ //
+ // - onApply([]) alone does nothing visible. It removes this panel's
+ // saved entry so useGroupingConfig falls back to the default, but
+ // when the panel is already on the default the search params do
+ // not change, so nothing re-renders and the useEffect that syncs
+ // innerConfig never fires.
+ // - Setting innerConfig to `config` is worse than useless: `config`
+ // is the grouping being discarded, so the rows being removed were
+ // briefly re-asserted and could survive the reset.
+ //
+ // defaultConfig is what onApply([]) will resolve to, computed by
+ // useGroupingConfig - which knows the control vs detection default,
+ // so this stays correct for detection benchmarks too.
+ setInnerConfig(withRowIds(defaultConfig) as any);
+ onApply([]);
+ }}
addLabel="Add grouping"
/>
diff --git a/ui/dashboard/src/components/dashboards/grouping/common/EditorAddItem.tsx b/ui/dashboard/src/components/dashboards/grouping/common/EditorAddItem.tsx
index a5f6a91a2..750ba40d5 100644
--- a/ui/dashboard/src/components/dashboards/grouping/common/EditorAddItem.tsx
+++ b/ui/dashboard/src/components/dashboards/grouping/common/EditorAddItem.tsx
@@ -39,6 +39,12 @@ const EditorAddItem = ({
+ {/* Say WHY Apply is disabled. The reason was only ever exposed as the
+ button's title attribute, so a blocked save looked like a dead
+ button - the user had no way to know what to change. */}
+ {!isValid.value && !!isValid.reason && (
+ {isValid.reason}
+ )}
{
}
}, [searchParams]);
+ // The grouping a panel falls back to when nothing is saved. Exposed so the
+ // editor's Reset can restore it explicitly rather than relying on a re-render
+ // that does not happen when the panel is already on the default.
+ const defaultGrouping = useMemo(() => {
+ if (!panel) {
+ return [] as DisplayGroup[];
+ }
+ if (
+ (!panel.benchmark_type || panel.benchmark_type === "control") &&
+ (panel.panel_type === "benchmark" || panel.panel_type === "control")
+ ) {
+ return [
+ { type: "benchmark" },
+ { type: "control" },
+ { type: "result" },
+ ] as DisplayGroup[];
+ }
+ if (
+ (panel.benchmark_type === "detection" &&
+ panel.panel_type === "benchmark") ||
+ panel.panel_type === "detection"
+ ) {
+ return [
+ { type: "benchmark" },
+ { type: "detection" },
+ { type: "result" },
+ ] as DisplayGroup[];
+ }
+ return [] as DisplayGroup[];
+ }, [panel]);
+
const grouping = useMemo(() => {
if (!panel) {
return [] as DisplayGroup[];
@@ -84,7 +115,7 @@ const useGroupingConfig = (panelName?: string) => {
});
};
- return { allGroupings, grouping, update };
+ return { allGroupings, grouping, defaultGrouping, update };
};
export default useGroupingConfig;