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); + }); +});