Skip to content
Merged
31 changes: 28 additions & 3 deletions ecosystem-explorer/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,39 @@
* limitations under the License.
*/
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { describe, it, expect, afterEach, vi } from "vitest";
import App from "./App";
import { ThemeProvider } from "./theme-context";

describe("App", () => {
it("renders the page title", async () => {
render(<App />);
afterEach(() => {
vi.unstubAllEnvs();
});

it("renders the legacy app when V1_REDESIGN is disabled", async () => {
vi.stubEnv("VITE_FEATURE_FLAG_V1_REDESIGN", "");

render(
<ThemeProvider>
<App />
</ThemeProvider>
);

const heading = await screen.findByRole("heading", { level: 1 });
expect(heading).toHaveTextContent("OpenTelemetry");
expect(heading).toHaveTextContent("Ecosystem Explorer");
});

it("renders the v1 app when V1_REDESIGN is enabled", async () => {
vi.stubEnv("VITE_FEATURE_FLAG_V1_REDESIGN", "true");

const { container } = render(
<ThemeProvider>
<App />
</ThemeProvider>
);

expect(await screen.findByLabelText("OpenTelemetry")).toBeInTheDocument();
expect(container.querySelector(".v1-app")).not.toBeNull();
});
});
124 changes: 10 additions & 114 deletions ecosystem-explorer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,121 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import { BrowserRouter } from "react-router-dom";
import { LegacyApp } from "@/LegacyApp";
import { V1App } from "@/v1";
import { isEnabled } from "@/lib/feature-flags";
import { ErrorBoundary } from "@/components/ui/error-boundary";

const HomePage = lazy(() =>
import("@/features/home/home-page").then((m) => ({ default: m.HomePage }))
);
const JavaAgentPage = lazy(() =>
import("@/features/java-agent/java-agent-page").then((m) => ({ default: m.JavaAgentPage }))
);
const CollectorPage = lazy(() =>
import("@/features/collector/collector-page").then((m) => ({ default: m.CollectorPage }))
);
const CollectorDetailPage = lazy(() =>
import("@/features/collector/collector-detail-page").then((m) => ({
default: m.CollectorDetailPage,
}))
);
const NotFoundPage = lazy(() =>
import("@/features/not-found/not-found-page").then((m) => ({ default: m.NotFoundPage }))
);
const JavaInstrumentationListPage = lazy(() =>
import("@/features/java-agent/java-instrumentation-list-page").then((m) => ({
default: m.JavaInstrumentationListPage,
}))
);
const JavaConfigurationListPage = lazy(() =>
import("@/features/java-agent/java-configuration-list-page").then((m) => ({
default: m.JavaConfigurationListPage,
}))
);
const JavaReleaseComparisonPage = lazy(() =>
import("@/features/java-agent/java-release-comparison-page").then((m) => ({
default: m.JavaReleaseComparisonPage,
}))
);
const InstrumentationDetailPage = lazy(() =>
import("@/features/java-agent/instrumentation-detail-page").then((m) => ({
default: m.InstrumentationDetailPage,
}))
);
const ConfigurationBuilderPage = lazy(() =>
import("@/features/java-agent/configuration/configuration-builder-page").then((m) => ({
default: m.ConfigurationBuilderPage,
}))
);
const AboutPage = lazy(() =>
import("@/features/about/about-page").then((m) => ({ default: m.AboutPage }))
);

/*
* Single V1_REDESIGN boundary read. See
* `projects/84-ui-ux-design/v1-routing-pivot.md` for the routing pivot context.
* Both sub-apps share canonical paths under a single <BrowserRouter>; per-deploy
* bundle selection is driven by netlify.toml's `feat/84-*` branch pattern.
Comment thread
vitorvasc marked this conversation as resolved.
*/
export default function App() {
return (
<BrowserRouter>
<div className="bg-background flex min-h-screen flex-col">
<Header />
<main className="flex-1 pt-16">
<ErrorBoundary>
<Suspense
fallback={
<div
className="flex min-h-[400px] items-center justify-center"
role="status"
aria-live="polite"
>
<div className="text-muted-foreground text-sm font-medium">Loading…</div>
</div>
}
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/java-agent" element={<JavaAgentPage />} />
<Route
path="/java-agent/instrumentation"
element={<JavaInstrumentationListPage />}
/>
<Route
path="/java-agent/instrumentation/:version"
element={<JavaInstrumentationListPage />}
/>
<Route
path="/java-agent/instrumentation/:version/:name"
element={<InstrumentationDetailPage />}
/>
<Route path="/java-agent/configuration" element={<JavaConfigurationListPage />} />
{isEnabled("JAVA_RELEASE_COMPARISON") && (
<Route path="/java-agent/releases" element={<JavaReleaseComparisonPage />} />
)}
{isEnabled("JAVA_CONFIG_BUILDER") && (
<Route
path="/java-agent/configuration/builder"
element={<ConfigurationBuilderPage />}
/>
)}
<Route path="/collector" element={<CollectorPage />} />
{isEnabled("COLLECTOR_PAGE") && (
<>
<Route path="/collector/components" element={<CollectorPage />} />
<Route path="/collector/components/:version" element={<CollectorPage />} />
<Route
path="/collector/components/:version/:id"
element={<CollectorDetailPage />}
/>
</>
)}
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</ErrorBoundary>
</main>
<Footer />
</div>
</BrowserRouter>
);
return <BrowserRouter>{isEnabled("V1_REDESIGN") ? <V1App /> : <LegacyApp />}</BrowserRouter>;
}
138 changes: 138 additions & 0 deletions ecosystem-explorer/src/LegacyApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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 { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import { isEnabled } from "@/lib/feature-flags";
import { ErrorBoundary } from "@/components/ui/error-boundary";

const HomePage = lazy(() =>
import("@/features/home/home-page").then((m) => ({ default: m.HomePage }))
);
const JavaAgentPage = lazy(() =>
import("@/features/java-agent/java-agent-page").then((m) => ({ default: m.JavaAgentPage }))
);
const CollectorPage = lazy(() =>
import("@/features/collector/collector-page").then((m) => ({ default: m.CollectorPage }))
);
const CollectorDetailPage = lazy(() =>
import("@/features/collector/collector-detail-page").then((m) => ({
default: m.CollectorDetailPage,
}))
);
const NotFoundPage = lazy(() =>
import("@/features/not-found/not-found-page").then((m) => ({ default: m.NotFoundPage }))
);
const JavaInstrumentationListPage = lazy(() =>
import("@/features/java-agent/java-instrumentation-list-page").then((m) => ({
default: m.JavaInstrumentationListPage,
}))
);
const JavaConfigurationListPage = lazy(() =>
import("@/features/java-agent/java-configuration-list-page").then((m) => ({
default: m.JavaConfigurationListPage,
}))
);
const JavaReleaseComparisonPage = lazy(() =>
import("@/features/java-agent/java-release-comparison-page").then((m) => ({
default: m.JavaReleaseComparisonPage,
}))
);
const InstrumentationDetailPage = lazy(() =>
import("@/features/java-agent/instrumentation-detail-page").then((m) => ({
default: m.InstrumentationDetailPage,
}))
);
const ConfigurationBuilderPage = lazy(() =>
import("@/features/java-agent/configuration/configuration-builder-page").then((m) => ({
default: m.ConfigurationBuilderPage,
}))
);
const AboutPage = lazy(() =>
import("@/features/about/about-page").then((m) => ({ default: m.AboutPage }))
);

/*
* Pre-pivot router subtree, reached when `isEnabled("V1_REDESIGN")` is false in
* `src/App.tsx`. The route table below MUST stay in sync with the table in
* `src/v1/V1App.tsx` until the cleanup deletes this file. Any new global
* route added here must also be added there, otherwise it's reachable only in
* legacy builds and 404s on `feat/84-*` preview deploys.
*
* When the cleanup PR removes this file, the route-sync warning becomes moot:
* v1's table becomes the only table.
*/
export function LegacyApp() {
return (
<div className="bg-background flex min-h-screen flex-col">
<Header />
<main className="flex-1 pt-16">
<ErrorBoundary>
<Suspense
fallback={
<div
className="flex min-h-[400px] items-center justify-center"
role="status"
aria-live="polite"
>
<div className="text-muted-foreground text-sm font-medium">Loading…</div>
</div>
}
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/java-agent" element={<JavaAgentPage />} />
<Route path="/java-agent/instrumentation" element={<JavaInstrumentationListPage />} />
<Route
path="/java-agent/instrumentation/:version"
element={<JavaInstrumentationListPage />}
/>
<Route
path="/java-agent/instrumentation/:version/:name"
element={<InstrumentationDetailPage />}
/>
<Route path="/java-agent/configuration" element={<JavaConfigurationListPage />} />
{isEnabled("JAVA_RELEASE_COMPARISON") && (
<Route path="/java-agent/releases" element={<JavaReleaseComparisonPage />} />
)}
{isEnabled("JAVA_CONFIG_BUILDER") && (
<Route
path="/java-agent/configuration/builder"
element={<ConfigurationBuilderPage />}
/>
)}
<Route path="/collector" element={<CollectorPage />} />
{isEnabled("COLLECTOR_PAGE") && (
<>
<Route path="/collector/components" element={<CollectorPage />} />
<Route path="/collector/components/:version" element={<CollectorPage />} />
<Route
path="/collector/components/:version/:id"
element={<CollectorDetailPage />}
/>
</>
)}
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</ErrorBoundary>
</main>
<Footer />
</div>
);
}
18 changes: 18 additions & 0 deletions ecosystem-explorer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ import { createRoot } from "react-dom/client";
import "./styles/index.css";
import App from "./App.tsx";
import { ThemeProvider } from "./theme-context";
import { isEnabled } from "./lib/feature-flags";

/*
* Apply `.v1-app` to <html> before React mounts so the body background uses
* v1 surface tokens (`src/v1/styles/tokens.css`) from the first paint. Without
* this, body would briefly render against the legacy navy palette during the
* React mount window before `<V1App />`'s wrapper div applies its override.
*
* This is the only flag read outside `src/App.tsx`'s boundary. The
* 2026-05-12 pivot collapsed per-component flag sprawl into a single
* App.tsx boundary read; this narrow early-paint marker is the carve-out
* needed to keep body bg painted correctly without a flash. CSS variables
* declared on `<html>` cascade to `<body>` via `body { background-color:
* hsl(var(--background-hsl)) }` in `src/styles/base.css`.
*/
if (isEnabled("V1_REDESIGN")) {
document.documentElement.classList.add("v1-app");
}

createRoot(document.getElementById("root")!).render(
<StrictMode>
Expand Down
8 changes: 4 additions & 4 deletions ecosystem-explorer/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
* - tokens.css — design-token CSS custom properties (per-theme HSL triplets)
* - base.css — global element resets
* - syntax.css — YAML / config syntax-highlight token classes
* - navbar.css — v1 navbar (`.td-navbar`) — mirrors opentelemetry.io
* - theme-toggle.css — v1 theme toggle dropdown — mirrors opentelemetry.io
*
* V1 chrome styles (`navbar.css`, `theme-toggle.css`, v1 surface-token overrides)
* live under `src/v1/styles/` and are imported by `<V1App />` so they only
* affect the v1 subtree. See `projects/84-ui-ux-design/v1-routing-pivot.md`.
*
* Tailwind v4 bundles @import rules via Lightning CSS at build time, so each
* partial is inlined into the final stylesheet — no extra network requests.
Expand All @@ -31,8 +33,6 @@
@import "./tokens.css";
@import "./base.css";
@import "./syntax.css";
@import "./navbar.css";
@import "./theme-toggle.css";

/**
* Tailwind theme — maps --*-hsl triplets to hsl() utility classes.
Expand Down
22 changes: 0 additions & 22 deletions ecosystem-explorer/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,26 +103,4 @@
--syntax-keyword-hsl: 265 70% 78%;
--syntax-punct-hsl: 220 22% 55%;
}

/*
* V1_REDESIGN page-surface overrides — gated on `<html data-v1-redesign>`
* which `main.tsx` sets at bundle load when the feature flag is on. Aligns
* the explorer's page bg/fg with opentelemetry.io's Bootstrap defaults so
* the new chrome doesn't sit on a mismatched navy backdrop. Other surface
* tokens (card, muted, border) stay on the explorer's palette for now;
* those move when their pages get redesigned.
* Dark: --bs-body-bg = #212529 (gray-900) / --bs-body-color = #dee2e6
* Light: --bs-body-bg = #fff / --bs-body-color = #212529
* Light values are already this in the explorer, so we override dark only.
*/
[data-v1-redesign="true"],
[data-v1-redesign="true"][data-theme="dark"] {
--background-hsl: 210 11% 15%; /* #212529 */
--foreground-hsl: 210 11% 88%; /* #dee2e6 */
}

[data-v1-redesign="true"][data-theme="light"] {
--background-hsl: 0 0% 100%;
--foreground-hsl: 210 11% 15%;
}
}
Loading
Loading