Skip to content

Commit a818970

Browse files
feat(config-builder): expand/collapse all reaches every nested section
Wire the union, list and plugin-select renderers into the section expansion context so Expand all and Collapse all drive every collapsible at any depth, on both the SDK and Instrumentation tabs. Extract the open-state resolution into a shared resolveBulkOpen helper and a useCollapsibleExpansion hook, keyed by path so repeated node keys (processors, exporter, attributes) no longer collide when toggling a single section. Add an integration test covering both tabs and restyle the toolbar buttons to match the existing toolbar.
1 parent 921b1d2 commit a818970

10 files changed

Lines changed: 231 additions & 70 deletions

ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { useState, type JSX } from "react";
16+
import type { JSX } from "react";
1717
import type { ConfigNode } from "@/types/configuration";
1818
import { SchemaRenderer } from "./schema-renderer";
1919
import { SectionCardShell } from "./section-card-shell";
2020
import { FieldSection } from "./field-section";
21-
import { useSectionExpansion } from "./section-expansion-context";
21+
import { useCollapsibleExpansion } from "./section-expansion-context";
2222

2323
export const GENERAL_SECTION_KEY = "general";
2424
export const GENERAL_SECTION_LABEL = "General";
@@ -40,17 +40,7 @@ export function GeneralSectionCard({
4040
sectionKey = GENERAL_SECTION_KEY,
4141
emptyMessage,
4242
}: GeneralSectionCardProps): JSX.Element {
43-
const [expanded, setExpanded] = useState(defaultExpanded);
44-
const { bulkAction, overrides, setOverride } = useSectionExpansion();
45-
const bulkOpen =
46-
sectionKey in overrides
47-
? overrides[sectionKey]
48-
: bulkAction === "expand"
49-
? true
50-
: bulkAction === "collapse"
51-
? false
52-
: null;
53-
const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded;
43+
const { open, onOpenChange } = useCollapsibleExpansion(sectionKey, defaultExpanded);
5444

5545
const headerNode = {
5646
controlType: "group" as const,
@@ -64,11 +54,8 @@ export function GeneralSectionCard({
6454
node={headerNode}
6555
level="section"
6656
asGroup={false}
67-
open={resolvedExpanded}
68-
onOpenChange={(next) => {
69-
setOverride(sectionKey, next);
70-
setExpanded(next);
71-
}}
57+
open={open}
58+
onOpenChange={onOpenChange}
7259
>
7360
<FieldSection.Header>
7461
<FieldSection.Chevron />

ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { SchemaRenderer } from "./schema-renderer";
2323
import { SectionCardShell } from "./section-card-shell";
2424
import { FieldSection } from "./field-section";
2525
import { useStarterPaths } from "./configuration-ui-context";
26-
import { useSectionExpansion } from "./section-expansion-context";
26+
import { resolveBulkOpen, useSectionExpansion } from "./section-expansion-context";
2727

2828
export interface GroupRendererProps {
2929
node: GroupNode;
@@ -58,16 +58,8 @@ export function GroupRenderer({
5858
if (enabled) setExpanded(true);
5959
}
6060

61-
const { bulkAction, overrides, setOverride } = useSectionExpansion();
62-
const sectionKey = node.key;
63-
const bulkOpen =
64-
sectionKey in overrides
65-
? overrides[sectionKey]
66-
: bulkAction === "expand" && (isTopLevel ? enabled : true)
67-
? true
68-
: bulkAction === "collapse"
69-
? false
70-
: null;
61+
const ctx = useSectionExpansion();
62+
const bulkOpen = resolveBulkOpen(ctx, path, isTopLevel ? enabled : true);
7163
const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded;
7264

7365
const value = getByPath(state.values, parsePath(path));
@@ -94,7 +86,7 @@ export function GroupRenderer({
9486
asGroup={false}
9587
open={resolvedExpanded}
9688
onOpenChange={(next) => {
97-
if (isTopLevel) setOverride(sectionKey, next);
89+
ctx.setOverride(path, next);
9890
setExpanded(next);
9991
}}
10092
>
@@ -127,7 +119,7 @@ export function GroupRenderer({
127119
open={resolvedExpanded}
128120
onOpenChange={(next) => {
129121
setExpanded(next);
130-
setOverride(sectionKey, next);
122+
ctx.setOverride(path, next);
131123
}}
132124
>
133125
<FieldSection.Header>

ecosystem-explorer/src/features/java-agent/configuration/components/list-renderer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { parsePath, getByPath } from "@/lib/config-path";
2323
import { deriveListItemLabel } from "@/lib/derive-list-item-label";
2424
import { FieldSection } from "./field-section";
2525
import { ListItemContext, useStarterPaths } from "./configuration-ui-context";
26+
import { useCollapsibleExpansion } from "./section-expansion-context";
2627

2728
export interface ListRendererProps {
2829
node: ListNode;
@@ -44,9 +45,10 @@ export function ListRenderer({ node, depth, path }: ListRendererProps): JSX.Elem
4445
? storedIds
4546
: items.map((_, i) => `${path}#${i}`);
4647
const itemHasTablist = node.itemSchema.controlType === "plugin_select";
48+
const { open, onOpenChange } = useCollapsibleExpansion(path, starterPaths.has(path));
4749

4850
return (
49-
<FieldSection node={node} level="field" value={items} defaultExpanded={starterPaths.has(path)}>
51+
<FieldSection node={node} level="field" value={items} open={open} onOpenChange={onOpenChange}>
5052
<FieldSection.Header>
5153
<FieldSection.Chevron />
5254
<FieldSection.Label />

ecosystem-explorer/src/features/java-agent/configuration/components/plugin-select-renderer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { SchemaRenderer } from "./schema-renderer";
2323
import { parsePath, getByPath } from "@/lib/config-path";
2424
import { FieldSection } from "./field-section";
2525
import { ListItemContext, useListItemContext, useStarterPaths } from "./configuration-ui-context";
26+
import { useCollapsibleExpansion } from "./section-expansion-context";
2627

2728
export interface PluginSelectRendererProps {
2829
node: PluginSelectNode;
@@ -66,6 +67,7 @@ export function PluginSelectRenderer({
6667
const customTabId = useId();
6768
const customPanelId = useId();
6869
const customActive = isCustom || customPickerOpen;
70+
const { open, onOpenChange } = useCollapsibleExpansion(path, starterPaths.has(path));
6971

7072
function closeCustomPicker() {
7173
setCustomPickerOpen(false);
@@ -216,7 +218,7 @@ export function PluginSelectRenderer({
216218
}
217219

218220
return (
219-
<FieldSection node={node} level="field" defaultExpanded={starterPaths.has(path)}>
221+
<FieldSection node={node} level="field" open={open} onOpenChange={onOpenChange}>
220222
<FieldSection.Header>
221223
<FieldSection.Chevron />
222224
<FieldSection.Label />

ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { useState, type JSX } from "react";
16+
import type { JSX } from "react";
1717
import { useTranslation } from "react-i18next";
1818
import type {
1919
ConfigNode,
@@ -39,7 +39,7 @@ import { UnionRenderer } from "./union-renderer";
3939
import { CircularRefPlaceholder } from "./circular-ref-placeholder";
4040
import { FieldSection } from "./field-section";
4141
import { useStarterPaths } from "./configuration-ui-context";
42-
import { useSectionExpansion } from "./section-expansion-context";
42+
import { useCollapsibleExpansion } from "./section-expansion-context";
4343

4444
export interface SchemaRendererProps {
4545
node: ConfigNode;
@@ -70,29 +70,10 @@ function WrappedLeaf({
7070
defaultExp: boolean;
7171
children: JSX.Element;
7272
}) {
73-
const { bulkAction, overrides, setOverride } = useSectionExpansion();
74-
const [expanded, setExpanded] = useState(defaultExp);
75-
76-
const bulkOpen =
77-
path in overrides
78-
? overrides[path]
79-
: bulkAction === "expand"
80-
? true
81-
: bulkAction === "collapse"
82-
? false
83-
: null;
84-
const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded;
73+
const { open, onOpenChange } = useCollapsibleExpansion(path, defaultExp);
8574

8675
return (
87-
<FieldSection
88-
node={node}
89-
level="field"
90-
open={resolvedExpanded}
91-
onOpenChange={(next) => {
92-
setExpanded(next);
93-
setOverride(path, next);
94-
}}
95-
>
76+
<FieldSection node={node} level="field" open={open} onOpenChange={onOpenChange}>
9677
<FieldSection.Header>
9778
<FieldSection.Chevron />
9879
<FieldSection.Label />
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { describe, it, expect } from "vitest";
17+
import { render, screen, fireEvent } from "@testing-library/react";
18+
import {
19+
SectionExpansionProvider,
20+
useCollapsibleExpansion,
21+
useSectionExpansion,
22+
} from "./section-expansion-context";
23+
24+
function Probe({ k, def }: { k: string; def: boolean }) {
25+
const { open, onOpenChange } = useCollapsibleExpansion(k, def);
26+
return (
27+
<button aria-expanded={open} onClick={() => onOpenChange(!open)}>
28+
{k}
29+
</button>
30+
);
31+
}
32+
33+
function Bulk() {
34+
const { expandAll, collapseAll } = useSectionExpansion();
35+
return (
36+
<>
37+
<button onClick={expandAll}>expand</button>
38+
<button onClick={collapseAll}>collapse</button>
39+
</>
40+
);
41+
}
42+
43+
describe("useCollapsibleExpansion", () => {
44+
it("starts at defaultExpanded and toggles locally", () => {
45+
render(
46+
<SectionExpansionProvider>
47+
<Probe k="a" def={false} />
48+
</SectionExpansionProvider>
49+
);
50+
const b = screen.getByRole("button", { name: "a" });
51+
expect(b).toHaveAttribute("aria-expanded", "false");
52+
fireEvent.click(b);
53+
expect(b).toHaveAttribute("aria-expanded", "true");
54+
});
55+
56+
it("collapseAll then individual expand overrides only that key", () => {
57+
render(
58+
<SectionExpansionProvider>
59+
<Bulk />
60+
<Probe k="a" def={true} />
61+
<Probe k="b" def={true} />
62+
</SectionExpansionProvider>
63+
);
64+
fireEvent.click(screen.getByRole("button", { name: "collapse" }));
65+
expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "false");
66+
fireEvent.click(screen.getByRole("button", { name: "a" }));
67+
expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "true");
68+
expect(screen.getByRole("button", { name: "b" })).toHaveAttribute("aria-expanded", "false");
69+
});
70+
71+
it("expandAll then individual collapse overrides only that key", () => {
72+
render(
73+
<SectionExpansionProvider>
74+
<Bulk />
75+
<Probe k="a" def={false} />
76+
<Probe k="b" def={false} />
77+
</SectionExpansionProvider>
78+
);
79+
fireEvent.click(screen.getByRole("button", { name: "expand" }));
80+
expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "true");
81+
fireEvent.click(screen.getByRole("button", { name: "a" }));
82+
expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "false");
83+
expect(screen.getByRole("button", { name: "b" })).toHaveAttribute("aria-expanded", "true");
84+
});
85+
86+
it("falls back to defaultExpanded with no provider", () => {
87+
render(<Probe k="a" def={true} />);
88+
expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "true");
89+
});
90+
});

ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,37 @@ export function useSectionExpansion(): SectionExpansionContextValue {
6767
const ctx = useContext(SectionExpansionContext);
6868
return ctx ?? NO_OP_CONTEXT;
6969
}
70+
71+
// eslint-disable-next-line react-refresh/only-export-components
72+
export function resolveBulkOpen(
73+
ctx: Pick<SectionExpansionContextValue, "bulkAction" | "overrides">,
74+
key: string,
75+
eligibleForBulkExpand = true
76+
): boolean | null {
77+
if (key in ctx.overrides) return ctx.overrides[key];
78+
if (ctx.bulkAction === "expand" && eligibleForBulkExpand) return true;
79+
if (ctx.bulkAction === "collapse") return false;
80+
return null;
81+
}
82+
83+
export interface CollapsibleExpansion {
84+
open: boolean;
85+
onOpenChange: (next: boolean) => void;
86+
}
87+
88+
// eslint-disable-next-line react-refresh/only-export-components
89+
export function useCollapsibleExpansion(
90+
key: string,
91+
defaultExpanded: boolean,
92+
options?: { eligibleForBulkExpand?: boolean }
93+
): CollapsibleExpansion {
94+
const ctx = useSectionExpansion();
95+
const [expanded, setExpanded] = useState(defaultExpanded);
96+
const bulkOpen = resolveBulkOpen(ctx, key, options?.eligibleForBulkExpand ?? true);
97+
const open = bulkOpen !== null ? bulkOpen : expanded;
98+
const onOpenChange = (next: boolean) => {
99+
setExpanded(next);
100+
ctx.setOverride(key, next);
101+
};
102+
return { open, onOpenChange };
103+
}

ecosystem-explorer/src/features/java-agent/configuration/components/union-renderer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { useConfigurationBuilder } from "@/hooks/use-configuration-builder";
2222
import { SchemaRenderer } from "./schema-renderer";
2323
import { parsePath, getByPath } from "@/lib/config-path";
2424
import { FieldSection } from "./field-section";
25+
import { useCollapsibleExpansion } from "./section-expansion-context";
2526

2627
export interface UnionRendererProps {
2728
node: UnionNode;
@@ -138,6 +139,8 @@ export function UnionRenderer({ node, depth, path }: UnionRendererProps): JSX.El
138139
selectedKey === null ? undefined : effectiveVariants.find((v) => v.key === selectedKey);
139140
const showChooser = renderable.length > 0;
140141

142+
const { open, onOpenChange } = useCollapsibleExpansion(`${path}#union`, true);
143+
141144
const handleChange = (nextKey: string) => {
142145
if (nextKey === selectedKey) return;
143146
const nextVariant = effectiveVariants.find((v) => v.key === nextKey);
@@ -187,7 +190,7 @@ export function UnionRenderer({ node, depth, path }: UnionRendererProps): JSX.El
187190
) : null;
188191

189192
return (
190-
<FieldSection node={node} level="field" defaultExpanded>
193+
<FieldSection node={node} level="field" open={open} onOpenChange={onOpenChange}>
191194
<FieldSection.Header>
192195
<FieldSection.Chevron />
193196
<FieldSection.Label />

ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
import { useEffect, useMemo, useRef, useState } from "react";
1717
import { useTranslation } from "react-i18next";
18+
import { ChevronsDownUp, ChevronsUpDown } from "lucide-react";
1819
import { Loader } from "@/components/ui/loader";
1920
import { BackButton } from "@/components/ui/back-button";
2021
import { BetaBadge } from "@/components/ui/beta-badge";
@@ -83,25 +84,19 @@ function PruneInstrumentationsForAgentVersion({ javaAgentVersion }: { javaAgentV
8384
return null;
8485
}
8586

87+
const EXPAND_TOOLBAR_BUTTON =
88+
"border-border/60 bg-card text-foreground hover:bg-card/80 focus-visible:ring-primary inline-flex cursor-pointer items-center gap-1 rounded-md border px-3 py-1.5 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none";
89+
8690
function ExpandCollapseToolbar() {
8791
const { expandAll, collapseAll } = useSectionExpansion();
8892
return (
8993
<div className="flex items-center gap-2">
90-
<button
91-
type="button"
92-
onClick={expandAll}
93-
className="text-muted-foreground hover:text-foreground text-xs font-medium underline-offset-2 hover:underline"
94-
>
94+
<button type="button" onClick={expandAll} className={EXPAND_TOOLBAR_BUTTON}>
95+
<ChevronsUpDown className="h-3 w-3" aria-hidden="true" />
9596
Expand all
9697
</button>
97-
<span className="text-border" aria-hidden="true">
98-
|
99-
</span>
100-
<button
101-
type="button"
102-
onClick={collapseAll}
103-
className="text-muted-foreground hover:text-foreground text-xs font-medium underline-offset-2 hover:underline"
104-
>
98+
<button type="button" onClick={collapseAll} className={EXPAND_TOOLBAR_BUTTON}>
99+
<ChevronsDownUp className="h-3 w-3" aria-hidden="true" />
105100
Collapse all
106101
</button>
107102
</div>

0 commit comments

Comments
 (0)