Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { ConfigNode } from "@/types/configuration";
import { SchemaRenderer } from "./schema-renderer";
import { SectionCardShell } from "./section-card-shell";
import { FieldSection } from "./field-section";
import { useCollapsibleExpansion } from "./section-expansion-context";

export const GENERAL_SECTION_KEY = "general";
export const GENERAL_SECTION_LABEL = "General";
Expand All @@ -39,6 +40,8 @@ export function GeneralSectionCard({
sectionKey = GENERAL_SECTION_KEY,
emptyMessage,
}: GeneralSectionCardProps): JSX.Element {
const { open, onOpenChange } = useCollapsibleExpansion(sectionKey, defaultExpanded);

const headerNode = {
controlType: "group" as const,
key: "__general__",
Expand All @@ -51,7 +54,8 @@ export function GeneralSectionCard({
node={headerNode}
level="section"
asGroup={false}
defaultExpanded={defaultExpanded}
open={open}
onOpenChange={onOpenChange}
>
<FieldSection.Header>
<FieldSection.Chevron />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -44,6 +46,10 @@ vi.mock("@/hooks/use-configuration-builder", () => ({
}),
}));

function renderWithProvider(ui: React.ReactElement) {
return render(<SectionExpansionProvider>{ui}</SectionExpansionProvider>);
}

const groupNode: GroupNode = {
controlType: "group",
key: "resource",
Expand All @@ -55,35 +61,35 @@ const groupNode: GroupNode = {

describe("GroupRenderer", () => {
it("at depth 0 renders a card with an enable switch", () => {
render(<GroupRenderer node={groupNode} depth={0} path="resource" />);
renderWithProvider(<GroupRenderer node={groupNode} depth={0} path="resource" />);
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(<GroupRenderer node={groupNode} depth={0} path="resource" />);
renderWithProvider(<GroupRenderer node={groupNode} depth={0} path="resource" />);
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(<GroupRenderer node={groupNode} depth={3} path="resource" />);
renderWithProvider(<GroupRenderer node={groupNode} depth={3} path="resource" />);
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();
});

it("hides the chevron button at depth 0 when the section is disabled", () => {
mockState.enabledSections.resource = false;
render(<GroupRenderer node={groupNode} depth={0} path="resource" />);
renderWithProvider(<GroupRenderer node={groupNode} depth={0} path="resource" />);
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(<GroupRenderer node={groupNode} depth={0} path="resource" />);
renderWithProvider(<GroupRenderer node={groupNode} depth={0} path="resource" />);
expect(screen.getByRole("button", { name: /Collapse Resource/ })).toBeInTheDocument();
mockState.enabledSections.resource = false; // restore for other tests
});
Expand All @@ -101,13 +107,17 @@ describe("GroupRenderer", () => {
},
],
};
const { rerender } = render(<GroupRenderer node={childNode} depth={0} path="resource" />);
const { rerender } = renderWithProvider(
<GroupRenderer node={childNode} depth={0} path="resource" />
);
const rerenderWithProvider = (ui: React.ReactElement) =>
rerender(<SectionExpansionProvider>{ui}</SectionExpansionProvider>);
// 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(<GroupRenderer node={childNode} depth={0} path="resource" />);
rerenderWithProvider(<GroupRenderer node={childNode} depth={0} path="resource" />);

// Child label should now appear (auto-expanded).
expect(screen.getByText("Schema URL")).toBeInTheDocument();
Expand Down Expand Up @@ -137,7 +147,9 @@ describe("GroupRenderer", () => {
processors: [{ batch: { exporter: { otlp_http: {} }, schedule_delay: 1000 } }],
},
};
render(<GroupRenderer node={batchNode} depth={3} path="tracer_provider.processors[0].batch" />);
renderWithProvider(
<GroupRenderer node={batchNode} depth={3} path="tracer_provider.processors[0].batch" />
);
expect(screen.queryByText("Schedule Delay")).toBeNull();
// The chevron is there to expand on demand.
expect(screen.getByRole("button", { name: /Expand Batch/ })).toBeInTheDocument();
Expand All @@ -161,7 +173,9 @@ describe("GroupRenderer", () => {
mockState.values = {
tracer_provider: { processors: [{ batch: { exporter: { otlp_http: {} } } }] },
};
render(<GroupRenderer node={samplerNode} depth={1} path="tracer_provider.sampler" />);
renderWithProvider(
<GroupRenderer node={samplerNode} depth={1} path="tracer_provider.sampler" />
);
expect(screen.queryByText("Ratio")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: /Expand Sampler/ }));
expect(screen.getByText("Ratio")).toBeInTheDocument();
Expand All @@ -176,7 +190,9 @@ describe("GroupRenderer", () => {
path: "resource",
children: [],
};
const { container } = render(<GroupRenderer node={resourceNode} depth={0} path="resource" />);
const { container } = renderWithProvider(
<GroupRenderer node={resourceNode} depth={0} path="resource" />
);
const section = container.querySelector<HTMLElement>('[data-section-key="resource"]');
expect(section).not.toBeNull();
expect(section?.getAttribute("tabindex")).toBe("-1");
Expand All @@ -192,7 +208,7 @@ describe("GroupRenderer", () => {
description: "Configure spans, samplers, and processors. Multiple processors are supported.",
children: [],
};
render(<GroupRenderer node={node} depth={0} path="tracer" />);
renderWithProvider(<GroupRenderer node={node} depth={0} path="tracer" />);
expect(screen.getByText("Configure spans, samplers, and processors.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Show more" })).toBeInTheDocument();
});
Expand All @@ -206,7 +222,7 @@ describe("GroupRenderer", () => {
description: "Batch span processor.",
children: [],
};
render(<GroupRenderer node={node} depth={1} path="tracer.processors[0].batch" />);
renderWithProvider(<GroupRenderer node={node} depth={1} path="tracer.processors[0].batch" />);
const allMatches = screen.getAllByText("Batch span processor.");
expect(allMatches).toHaveLength(1);
});
Expand All @@ -223,7 +239,9 @@ describe("GroupRenderer", () => {
} as unknown as ConfigNode,
],
};
const { container } = render(<GroupRenderer node={nodeWithChild} depth={1} path="resource" />);
const { container } = renderWithProvider(
<GroupRenderer node={nodeWithChild} depth={1} path="resource" />
);
fireEvent.click(screen.getByRole("button", { name: /Expand Resource/ }));
const indented = container.querySelectorAll<HTMLElement>(".pl-3");
expect(indented.length).toBeGreaterThan(0);
Expand All @@ -241,14 +259,14 @@ describe("GroupRenderer", () => {
} as unknown as ConfigNode,
],
};
const { container } = render(
const { container } = renderWithProvider(
<GroupRenderer node={nodeWithChild} depth={1} path="resource" headless />
);
expect(container.querySelectorAll<HTMLElement>(".pl-3")).toHaveLength(0);
});

