From 461a31e5d8be0a3965d3fe6729792f8005b50d1e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 8 Apr 2026 21:10:03 +0100 Subject: [PATCH] feat(site): add under-construction navbar stripes for pre-release builds (#24157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dev and RC builds now show diagonal warning stripes in the navbar plus a centered version badge, making it impossible to miss which build you're running. **Devel build:** amber "warning" from theme **RC build:** sky "pending" from theme > 🤖 Written by a Coder Agent. Will be reviewed by a human. --- site/src/index.css | 44 ++++++++ .../dashboard/Navbar/NavbarView.stories.tsx | 31 ++++++ .../modules/dashboard/Navbar/NavbarView.tsx | 30 +++++- site/src/utils/buildInfo.test.ts | 100 ++++++++++++++++++ site/src/utils/buildInfo.ts | 12 +++ 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 site/src/utils/buildInfo.test.ts diff --git a/site/src/index.css b/site/src/index.css index ba70176baa..1c42f0d071 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -156,6 +156,50 @@ } } +@layer components { + /* Map each stripe variant to a color token so the + pseudo-element rules can stay DRY. */ + .navbar-stripe-devel { + --stripe-color: var(--content-warning); + } + + .navbar-stripe-rc { + --stripe-color: var(--border-sky); + } + + /* Thin stripe bars at the top and bottom edges of the + navbar. Using pseudo-elements keeps the stripes out of + the content area so nav links stay readable. */ + .navbar-stripe-devel::before, + .navbar-stripe-devel::after, + .navbar-stripe-rc::before, + .navbar-stripe-rc::after { + content: ""; + position: absolute; + left: 0; + right: 0; + height: 4px; + background: repeating-linear-gradient( + -45deg, + transparent, + transparent 4px, + hsl(var(--stripe-color) / 0.5) 4px, + hsl(var(--stripe-color) / 0.5) 8px + ); + pointer-events: none; + } + + .navbar-stripe-devel::before, + .navbar-stripe-rc::before { + top: 0; + } + + .navbar-stripe-devel::after, + .navbar-stripe-rc::after { + bottom: 0; + } +} + @layer base { * { @apply border-border; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index 2ce2d63aef..435c7d4cbf 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -3,6 +3,7 @@ import { userEvent, within } from "storybook/test"; import type { TasksFilter } from "#/api/typesGenerated"; import { chromaticWithTablet } from "#/testHelpers/chromatic"; import { + MockBuildInfo, MockTasks, MockUserMember, MockUserOwner, @@ -170,3 +171,33 @@ export const DefaultSupportLinks: Story = { ], }, }; + +export const DevelBuild: Story = { + args: { + buildInfo: { + ...MockBuildInfo, + version: "v2.21.0-devel+abc123", + external_url: "https://github.com/coder/coder/commit/abc123", + }, + }, +}; + +export const RcBuild: Story = { + args: { + buildInfo: { + ...MockBuildInfo, + version: "v2.21.0-rc.1+def456", + external_url: "https://github.com/coder/coder/releases/tag/v2.21.0-rc.1", + }, + }, +}; + +export const RcDevelBuild: Story = { + args: { + buildInfo: { + ...MockBuildInfo, + version: "v2.33.0-rc.1-devel+727ec00f7", + external_url: "https://github.com/coder/coder/commit/727ec00f7", + }, + }, +}; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 24b5984f02..8afef07382 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -16,7 +16,7 @@ import type { ProxyContextValue } from "#/contexts/ProxyContext"; import { useEmbeddedMetadata } from "#/hooks/useEmbeddedMetadata"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { NotificationsInbox } from "#/modules/notifications/NotificationsInbox/NotificationsInbox"; -import { isDevBuild } from "#/utils/buildInfo"; +import { isDevBuild, isRcBuild } from "#/utils/buildInfo"; import { cn } from "#/utils/cn"; import { DeploymentDropdown } from "./DeploymentDropdown"; import { MobileMenu } from "./MobileMenu"; @@ -59,8 +59,17 @@ export const NavbarView: FC = ({ canViewAIBridge, proxyContextValue, }) => { + const isDev = buildInfo ? isDevBuild(buildInfo) : false; + const isRc = buildInfo ? isRcBuild(buildInfo) : false; + const isPreRelease = isDev || isRc; + return ( -
+
{logo_url ? ( @@ -71,6 +80,23 @@ export const NavbarView: FC = ({ + {isPreRelease && buildInfo?.version && ( + + + {buildInfo.version} + + + )} +
{supportLinks.filter(isNavbarLink).map((link) => (
diff --git a/site/src/utils/buildInfo.test.ts b/site/src/utils/buildInfo.test.ts new file mode 100644 index 0000000000..7f99daa48c --- /dev/null +++ b/site/src/utils/buildInfo.test.ts @@ -0,0 +1,100 @@ +import type { BuildInfoResponse } from "#/api/typesGenerated"; +import { isDevBuild, isRcBuild } from "./buildInfo"; + +const baseBuildInfo: BuildInfoResponse = { + agent_api_version: "1.0", + provisioner_api_version: "1.1", + external_url: "https://github.com/coder/coder", + version: "", + dashboard_url: "https://example.com", + workspace_proxy: false, + upgrade_message: "", + deployment_id: "test", + telemetry: false, +}; + +describe("isDevBuild", () => { + it("returns true for -devel versions", () => { + expect( + isDevBuild({ ...baseBuildInfo, version: "v2.16.0-devel+abc123" }), + ).toBe(true); + }); + + it("returns true for bare -devel versions", () => { + expect(isDevBuild({ ...baseBuildInfo, version: "v2.32.0-devel" })).toBe( + true, + ); + }); + + it("returns true for v0.0.0", () => { + expect(isDevBuild({ ...baseBuildInfo, version: "v0.0.0" })).toBe(true); + }); + + it("returns false for release versions", () => { + expect(isDevBuild({ ...baseBuildInfo, version: "v2.16.0" })).toBe(false); + }); + + it("returns false for RC versions", () => { + expect(isDevBuild({ ...baseBuildInfo, version: "v2.32.0-rc.1" })).toBe( + false, + ); + }); + + it("returns true for combined rc+devel versions", () => { + expect( + isDevBuild({ + ...baseBuildInfo, + version: "v2.33.0-rc.1-devel+727ec00f7", + }), + ).toBe(true); + }); + + it("returns false for empty version", () => { + expect(isDevBuild({ ...baseBuildInfo, version: "" })).toBe(false); + }); +}); + +describe("isRcBuild", () => { + it("returns true for -rc.0 versions", () => { + expect(isRcBuild({ ...baseBuildInfo, version: "v2.32.0-rc.0" })).toBe(true); + }); + + it("returns true for -rc.1 with build metadata", () => { + expect( + isRcBuild({ ...baseBuildInfo, version: "v2.32.0-rc.1+abc123" }), + ).toBe(true); + }); + + it("returns true for higher RC numbers", () => { + expect(isRcBuild({ ...baseBuildInfo, version: "v2.32.0-rc.12" })).toBe( + true, + ); + }); + + it("returns false for release versions", () => { + expect(isRcBuild({ ...baseBuildInfo, version: "v2.16.0" })).toBe(false); + }); + + it("returns false for devel versions", () => { + expect( + isRcBuild({ ...baseBuildInfo, version: "v2.16.0-devel+abc123" }), + ).toBe(false); + }); + + it("returns false for empty version", () => { + expect(isRcBuild({ ...baseBuildInfo, version: "" })).toBe(false); + }); + + it("returns false for versions with rc but no dot", () => { + expect(isRcBuild({ ...baseBuildInfo, version: "v2.32.0-rc" })).toBe(false); + }); + + it("returns true for combined rc+devel versions", () => { + expect( + isRcBuild({ + ...baseBuildInfo, + version: "v2.33.0-rc.1-devel+727ec00f7", + }), + ).toBe(true); + }); +}); diff --git a/site/src/utils/buildInfo.ts b/site/src/utils/buildInfo.ts index dae7dd2c9f..0306dc0a1d 100644 --- a/site/src/utils/buildInfo.ts +++ b/site/src/utils/buildInfo.ts @@ -35,3 +35,15 @@ export const isDevBuild = (input: BuildInfoResponse): boolean => { // Check for dev version pattern (contains "-devel") or no version (v0.0.0) return version.includes("-devel") || version === "v0.0.0"; }; + +// Check if the current build is a release candidate. Release +// candidates have versions containing "-rc." (e.g. v2.32.0-rc.0, +// v2.32.0-rc.1+abc123, v2.33.0-rc.1-devel+727ec00f7). +export const isRcBuild = (input: BuildInfoResponse): boolean => { + const version = input.version; + if (!version) { + return false; + } + + return version.includes("-rc."); +};