From 8549a286113c2cc1037cd36fb83e48e87a58b7a6 Mon Sep 17 00:00:00 2001 From: default Date: Thu, 20 Aug 2026 16:01:28 +0000 Subject: [PATCH 1/4] feat(site/src): link premium paywall pages to their feature docs Every premium paywall linked to coder.com/pricing with a "Learn more about premium" link, which duplicated the trial CTA and told users nothing about the feature they were looking at. Remove that link from PaywallPremium and PaywallSmall, and instead give each paywalled page a top-right "Read the docs" button pointing at the docs page for that feature, per the Premium Copy Updates doc. Pages that already had an inline "View docs" link in their description now use the header button instead, so the affordance is consistent. Template permissions moves its header into the page container so the button also renders in the unentitled state. --- site/src/components/Paywall/Paywall.tsx | 22 +---------------- .../Paywall/PaywallPremium.stories.tsx | 14 ++++------- .../src/components/Paywall/PaywallPremium.tsx | 11 --------- .../Paywall/PaywallSmall.stories.tsx | 4 ++-- site/src/components/Paywall/PaywallSmall.tsx | 5 ---- .../AIBridgePage/AIBridgeSessionsLayout.tsx | 17 ++++++------- .../GatewayKeysPage/GatewayKeysPageView.tsx | 24 ++++++++++++------- site/src/pages/AuditPage/AuditPageView.tsx | 8 ++++++- .../ConnectionLogPageView.tsx | 10 +++++++- .../AIGovernanceSettingsPageView.tsx | 2 +- .../AppearanceSettingsPageView.tsx | 8 ++++++- .../IdpOrgSyncPage/IdpOrgSyncPage.tsx | 12 ++++++---- .../ObservabilitySettingsPageView.tsx | 4 +++- site/src/pages/GroupsPage/GroupsPage.tsx | 8 ++++++- .../CreateOrganizationPageView.tsx | 5 ++++ .../CustomRolesPage/CustomRolesPage.tsx | 10 +++++++- .../IdpSyncPage/IdpSyncPage.tsx | 4 ++-- .../OrganizationProvisionerKeysPageView.tsx | 9 +++---- .../OrganizationProvisionersPageView.tsx | 8 ++++--- .../TemplatePermissionsPage.tsx | 24 +++++++++++++++++-- .../TemplatePermissionsPageView.tsx | 12 ---------- 21 files changed, 119 insertions(+), 102 deletions(-) diff --git a/site/src/components/Paywall/Paywall.tsx b/site/src/components/Paywall/Paywall.tsx index 4d1a1c81001..161ba9600ea 100644 --- a/site/src/components/Paywall/Paywall.tsx +++ b/site/src/components/Paywall/Paywall.tsx @@ -1,4 +1,4 @@ -import { ArrowRightIcon, CheckIcon, ExternalLinkIcon } from "lucide-react"; +import { ArrowRightIcon, CheckIcon } from "lucide-react"; import type React from "react"; import type { FC } from "react"; import { type LinkProps, Link as RouterLink } from "react-router"; @@ -14,7 +14,6 @@ export const PREMIUM_FEATURES = [ ]; export const PREMIUM_PAGE_PATH = "/deployment/premium"; -export const PREMIUM_PRICING_LINK = "https://coder.com/pricing"; export const PREMIUM_DEFAULT_DESCRIPTION = "You need a Premium license to use this feature."; export const PREMIUM_DEFAULT_HERO = "Get Access with Coder Premium"; @@ -113,25 +112,6 @@ export const PaywallDescription: FC> = ({ ); }; -export const PaywallDocumentationLink: FC> = ({ - children = "Read the documentation", - className, - href, - ...props -}) => { - return ( - - {children} - - ); -}; - export const PaywallSupergraphic: FC> = ({ className, ...props diff --git a/site/src/components/Paywall/PaywallPremium.stories.tsx b/site/src/components/Paywall/PaywallPremium.stories.tsx index a72e4fbbced..4d7160116d0 100644 --- a/site/src/components/Paywall/PaywallPremium.stories.tsx +++ b/site/src/components/Paywall/PaywallPremium.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, within } from "storybook/test"; -import { PREMIUM_PRICING_LINK } from "#/components/Paywall/Paywall"; import { PaywallPremium } from "./PaywallPremium"; const meta: Meta = { @@ -30,12 +29,9 @@ export const CanViewLicenses: Story = { await expect(cta).toHaveAttribute("href", "/deployment/premium"); await expect(cta).not.toHaveAttribute("target", "_blank"); - const pricing = canvas.getByRole("link", { - name: "Learn more about premium", - }); - await expect(pricing).toBeVisible(); - await expect(pricing).toHaveAttribute("href", PREMIUM_PRICING_LINK); - await expect(pricing).toHaveAttribute("target", "_blank"); + await expect( + canvas.queryByRole("link", { name: "Learn more about premium" }), + ).not.toBeInTheDocument(); await expect( canvas.getByRole("heading", { @@ -65,8 +61,8 @@ export const CannotViewLicenses: Story = { canvas.queryByRole("link", { name: "Start trial for free" }), ).not.toBeInTheDocument(); await expect( - canvas.getByRole("link", { name: "Learn more about premium" }), - ).toBeVisible(); + canvas.queryByRole("link", { name: "Learn more about premium" }), + ).not.toBeInTheDocument(); }, }; diff --git a/site/src/components/Paywall/PaywallPremium.tsx b/site/src/components/Paywall/PaywallPremium.tsx index cc3638cb76c..95297b6656f 100644 --- a/site/src/components/Paywall/PaywallPremium.tsx +++ b/site/src/components/Paywall/PaywallPremium.tsx @@ -1,4 +1,3 @@ -import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; import { Supergraphic } from "#/components/Supergraphic/Supergraphic"; import { cn } from "#/utils/cn"; @@ -13,7 +12,6 @@ import { PREMIUM_DEFAULT_HERO, PREMIUM_FEATURES, PREMIUM_PAGE_PATH, - PREMIUM_PRICING_LINK, } from "./Paywall"; const DEFAULT_HERO_SUBTITLE = "Start a 30-day trial today."; @@ -88,15 +86,6 @@ const PaywallPremium = ({

{description}

- - Learn more about premium - {features.map((feature) => ( diff --git a/site/src/components/Paywall/PaywallSmall.stories.tsx b/site/src/components/Paywall/PaywallSmall.stories.tsx index a0cefd14ba0..b96d238db4c 100644 --- a/site/src/components/Paywall/PaywallSmall.stories.tsx +++ b/site/src/components/Paywall/PaywallSmall.stories.tsx @@ -37,8 +37,8 @@ export const CannotViewLicenses: Story = { const canvas = within(canvasElement); await expect( - canvas.getByRole("link", { name: "Learn more about premium" }), - ).toBeVisible(); + canvas.queryByRole("link", { name: "Learn more about premium" }), + ).not.toBeInTheDocument(); await expect( canvas.getByText(/contact your deployment administrator/i), ).toBeVisible(); diff --git a/site/src/components/Paywall/PaywallSmall.tsx b/site/src/components/Paywall/PaywallSmall.tsx index e357840f4d2..5833f3c518d 100644 --- a/site/src/components/Paywall/PaywallSmall.tsx +++ b/site/src/components/Paywall/PaywallSmall.tsx @@ -4,7 +4,6 @@ import { PaywallContent, PaywallCTALink, PaywallDescription, - PaywallDocumentationLink, PaywallFeature, PaywallFeatures, PaywallGuidance, @@ -16,7 +15,6 @@ import { PREMIUM_DEFAULT_HERO, PREMIUM_FEATURES, PREMIUM_PAGE_PATH, - PREMIUM_PRICING_LINK, } from "./Paywall"; const PaywallSmall = ({ @@ -50,9 +48,6 @@ const PaywallSmall = ({ > {description} - - Learn more about premium - diff --git a/site/src/pages/AIBridgePage/AIBridgeSessionsLayout.tsx b/site/src/pages/AIBridgePage/AIBridgeSessionsLayout.tsx index 0f40f32d3ab..31a27c1a1a4 100644 --- a/site/src/pages/AIBridgePage/AIBridgeSessionsLayout.tsx +++ b/site/src/pages/AIBridgePage/AIBridgeSessionsLayout.tsx @@ -1,18 +1,22 @@ import type { FC, PropsWithChildren } from "react"; import { Outlet } from "react-router"; -import { Link } from "#/components/Link/Link"; import { Margins } from "#/components/Margins/Margins"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, } from "#/components/PageHeader/PageHeader"; +import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader"; import { docs } from "#/utils/docs"; const AIBridgeSessionsLayout: FC = () => { return ( - + + } + >
AI Sessions @@ -20,14 +24,7 @@ const AIBridgeSessionsLayout: FC = () => { Review and audit AI activity, token usage, and prompt history across - sessions.{" "} - - Learn how to audit AI sessions - + sessions. diff --git a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx index 5c151d415b5..9708010aaa7 100644 --- a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx +++ b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx @@ -3,11 +3,11 @@ import type { FC } from "react"; import type { AIGatewayKey } from "#/api/typesGenerated"; import { ErrorAlert } from "#/components/Alert/ErrorAlert"; import { Button } from "#/components/Button/Button"; -import { Link } from "#/components/Link/Link"; import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; import { SettingsHeader, SettingsHeaderDescription, + SettingsHeaderDocsLink, SettingsHeaderTitle, } from "#/components/SettingsHeader/SettingsHeader"; import { @@ -47,19 +47,25 @@ export const GatewayKeysPageView: FC = ({
- - Create key - - ) +
+ + {!showPaywall && ( + + )} +
} > AI Gateway Keys Keys authenticate standalone AI Gateway replicas to this deployment. - The key value is shown only once when created.{" "} - View docs + The key value is shown only once when created.
diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 2ed54a7fa3e..c6f9062253a 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -11,11 +11,13 @@ import { type PaginationResult, } from "#/components/PaginationWidget/PaginationContainer"; import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; +import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader"; import { Table, TableBody } from "#/components/Table/Table"; import { TableEmpty } from "#/components/TableEmpty/TableEmpty"; import { TableLoader } from "#/components/TableLoader/TableLoader"; import { Timeline } from "#/components/Timeline/Timeline"; import type { Permissions } from "#/modules/permissions"; +import { docs } from "#/utils/docs"; import { AuditFilter } from "./AuditFilter"; import { AuditHelpPopover } from "./AuditHelpPopover"; import { AuditLogRow } from "./AuditLogRow/AuditLogRow"; @@ -49,7 +51,11 @@ export const AuditPageView: FC = ({ return ( - + + } + >
Audit diff --git a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx index 921ab0f4273..c0b9da41b89 100644 --- a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx +++ b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx @@ -11,11 +11,13 @@ import { type PaginationResult, } from "#/components/PaginationWidget/PaginationContainer"; import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; +import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader"; import { Table, TableBody } from "#/components/Table/Table"; import { TableEmpty } from "#/components/TableEmpty/TableEmpty"; import { TableLoader } from "#/components/TableLoader/TableLoader"; import { Timeline } from "#/components/Timeline/Timeline"; import type { Permissions } from "#/modules/permissions"; +import { docs } from "#/utils/docs"; import { ConnectionLogFilter } from "./ConnectionLogFilter"; import { ConnectionLogHelpPopover } from "./ConnectionLogHelpPopover"; import { ConnectionLogRow } from "./ConnectionLogRow/ConnectionLogRow"; @@ -48,7 +50,13 @@ export const ConnectionLogPageView: FC = ({ return ( - + + } + >
Connection Log diff --git a/site/src/pages/DeploymentSettingsPage/AIGovernanceSettingsPage/AIGovernanceSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/AIGovernanceSettingsPage/AIGovernanceSettingsPageView.tsx index 00236f80e4a..f473196315e 100644 --- a/site/src/pages/DeploymentSettingsPage/AIGovernanceSettingsPage/AIGovernanceSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/AIGovernanceSettingsPage/AIGovernanceSettingsPageView.tsx @@ -31,7 +31,7 @@ export const AIGovernanceSettingsPageView: FC<
+ } > diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index 7c20768fc3b..dce55b3def7 100644 --- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx @@ -14,9 +14,11 @@ import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; import { SettingsHeader, SettingsHeaderDescription, + SettingsHeaderDocsLink, SettingsHeaderTitle, } from "#/components/SettingsHeader/SettingsHeader"; import { Spinner } from "#/components/Spinner/Spinner"; +import { docs } from "#/utils/docs"; import { getFormHelpers } from "#/utils/formUtils"; import { AnnouncementBannerSettings } from "./AnnouncementBannerSettings"; @@ -47,7 +49,11 @@ export const AppearanceSettingsPageView: FC< return (
- + + } + > Appearance Customize the look and feel of your Coder deployment. diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx index e9e09002033..9b8c35fa2d6 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx @@ -7,9 +7,9 @@ import { organizationIdpSyncSettings, patchOrganizationSyncSettings, } from "#/api/queries/idpsync"; -import { Link } from "#/components/Link/Link"; import { Loader } from "#/components/Loader/Loader"; import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; +import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; @@ -70,12 +70,14 @@ const IdpOrgSyncPage: FC = () => {

Automatically assign users to an organization based on their IdP claims. - - View docs -

- +
+ + +
{!isIdpSyncEnabled ? (
} + actions={ + + } > Observability diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 993f30fc1eb..22c96a2998b 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -14,12 +14,14 @@ import { Loader } from "#/components/Loader/Loader"; import { SettingsHeader, SettingsHeaderDescription, + SettingsHeaderDocsLink, SettingsHeaderTitle, } from "#/components/SettingsHeader/SettingsHeader"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { usePaginatedQuery } from "#/hooks/usePaginatedQuery"; import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; import { RequirePermission } from "#/modules/permissions/RequirePermission"; +import { docs } from "#/utils/docs"; import { pageTitle } from "#/utils/page"; import { useGroupsSettings } from "./GroupsPageProvider"; import { GroupsPageView, joinGroupsSpend } from "./GroupsPageView"; @@ -112,7 +114,11 @@ const GroupsPage: FC = () => { {title}
- + + } + > Groups Manage groups for this{" "} diff --git a/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx index eaa5d641a76..76b4a914861 100644 --- a/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx @@ -12,10 +12,12 @@ import { FormField } from "#/components/FormField/FormField"; import { IconField } from "#/components/IconField/IconField"; import { Label } from "#/components/Label/Label"; import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; +import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader"; import { Spinner } from "#/components/Spinner/Spinner"; import { Textarea } from "#/components/Textarea/Textarea"; import type { Permissions } from "#/modules/permissions"; import { cn } from "#/utils/cn"; +import { docs } from "#/utils/docs"; import { displayNameValidator, getFormHelpers, @@ -74,6 +76,9 @@ export const CreateOrganizationPageView: FC< Go Back
+
+ +
{Boolean(error) && !isApiValidationError(error) && ( diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index c80cbd8d8d9..7506e24402e 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -11,6 +11,7 @@ import { EmptyState } from "#/components/EmptyState/EmptyState"; import { SettingsHeader, SettingsHeaderDescription, + SettingsHeaderDocsLink, SettingsHeaderTitle, } from "#/components/SettingsHeader/SettingsHeader"; import { useAuthenticated } from "#/hooks/useAuthenticated"; @@ -18,6 +19,7 @@ import { useDashboard } from "#/modules/dashboard/useDashboard"; import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; import { useOrganizationSettings } from "#/modules/management/OrganizationSettingsLayout"; import { RequirePermission } from "#/modules/permissions/RequirePermission"; +import { docs } from "#/utils/docs"; import { pageTitle } from "#/utils/page"; import { CustomRolesPageView } from "./CustomRolesPageView"; @@ -81,7 +83,13 @@ const CustomRolesPage: FC = () => { isFeatureVisible={organizationPermissions?.viewOrgRoles ?? false} >
- + + } + > Roles Manage roles for this organization. diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 66748622260..13e51bf326f 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -13,8 +13,8 @@ import { } from "#/api/queries/organizations"; import { organizationRoles } from "#/api/queries/roles"; import { EmptyState } from "#/components/EmptyState/EmptyState"; -import { Link } from "#/components/Link/Link"; import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; +import { SettingsHeaderDocsLink } from "#/components/SettingsHeader/SettingsHeader"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; import { useOrganizationSettings } from "#/modules/management/OrganizationSettingsLayout"; @@ -125,9 +125,9 @@ const IdpSyncPage: FC = () => {

Automatically assign groups or roles to a user based on their IdP claims. - View docs

+ {!isIdpSyncEnabled ? ( - + } + > Provisioner Keys - Manage provisioner keys used to authenticate provisioner instances.{" "} - View docs + Manage provisioner keys used to authenticate provisioner instances. diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx index c164ced0bc8..5d291a40c2d 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx @@ -9,6 +9,7 @@ import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; import { SettingsHeader, SettingsHeaderDescription, + SettingsHeaderDocsLink, SettingsHeaderTitle, } from "#/components/SettingsHeader/SettingsHeader"; import { @@ -60,12 +61,13 @@ export const OrganizationProvisionersPageView: FC< }) => { return (
- + } + > Provisioners Coder server runs provisioner daemons which execute terraform during - workspace and template builds.{" "} - View docs + workspace and template builds. diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 6f172adfbbf..3efd877be5a 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -8,8 +8,15 @@ import { templateACL, } from "#/api/queries/templates"; import { PaywallPremium } from "#/components/Paywall/PaywallPremium"; +import { + SettingsHeader, + SettingsHeaderDescription, + SettingsHeaderDocsLink, + SettingsHeaderTitle, +} from "#/components/SettingsHeader/SettingsHeader"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; +import { docs } from "#/utils/docs"; import { pageTitle } from "#/utils/page"; import { useTemplateSettings } from "../TemplateSettingsLayout"; import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView"; @@ -30,9 +37,22 @@ const TemplatePermissionsPage: FC = () => { const removeGroupMutation = useMutation(setGroupRole(queryClient)); return ( - <> +
Codestin Search App + + } + > + Permissions + + Manage which members and groups can use this template. + + + {!isTemplateRBACEnabled ? ( { }} /> )} - +
); }; diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx index 764840d3a7e..ee0a9854fa4 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx @@ -24,11 +24,6 @@ import { SelectTrigger, SelectValue, } from "#/components/Select/Select"; -import { - SettingsHeader, - SettingsHeaderDescription, - SettingsHeaderTitle, -} from "#/components/SettingsHeader/SettingsHeader"; import { Spinner } from "#/components/Spinner/Spinner"; import { Table, @@ -227,13 +222,6 @@ export const TemplatePermissionsPageView: FC< }) => { return (
- - Permissions - - Manage which members and groups can use this template. - - -
{canUpdatePermissions && ( Date: Thu, 20 Aug 2026 18:25:19 +0000 Subject: [PATCH 2/4] fix(site/src/components/Paywall): update premium trial CTA copy Change the paywall hero and subtitle to lead with the trial rather than the Premium plan name: "Get access with a Coder trial" / "Start an unlimited 30-day trial today". Update the matching Storybook interaction assertions. --- site/src/components/Paywall/Paywall.tsx | 2 +- site/src/components/Paywall/PaywallPremium.stories.tsx | 6 ++++-- site/src/components/Paywall/PaywallPremium.tsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/site/src/components/Paywall/Paywall.tsx b/site/src/components/Paywall/Paywall.tsx index 161ba9600ea..a13ffb3386c 100644 --- a/site/src/components/Paywall/Paywall.tsx +++ b/site/src/components/Paywall/Paywall.tsx @@ -16,7 +16,7 @@ export const PREMIUM_FEATURES = [ export const PREMIUM_PAGE_PATH = "/deployment/premium"; export const PREMIUM_DEFAULT_DESCRIPTION = "You need a Premium license to use this feature."; -export const PREMIUM_DEFAULT_HERO = "Get Access with Coder Premium"; +export const PREMIUM_DEFAULT_HERO = "Get access with a Coder trial"; export type PaywallProps = React.ComponentProps<"div"> & { message: string; diff --git a/site/src/components/Paywall/PaywallPremium.stories.tsx b/site/src/components/Paywall/PaywallPremium.stories.tsx index 4d7160116d0..7344046e2e5 100644 --- a/site/src/components/Paywall/PaywallPremium.stories.tsx +++ b/site/src/components/Paywall/PaywallPremium.stories.tsx @@ -20,9 +20,11 @@ export const CanViewLicenses: Story = { const canvas = within(canvasElement); await expect( - canvas.getByRole("heading", { name: /Coder Premium/ }), + canvas.getByRole("heading", { name: /Get access with a Coder trial/ }), + ).toBeVisible(); + await expect( + canvas.getByText("Start an unlimited 30-day trial today"), ).toBeVisible(); - await expect(canvas.getByText("Start a 30-day trial today.")).toBeVisible(); const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toBeVisible(); diff --git a/site/src/components/Paywall/PaywallPremium.tsx b/site/src/components/Paywall/PaywallPremium.tsx index 95297b6656f..a480efa66ad 100644 --- a/site/src/components/Paywall/PaywallPremium.tsx +++ b/site/src/components/Paywall/PaywallPremium.tsx @@ -14,7 +14,7 @@ import { PREMIUM_PAGE_PATH, } from "./Paywall"; -const DEFAULT_HERO_SUBTITLE = "Start a 30-day trial today."; +const DEFAULT_HERO_SUBTITLE = "Start an unlimited 30-day trial today"; const PaywallPremiumHeader: FC> = ({ children, From 86d1509e34cbedc7844f003f4371b6ad94abfe70 Mon Sep 17 00:00:00 2001 From: default Date: Thu, 20 Aug 2026 21:30:44 +0000 Subject: [PATCH 3/4] fix(site/src): align docs links with header layout and cover them in stories Drop the flex wrappers around SettingsHeader on the groups and roles pages so the docs button reaches the right edge like every other settings page, keep the template permissions Codestin Search App - - } - > - Permissions - - Manage which members and groups can use this template. - - +
+ + } + > + Permissions + + Manage which members and groups can use this template. + + - {!isTemplateRBACEnabled ? ( - - ) : ( - { - await addUserMutation.mutateAsync({ - templateId: template.id, - userId: user.id, - role, - }); - reset(); - }} - isAddingUser={addUserMutation.isPending} - onUpdateUser={async (user, role) => { - await updateUserMutation.mutateAsync( - { + {!isTemplateRBACEnabled ? ( + + ) : ( + { + await addUserMutation.mutateAsync({ templateId: template.id, userId: user.id, role, - }, - { - onSuccess: () => { - toast.success( - `Role for "${user.username}" updated to "${role}" successfully.`, - ); + }); + reset(); + }} + isAddingUser={addUserMutation.isPending} + onUpdateUser={async (user, role) => { + await updateUserMutation.mutateAsync( + { + templateId: template.id, + userId: user.id, + role, }, - onError: (error) => { - toast.error( - getErrorMessage( - error, - `Failed to update role for "${user.username}".`, - ), - { - description: getErrorDetail(error), - }, - ); + { + onSuccess: () => { + toast.success( + `Role for "${user.username}" updated to "${role}" successfully.`, + ); + }, + onError: (error) => { + toast.error( + getErrorMessage( + error, + `Failed to update role for "${user.username}".`, + ), + { + description: getErrorDetail(error), + }, + ); + }, }, - }, - ); - }} - updatingUserId={ - updateUserMutation.isPending - ? updateUserMutation.variables?.userId - : undefined - } - onRemoveUser={async (user) => { - await removeUserMutation.mutateAsync( - { - templateId: template.id, - userId: user.id, - role: "", - }, - { - onSuccess: () => { - toast.success( - `User "${user.username}" removed successfully.`, - ); + ); + }} + updatingUserId={ + updateUserMutation.isPending + ? updateUserMutation.variables?.userId + : undefined + } + onRemoveUser={async (user) => { + await removeUserMutation.mutateAsync( + { + templateId: template.id, + userId: user.id, + role: "", }, - onError: (error) => { - toast.error( - getErrorMessage( - error, - `Failed to remove user "${user.username}".`, - ), - { - description: getErrorDetail(error), - }, - ); + { + onSuccess: () => { + toast.success( + `User "${user.username}" removed successfully.`, + ); + }, + onError: (error) => { + toast.error( + getErrorMessage( + error, + `Failed to remove user "${user.username}".`, + ), + { + description: getErrorDetail(error), + }, + ); + }, }, - }, - ); - }} - onAddGroup={async (group, role, reset) => { - await addGroupMutation.mutateAsync({ - templateId: template.id, - groupId: group.id, - role, - }); - reset(); - }} - isAddingGroup={addGroupMutation.isPending} - onUpdateGroup={async (group, role) => { - await updateGroupMutation.mutateAsync( - { + ); + }} + onAddGroup={async (group, role, reset) => { + await addGroupMutation.mutateAsync({ templateId: template.id, groupId: group.id, role, - }, - { - onSuccess: () => { - toast.success( - `Role for "${group.display_name || group.name}" updated to "${role}" successfully.`, - ); + }); + reset(); + }} + isAddingGroup={addGroupMutation.isPending} + onUpdateGroup={async (group, role) => { + await updateGroupMutation.mutateAsync( + { + templateId: template.id, + groupId: group.id, + role, }, - onError: (error) => { - toast.error( - getErrorMessage( - error, - `Failed to update role for "${group.display_name || group.name}".`, - ), - { - description: getErrorDetail(error), - }, - ); + { + onSuccess: () => { + toast.success( + `Role for "${group.display_name || group.name}" updated to "${role}" successfully.`, + ); + }, + onError: (error) => { + toast.error( + getErrorMessage( + error, + `Failed to update role for "${group.display_name || group.name}".`, + ), + { + description: getErrorDetail(error), + }, + ); + }, }, - }, - ); - }} - updatingGroupId={ - updateGroupMutation.isPending - ? updateGroupMutation.variables?.groupId - : undefined - } - onRemoveGroup={async (group) => { - await removeGroupMutation.mutateAsync( - { - groupId: group.id, - templateId: template.id, - role: "", - }, - { - onSuccess: () => { - toast.success( - `Group "${group.display_name || group.name}" removed successfully.`, - ); + ); + }} + updatingGroupId={ + updateGroupMutation.isPending + ? updateGroupMutation.variables?.groupId + : undefined + } + onRemoveGroup={async (group) => { + await removeGroupMutation.mutateAsync( + { + groupId: group.id, + templateId: template.id, + role: "", }, - onError: (error) => { - toast.error( - getErrorMessage( - error, - `Failed to remove group "${group.display_name || group.name}".`, - ), - { - description: getErrorDetail(error), - }, - ); + { + onSuccess: () => { + toast.success( + `Group "${group.display_name || group.name}" removed successfully.`, + ); + }, + onError: (error) => { + toast.error( + getErrorMessage( + error, + `Failed to remove group "${group.display_name || group.name}".`, + ), + { + description: getErrorDetail(error), + }, + ); + }, }, - }, - ); - }} - /> - )} -
+ ); + }} + /> + )} +
+ ); }; From bcb1c66185ac7f68dac267b4e23358cb33248dd5 Mon Sep 17 00:00:00 2001 From: default Date: Thu, 20 Aug 2026 21:44:39 +0000 Subject: [PATCH 4/4] chore(.github/workflows): allow SIEMs in the typos config The connection log paywall bullet mentions SIEMs, which typos splits into the "SIE" identifier and flags as a misspelling of SIZE. --- .github/workflows/typos.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index f2765a3ec97..88bfebbdc75 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -10,6 +10,8 @@ MacOS = "macOS" AKS = "AKS" O_WRONLY = "O_WRONLY" AIBridge = "AI Bridge" +# plural of SIEM, as in security information and event management +SIEMs = "SIEMs" [default.extend-words] AKS = "AKS"