diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.test.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.test.tsx
index 27ebee42..2227b68b 100644
--- a/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.test.tsx
+++ b/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.test.tsx
@@ -52,16 +52,10 @@ vi.mock("@/hooks/use-configuration-builder", () => ({
}),
}));
-const useLatestJavaAgentVersionMock = vi.fn();
-vi.mock("@/hooks/use-latest-java-agent-version", () => ({
- useLatestJavaAgentVersion: () => useLatestJavaAgentVersionMock(),
-}));
-
const downloadSpy = vi.spyOn(downloadModule, "downloadText").mockImplementation(() => {});
describe("PreviewCard", () => {
beforeEach(() => {
- useLatestJavaAgentVersionMock.mockReturnValue("2.27.0");
downloadSpy.mockClear();
});
@@ -71,7 +65,7 @@ describe("PreviewCard", () => {
});
it("renders the Output Preview title and action buttons", () => {
- render();
+ render();
expect(screen.getByText("Output Preview")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /copy/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /download/i })).toBeInTheDocument();
@@ -80,25 +74,25 @@ describe("PreviewCard", () => {
});
it("triggers enableAllSections on Add all click", () => {
- render();
+ render();
fireEvent.click(screen.getByRole("button", { name: /add all/i }));
expect(enableAllSections).toHaveBeenCalledTimes(1);
});
it("Reset calls resetToDefaults (no confirm when state is clean)", () => {
- render();
+ render();
fireEvent.click(screen.getByRole("button", { name: /reset/i }));
expect(resetToDefaults).toHaveBeenCalledTimes(1);
});
it("triggers validateAll on Copy click regardless of clipboard availability", () => {
- render();
+ render();
fireEvent.click(screen.getByRole("button", { name: /copy/i }));
expect(validateAll).toHaveBeenCalledTimes(1);
});
it("renders the YAML output via YamlCodeBlock with token spans", () => {
- const { container } = render();
+ const { container } = render();
const pre = container.querySelector("pre");
expect(pre).not.toBeNull();
expect(pre?.querySelectorAll("span.y-key").length).toBeGreaterThan(0);
@@ -107,7 +101,7 @@ describe("PreviewCard", () => {
});
it("includes the resolved Java agent version in the rendered YAML header", () => {
- render();
+ render();
const codeBlock = screen.getByLabelText("Output Preview").querySelector("pre");
expect(codeBlock).not.toBeNull();
expect(codeBlock?.textContent).toContain("Schema version: 1.0.0");
@@ -115,15 +109,20 @@ describe("PreviewCard", () => {
});
it("renders the header without the agent line while the version is still loading", () => {
- useLatestJavaAgentVersionMock.mockReturnValue(undefined);
- render();
+ render();
const codeBlock = screen.getByLabelText("Output Preview").querySelector("pre");
expect(codeBlock?.textContent).toContain("Schema version: 1.0.0");
expect(codeBlock?.textContent).not.toContain("Java agent:");
});
+ it("reflects a non-latest Java agent version selection in the YAML header", () => {
+ render();
+ const codeBlock = screen.getByLabelText("Output Preview").querySelector("pre");
+ expect(codeBlock?.textContent).toContain("Java agent: 2.26.1");
+ });
+
it("downloads the YAML with the schema-versioned filename and agent-stamped content", () => {
- render();
+ render();
fireEvent.click(screen.getByRole("button", { name: /download/i }));
expect(downloadSpy).toHaveBeenCalledTimes(1);
const [filename, body, mime] = downloadSpy.mock.calls[0];
diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.tsx
index 45cfde16..f1d4ce26 100644
--- a/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.tsx
+++ b/ecosystem-explorer/src/features/java-agent/configuration/components/preview-card.tsx
@@ -17,7 +17,6 @@ import { useMemo, type JSX } from "react";
import { Download, RefreshCcw, ListPlus } from "lucide-react";
import type { ConfigNode } from "@/types/configuration";
import { useConfigurationBuilder } from "@/hooks/use-configuration-builder";
-import { useLatestJavaAgentVersion } from "@/hooks/use-latest-java-agent-version";
import { generateYaml } from "@/lib/yaml-generator";
import { downloadText } from "@/lib/download-text";
import { CopyButton } from "@/components/ui/copy-button";
@@ -25,13 +24,13 @@ import { YamlCodeBlock } from "./yaml-code-block";
interface PreviewCardProps {
schema: ConfigNode;
+ javaAgentVersion: string;
}
-export function PreviewCard({ schema }: PreviewCardProps): JSX.Element {
+export function PreviewCard({ schema, javaAgentVersion }: PreviewCardProps): JSX.Element {
const { state, enableAllSections, resetToDefaults, validateAll } = useConfigurationBuilder();
- const javaAgentVersion = useLatestJavaAgentVersion();
const yaml = useMemo(
- () => generateYaml(state, schema, { javaAgentVersion }),
+ () => generateYaml(state, schema, { javaAgentVersion: javaAgentVersion || undefined }),
[state, schema, javaAgentVersion]
);
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 6a639db7..5fa5b61b 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
@@ -26,8 +26,7 @@ import {
} from "@/hooks/use-configuration-data";
import { ConfigurationBuilderProvider } from "@/hooks/configuration-builder-provider";
import { useConfigurationBuilder } from "@/hooks/use-configuration-builder";
-import { useInstrumentations } from "@/hooks/use-javaagent-data";
-import { useLatestJavaAgentVersion } from "@/hooks/use-latest-java-agent-version";
+import { useInstrumentations, useVersions } from "@/hooks/use-javaagent-data";
import { groupByModule } from "@/lib/normalize-instrumentation";
import { useCustomizedModules } from "@/hooks/use-customized-modules";
import type { GroupNode } from "@/types/configuration";
@@ -74,11 +73,18 @@ const GENERAL_SETTINGS_LABEL = "General settings";
interface SdkTabContentProps {
schema: GroupNode;
starter: ReturnType["data"];
- version: string;
+ schemaVersion: string;
+ javaAgentVersion: string;
activeTab: string;
}
-function SdkTabContent({ schema, starter, version, activeTab }: SdkTabContentProps) {
+function SdkTabContent({
+ schema,
+ starter,
+ schemaVersion,
+ javaAgentVersion,
+ activeTab,
+}: SdkTabContentProps) {
const { groupChildren, leafChildren } = useMemo(() => {
const visible = schema.children.filter((c) => !SDK_HIDDEN_KEYS.has(c.key));
return {
@@ -99,7 +105,12 @@ function SdkTabContent({ schema, starter, version, activeTab }: SdkTabContentPro
const { activeKey, scrollToSection } = useActiveSection(sectionKeys, sectionsContainerRef);
return (
-
+
))}
-
+
);
@@ -124,14 +135,16 @@ function SdkTabContent({ schema, starter, version, activeTab }: SdkTabContentPro
interface InstrumentationTabContentProps {
schema: GroupNode;
starter: ReturnType["data"];
- version: string;
+ schemaVersion: string;
+ javaAgentVersion: string;
activeTab: string;
}
function InstrumentationTabContent({
schema,
starter,
- version,
+ schemaVersion,
+ javaAgentVersion,
activeTab,
}: InstrumentationTabContentProps) {
const generalNode = useMemo(() => {
@@ -143,8 +156,18 @@ function InstrumentationTabContent({
}, [schema]);
return (
-
-
+
+
);
}
@@ -153,17 +176,18 @@ interface InstrumentationTabBodyProps {
activeTab: string;
schema: GroupNode;
generalNode: GroupNode | null;
+ javaAgentVersion: string;
}
-function InstrumentationTabBody({ activeTab, schema, generalNode }: InstrumentationTabBodyProps) {
+function InstrumentationTabBody({
+ activeTab,
+ schema,
+ generalNode,
+ javaAgentVersion,
+}: InstrumentationTabBodyProps) {
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
- // The page-level `version` is the SDK config schema version (e.g. "1.0.0").
- // The instrumentation registry is keyed by Java agent version (e.g. "2.27.0"):
- // a separate namespace. Resolve the latest agent version via the shared hook.
- const javaAgentVersion = useLatestJavaAgentVersion() ?? "";
-
const tocSections: TocSection[] = useMemo(
() => [
{ key: GENERAL_SECTION_KEY, label: GENERAL_SETTINGS_LABEL },
@@ -237,26 +261,39 @@ function InstrumentationTabBody({ activeTab, schema, generalNode }: Instrumentat
onJumpToGeneral={scrollToSection}
/>
-
+
);
}
export function ConfigurationBuilderPage() {
- const versions = useConfigVersions();
- const latest = useMemo(
+ const schemaVersionsState = useConfigVersions();
+ const latestSchemaVersion = useMemo(
() =>
- versions.data?.versions.find((v) => v.is_latest)?.version ??
- versions.data?.versions[0]?.version ??
+ schemaVersionsState.data?.versions.find((v) => v.is_latest)?.version ??
+ schemaVersionsState.data?.versions[0]?.version ??
"",
- [versions.data]
+ [schemaVersionsState.data]
);
- const [currentVersion, setCurrentVersion] = useState("");
- const version = currentVersion || latest;
+ const [currentSchemaVersion, setCurrentSchemaVersion] = useState("");
+ const schemaVersion = currentSchemaVersion || latestSchemaVersion;
const [activeTab, setActiveTab] = useState("sdk");
- const schema = useConfigSchema(version);
- const starter = useConfigStarter(version);
+ const javaAgentVersionsState = useVersions();
+ const javaAgentVersions = useMemo(
+ () => javaAgentVersionsState.data?.versions ?? [],
+ [javaAgentVersionsState.data]
+ );
+ const latestJavaAgentVersion = useMemo(
+ () =>
+ javaAgentVersions.find((v) => v.is_latest)?.version ?? javaAgentVersions[0]?.version ?? "",
+ [javaAgentVersions]
+ );
+ const [currentJavaAgentVersion, setCurrentJavaAgentVersion] = useState("");
+ const javaAgentVersion = currentJavaAgentVersion || latestJavaAgentVersion;
+
+ const schema = useConfigSchema(schemaVersion);
+ const starter = useConfigStarter(schemaVersion);
const root = (schema.data as GroupNode | null) ?? null;
return (
@@ -294,17 +331,30 @@ export function ConfigurationBuilderPage() {
- {versions.data && version ? (
-
- ) : null}
+
+ {schemaVersionsState.data && schemaVersion ? (
+
+ ) : null}
+ {javaAgentVersions.length > 0 && javaAgentVersion ? (
+
+ ) : null}
+
- {!version || schema.loading || starter.loading ? (
+ {!schemaVersion || schema.loading || starter.loading ? (
Loading schema…
) : schema.error ? (
Failed to load schema.
@@ -314,13 +364,14 @@ export function ConfigurationBuilderPage() {
) : null}
- {!version || schema.loading || starter.loading ? (
+ {!schemaVersion || schema.loading || starter.loading ? (
Loading schema…
) : schema.error ? (
Failed to load schema.
@@ -330,7 +381,8 @@ export function ConfigurationBuilderPage() {
) : null}
diff --git a/ecosystem-explorer/src/hooks/use-latest-java-agent-version.test.ts b/ecosystem-explorer/src/hooks/use-latest-java-agent-version.test.ts
deleted file mode 100644
index c8ccbc6d..00000000
--- a/ecosystem-explorer/src/hooks/use-latest-java-agent-version.test.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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, vi, beforeEach } from "vitest";
-import { renderHook } from "@testing-library/react";
-import type { VersionsIndex } from "@/types/javaagent";
-
-const useVersionsMock = vi.fn();
-vi.mock("@/hooks/use-javaagent-data", () => ({
- useVersions: () => useVersionsMock(),
-}));
-
-import { useLatestJavaAgentVersion } from "./use-latest-java-agent-version";
-
-describe("useLatestJavaAgentVersion", () => {
- beforeEach(() => useVersionsMock.mockReset());
-
- it("returns the version flagged is_latest", () => {
- const data: VersionsIndex = {
- versions: [
- { version: "2.26.1", is_latest: false },
- { version: "2.27.0", is_latest: true },
- ],
- };
- useVersionsMock.mockReturnValue({ data, loading: false, error: null });
- const { result } = renderHook(() => useLatestJavaAgentVersion());
- expect(result.current).toBe("2.27.0");
- });
-
- it("falls back to the first version when none is flagged is_latest", () => {
- const data: VersionsIndex = {
- versions: [
- { version: "2.26.1", is_latest: false },
- { version: "2.27.0", is_latest: false },
- ],
- };
- useVersionsMock.mockReturnValue({ data, loading: false, error: null });
- const { result } = renderHook(() => useLatestJavaAgentVersion());
- expect(result.current).toBe("2.26.1");
- });
-
- it("returns undefined while loading", () => {
- useVersionsMock.mockReturnValue({ data: null, loading: true, error: null });
- const { result } = renderHook(() => useLatestJavaAgentVersion());
- expect(result.current).toBeUndefined();
- });
-
- it("returns undefined when versions is empty", () => {
- useVersionsMock.mockReturnValue({
- data: { versions: [] },
- loading: false,
- error: null,
- });
- const { result } = renderHook(() => useLatestJavaAgentVersion());
- expect(result.current).toBeUndefined();
- });
-});
diff --git a/ecosystem-explorer/src/hooks/use-latest-java-agent-version.ts b/ecosystem-explorer/src/hooks/use-latest-java-agent-version.ts
deleted file mode 100644
index 743e901a..00000000
--- a/ecosystem-explorer/src/hooks/use-latest-java-agent-version.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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 { useMemo } from "react";
-import { useVersions } from "@/hooks/use-javaagent-data";
-
-export function useLatestJavaAgentVersion(): string | undefined {
- const { data } = useVersions();
- return useMemo(
- () => data?.versions.find((v) => v.is_latest)?.version ?? data?.versions[0]?.version,
- [data]
- );
-}
diff --git a/ecosystem-explorer/src/test/integration/configuration-builder-instrumentation-version.integration.test.tsx b/ecosystem-explorer/src/test/integration/configuration-builder-instrumentation-version.integration.test.tsx
new file mode 100644
index 00000000..61cabb98
--- /dev/null
+++ b/ecosystem-explorer/src/test/integration/configuration-builder-instrumentation-version.integration.test.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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, afterAll, beforeEach } from "vitest";
+import { screen, waitFor, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import schemaVersionsIndex from "../../../public/data/configuration/versions-index.json";
+import javaAgentVersionsIndex from "../../../public/data/javaagent/versions-index.json";
+import { installFetchInterceptor, uninstallFetchInterceptor } from "./helpers/fetch-interceptor";
+import { renderBuilderPage as renderPage } from "./helpers/render-builder-page";
+
+const latestSchemaVersion = schemaVersionsIndex.versions.find((v) => v.is_latest)!.version;
+const latestAgentVersion = javaAgentVersionsIndex.versions.find((v) => v.is_latest)!.version;
+const otherAgentVersion = javaAgentVersionsIndex.versions.find((v) => !v.is_latest)?.version;
+
+beforeAll(() => installFetchInterceptor());
+afterAll(() => uninstallFetchInterceptor());
+beforeEach(() => localStorage.clear());
+
+async function findAgentSelector(): Promise {
+ return (await screen.findByLabelText("Agent", {}, { timeout: 10_000 })) as HTMLSelectElement;
+}
+
+async function openInstrumentationTab(user: ReturnType) {
+ await screen.findByRole("switch", { name: /Enable Resource/i }, { timeout: 10_000 });
+ const sidebar = screen.getByRole("complementary");
+ await user.click(within(sidebar).getByRole("tab", { name: /Instrumentation/i }));
+}
+
+describe("ConfigurationBuilderPage version selectors", () => {
+ it("renders Schema and Agent selectors side by side in the page header", async () => {
+ renderPage();
+ const schema = (await screen.findByLabelText(
+ "Schema",
+ {},
+ { timeout: 10_000 }
+ )) as HTMLSelectElement;
+ const agent = await findAgentSelector();
+ expect(schema.value).toBe(latestSchemaVersion);
+ expect(agent.value).toBe(latestAgentVersion);
+ });
+
+ it("re-runs the registry lookup and updates the Instrumentation tab when the Agent version changes", async () => {
+ if (!otherAgentVersion) return;
+ renderPage();
+ const user = userEvent.setup();
+ const agent = await findAgentSelector();
+
+ await openInstrumentationTab(user);
+ const reactorRow = (await screen.findByTestId(
+ "instrumentation-row-reactor",
+ {},
+ { timeout: 10_000 }
+ )) as HTMLElement;
+ expect(within(reactorRow).getByText("2 versions")).toBeInTheDocument();
+
+ await user.selectOptions(agent, otherAgentVersion);
+
+ await waitFor(
+ () => {
+ const updated = screen.getByTestId("instrumentation-row-reactor");
+ expect(within(updated).queryByText("2 versions")).toBeNull();
+ },
+ { timeout: 10_000 }
+ );
+ });
+
+ it("updates the YAML preview header from the SDK tab when the Agent version changes", async () => {
+ if (!otherAgentVersion) return;
+ renderPage();
+ const user = userEvent.setup();
+ const preview = (await screen.findByLabelText(
+ "Output Preview",
+ {},
+ { timeout: 10_000 }
+ )) as HTMLElement;
+ expect(preview.textContent).toContain(`Java agent: ${latestAgentVersion}`);
+
+ const agent = await findAgentSelector();
+ await user.selectOptions(agent, otherAgentVersion);
+
+ await waitFor(() => {
+ expect(preview.textContent).toContain(`Java agent: ${otherAgentVersion}`);
+ });
+ expect(preview.textContent).not.toContain(`Java agent: ${latestAgentVersion}`);
+ });
+
+ it("preserves user-entered configuration values when the Agent version changes", async () => {
+ if (!otherAgentVersion) return;
+ renderPage();
+ const user = userEvent.setup();
+ const resourceToggle = await screen.findByRole(
+ "switch",
+ { name: /Enable Resource/i },
+ { timeout: 10_000 }
+ );
+ expect(resourceToggle).toHaveAttribute("aria-checked", "true");
+ await user.click(resourceToggle);
+ await waitFor(() => expect(resourceToggle).toHaveAttribute("aria-checked", "false"));
+
+ const agent = await findAgentSelector();
+ await user.selectOptions(agent, otherAgentVersion);
+
+ expect(resourceToggle).toHaveAttribute("aria-checked", "false");
+ });
+
+ it("does not persist the Agent selection: remount resets to latest with empty localStorage", async () => {
+ if (!otherAgentVersion) return;
+ const { unmount } = renderPage();
+ const user = userEvent.setup();
+ const agent = await findAgentSelector();
+ await user.selectOptions(agent, otherAgentVersion);
+ expect(agent.value).toBe(otherAgentVersion);
+
+ unmount();
+ expect(localStorage.length).toBe(0);
+
+ renderPage();
+ const reloaded = await findAgentSelector();
+ expect(reloaded.value).toBe(latestAgentVersion);
+ });
+});