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" diff --git a/site/src/components/Paywall/Paywall.tsx b/site/src/components/Paywall/Paywall.tsx index 4d1a1c81001..a13ffb3386c 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,10 +14,9 @@ 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"; +export const PREMIUM_DEFAULT_HERO = "Get access with a Coder trial"; export type PaywallProps = React.ComponentProps<"div"> & { message: string; @@ -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/PaywallAIGovernance.stories.tsx b/site/src/components/Paywall/PaywallAIGovernance.stories.tsx index a7413097af3..58a9dc88dd6 100644 --- a/site/src/components/Paywall/PaywallAIGovernance.stories.tsx +++ b/site/src/components/Paywall/PaywallAIGovernance.stories.tsx @@ -10,3 +10,9 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const Sessions: Story = { + args: { + variant: "sessions", + }, +}; diff --git a/site/src/components/Paywall/PaywallAIGovernance.tsx b/site/src/components/Paywall/PaywallAIGovernance.tsx index 5f579b9f4f6..94b85f286a9 100644 --- a/site/src/components/Paywall/PaywallAIGovernance.tsx +++ b/site/src/components/Paywall/PaywallAIGovernance.tsx @@ -1,20 +1,46 @@ import { PaywallSmall } from "#/components/Paywall/PaywallSmall"; -const PAYWALL_AIGATEWAY_DESCRIPTION = - "AI Gateway provides auditable visibility into user prompts and LLM tool calls from developer tools within Coder Workspaces. AI Gateway requires a Premium license with AI Governance add-on."; +type PaywallAIGovernanceVariant = "governance" | "sessions"; + +const PAYWALL_AIGOVERNANCE_COPY: Record< + PaywallAIGovernanceVariant, + { description: string; features: string[] } +> = { + governance: { + description: + "Get a full audit trail of every prompt, tool call, and model response, so AI adoption stays visible, secure, and accountable.", + features: [ + "Centralized auth, no scattered API keys", + "Approve MCP servers & tools org-wide", + "Per-user token spend & usage tracking", + ], + }, + sessions: { + description: + "Trace every AI coding session step by step to see which prompt triggered which tool call, and who was behind it.", + features: [ + "Full session & thread-level detail", + "Attribute every action to a user", + "Filter by user, project, or date", + ], + }, +}; + +type PaywallAIGovernanceProps = { + variant?: PaywallAIGovernanceVariant; +}; + +const PaywallAIGovernance = ({ + variant = "governance", +}: PaywallAIGovernanceProps) => { + const { description, features } = PAYWALL_AIGOVERNANCE_COPY[variant]; -const PaywallAIGovernance = () => { return ( ); }; diff --git a/site/src/components/Paywall/PaywallPremium.stories.tsx b/site/src/components/Paywall/PaywallPremium.stories.tsx index a72e4fbbced..7344046e2e5 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 = { @@ -21,21 +20,20 @@ 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(); 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 +63,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..a480efa66ad 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,10 +12,9 @@ import { PREMIUM_DEFAULT_HERO, PREMIUM_FEATURES, PREMIUM_PAGE_PATH, - PREMIUM_PRICING_LINK, } 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, @@ -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/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx index 21299d54c36..534925a5dd3 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx @@ -64,7 +64,7 @@ export const ListSessionsPageView: FC = ({ onSessionRowClick, }) => { if (!isAISessionsEntitled) { - return ; + return ; } if (!isAISessionsEnabled) { diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx index ba12f4eca13..0a02cedbfe4 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx @@ -62,7 +62,7 @@ export const SessionThreadsPageView: FC = ({ onBackClicked, }) => { if (!isAISessionsEntitled) { - return ; + return ; } if (!isAISessionsEnabled) { diff --git a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.stories.tsx b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.stories.tsx index 593263a66c3..cf2d6169770 100644 --- a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.stories.tsx @@ -5,6 +5,7 @@ import { MockPermissions, mockApiError, } from "#/testHelpers/entities"; +import { docs } from "#/utils/docs"; import { GatewayKeysPageView } from "./GatewayKeysPageView"; const meta: Meta = { @@ -81,6 +82,12 @@ export const Paywall: Story = { const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toHaveAttribute("href", "/deployment/premium"); + await expect( + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute( + "href", + docs("/ai-coder/ai-gateway/standalone#create-a-gateway-key"), + ); }, }; diff --git a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx index 6dfde035db3..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,26 +47,36 @@ 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.
{showPaywall && ( )} diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index d9d94536cb0..030ed84c0c5 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -18,6 +18,7 @@ import { MockUserOwner, } from "#/testHelpers/entities"; import { pixelWithTablet } from "#/testHelpers/pixel"; +import { docs } from "#/utils/docs"; import { useResourceTypeFilterMenu } from "./AuditFilter"; import { AuditPageView } from "./AuditPageView"; @@ -100,6 +101,9 @@ export const NotVisible: Story = { const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toHaveAttribute("href", "/deployment/premium"); + await expect( + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute("href", docs("/admin/security/audit-logs")); }, }; diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 71d0eafe03d..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 @@ -84,7 +90,12 @@ export const AuditPageView: FC = ({ ) : ( )} diff --git a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.stories.tsx b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.stories.tsx index a981770aad9..ff238731a5c 100644 --- a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.stories.tsx +++ b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.stories.tsx @@ -17,6 +17,7 @@ import { MockUserOwner, } from "#/testHelpers/entities"; import { pixelWithTablet } from "#/testHelpers/pixel"; +import { docs } from "#/utils/docs"; import { ConnectionLogPageView } from "./ConnectionLogPageView"; type FilterProps = ComponentProps["filterProps"]; @@ -100,6 +101,9 @@ export const NotVisible: Story = { const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toHaveAttribute("href", "/deployment/premium"); + await expect( + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute("href", docs("/admin/monitoring/connection-logs")); }, }; diff --git a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx index aeebf9a073b..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 @@ -84,7 +92,12 @@ export const ConnectionLogPageView: FC = ({ ) : ( )} 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.stories.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx index c917ccd729d..bc81d061c79 100644 --- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, within } from "storybook/test"; import { MockPermissions } from "#/testHelpers/entities"; +import { docs } from "#/utils/docs"; import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView"; const meta: Meta = { @@ -56,6 +57,9 @@ export const NotEntitled: Story = { const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toHaveAttribute("href", "/deployment/premium"); + await expect( + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute("href", docs("/admin/setup/appearance")); await expect( canvas.queryByRole("form", { name: "Appearance settings" }), ).not.toBeInTheDocument(); diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index 9ee5c1f381f..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. @@ -57,7 +63,13 @@ export const AppearanceSettingsPageView: FC< {!isEntitled ? ( ) : ( diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx index cf02bd65710..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,17 +70,24 @@ const IdpOrgSyncPage: FC = () => {

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

- +
+ + +
{!isIdpSyncEnabled ? ( ) : ( diff --git a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx index 1c397954c8e..87ef677ef32 100644 --- a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, within } from "storybook/test"; import type { SerpentGroup } from "#/api/typesGenerated"; import { MockPermissions } from "#/testHelpers/entities"; +import { docs } from "#/utils/docs"; import { ObservabilitySettingsPageView } from "./ObservabilitySettingsPageView"; const introspectionGroup: SerpentGroup = { @@ -82,8 +83,8 @@ export const OSS: Story = { canvas.getByRole("link", { name: "Start trial for free" }), ).toHaveAttribute("href", "/deployment/premium"); await expect( - canvas.getByRole("link", { name: "Learn more about premium" }), - ).toBeVisible(); + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute("href", docs("/admin/security/audit-logs")); await expect( canvas.queryByText("Audit Logs Retention"), ).not.toBeInTheDocument(); diff --git a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx index 7667e09cdf0..25a5ba91f7a 100644 --- a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx @@ -24,7 +24,9 @@ export const ObservabilitySettingsPageView: FC<
} + actions={ + + } > Observability @@ -45,7 +47,13 @@ export const ObservabilitySettingsPageView: FC< ) : ( )} diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 993f30fc1eb..fcf2eff29e4 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"; @@ -111,15 +113,17 @@ const GroupsPage: FC = () => {
{title} -
- - Groups - - Manage groups for this{" "} - {showOrganizations ? "organization" : "deployment"}. - - -
+ + } + > + Groups + + Manage groups for this{" "} + {showOrganizations ? "organization" : "deployment"}. + + = ({ return ( ); diff --git a/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx index 9d2322cb043..453db8c8aca 100644 --- a/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, screen, userEvent, within } from "storybook/test"; import { MockPermissions, mockApiError } from "#/testHelpers/entities"; +import { docs } from "#/utils/docs"; import { CreateOrganizationPageView } from "./CreateOrganizationPageView"; const meta: Meta = { @@ -35,8 +36,8 @@ export const NotEntitled: Story = { const canvas = within(canvasElement); await expect( - canvas.getByRole("link", { name: "Learn more about premium" }), - ).toBeVisible(); + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute("href", docs("/admin/users/organizations")); const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toHaveAttribute("href", "/deployment/premium"); }, diff --git a/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx index 1b0e4cdc6e3..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) && ( @@ -100,7 +105,12 @@ export const CreateOrganizationPageView: FC<
diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index c80cbd8d8d9..611117319c4 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"; @@ -80,14 +82,16 @@ const CustomRolesPage: FC = () => { -
- - Roles - - Manage roles for this organization. - - -
+ + } + > + Roles + + Manage roles for this organization. + + = ({ {!isCustomRolesEnabled && ( )} diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 3cb3cb3c252..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,14 +125,20 @@ const IdpSyncPage: FC = () => {

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

+ {!isIdpSyncEnabled ? ( ) : ( diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx index 1540a8211d5..ce34141fe94 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx @@ -12,6 +12,7 @@ import { MockProvisionerKey, mockApiError, } from "#/testHelpers/entities"; +import { docs } from "#/utils/docs"; import { OrganizationProvisionerKeysPageView } from "./OrganizationProvisionerKeysPageView"; const mockProvisionerKeyDaemons: ProvisionerKeyDaemons[] = [ @@ -99,6 +100,9 @@ export const Paywalled: Story = { const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toHaveAttribute("href", "/deployment/premium"); + await expect( + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute("href", docs("/admin/provisioners")); }, }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx index bbab0b5d3ff..9b81750de9d 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx @@ -6,11 +6,11 @@ import { ProvisionerKeyIDUserAuth, } from "#/api/typesGenerated"; 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 { @@ -51,18 +51,25 @@ export const OrganizationProvisionerKeysPageView: FC< return (
- + } + > Provisioner Keys - Manage provisioner keys used to authenticate provisioner instances.{" "} - View docs + Manage provisioner keys used to authenticate provisioner instances. {showPaywall ? ( ) : ( diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx index 5d953a3acb6..340a75608bd 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx @@ -8,6 +8,7 @@ import { MockUserProvisioner, mockApiError, } from "#/testHelpers/entities"; +import { docs } from "#/utils/docs"; import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView"; const meta: Meta = { @@ -75,6 +76,9 @@ export const Paywall: Story = { const cta = canvas.getByRole("link", { name: "Start trial for free" }); await expect(cta).toHaveAttribute("href", "/deployment/premium"); + await expect( + canvas.getByRole("link", { name: /Read the docs/ }), + ).toHaveAttribute("href", docs("/admin/provisioners")); }, }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx index 03626589d08..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. @@ -99,7 +101,13 @@ export const OrganizationProvisionersPageView: FC< {showPaywall ? ( ) : ( diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 5e2ad77e6d4..f2d87a851e6 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"; @@ -33,155 +40,174 @@ const TemplatePermissionsPage: FC = () => { <> {pageTitle(template.name, "Permissions")} - {!isTemplateRBACEnabled ? ( - - ) : ( - { - await addUserMutation.mutateAsync({ - templateId: template.id, - userId: user.id, - role, - }); - reset(); - }} - isAddingUser={addUserMutation.isPending} - onUpdateUser={async (user, role) => { - await updateUserMutation.mutateAsync( - { +
+ + } + > + Permissions + + Manage which members and groups can use this template. + + + + {!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), + }, + ); + }, }, - }, - ); - }} - /> - )} + ); + }} + /> + )} +
); }; 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 && ( = ({ {showPaywall ? ( ) : (