From 151abaf400442cc6b1503f72ecbc5a5b60acf084 Mon Sep 17 00:00:00 2001 From: Melove Date: Mon, 18 May 2026 17:16:28 +0530 Subject: [PATCH 01/11] feat(config-builder): add Expand all / Collapse all controls to SDK and Instrumentation tabs --- .../components/general-section-card.tsx | 14 +- .../components/group-renderer.tsx | 10 +- .../components/section-expansion-context.tsx | 59 ++++++ .../configuration-builder-page.tsx | 176 +++++++++++------- 4 files changed, 188 insertions(+), 71 deletions(-) create mode 100644 ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx index 74ac9ff2f..0a5325464 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { JSX } from "react"; +import { useState, useEffect, type JSX } from "react"; import type { ConfigNode } from "@/types/configuration"; import { SchemaRenderer } from "./schema-renderer"; import { SectionCardShell } from "./section-card-shell"; import { FieldSection } from "./field-section"; +import { useSectionExpansion } from "./section-expansion-context"; export const GENERAL_SECTION_KEY = "general"; export const GENERAL_SECTION_LABEL = "General"; @@ -39,6 +40,14 @@ export function GeneralSectionCard({ sectionKey = GENERAL_SECTION_KEY, emptyMessage, }: GeneralSectionCardProps): JSX.Element { + const [expanded, setExpanded] = useState(defaultExpanded); + const { signal } = useSectionExpansion(); + useEffect(() => { + if (!signal) return; + if (signal.action === "expand") setExpanded(true); + if (signal.action === "collapse") setExpanded(false); + }, [signal]); + const headerNode = { controlType: "group" as const, key: "__general__", @@ -51,7 +60,8 @@ export function GeneralSectionCard({ node={headerNode} level="section" asGroup={false} - defaultExpanded={defaultExpanded} + open={expanded} + onOpenChange={setExpanded} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx index 4d9f9964b..ac5a8a93e 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, type JSX } from "react"; +import { useState, useEffect, type JSX } from "react"; import type { GroupNode } from "@/types/configuration"; import { useConfigurationBuilder } from "@/hooks/use-configuration-builder"; import { getByPath, parsePath } from "@/lib/config-path"; @@ -22,6 +22,7 @@ import { SchemaRenderer } from "./schema-renderer"; import { SectionCardShell } from "./section-card-shell"; import { FieldSection } from "./field-section"; import { useStarterPaths } from "./configuration-ui-context"; +import { useSectionExpansion } from "./section-expansion-context"; export interface GroupRendererProps { node: GroupNode; @@ -55,6 +56,13 @@ export function GroupRenderer({ if (enabled) setExpanded(true); } + const { signal } = useSectionExpansion(); + useEffect(() => { + if (!signal || !isTopLevel) return; + if (signal.action === "expand" && enabled) setExpanded(true); + if (signal.action === "collapse") setExpanded(false); + }, [signal, isTopLevel, enabled]); + const value = getByPath(state.values, parsePath(path)); const childNodes = node.children.map((child) => ( diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx new file mode 100644 index 000000000..fa6aa11b8 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createContext, useCallback, useContext, useState, type ReactNode } from "react"; + +interface ExpansionSignal { + action: "expand" | "collapse"; + nonce: number; +} + +interface SectionExpansionContextValue { + expandAll: () => void; + collapseAll: () => void; + signal: ExpansionSignal | null; +} + +const SectionExpansionContext = createContext(null); + +export function SectionExpansionProvider({ children }: { children: ReactNode }) { + const [signal, setSignal] = useState(null); + + const expandAll = useCallback(() => { + setSignal((prev) => ({ action: "expand", nonce: (prev?.nonce ?? 0) + 1 })); + }, []); + + const collapseAll = useCallback(() => { + setSignal((prev) => ({ action: "collapse", nonce: (prev?.nonce ?? 0) + 1 })); + }, []); + + return ( + + {children} + + ); +} + +export function useSectionExpansion(): SectionExpansionContextValue { + const ctx = useContext(SectionExpansionContext); + if (!ctx) { + return { + expandAll: () => {}, + collapseAll: () => {}, + signal: null, + }; + } + return ctx; +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx b/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx index 04b11278e..4572e66a7 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx @@ -46,6 +46,10 @@ import { } from "./components/general-section-card"; import { InstrumentationBrowser } from "./components/instrumentation-browser"; import { useActiveSection } from "./hooks/use-active-section"; +import { + SectionExpansionProvider, + useSectionExpansion, +} from "./components/section-expansion-context"; // Per-tab hidden-keys: SDK hides the entire `instrumentation/development` // subtree (the Instrumentation tab owns it), while the Instrumentation tab @@ -84,6 +88,31 @@ function PruneInstrumentationsForAgentVersion({ javaAgentVersion }: { javaAgentV return null; } +function ExpandCollapseToolbar() { + const { expandAll, collapseAll } = useSectionExpansion(); + return ( +
+ + + +
+ ); +} + interface SdkTabContentProps { schema: GroupNode; starter: ReturnType["data"]; @@ -101,10 +130,6 @@ function SdkTabContent({ }: SdkTabContentProps) { const [activePreviewKey, setActivePreviewKey] = useState(null); - // Leaf keys take precedence over the enclosing section key. The General card uses - // a synthetic section key ("general") that doesn't map to any top-level YAML key, - // so its individual leaf fields (`disabled`, `log_level`, ...) tag themselves with - // `data-yaml-section-key` so their real YAML key can be highlighted instead. const handleInteraction = (e: React.BaseSyntheticEvent) => { const target = e.target as HTMLElement; const leafKey = target @@ -144,32 +169,39 @@ function SdkTabContent({ starter={starter} > -
- -
- {hasGeneralLeaves && ( - {leafChildren} - )} - {groupChildren.map((child) => ( - - ))} + +
+ +
+
+ +
+
+ {hasGeneralLeaves && ( + {leafChildren} + )} + {groupChildren.map((child) => ( + + ))} +
+
+
- -
+ ); } @@ -246,6 +278,7 @@ function InstrumentationTabBody({ setActivePreviewKey(key); } }; + const tocSections: TocSection[] = useMemo( () => [ { key: GENERAL_SECTION_KEY, label: GENERAL_SETTINGS_LABEL }, @@ -294,48 +327,55 @@ function InstrumentationTabBody({ }, [hasDistributionContent, isDistributionEnabled, setEnabled]); return ( -
- -
- - {generalNode?.children ?? []} - - +
+ +
+
+ +
+
+ + {generalNode?.children ?? []} + + +
+
+
- -
+ ); } @@ -478,4 +518,4 @@ export function ConfigurationBuilderPage() {
); -} +} \ No newline at end of file From 6ffb235f6793b9b70e7f1ac0f6a9784136d2f815 Mon Sep 17 00:00:00 2001 From: Melove Date: Mon, 18 May 2026 17:30:56 +0530 Subject: [PATCH 02/11] fix(config-builder): replace setState-in-effect with useMemo for signal-driven expansion --- .../components/general-section-card.tsx | 14 ++++++++------ .../configuration/components/group-renderer.tsx | 14 ++++++++------ .../components/section-expansion-context.tsx | 1 + 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx index 0a5325464..20520bf5c 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useEffect, type JSX } from "react"; +import { useState, useMemo, type JSX } from "react"; import type { ConfigNode } from "@/types/configuration"; import { SchemaRenderer } from "./schema-renderer"; import { SectionCardShell } from "./section-card-shell"; @@ -42,11 +42,13 @@ export function GeneralSectionCard({ }: GeneralSectionCardProps): JSX.Element { const [expanded, setExpanded] = useState(defaultExpanded); const { signal } = useSectionExpansion(); - useEffect(() => { - if (!signal) return; - if (signal.action === "expand") setExpanded(true); - if (signal.action === "collapse") setExpanded(false); + const signalExpanded = useMemo(() => { + if (!signal) return null; + if (signal.action === "expand") return true; + if (signal.action === "collapse") return false; + return null; }, [signal]); + const resolvedExpanded = signalExpanded !== null ? signalExpanded : expanded; const headerNode = { controlType: "group" as const, @@ -60,7 +62,7 @@ export function GeneralSectionCard({ node={headerNode} level="section" asGroup={false} - open={expanded} + open={resolvedExpanded} onOpenChange={setExpanded} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx index ac5a8a93e..01842be45 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useEffect, type JSX } from "react"; +import { useState, useMemo, type JSX } from "react"; import type { GroupNode } from "@/types/configuration"; import { useConfigurationBuilder } from "@/hooks/use-configuration-builder"; import { getByPath, parsePath } from "@/lib/config-path"; @@ -57,11 +57,13 @@ export function GroupRenderer({ } const { signal } = useSectionExpansion(); - useEffect(() => { - if (!signal || !isTopLevel) return; - if (signal.action === "expand" && enabled) setExpanded(true); - if (signal.action === "collapse") setExpanded(false); + const signalExpanded = useMemo(() => { + if (!signal || !isTopLevel) return null; + if (signal.action === "expand") return enabled ? true : null; + if (signal.action === "collapse") return false; + return null; }, [signal, isTopLevel, enabled]); + const resolvedExpanded = signalExpanded !== null ? signalExpanded : expanded; const value = getByPath(state.values, parsePath(path)); @@ -85,7 +87,7 @@ export function GroupRenderer({ level="section" value={value} asGroup={false} - open={expanded} + open={resolvedExpanded} onOpenChange={setExpanded} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx index fa6aa11b8..85ed8dba4 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx @@ -46,6 +46,7 @@ export function SectionExpansionProvider({ children }: { children: ReactNode }) ); } +// eslint-disable-next-line react-refresh/only-export-components export function useSectionExpansion(): SectionExpansionContextValue { const ctx = useContext(SectionExpansionContext); if (!ctx) { From c09ec905079c3d1af306635aad2cc0ae98cdfd80 Mon Sep 17 00:00:00 2001 From: Melove Date: Tue, 19 May 2026 17:02:49 +0530 Subject: [PATCH 03/11] fix: use nonce state instead of ref to fix individual toggle after bulk expand/collapse --- .../components/general-section-card.tsx | 17 ++++++++--------- .../configuration/components/group-renderer.tsx | 17 ++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx index 20520bf5c..48348a41b 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useMemo, type JSX } from "react"; +import { useState, type JSX } from "react"; import type { ConfigNode } from "@/types/configuration"; import { SchemaRenderer } from "./schema-renderer"; import { SectionCardShell } from "./section-card-shell"; @@ -42,13 +42,12 @@ export function GeneralSectionCard({ }: GeneralSectionCardProps): JSX.Element { const [expanded, setExpanded] = useState(defaultExpanded); const { signal } = useSectionExpansion(); - const signalExpanded = useMemo(() => { - if (!signal) return null; - if (signal.action === "expand") return true; - if (signal.action === "collapse") return false; - return null; - }, [signal]); - const resolvedExpanded = signalExpanded !== null ? signalExpanded : expanded; + const [lastNonce, setLastNonce] = useState(null); + if (signal && signal.nonce !== lastNonce) { + setLastNonce(signal.nonce); + if (signal.action === "expand") setExpanded(true); + if (signal.action === "collapse") setExpanded(false); + } const headerNode = { controlType: "group" as const, @@ -62,7 +61,7 @@ export function GeneralSectionCard({ node={headerNode} level="section" asGroup={false} - open={resolvedExpanded} + open={expanded} onOpenChange={setExpanded} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx index 01842be45..54aa7ec2b 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useMemo, type JSX } from "react"; +import { useState, type JSX } from "react"; import type { GroupNode } from "@/types/configuration"; import { useConfigurationBuilder } from "@/hooks/use-configuration-builder"; import { getByPath, parsePath } from "@/lib/config-path"; @@ -57,13 +57,12 @@ export function GroupRenderer({ } const { signal } = useSectionExpansion(); - const signalExpanded = useMemo(() => { - if (!signal || !isTopLevel) return null; - if (signal.action === "expand") return enabled ? true : null; - if (signal.action === "collapse") return false; - return null; - }, [signal, isTopLevel, enabled]); - const resolvedExpanded = signalExpanded !== null ? signalExpanded : expanded; + const [lastNonce, setLastNonce] = useState(null); + if (signal && isTopLevel && signal.nonce !== lastNonce) { + setLastNonce(signal.nonce); + if (signal.action === "expand" && enabled) setExpanded(true); + if (signal.action === "collapse") setExpanded(false); + } const value = getByPath(state.values, parsePath(path)); @@ -87,7 +86,7 @@ export function GroupRenderer({ level="section" value={value} asGroup={false} - open={resolvedExpanded} + open={expanded} onOpenChange={setExpanded} > From 69dec9c0090e235fc28de917a481e4ed2c2b8c01 Mon Sep 17 00:00:00 2001 From: Melove Date: Wed, 20 May 2026 15:35:29 +0530 Subject: [PATCH 04/11] fix: use override map for per-section toggle after bulk action, fix test provider wrapping --- .../components/general-section-card.tsx | 24 ++++++---- .../components/group-renderer.test.tsx | 48 +++++++++++++------ .../components/group-renderer.tsx | 25 ++++++---- .../components/section-expansion-context.tsx | 35 +++++++------- 4 files changed, 83 insertions(+), 49 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx index 48348a41b..26a6ac157 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx @@ -41,13 +41,16 @@ export function GeneralSectionCard({ emptyMessage, }: GeneralSectionCardProps): JSX.Element { const [expanded, setExpanded] = useState(defaultExpanded); - const { signal } = useSectionExpansion(); - const [lastNonce, setLastNonce] = useState(null); - if (signal && signal.nonce !== lastNonce) { - setLastNonce(signal.nonce); - if (signal.action === "expand") setExpanded(true); - if (signal.action === "collapse") setExpanded(false); - } + const { bulkAction, overrides, setOverride } = useSectionExpansion(); + const bulkOpen = + sectionKey in overrides + ? overrides[sectionKey] + : bulkAction === "expand" + ? true + : bulkAction === "collapse" + ? false + : null; + const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded; const headerNode = { controlType: "group" as const, @@ -61,8 +64,11 @@ export function GeneralSectionCard({ node={headerNode} level="section" asGroup={false} - open={expanded} - onOpenChange={setExpanded} + open={resolvedExpanded} + onOpenChange={(next) => { + setOverride(sectionKey, next); + setExpanded(next); + }} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.test.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.test.tsx index 7298bc173..99f4717d6 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.test.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.test.tsx @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import React from "react"; import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { GroupRenderer } from "./group-renderer"; +import { SectionExpansionProvider } from "./section-expansion-context"; import { StarterPathsContext } from "./configuration-ui-context"; import type { ConfigNode, GroupNode } from "@/types/configuration"; @@ -44,6 +46,10 @@ vi.mock("@/hooks/use-configuration-builder", () => ({ }), })); +function renderWithProvider(ui: React.ReactElement) { + return render({ui}); +} + const groupNode: GroupNode = { controlType: "group", key: "resource", @@ -55,20 +61,20 @@ const groupNode: GroupNode = { describe("GroupRenderer", () => { it("at depth 0 renders a card with an enable switch", () => { - render(); + renderWithProvider(); expect(screen.getByText("Resource")).toBeInTheDocument(); const sw = screen.getByRole("switch", { name: /Enable Resource/i }); expect(sw).toHaveAttribute("aria-checked", "false"); }); it("at depth 0, flipping the switch dispatches setEnabled", () => { - render(); + renderWithProvider(); fireEvent.click(screen.getByRole("switch", { name: /Enable Resource/i })); expect(setEnabled).toHaveBeenCalledWith("resource", true); }); it("at depth >= 3 still renders a chevron button so the user can collapse the group", () => { - render(); + renderWithProvider(); expect(screen.getByText("Resource")).toBeInTheDocument(); // Collapsed by default at depth >= 1, so the chevron is in the "Expand" state. expect(screen.getByRole("button", { name: /Expand Resource/ })).toBeInTheDocument(); @@ -76,14 +82,14 @@ describe("GroupRenderer", () => { it("hides the chevron button at depth 0 when the section is disabled", () => { mockState.enabledSections.resource = false; - render(); + renderWithProvider(); expect(screen.queryByRole("button", { name: /Expand Resource/ })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Collapse Resource/ })).not.toBeInTheDocument(); }); it("shows a labeled chevron button at depth 0 when the section is enabled", () => { mockState.enabledSections.resource = true; - render(); + renderWithProvider(); expect(screen.getByRole("button", { name: /Collapse Resource/ })).toBeInTheDocument(); mockState.enabledSections.resource = false; // restore for other tests }); @@ -101,13 +107,17 @@ describe("GroupRenderer", () => { }, ], }; - const { rerender } = render(); + const { rerender } = renderWithProvider( + + ); + const rerenderWithProvider = (ui: React.ReactElement) => + rerender({ui}); // Initially disabled and collapsed — child label should not appear. expect(screen.queryByText("Schema URL")).not.toBeInTheDocument(); // Flip enabled on and re-render with the same node reference. mockState.enabledSections.resource = true; - rerender(); + rerenderWithProvider(); // Child label should now appear (auto-expanded). expect(screen.getByText("Schema URL")).toBeInTheDocument(); @@ -137,7 +147,9 @@ describe("GroupRenderer", () => { processors: [{ batch: { exporter: { otlp_http: {} }, schedule_delay: 1000 } }], }, }; - render(); + renderWithProvider( + + ); expect(screen.queryByText("Schedule Delay")).toBeNull(); // The chevron is there to expand on demand. expect(screen.getByRole("button", { name: /Expand Batch/ })).toBeInTheDocument(); @@ -161,7 +173,9 @@ describe("GroupRenderer", () => { mockState.values = { tracer_provider: { processors: [{ batch: { exporter: { otlp_http: {} } } }] }, }; - render(); + renderWithProvider( + + ); expect(screen.queryByText("Ratio")).toBeNull(); fireEvent.click(screen.getByRole("button", { name: /Expand Sampler/ })); expect(screen.getByText("Ratio")).toBeInTheDocument(); @@ -176,7 +190,9 @@ describe("GroupRenderer", () => { path: "resource", children: [], }; - const { container } = render(); + const { container } = renderWithProvider( + + ); const section = container.querySelector('[data-section-key="resource"]'); expect(section).not.toBeNull(); expect(section?.getAttribute("tabindex")).toBe("-1"); @@ -192,7 +208,7 @@ describe("GroupRenderer", () => { description: "Configure spans, samplers, and processors. Multiple processors are supported.", children: [], }; - render(); + renderWithProvider(); expect(screen.getByText("Configure spans, samplers, and processors.")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Show more" })).toBeInTheDocument(); }); @@ -206,7 +222,7 @@ describe("GroupRenderer", () => { description: "Batch span processor.", children: [], }; - render(); + renderWithProvider(); const allMatches = screen.getAllByText("Batch span processor."); expect(allMatches).toHaveLength(1); }); @@ -223,7 +239,9 @@ describe("GroupRenderer", () => { } as unknown as ConfigNode, ], }; - const { container } = render(); + const { container } = renderWithProvider( + + ); fireEvent.click(screen.getByRole("button", { name: /Expand Resource/ })); const indented = container.querySelectorAll(".pl-3"); expect(indented.length).toBeGreaterThan(0); @@ -241,14 +259,14 @@ describe("GroupRenderer", () => { } as unknown as ConfigNode, ], }; - const { container } = render( + const { container } = renderWithProvider( ); expect(container.querySelectorAll(".pl-3")).toHaveLength(0); }); it("at depth >= 1 starts expanded when its path is in StarterPathsContext", () => { - render( + renderWithProvider( diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx index 54aa7ec2b..7da9a31ea 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx @@ -56,13 +56,17 @@ export function GroupRenderer({ if (enabled) setExpanded(true); } - const { signal } = useSectionExpansion(); - const [lastNonce, setLastNonce] = useState(null); - if (signal && isTopLevel && signal.nonce !== lastNonce) { - setLastNonce(signal.nonce); - if (signal.action === "expand" && enabled) setExpanded(true); - if (signal.action === "collapse") setExpanded(false); - } + const { bulkAction, overrides, setOverride } = useSectionExpansion(); + const sectionKey = node.key; + const bulkOpen = + isTopLevel && sectionKey in overrides + ? overrides[sectionKey] + : isTopLevel && bulkAction === "expand" && enabled + ? true + : isTopLevel && bulkAction === "collapse" + ? false + : null; + const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded; const value = getByPath(state.values, parsePath(path)); @@ -86,8 +90,11 @@ export function GroupRenderer({ level="section" value={value} asGroup={false} - open={expanded} - onOpenChange={setExpanded} + open={resolvedExpanded} + onOpenChange={(next) => { + if (isTopLevel) setOverride(sectionKey, next); + setExpanded(next); + }} > {enabled && } diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx index 85ed8dba4..916e5b31f 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx @@ -15,32 +15,41 @@ */ import { createContext, useCallback, useContext, useState, type ReactNode } from "react"; -interface ExpansionSignal { - action: "expand" | "collapse"; - nonce: number; -} +type BulkAction = "expand" | "collapse" | null; interface SectionExpansionContextValue { expandAll: () => void; collapseAll: () => void; - signal: ExpansionSignal | null; + bulkAction: BulkAction; + /** Overrides per section key set after a bulk action. */ + overrides: Record; + setOverride: (key: string, value: boolean) => void; } const SectionExpansionContext = createContext(null); export function SectionExpansionProvider({ children }: { children: ReactNode }) { - const [signal, setSignal] = useState(null); + const [bulkAction, setBulkAction] = useState(null); + const [overrides, setOverrides] = useState>({}); const expandAll = useCallback(() => { - setSignal((prev) => ({ action: "expand", nonce: (prev?.nonce ?? 0) + 1 })); + setBulkAction("expand"); + setOverrides({}); }, []); const collapseAll = useCallback(() => { - setSignal((prev) => ({ action: "collapse", nonce: (prev?.nonce ?? 0) + 1 })); + setBulkAction("collapse"); + setOverrides({}); + }, []); + + const setOverride = useCallback((key: string, value: boolean) => { + setOverrides((prev) => ({ ...prev, [key]: value })); }, []); return ( - + {children} ); @@ -49,12 +58,6 @@ export function SectionExpansionProvider({ children }: { children: ReactNode }) // eslint-disable-next-line react-refresh/only-export-components export function useSectionExpansion(): SectionExpansionContextValue { const ctx = useContext(SectionExpansionContext); - if (!ctx) { - return { - expandAll: () => {}, - collapseAll: () => {}, - signal: null, - }; - } + if (!ctx) throw new Error("useSectionExpansion must be used within a SectionExpansionProvider"); return ctx; } From 24b8e364405611c7b48430602bf7e4997fc5b298 Mon Sep 17 00:00:00 2001 From: Melove Date: Sun, 24 May 2026 04:45:21 +0530 Subject: [PATCH 05/11] feat: expand/collapse all recursively in SDK tab and all module rows in Instrumentation tab --- .../components/group-renderer.tsx | 6 +-- .../instrumentation-browser.test.tsx | 40 +++++++++------- .../components/instrumentation-browser.tsx | 47 +++++++++++++++---- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx index 7da9a31ea..12f596561 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx @@ -59,11 +59,11 @@ export function GroupRenderer({ const { bulkAction, overrides, setOverride } = useSectionExpansion(); const sectionKey = node.key; const bulkOpen = - isTopLevel && sectionKey in overrides + sectionKey in overrides ? overrides[sectionKey] - : isTopLevel && bulkAction === "expand" && enabled + : bulkAction === "expand" && (isTopLevel ? enabled : true) ? true - : isTopLevel && bulkAction === "collapse" + : bulkAction === "collapse" ? false : null; const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded; diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.test.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.test.tsx index bf692c9a7..ff7e5c724 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.test.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -21,6 +22,7 @@ import { useConfigurationBuilder } from "@/hooks/use-configuration-builder"; import { useCustomizationStatusMap } from "@/hooks/use-customization-status"; import { useCustomizedModules } from "@/hooks/use-customized-modules"; import { InstrumentationBrowser } from "./instrumentation-browser"; +import { SectionExpansionProvider } from "./section-expansion-context"; vi.mock("@/hooks/use-configuration-builder"); vi.mock("@/hooks/use-customization-status"); @@ -76,9 +78,13 @@ beforeEach(() => { vi.mocked(useCustomizedModules).mockReturnValue(new Set()); }); +function renderWithProvider(ui: React.ReactElement) { + return render({ui}); +} + describe("InstrumentationBrowser", () => { it("groups entries into module rows with version count", () => { - render( + renderWithProvider( { }); it("matches search against the registry name of any covered entry", () => { - render( + renderWithProvider( { }); it("matches search against display_name on any covered entry", () => { - render( + renderWithProvider( { }); it("matches search against description on any covered entry", () => { - render( + renderWithProvider( { }); it("search is case-insensitive", () => { - render( + renderWithProvider( { it("filters to customized when statusFilter='customized'", () => { vi.mocked(useCustomizedModules).mockReturnValue(new Set(["cassandra"])); - render( + renderWithProvider( { }); it("calls setCustomization('cassandra', 'disabled') when + Customize is clicked on a default-enabled module", () => { - render( + renderWithProvider( { }); it("calls setCustomization('jmx_metrics', 'enabled') when + Customize is clicked on a default-disabled module", () => { - render( + renderWithProvider( { it("calls setCustomization(name, 'none') when ✕ is clicked on an customized row", () => { mockedCustomization.mockReturnValue(new Map([["cassandra", "disabled"]])); - render( + renderWithProvider( { it("calls setCustomization('cassandra', 'enabled') when toggling customized Disabled→Enabled", () => { mockedCustomization.mockReturnValue(new Map([["cassandra", "disabled"]])); - render( + renderWithProvider( { }); it("renders empty state for unmatched search", () => { - render( + renderWithProvider( { }); it("shows loading state", () => { - render( + renderWithProvider( { }); it("shows error state", () => { - render( + renderWithProvider( it("expands a row when its toggle button is clicked", async () => { const user = userEvent.setup(); - render( + renderWithProvider( it("keeps multiple rows expanded simultaneously", async () => { const user = userEvent.setup(); - render( + renderWithProvider( it("uses useCustomizedModules to filter when statusFilter is 'customized'", () => { vi.mocked(useCustomizedModules).mockReturnValue(new Set(["cassandra"])); - render( + renderWithProvider( it("renders the customization count in the header from useCustomizedModules", () => { vi.mocked(useCustomizedModules).mockReturnValue(new Set(["cassandra"])); - render( + renderWithProvider( >(() => new Set()); - const toggleExpand = useCallback((name: string) => { - setExpandedSet((prev) => { - const next = new Set(prev); - if (next.has(name)) next.delete(name); - else next.add(name); - return next; - }); - }, []); + const resolvedExpandedSet = useMemo(() => { + if (bulkAction === "expand") { + const all = new Set(modules.map((m) => m.name)); + // apply individual overrides on top + for (const [key, val] of Object.entries(overrides)) { + if (!val) all.delete(key); + } + return all; + } + if (bulkAction === "collapse") { + const overrideExpanded = new Set(); + for (const [key, val] of Object.entries(overrides)) { + if (val) overrideExpanded.add(key); + } + return overrideExpanded; + } + return expandedSet; + }, [bulkAction, overrides, modules, expandedSet]); + + const toggleExpand = useCallback( + (name: string) => { + const currentlyExpanded = resolvedExpandedSet.has(name); + setOverride(name, !currentlyExpanded); + setExpandedSet((prev) => { + const next = new Set(prev); + if (next.has(name)) next.delete(name); + else next.add(name); + return next; + }); + }, + [resolvedExpandedSet, setOverride] + ); const trimmedSearch = search.trim(); const filtered = useMemo(() => { @@ -119,7 +146,7 @@ export function InstrumentationBrowser({ total={modules.length} filtered={filtered} customizationMap={customizationMap} - expandedSet={expandedSet} + expandedSet={resolvedExpandedSet} search={trimmedSearch} statusFilter={statusFilter} customizationCount={customizationCount} From a2dd654abe2368d77c4df90cc4da69097535056b Mon Sep 17 00:00:00 2001 From: Melove Date: Fri, 12 Jun 2026 13:01:44 +0530 Subject: [PATCH 06/11] fix: resolved all the merge conflicts and formatted the changes --- .../java-agent/configuration/configuration-builder-page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx b/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx index 4572e66a7..02a4dd9f5 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx @@ -188,7 +188,9 @@ function SdkTabContent({ onPointerDown={handleInteraction} > {hasGeneralLeaves && ( - {leafChildren} + + {leafChildren} + )} {groupChildren.map((child) => ( @@ -518,4 +520,4 @@ export function ConfigurationBuilderPage() {
); -} \ No newline at end of file +} From 380e45bcf38f394962e801fcc56f97ea3f6b4a48 Mon Sep 17 00:00:00 2001 From: Melove Date: Sun, 14 Jun 2026 23:32:49 +0530 Subject: [PATCH 07/11] fixed formatting changes --- .../configuration/components/instrumentation-browser.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.tsx index 13575232e..d409d31a2 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.tsx @@ -15,7 +15,7 @@ */ import { useState, useCallback, useMemo, type JSX } from "react"; import { useSectionExpansion } from "./section-expansion-context"; -import type { InstrumentationData, InstrumentationModule } from "@/types/javaagent"; +import type { InstrumentationListEntry, InstrumentationModule } from "@/types/javaagent"; import { Loader } from "@/components/ui/loader"; import { useConfigurationBuilder } from "@/hooks/use-configuration-builder"; import { From d06170cddc00677e1386be3c8fec69d228367c5a Mon Sep 17 00:00:00 2001 From: Melove Date: Sun, 14 Jun 2026 23:59:01 +0530 Subject: [PATCH 08/11] fix(config-builder): expand all now recursively expands leaf fields and nested groups --- .../components/group-renderer.tsx | 7 ++- .../components/schema-renderer.tsx | 60 +++++++++++++++---- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx index 12f596561..29bac72a8 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx @@ -122,8 +122,11 @@ export function GroupRenderer({ value={value} headless={headless} asGroup={!headless} - open={expanded} - onOpenChange={setExpanded} + open={resolvedExpanded} + onOpenChange={(next) => { + setExpanded(next); + setOverride(sectionKey, next); + }} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx index 8f9b2357f..88c0aaa4e 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { JSX } from "react"; +import { useState, type JSX } from "react"; import type { ConfigNode, ConfigNodeBase, @@ -38,6 +38,7 @@ import { UnionRenderer } from "./union-renderer"; import { CircularRefPlaceholder } from "./circular-ref-placeholder"; import { FieldSection } from "./field-section"; import { useStarterPaths } from "./configuration-ui-context"; +import { useSectionExpansion } from "./section-expansion-context"; export interface SchemaRendererProps { node: ConfigNode; @@ -57,6 +58,51 @@ function withHiddenLabel(node: T): T { return { ...node, hideLabel: true }; } +function WrappedLeaf({ + node, + path, + defaultExp, + children, +}: { + node: ConfigNodeBase; + path: string; + defaultExp: boolean; + children: JSX.Element; +}) { + const { bulkAction, overrides, setOverride } = useSectionExpansion(); + const [expanded, setExpanded] = useState(defaultExp); + + const bulkOpen = + path in overrides + ? overrides[path] + : bulkAction === "expand" + ? true + : bulkAction === "collapse" + ? false + : null; + const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded; + + return ( + { + setExpanded(next); + setOverride(path, next); + }} + > + + + + + + + {children} + + ); +} + export function SchemaRenderer({ node, depth, @@ -79,15 +125,9 @@ export function SchemaRenderer({ function wrap(control: JSX.Element): JSX.Element { if (!wrappable) return control; return ( - - - - - - - - {control} - + + {control} + ); } From 595f50d46b1e58bb5aa62ced2c37015a5e3e55e0 Mon Sep 17 00:00:00 2001 From: Melove Date: Mon, 15 Jun 2026 11:21:09 +0530 Subject: [PATCH 09/11] fix(config-builder): make useSectionExpansion safe outside provider context --- .../components/section-expansion-context.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx index 916e5b31f..8f70a497b 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx @@ -56,8 +56,15 @@ export function SectionExpansionProvider({ children }: { children: ReactNode }) } // eslint-disable-next-line react-refresh/only-export-components +const NO_OP_CONTEXT: SectionExpansionContextValue = { + expandAll: () => {}, + collapseAll: () => {}, + bulkAction: null, + overrides: {}, + setOverride: () => {}, +}; + export function useSectionExpansion(): SectionExpansionContextValue { const ctx = useContext(SectionExpansionContext); - if (!ctx) throw new Error("useSectionExpansion must be used within a SectionExpansionProvider"); - return ctx; + return ctx ?? NO_OP_CONTEXT; } From cef551ffd8c75feeb3db63b040ab7d7832e36162 Mon Sep 17 00:00:00 2001 From: Melove Date: Mon, 15 Jun 2026 11:26:17 +0530 Subject: [PATCH 10/11] fix(config-builder): make useSectionExpansion safe outside provider context --- .../configuration/components/section-expansion-context.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx index 8f70a497b..4151147e9 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx @@ -55,7 +55,6 @@ export function SectionExpansionProvider({ children }: { children: ReactNode }) ); } -// eslint-disable-next-line react-refresh/only-export-components const NO_OP_CONTEXT: SectionExpansionContextValue = { expandAll: () => {}, collapseAll: () => {}, @@ -63,7 +62,7 @@ const NO_OP_CONTEXT: SectionExpansionContextValue = { overrides: {}, setOverride: () => {}, }; - +// eslint-disable-next-line react-refresh/only-export-components export function useSectionExpansion(): SectionExpansionContextValue { const ctx = useContext(SectionExpansionContext); return ctx ?? NO_OP_CONTEXT; From a8189706b175581c14d9ac411f9e5e7e34c06b3d Mon Sep 17 00:00:00 2001 From: Luca Cavenaghi Date: Tue, 16 Jun 2026 23:17:16 +0200 Subject: [PATCH 11/11] 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. --- .../components/general-section-card.tsx | 23 ++--- .../components/group-renderer.tsx | 18 ++-- .../components/list-renderer.tsx | 4 +- .../components/plugin-select-renderer.tsx | 4 +- .../components/schema-renderer.tsx | 27 +----- .../section-expansion-context.test.tsx | 90 +++++++++++++++++++ .../components/section-expansion-context.tsx | 34 +++++++ .../components/union-renderer.tsx | 5 +- .../configuration-builder-page.tsx | 21 ++--- ...ilder-expand-collapse.integration.test.tsx | 75 ++++++++++++++++ 10 files changed, 231 insertions(+), 70 deletions(-) create mode 100644 ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.test.tsx create mode 100644 ecosystem-explorer/src/test/integration/configuration-builder-expand-collapse.integration.test.tsx diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx index 26a6ac157..80852a09c 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/general-section-card.tsx @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, type JSX } from "react"; +import type { JSX } from "react"; import type { ConfigNode } from "@/types/configuration"; import { SchemaRenderer } from "./schema-renderer"; import { SectionCardShell } from "./section-card-shell"; import { FieldSection } from "./field-section"; -import { useSectionExpansion } from "./section-expansion-context"; +import { useCollapsibleExpansion } from "./section-expansion-context"; export const GENERAL_SECTION_KEY = "general"; export const GENERAL_SECTION_LABEL = "General"; @@ -40,17 +40,7 @@ export function GeneralSectionCard({ sectionKey = GENERAL_SECTION_KEY, emptyMessage, }: GeneralSectionCardProps): JSX.Element { - const [expanded, setExpanded] = useState(defaultExpanded); - const { bulkAction, overrides, setOverride } = useSectionExpansion(); - const bulkOpen = - sectionKey in overrides - ? overrides[sectionKey] - : bulkAction === "expand" - ? true - : bulkAction === "collapse" - ? false - : null; - const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded; + const { open, onOpenChange } = useCollapsibleExpansion(sectionKey, defaultExpanded); const headerNode = { controlType: "group" as const, @@ -64,11 +54,8 @@ export function GeneralSectionCard({ node={headerNode} level="section" asGroup={false} - open={resolvedExpanded} - onOpenChange={(next) => { - setOverride(sectionKey, next); - setExpanded(next); - }} + open={open} + onOpenChange={onOpenChange} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx index d539faed0..ec753987f 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/group-renderer.tsx @@ -23,7 +23,7 @@ import { SchemaRenderer } from "./schema-renderer"; import { SectionCardShell } from "./section-card-shell"; import { FieldSection } from "./field-section"; import { useStarterPaths } from "./configuration-ui-context"; -import { useSectionExpansion } from "./section-expansion-context"; +import { resolveBulkOpen, useSectionExpansion } from "./section-expansion-context"; export interface GroupRendererProps { node: GroupNode; @@ -58,16 +58,8 @@ export function GroupRenderer({ if (enabled) setExpanded(true); } - const { bulkAction, overrides, setOverride } = useSectionExpansion(); - const sectionKey = node.key; - const bulkOpen = - sectionKey in overrides - ? overrides[sectionKey] - : bulkAction === "expand" && (isTopLevel ? enabled : true) - ? true - : bulkAction === "collapse" - ? false - : null; + const ctx = useSectionExpansion(); + const bulkOpen = resolveBulkOpen(ctx, path, isTopLevel ? enabled : true); const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded; const value = getByPath(state.values, parsePath(path)); @@ -94,7 +86,7 @@ export function GroupRenderer({ asGroup={false} open={resolvedExpanded} onOpenChange={(next) => { - if (isTopLevel) setOverride(sectionKey, next); + ctx.setOverride(path, next); setExpanded(next); }} > @@ -127,7 +119,7 @@ export function GroupRenderer({ open={resolvedExpanded} onOpenChange={(next) => { setExpanded(next); - setOverride(sectionKey, next); + ctx.setOverride(path, next); }} > diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/list-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/list-renderer.tsx index 2ab6ea753..7a1bc0b4e 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/list-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/list-renderer.tsx @@ -23,6 +23,7 @@ import { parsePath, getByPath } from "@/lib/config-path"; import { deriveListItemLabel } from "@/lib/derive-list-item-label"; import { FieldSection } from "./field-section"; import { ListItemContext, useStarterPaths } from "./configuration-ui-context"; +import { useCollapsibleExpansion } from "./section-expansion-context"; export interface ListRendererProps { node: ListNode; @@ -44,9 +45,10 @@ export function ListRenderer({ node, depth, path }: ListRendererProps): JSX.Elem ? storedIds : items.map((_, i) => `${path}#${i}`); const itemHasTablist = node.itemSchema.controlType === "plugin_select"; + const { open, onOpenChange } = useCollapsibleExpansion(path, starterPaths.has(path)); return ( - + diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/plugin-select-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/plugin-select-renderer.tsx index 6ea4a0632..1e5bfc499 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/plugin-select-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/plugin-select-renderer.tsx @@ -23,6 +23,7 @@ import { SchemaRenderer } from "./schema-renderer"; import { parsePath, getByPath } from "@/lib/config-path"; import { FieldSection } from "./field-section"; import { ListItemContext, useListItemContext, useStarterPaths } from "./configuration-ui-context"; +import { useCollapsibleExpansion } from "./section-expansion-context"; export interface PluginSelectRendererProps { node: PluginSelectNode; @@ -66,6 +67,7 @@ export function PluginSelectRenderer({ const customTabId = useId(); const customPanelId = useId(); const customActive = isCustom || customPickerOpen; + const { open, onOpenChange } = useCollapsibleExpansion(path, starterPaths.has(path)); function closeCustomPicker() { setCustomPickerOpen(false); @@ -216,7 +218,7 @@ export function PluginSelectRenderer({ } return ( - + diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx index 75cd6dff6..1e62c3658 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/schema-renderer.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, type JSX } from "react"; +import type { JSX } from "react"; import { useTranslation } from "react-i18next"; import type { ConfigNode, @@ -39,7 +39,7 @@ import { UnionRenderer } from "./union-renderer"; import { CircularRefPlaceholder } from "./circular-ref-placeholder"; import { FieldSection } from "./field-section"; import { useStarterPaths } from "./configuration-ui-context"; -import { useSectionExpansion } from "./section-expansion-context"; +import { useCollapsibleExpansion } from "./section-expansion-context"; export interface SchemaRendererProps { node: ConfigNode; @@ -70,29 +70,10 @@ function WrappedLeaf({ defaultExp: boolean; children: JSX.Element; }) { - const { bulkAction, overrides, setOverride } = useSectionExpansion(); - const [expanded, setExpanded] = useState(defaultExp); - - const bulkOpen = - path in overrides - ? overrides[path] - : bulkAction === "expand" - ? true - : bulkAction === "collapse" - ? false - : null; - const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded; + const { open, onOpenChange } = useCollapsibleExpansion(path, defaultExp); return ( - { - setExpanded(next); - setOverride(path, next); - }} - > + diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.test.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.test.tsx new file mode 100644 index 000000000..d5217ec8e --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { + SectionExpansionProvider, + useCollapsibleExpansion, + useSectionExpansion, +} from "./section-expansion-context"; + +function Probe({ k, def }: { k: string; def: boolean }) { + const { open, onOpenChange } = useCollapsibleExpansion(k, def); + return ( + + ); +} + +function Bulk() { + const { expandAll, collapseAll } = useSectionExpansion(); + return ( + <> + + + + ); +} + +describe("useCollapsibleExpansion", () => { + it("starts at defaultExpanded and toggles locally", () => { + render( + + + + ); + const b = screen.getByRole("button", { name: "a" }); + expect(b).toHaveAttribute("aria-expanded", "false"); + fireEvent.click(b); + expect(b).toHaveAttribute("aria-expanded", "true"); + }); + + it("collapseAll then individual expand overrides only that key", () => { + render( + + + + + + ); + fireEvent.click(screen.getByRole("button", { name: "collapse" })); + expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "false"); + fireEvent.click(screen.getByRole("button", { name: "a" })); + expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByRole("button", { name: "b" })).toHaveAttribute("aria-expanded", "false"); + }); + + it("expandAll then individual collapse overrides only that key", () => { + render( + + + + + + ); + fireEvent.click(screen.getByRole("button", { name: "expand" })); + expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "true"); + fireEvent.click(screen.getByRole("button", { name: "a" })); + expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "false"); + expect(screen.getByRole("button", { name: "b" })).toHaveAttribute("aria-expanded", "true"); + }); + + it("falls back to defaultExpanded with no provider", () => { + render(); + expect(screen.getByRole("button", { name: "a" })).toHaveAttribute("aria-expanded", "true"); + }); +}); diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx index 4151147e9..eea210576 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/section-expansion-context.tsx @@ -67,3 +67,37 @@ export function useSectionExpansion(): SectionExpansionContextValue { const ctx = useContext(SectionExpansionContext); return ctx ?? NO_OP_CONTEXT; } + +// eslint-disable-next-line react-refresh/only-export-components +export function resolveBulkOpen( + ctx: Pick, + key: string, + eligibleForBulkExpand = true +): boolean | null { + if (key in ctx.overrides) return ctx.overrides[key]; + if (ctx.bulkAction === "expand" && eligibleForBulkExpand) return true; + if (ctx.bulkAction === "collapse") return false; + return null; +} + +export interface CollapsibleExpansion { + open: boolean; + onOpenChange: (next: boolean) => void; +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useCollapsibleExpansion( + key: string, + defaultExpanded: boolean, + options?: { eligibleForBulkExpand?: boolean } +): CollapsibleExpansion { + const ctx = useSectionExpansion(); + const [expanded, setExpanded] = useState(defaultExpanded); + const bulkOpen = resolveBulkOpen(ctx, key, options?.eligibleForBulkExpand ?? true); + const open = bulkOpen !== null ? bulkOpen : expanded; + const onOpenChange = (next: boolean) => { + setExpanded(next); + ctx.setOverride(key, next); + }; + return { open, onOpenChange }; +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/union-renderer.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/union-renderer.tsx index b0102687f..f5b0da385 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/components/union-renderer.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/union-renderer.tsx @@ -22,6 +22,7 @@ import { useConfigurationBuilder } from "@/hooks/use-configuration-builder"; import { SchemaRenderer } from "./schema-renderer"; import { parsePath, getByPath } from "@/lib/config-path"; import { FieldSection } from "./field-section"; +import { useCollapsibleExpansion } from "./section-expansion-context"; export interface UnionRendererProps { node: UnionNode; @@ -138,6 +139,8 @@ export function UnionRenderer({ node, depth, path }: UnionRendererProps): JSX.El selectedKey === null ? undefined : effectiveVariants.find((v) => v.key === selectedKey); const showChooser = renderable.length > 0; + const { open, onOpenChange } = useCollapsibleExpansion(`${path}#union`, true); + const handleChange = (nextKey: string) => { if (nextKey === selectedKey) return; const nextVariant = effectiveVariants.find((v) => v.key === nextKey); @@ -187,7 +190,7 @@ export function UnionRenderer({ node, depth, path }: UnionRendererProps): JSX.El ) : null; return ( - + diff --git a/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx b/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx index 03906c4bb..d92024c36 100644 --- a/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx +++ b/ecosystem-explorer/src/features/java-agent/configuration/configuration-builder-page.tsx @@ -15,6 +15,7 @@ */ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { ChevronsDownUp, ChevronsUpDown } from "lucide-react"; import { Loader } from "@/components/ui/loader"; import { BackButton } from "@/components/ui/back-button"; import { BetaBadge } from "@/components/ui/beta-badge"; @@ -83,25 +84,19 @@ function PruneInstrumentationsForAgentVersion({ javaAgentVersion }: { javaAgentV return null; } +const EXPAND_TOOLBAR_BUTTON = + "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"; + function ExpandCollapseToolbar() { const { expandAll, collapseAll } = useSectionExpansion(); return (
- - -
diff --git a/ecosystem-explorer/src/test/integration/configuration-builder-expand-collapse.integration.test.tsx b/ecosystem-explorer/src/test/integration/configuration-builder-expand-collapse.integration.test.tsx new file mode 100644 index 000000000..6f807ac41 --- /dev/null +++ b/ecosystem-explorer/src/test/integration/configuration-builder-expand-collapse.integration.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeAll, beforeEach, afterAll } from "vitest"; +import { screen, cleanup } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { installFetchInterceptor, uninstallFetchInterceptor } from "./helpers/fetch-interceptor"; +import { renderBuilderPage as renderPage } from "./helpers/render-builder-page"; +import { openInstrumentationTab } from "./helpers/open-instrumentation-tab"; + +beforeAll(() => { + installFetchInterceptor(); +}); +afterAll(() => { + uninstallFetchInterceptor(); +}); +beforeEach(() => { + localStorage.clear(); + cleanup(); +}); + +describe("ConfigurationBuilderPage Expand all / Collapse all", () => { + it("drives every collapsible section chevron on the SDK tab", async () => { + renderPage(); + const user = userEvent.setup(); + await screen.findByRole("switch", { name: /Enable Resource/i }, { timeout: 10_000 }); + + // Scope to the section chevrons only. The toolbar "Expand all"/"Collapse all" + // buttons match the name regex but carry no aria-expanded, and the + // "Expand YAML preview" dialog trigger lives in the Output Preview region — + // both must be excluded so the assertion targets disclosure chevrons. + const previewRegion = screen.getByRole("region", { name: /Output Preview/i }); + const sectionChevrons = () => + screen + .getAllByRole("button", { name: /^(Expand|Collapse) / }) + .filter((b) => b.hasAttribute("aria-expanded") && !previewRegion.contains(b)); + + await user.click(screen.getByRole("button", { name: /^Expand all$/i })); + const opened = sectionChevrons(); + expect(opened.length).toBeGreaterThan(0); + expect(opened.every((b) => b.getAttribute("aria-expanded") === "true")).toBe(true); + + await user.click(screen.getByRole("button", { name: /^Collapse all$/i })); + expect(sectionChevrons().every((b) => b.getAttribute("aria-expanded") === "false")).toBe(true); + }); + + it("drives every instrumentation module row on the Instrumentation tab", async () => { + renderPage(); + const user = userEvent.setup(); + await openInstrumentationTab(user); + await screen.findAllByRole("button", { name: /Toggle details for/i }, { timeout: 10_000 }); + + const rows = () => screen.getAllByRole("button", { name: /Toggle details for/i }); + + await user.click(screen.getByRole("button", { name: /^Expand all$/i })); + const opened = rows(); + expect(opened.length).toBeGreaterThan(0); + expect(opened.every((b) => b.getAttribute("aria-expanded") === "true")).toBe(true); + + await user.click(screen.getByRole("button", { name: /^Collapse all$/i })); + expect(rows().every((b) => b.getAttribute("aria-expanded") === "false")).toBe(true); + }); +});