it("at depth >= 1 starts expanded when its path is in StarterPathsContext", () => {
render(
renderWithProvider(
<StarterPathsContext.Provider value={new Set(["resource"])}>
<GroupRenderer node={groupNode} depth={1} path="resource" />
</StarterPathsContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +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 { resolveBulkOpen, useSectionExpansion } from "./section-expansion-context";

export interface GroupRendererProps {
node: GroupNode;
Expand Down Expand Up @@ -57,6 +58,10 @@ export function GroupRenderer({
if (enabled) setExpanded(true);
}

const ctx = useSectionExpansion();
const bulkOpen = resolveBulkOpen(ctx, path, isTopLevel ? enabled : true);
const resolvedExpanded = bulkOpen !== null ? bulkOpen : expanded;

const value = getByPath(state.values, parsePath(path));

const childNodes = node.children.map((child) => (
Expand All @@ -79,8 +84,11 @@ export function GroupRenderer({
level="section"
value={value}
asGroup={false}
open={expanded}
onOpenChange={setExpanded}
open={resolvedExpanded}
onOpenChange={(next) => {
ctx.setOverride(path, next);
setExpanded(next);
}}
>
<FieldSection.Header>
{enabled && <FieldSection.Chevron />}
Expand Down Expand Up @@ -108,8 +116,11 @@ export function GroupRenderer({
value={value}
headless={headless}
asGroup={!headless}
open={expanded}
onOpenChange={setExpanded}
open={resolvedExpanded}
onOpenChange={(next) => {
setExpanded(next);
ctx.setOverride(path, next);
}}
>
<FieldSection.Header>
<FieldSection.Chevron />
Expand Down
Loading
Loading