fix: refactor <Paywall /> component to be universal (#21740)

During development of #21659 I approved some `<Paywall />` code that had
an extensive props system, however, I wasn't a huge fan of this. This
approach attempts to take it further like something `shadcn` would,
where-in we define the `<Paywall />` (and its subset of components) and
we wrap around those when needed for `<PaywallAIGovernance />` and
`<PaywallPremium />`.

Theoretically there is no real CSS/Design changes here. However
screenshot for prosperity.

| Previously | Now |
| --- | --- |
| <img width="2306" height="614" alt="CleanShot 2026-01-29 at 10 56
05@2x"
src="https://github.com/user-attachments/assets/83a4aa1b-da74-459d-ae11-fae06c1a8371"
/> | <img width="2308" height="622" alt="CleanShot 2026-01-29 at 10 55
05@2x"
src="https://github.com/user-attachments/assets/4aa43b09-6705-4af3-86cc-edc0c08e53b1"
/> |

---------

Co-authored-by: Ben Potter <me@bpmct.net>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jake Howell
2026-01-30 23:44:07 +11:00
committed by GitHub
co-authored by Ben Potter Claude Opus 4.5
parent 036ed5672f
commit e45635aab6
24 changed files with 451 additions and 315 deletions
+9 -10
View File
@@ -1,4 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import { Badge } from "components/Badge/Badge";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
@@ -11,6 +12,7 @@ import {
type HTMLAttributes,
type PropsWithChildren,
} from "react";
import { cn } from "utils/cn";
const styles = {
badge: {
@@ -145,18 +147,15 @@ export const PremiumBadge: FC<PremiumBadgeProps> = ({
children = "Premium",
}) => {
return (
<span
css={[
styles.badge,
(theme) => ({
backgroundColor: theme.branding.premium.background,
border: `1px solid ${theme.branding.premium.border}`,
color: theme.branding.premium.text,
}),
]}
<Badge
size="sm"
className={cn(
"bg-surface-purple border border-solid border-border-purple uppercase",
"tracking-[0.085em] text-white rounded-full px-3 font-semibold",
)}
>
{children}
</span>
</Badge>
);
};
@@ -1,27 +0,0 @@
import type { ComponentProps } from "react";
import type { Paywall } from "./Paywall";
// Centralized AI Bridge paywall configuration to avoid duplication.
export const aiBridgePaywallConfig: Omit<ComponentProps<typeof Paywall>, ""> = {
message: "AI Bridge",
description:
"AI Bridge provides auditable visibility into user prompts and LLM tool calls from developer tools within Coder Workspaces. AI Bridge requires a Premium license with AI Governance add-on.",
documentationLink: "https://coder.com/docs/ai-coder/ai-governance",
documentationLinkText: "Learn about AI Governance",
badgeText: "AI Governance",
ctaText: "Contact Sales",
ctaLink: "https://coder.com/contact",
features: [
{ text: "Auditable history of user prompts" },
{ text: "Logged LLM tool invocations" },
{ text: "Token usage and consumption visibility" },
{ text: "Centrally-managed MCP servers" },
{
text: "Visit",
link: {
href: "https://coder.com/docs/ai-coder/ai-bridge",
text: "AI Bridge Docs",
},
},
],
};
@@ -1,5 +1,17 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Paywall } from "./Paywall";
import { PremiumBadge } from "components/Badges/Badges";
import {
Paywall,
PaywallContent,
PaywallCTA,
PaywallDescription,
PaywallFeature,
PaywallFeatures,
PaywallHeading,
PaywallSeparator,
PaywallStack,
PaywallTitle,
} from "./Paywall";
const meta: Meta<typeof Paywall> = {
title: "components/Paywall",
@@ -9,10 +21,40 @@ const meta: Meta<typeof Paywall> = {
export default meta;
type Story = StoryObj<typeof Paywall>;
export const Premium: Story = {
export const Default: Story = {
args: {
message: "Black Lotus",
description:
"Adds 3 mana of any single color of your choice to your mana pool, then is discarded. Tapping this artifact can be played as an interrupt.",
children: (
<>
<PaywallContent>
<PaywallHeading>
<PaywallTitle>Black Lotus</PaywallTitle>
<PremiumBadge />
</PaywallHeading>
<PaywallDescription>
Adds 3 mana of any single color of your choice to your mana pool,
then is discarded. Tapping this artifact can be played as an
interrupt.
</PaywallDescription>
</PaywallContent>
<PaywallSeparator />
<PaywallStack>
<PaywallFeatures>
<PaywallFeature>
High availability & workspace proxies
</PaywallFeature>
<PaywallFeature>
Multi-org & role-based access control
</PaywallFeature>
<PaywallFeature>24x7 global support with SLA</PaywallFeature>
<PaywallFeature>
Unlimited Git & external auth integrations
</PaywallFeature>
</PaywallFeatures>
<PaywallCTA href="https://coder.com/pricing#compare-plans">
Learn about Premium
</PaywallCTA>
</PaywallStack>
</>
),
},
};
+179 -121
View File
@@ -1,134 +1,192 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import { PremiumBadge } from "components/Badges/Badges";
import { Button } from "components/Button/Button";
import { Stack } from "components/Stack/Stack";
import { CircleCheckBigIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
import type React from "react";
import type { FC } from "react";
import { cn } from "utils/cn";
interface PaywallProps {
message: string;
description?: ReactNode;
documentationLink?: string;
documentationLinkText?: string;
features?: Array<{
text: string;
link?: { href: string; text: string };
}>;
badgeText?: string;
ctaText?: string;
ctaLink?: string;
}
export const Paywall: FC<PaywallProps> = ({
message,
description,
documentationLink,
documentationLinkText = "Read the documentation",
features,
badgeText = "Premium",
ctaText = "Learn about Premium",
ctaLink = "https://coder.com/pricing#compare-plans",
}) => {
const defaultFeatures: Array<{
text: string;
link?: { href: string; text: string };
}> = [
{ text: "High availability & workspace proxies" },
{ text: "Multi-org & role-based access control" },
{ text: "24x7 global support with SLA" },
{ text: "Unlimited Git & external auth integrations" },
];
const displayFeatures = features ?? defaultFeatures;
export const Paywall = ({
className,
children,
...props
}: React.ComponentProps<"div">) => {
return (
<div css={styles.root}>
<div>
<Stack direction="row" alignItems="center" className="mb-6">
<h5 className="font-semibold font-inherit text-xl m-0">{message}</h5>
<PremiumBadge>{badgeText}</PremiumBadge>
</Stack>
{description && (
<p className="font-inherit max-w-md text-sm mb-4">{description}</p>
)}
{documentationLink && (
<Link
href={documentationLink}
target="_blank"
rel="noreferrer"
className="font-semibold"
>
{documentationLinkText}
</Link>
)}
</div>
<div className="w-px h-[220px] bg-highlight-purple/50 ml-2" />
<Stack direction="column" alignItems="left" spacing={3}>
<ul className="m-0 px-6 text-sm font-medium">
{displayFeatures.map((feature, index) => (
<li key={index} css={styles.feature}>
<FeatureIcon />
{feature.text}
{feature.link && (
<>
{" "}
<Link
href={feature.link.href}
target="_blank"
rel="noreferrer"
className="font-semibold text-sm"
>
{feature.link.text}
</Link>
</>
)}
</li>
))}
</ul>
<div className="px-7">
<Button asChild>
<a href={ctaLink} target="_blank" rel="noreferrer">
{ctaText}
</a>
</Button>
</div>
</Stack>
<div
className={cn(
"flex flex-row items-center justify-center min-h-[280px] p-4 rounded-lg gap-8",
"border border-solid border-border-purple bg-[linear-gradient(160deg,transparent,hsl(var(--surface-purple)))]",
className,
)}
{...props}
>
{children}
</div>
);
};
const FeatureIcon: FC = () => {
export const PaywallContent: FC<React.ComponentProps<"div">> = ({
children,
...props
}) => {
return <div {...props}>{children}</div>;
};
export const PaywallHeading: FC<React.ComponentProps<"div">> = ({
children,
className,
...props
}) => {
return (
<CircleCheckBigIcon
aria-hidden="true"
className="size-icon-sm"
css={[
(theme) => ({
color: theme.branding.premium.border,
}),
]}
<div
className={cn("flex flex-row gap-4 items-center mb-6", className)}
{...props}
>
{children}
</div>
);
};
export const PaywallTitle: FC<React.ComponentProps<"h5">> = ({
children,
className,
...props
}) => {
return (
<h5
className={cn("font-semibold font-inherit text-xl m-0", className)}
{...props}
>
{children}
</h5>
);
};
export const PaywallDescription: FC<React.ComponentProps<"p">> = ({
children,
className,
...props
}) => {
return (
<p
className={cn("font-inherit max-w-md text-sm mb-4", className)}
{...props}
>
{children}
</p>
);
};
export const PaywallDocumentationLink: FC<React.ComponentProps<"a">> = ({
children = "Read the documentation",
className,
href,
...props
}) => {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className={cn("text-content-link font-medium", className)}
{...props}
>
{children}
</a>
);
};
export const PaywallSeparator: FC<React.ComponentProps<"hr">> = ({
className,
...props
}) => {
return (
<hr
className={cn(
"w-0 h-[220px] border-0 border-l border-highlight-purple/50 ml-2 mr-0",
className,
)}
{...props}
/>
);
};
const styles = {
root: (theme) => ({
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
minHeight: 280,
padding: 24,
borderRadius: 8,
gap: 32,
backgroundImage: `linear-gradient(160deg, transparent, ${theme.branding.premium.background})`,
border: `1px solid ${theme.branding.premium.border}`,
}),
feature: {
display: "flex",
alignItems: "center",
padding: 3,
gap: 8,
},
} satisfies Record<string, Interpolation<Theme>>;
export const PaywallStack: FC<React.ComponentProps<"div">> = ({
children,
className,
...props
}) => {
return (
<div
className={cn("flex flex-col items-start gap-6", className)}
{...props}
>
{children}
</div>
);
};
export const PaywallFeatures: FC<React.ComponentProps<"ul">> = ({
children,
className,
...props
}) => {
return (
<ul
className={cn("list-none m-0 px-6 text-sm font-medium", className)}
{...props}
>
{children}
</ul>
);
};
export const PaywallFeature: FC<React.ComponentProps<"li">> = ({
children,
className,
...props
}) => {
return (
<li className={cn("flex items-center gap-2 p-[3px]", className)} {...props}>
<FeatureIcon className="flex-shrink-0" />
<span className="flex-1">{children}</span>
</li>
);
};
export const PaywallCTA: FC<React.ComponentProps<"a">> = ({
children,
className,
href,
target = "_blank",
rel = "noreferrer",
...props
}) => {
return (
<Button asChild>
<a
href={href}
target={target}
rel={rel}
className={cn("mx-7", className)}
{...props}
>
{children}
</a>
</Button>
);
};
const FeatureIcon: FC<React.ComponentProps<"svg">> = ({
className,
...props
}) => {
return (
<CircleCheckBigIcon
aria-hidden="true"
className="size-icon-sm"
css={(theme) => ({
color: theme.branding.premium.border,
})}
{...props}
/>
);
};
@@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { PaywallAIGovernance } from "./PaywallAIGovernance";
const meta: Meta<typeof PaywallAIGovernance> = {
title: "components/Paywall/AIGovernance",
component: PaywallAIGovernance,
};
export default meta;
type Story = StoryObj<typeof PaywallAIGovernance>;
export const Default: Story = {};
@@ -0,0 +1,63 @@
import { PremiumBadge } from "components/Badges/Badges";
import { docs } from "utils/docs";
import {
Paywall,
PaywallContent,
PaywallCTA,
PaywallDescription,
PaywallDocumentationLink,
PaywallFeature,
PaywallFeatures,
PaywallHeading,
PaywallSeparator,
PaywallStack,
PaywallTitle,
} from "./Paywall";
const PaywallAIGovernance = () => {
return (
<Paywall>
<PaywallContent>
<PaywallHeading>
<PaywallTitle>AI Bridge</PaywallTitle>
<PremiumBadge>AI Governance</PremiumBadge>
</PaywallHeading>
<PaywallDescription>
AI Bridge provides auditable visibility into user prompts and LLM tool
calls from developer tools within Coder Workspaces. AI Bridge requires
a Premium license with AI Governance add-on.
</PaywallDescription>
<PaywallDocumentationLink href={docs("/ai-coder/ai-governance")}>
Learn about AI Governance
</PaywallDocumentationLink>
</PaywallContent>
<PaywallSeparator />
<PaywallStack>
<PaywallFeatures>
<PaywallFeature>Auditable history of user prompts</PaywallFeature>
<PaywallFeature>Logged LLM tool invocations</PaywallFeature>
<PaywallFeature>
Token usage and consumption visibility
</PaywallFeature>
<PaywallFeature>Centrally-managed MCP servers</PaywallFeature>
<PaywallFeature>
<span>
Visit{" "}
<a
href="https://coder.com/docs/ai-coder/ai-bridge"
target="_blank"
rel="noreferrer"
className="text-content-link"
>
AI Bridge Docs
</a>
</span>
</PaywallFeature>
</PaywallFeatures>
<PaywallCTA href="https://coder.com/contact">Contact Sales</PaywallCTA>
</PaywallStack>
</Paywall>
);
};
export { PaywallAIGovernance };
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { PaywallPremium } from "./PaywallPremium";
const meta: Meta<typeof PaywallPremium> = {
title: "components/Paywall/Premium",
component: PaywallPremium,
};
export default meta;
type Story = StoryObj<typeof PaywallPremium>;
export const Default: Story = {
args: {
message: "Black Lotus",
description:
"Adds 3 mana of any single color of your choice to your mana pool, then is discarded. Tapping this artifact can be played as an interrupt.",
documentationLink: "https://coder.com/docs",
},
};
@@ -0,0 +1,87 @@
import { PremiumBadge } from "components/Badges/Badges";
import type { ReactNode } from "react";
import { cn } from "utils/cn";
import {
Paywall,
PaywallContent,
PaywallCTA,
PaywallDescription,
PaywallDocumentationLink,
PaywallFeature,
PaywallFeatures,
PaywallHeading,
PaywallSeparator,
PaywallStack,
PaywallTitle,
} from "./Paywall";
type PaywallPremiumProps = React.ComponentProps<"div"> & {
message: string;
description: ReactNode;
documentationLink: string;
compact?: boolean;
};
const PaywallPremium = ({
message,
description,
documentationLink,
compact = false,
className,
...props
}: PaywallPremiumProps) => {
const PREMIUM_FEATURES = [
"High availability & workspace proxies",
"Multi-org & role-based access control",
"24x7 global support with SLA",
"Unlimited Git & external auth integrations",
];
return (
<Paywall
className={cn(
compact && "max-w-[770px] py-4 px-[36px] gap-[18px] min-h-[230px]",
className,
)}
{...props}
>
<PaywallContent>
<PaywallHeading className={cn(compact && "mb-[18px]")}>
<PaywallTitle className={cn(compact && "text-lg leading-none")}>
{message}
</PaywallTitle>
<PremiumBadge />
</PaywallHeading>
<PaywallDescription
className={cn(
compact &&
"text-sm max-w-[360px] mt-2 mb-3.5 leading-relaxed text-content-secondary",
)}
>
{description}
</PaywallDescription>
<PaywallDocumentationLink href={documentationLink}>
Read the documentation
</PaywallDocumentationLink>
</PaywallContent>
<PaywallSeparator className="h-[180px]" />
<PaywallStack className={cn(compact && "gap-4")}>
<PaywallFeatures className={cn(compact && "pr-0")}>
{PREMIUM_FEATURES.map((feature) => (
<PaywallFeature
className={cn(compact && "text-[13px] leading-tight")}
key={feature}
>
{feature}
</PaywallFeature>
))}
</PaywallFeatures>
<PaywallCTA href="https://coder.com/pricing#compare-plans">
Learn about Premium
</PaywallCTA>
</PaywallStack>
</Paywall>
);
};
export { PaywallPremium };
@@ -14,5 +14,6 @@ export const Premium: Story = {
message: "Black Lotus",
description:
"Adds 3 mana of any single color of your choice to your mana pool, then is discarded. Tapping this artifact can be played as an interrupt.",
documentationLink: "https://coder.com/docs",
},
};
+7 -126
View File
@@ -1,142 +1,23 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import { PremiumBadge } from "components/Badges/Badges";
import { Button } from "components/Button/Button";
import { Stack } from "components/Stack/Stack";
import { CircleCheckBigIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
import { PaywallPremium } from "./PaywallPremium";
interface PopoverPaywallProps {
message: string;
description?: ReactNode;
documentationLink?: string;
badgeText?: string;
ctaText?: string;
ctaLink?: string;
documentationLink: string;
}
export const PopoverPaywall: FC<PopoverPaywallProps> = ({
message,
description,
documentationLink,
badgeText = "Premium",
ctaText = "Learn about Premium",
ctaLink = "https://coder.com/pricing#compare-plans",
}) => {
return (
<div
css={[
styles.root,
(theme) => ({
backgroundImage: `linear-gradient(160deg, transparent, ${theme.branding.premium.background})`,
border: `1px solid ${theme.branding.premium.border}`,
}),
]}
>
<div>
<Stack direction="row" alignItems="center" css={{ marginBottom: 18 }}>
<h5 css={styles.title}>{message}</h5>
<PremiumBadge>{badgeText}</PremiumBadge>
</Stack>
{description && <p css={styles.description}>{description}</p>}
<Link
href={documentationLink}
target="_blank"
rel="noreferrer"
css={{ fontWeight: 600 }}
>
Read the documentation
</Link>
</div>
<div css={styles.separator} />
<Stack direction="column" alignItems="left" spacing={2}>
<ul css={styles.featureList}>
<li css={styles.feature}>
<FeatureIcon /> High availability & workspace proxies
</li>
<li css={styles.feature}>
<FeatureIcon /> Multi-org & role-based access control
</li>
<li css={styles.feature}>
<FeatureIcon /> 24x7 global support with SLA
</li>
<li css={styles.feature}>
<FeatureIcon /> Unlimited Git & external auth integrations
</li>
</ul>
<div css={styles.learnButton}>
<Button asChild>
<a href={ctaLink} target="_blank" rel="noreferrer">
{ctaText}
</a>
</Button>
</div>
</Stack>
</div>
);
};
const FeatureIcon: FC = () => {
return (
<CircleCheckBigIcon
aria-hidden="true"
className="size-icon-sm"
css={[
(theme) => ({
color: theme.branding.premium.border,
}),
]}
<PaywallPremium
message={message}
description={description}
documentationLink={documentationLink}
compact={true}
/>
);
};
const styles = {
root: {
display: "flex",
flexDirection: "row",
alignItems: "center",
maxWidth: 770,
padding: "24px 36px",
borderRadius: 8,
gap: 18,
},
title: {
fontWeight: 600,
fontFamily: "inherit",
fontSize: 18,
margin: 0,
},
description: (theme) => ({
marginTop: 8,
fontFamily: "inherit",
maxWidth: 360,
lineHeight: "160%",
color: theme.palette.text.secondary,
fontSize: 14,
}),
separator: (theme) => ({
width: 1,
height: 180,
backgroundColor: theme.palette.divider,
marginLeft: 8,
}),
featureList: {
listStyle: "none",
margin: 0,
marginRight: 8,
padding: "0 0 0 24px",
fontSize: 13,
fontWeight: 500,
},
learnButton: {
padding: "0 28px",
},
feature: {
display: "flex",
alignItems: "center",
padding: 3,
gap: 8,
lineHeight: 1.2,
},
} satisfies Record<string, Interpolation<Theme>>;
+2
View File
@@ -31,6 +31,7 @@
--surface-sky: 201 94% 86%;
--surface-red: 0 93% 94%;
--surface-purple: 251 91% 95%;
--border-purple: 255 92% 76%;
--border-default: 240 6% 90%;
--border-success: 142 76% 36%;
--border-sky: 203 90% 40%;
@@ -74,6 +75,7 @@
--surface-sky: 204 80% 16%;
--surface-red: 0 75% 15%;
--surface-purple: 261 73% 23%;
--border-purple: 255 92% 76%;
--border-default: 240 4% 16%;
--border-success: 142 76% 36%;
--border-warning: 30.66, 97.16%, 72.35%;
@@ -3,8 +3,7 @@ import {
PaginationContainer,
type PaginationResult,
} from "components/PaginationWidget/PaginationContainer";
import { aiBridgePaywallConfig } from "components/Paywall/AIBridgePaywallConfig";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallAIGovernance } from "components/Paywall/PaywallAIGovernance";
import {
Table,
TableBody,
@@ -34,7 +33,7 @@ export const RequestLogsPageView: FC<RequestLogsPageViewProps> = ({
filterProps,
}) => {
if (!isRequestLogsVisible) {
return <Paywall {...aiBridgePaywallConfig} />;
return <PaywallAIGovernance />;
}
return (
+2 -2
View File
@@ -11,7 +11,7 @@ import {
PaginationContainer,
type PaginationResult,
} from "components/PaginationWidget/PaginationContainer";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import { Stack } from "components/Stack/Stack";
import { Table, TableBody, TableCell, TableRow } from "components/Table/Table";
import { TableLoader } from "components/TableLoader/TableLoader";
@@ -130,7 +130,7 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
</Cond>
<Cond>
<Paywall
<PaywallPremium
message="Audit logs"
description="Audit logs allow you to monitor user operations on your deployment. You need a Premium license to use this feature."
documentationLink={docs("/admin/security/audit-logs")}
@@ -11,7 +11,7 @@ import {
PaginationContainer,
type PaginationResult,
} from "components/PaginationWidget/PaginationContainer";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import { Stack } from "components/Stack/Stack";
import { Table, TableBody, TableCell, TableRow } from "components/Table/Table";
import { TableLoader } from "components/TableLoader/TableLoader";
@@ -125,7 +125,7 @@ export const ConnectionLogPageView: FC<ConnectionLogPageViewProps> = ({
</Cond>
<Cond>
<Paywall
<PaywallPremium
message="Connection logs"
description="Connection logs allow you to see how and when users connect to workspaces. You need a Premium license to use this feature."
documentationLink={docs("/admin/monitoring/connection-logs")}
@@ -1,6 +1,5 @@
import type { SerpentOption } from "api/typesGenerated";
import { aiBridgePaywallConfig } from "components/Paywall/AIBridgePaywallConfig";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallAIGovernance } from "components/Paywall/PaywallAIGovernance";
import {
SettingsHeader,
SettingsHeaderDescription,
@@ -49,7 +48,7 @@ export const AIGovernanceSettingsPageView: FC<
/>
</div>
) : (
<Paywall {...aiBridgePaywallConfig} />
<PaywallAIGovernance />
)}
</Stack>
);
@@ -8,7 +8,7 @@ import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Link } from "components/Link/Link";
import { Loader } from "components/Loader/Loader";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { type FC, useEffect, useState } from "react";
@@ -78,7 +78,7 @@ const IdpOrgSyncPage: FC = () => {
</header>
<ChooseOne>
<Cond condition={!isIdpSyncEnabled}>
<Paywall
<PaywallPremium
message="IdP Organization Sync"
description="Configure organization mappings to synchronize claims in your auth provider to organizations within Coder. You need a Premium license to use this feature."
documentationLink={docs("/admin/users/idp-sync")}
+2 -2
View File
@@ -8,7 +8,7 @@ import { Badge } from "components/Badge/Badge";
import { Button } from "components/Button/Button";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import {
Table,
TableBody,
@@ -44,7 +44,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
return (
<ChooseOne>
<Cond condition={!groupsEnabled}>
<Paywall
<PaywallPremium
message="Groups"
description="Organize users into groups with restricted access to templates. You need a Premium license to use this feature."
documentationLink={docs("/admin/users/groups-roles")}
@@ -6,7 +6,7 @@ import { Badges, PremiumBadge } from "components/Badges/Badges";
import { Button } from "components/Button/Button";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { IconField } from "components/IconField/IconField";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
import { Spinner } from "components/Spinner/Spinner";
import {
@@ -115,7 +115,7 @@ export const CreateOrganizationPageView: FC<
<ChooseOne>
<Cond condition={!isEntitled}>
<div className="min-w-fit mx-auto">
<Paywall
<PaywallPremium
message="Organizations"
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
documentationLink={docs("/admin/users/organizations")}
@@ -10,7 +10,7 @@ import {
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import { Stack } from "components/Stack/Stack";
import {
Table,
@@ -52,7 +52,7 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
return (
<Stack spacing={4}>
{!isCustomRolesEnabled && (
<Paywall
<PaywallPremium
message="Custom Roles"
description="Create custom roles to grant users a tailored set of granular permissions."
documentationLink={docs("/admin/users/groups-roles")}
@@ -12,7 +12,7 @@ import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Link } from "components/Link/Link";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { RequirePermission } from "modules/permissions/RequirePermission";
@@ -130,7 +130,7 @@ const IdpSyncPage: FC = () => {
</header>
<ChooseOne>
<Cond condition={!isIdpSyncEnabled}>
<Paywall
<PaywallPremium
message="IdP Sync"
description="Configure group and role mappings to manage permissions outside of Coder. You need a Premium license to use this feature."
documentationLink={docs("/admin/users/idp-sync")}
@@ -8,7 +8,7 @@ import { Button } from "components/Button/Button";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Link } from "components/Link/Link";
import { Loader } from "components/Loader/Loader";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import {
SettingsHeader,
SettingsHeaderDescription,
@@ -55,7 +55,7 @@ export const OrganizationProvisionerKeysPageView: FC<
</SettingsHeader>
{showPaywall ? (
<Paywall
<PaywallPremium
message="Provisioners"
description="Provisioners run your Terraform to create templates and workspaces. You need a Premium license to use this feature for multiple organizations."
documentationLink={docs("/")}
@@ -5,7 +5,7 @@ import { Checkbox } from "components/Checkbox/Checkbox";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Link } from "components/Link/Link";
import { Loader } from "components/Loader/Loader";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import {
SettingsHeader,
SettingsHeaderDescription,
@@ -95,7 +95,7 @@ export const OrganizationProvisionersPageView: FC<
)}
{showPaywall ? (
<Paywall
<PaywallPremium
message="Provisioners"
description="Provisioners run your Terraform to create templates and workspaces. You need a Premium license to use this feature for multiple organizations."
documentationLink={docs("/")}
@@ -1,6 +1,6 @@
import { setGroupRole, setUserRole, templateACL } from "api/queries/templates";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { Paywall } from "components/Paywall/Paywall";
import { PaywallPremium } from "components/Paywall/PaywallPremium";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
@@ -28,7 +28,7 @@ const TemplatePermissionsPage: FC = () => {
<title>{pageTitle(template.name, "Permissions")}</title>
{!isTemplateRBACEnabled ? (
<Paywall
<PaywallPremium
message="Template permissions"
description="Control access of templates for users and groups to templates. You need a Premium license to use this feature."
documentationLink={docs("/admin/templates/template-permissions")}
+1
View File
@@ -63,6 +63,7 @@ module.exports = {
destructive: "hsl(var(--border-destructive))",
success: "hsl(var(--border-success))",
hover: "hsl(var(--border-hover))",
purple: "hsl(var(--border-purple))",
},
overlay: "hsla(var(--overlay-default))",
input: "hsl(var(--input))",