feat: add premium license behavior for create organization page (#14650)

* fix: type

* chore: paywall improvements

* feat: update create org for premium license

* fix: remove []

* feat: update licensing pills to reflect new premium tier (#14670)

* feat: update licensing pills to reflect new premium tier

* fix: format
This commit is contained in:
Jaayden Halko
2024-09-13 14:37:42 -04:00
committed by GitHub
parent 9006b21758
commit c6bc7414aa
16 changed files with 257 additions and 153 deletions
+20 -3
View File
@@ -106,15 +106,32 @@ export const DisabledBadge: FC = forwardRef<
);
});
export const EnterpriseBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
backgroundColor: theme.branding.enterprise.background,
border: `1px solid ${theme.branding.enterprise.border}`,
color: theme.branding.enterprise.text,
}),
]}
>
Enterprise
</span>
);
};
export const PremiumBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
backgroundColor: theme.branding.background,
border: `1px solid ${theme.branding.border}`,
color: theme.roles.notice.text,
backgroundColor: theme.branding.premium.background,
border: `1px solid ${theme.branding.premium.border}`,
color: theme.branding.premium.text,
}),
]}
>
+22 -23
View File
@@ -19,15 +19,7 @@ export const Paywall: FC<PaywallProps> = ({
documentationLink,
}) => {
return (
<div
css={[
styles.root,
(theme) => ({
backgroundImage: `linear-gradient(160deg, transparent, ${theme.branding.background})`,
border: `1px solid ${theme.branding.border}`,
}),
]}
>
<div css={styles.root}>
<div>
<Stack direction="row" alignItems="center" css={{ marginBottom: 24 }}>
<h5 css={styles.title}>{message}</h5>
@@ -45,7 +37,7 @@ export const Paywall: FC<PaywallProps> = ({
</Link>
</div>
<div css={styles.separator} />
<Stack direction="column" alignItems="center" spacing={3}>
<Stack direction="column" alignItems="left" spacing={3}>
<ul css={styles.featureList}>
<li css={styles.feature}>
<FeatureIcon />
@@ -64,16 +56,18 @@ export const Paywall: FC<PaywallProps> = ({
Unlimited Git & external auth integrations
</li>
</ul>
<Button
href={docs("/enterprise")}
target="_blank"
rel="noreferrer"
startIcon={<span css={{ fontSize: 22 }}>&rarr;</span>}
variant="outlined"
color="neutral"
>
Learn about Premium
</Button>
<div css={styles.learnButton}>
<Button
href={docs("/enterprise")}
target="_blank"
rel="noreferrer"
startIcon={<span css={{ fontSize: 22 }}>&rarr;</span>}
variant="outlined"
color="neutral"
>
Learn about Premium
</Button>
</div>
</Stack>
</div>
);
@@ -84,7 +78,7 @@ const FeatureIcon: FC = () => {
<TaskAltIcon
css={[
(theme) => ({
color: theme.branding.border,
color: theme.branding.premium.border,
}),
]}
/>
@@ -92,7 +86,7 @@ const FeatureIcon: FC = () => {
};
const styles = {
root: () => ({
root: (theme) => ({
display: "flex",
flexDirection: "row",
justifyContent: "center",
@@ -101,6 +95,8 @@ const styles = {
padding: 24,
borderRadius: 8,
gap: 32,
backgroundImage: `linear-gradient(160deg, transparent, ${theme.branding.premium.background})`,
border: `1px solid ${theme.branding.premium.border}`,
}),
title: {
fontWeight: 600,
@@ -116,9 +112,12 @@ const styles = {
separator: (theme) => ({
width: 1,
height: 220,
backgroundColor: theme.branding.divider,
backgroundColor: theme.branding.premium.divider,
marginLeft: 8,
}),
learnButton: {
padding: "0 28px",
},
featureList: {
listStyle: "none",
margin: 0,
+22 -17
View File
@@ -23,8 +23,8 @@ export const PopoverPaywall: FC<PopoverPaywallProps> = ({
css={[
styles.root,
(theme) => ({
backgroundImage: `linear-gradient(160deg, transparent, ${theme.branding.background})`,
border: `1px solid ${theme.branding.border}`,
backgroundImage: `linear-gradient(160deg, transparent, ${theme.branding.premium.background})`,
border: `1px solid ${theme.branding.premium.border}`,
}),
]}
>
@@ -45,7 +45,7 @@ export const PopoverPaywall: FC<PopoverPaywallProps> = ({
</Link>
</div>
<div css={styles.separator} />
<Stack direction="column" alignItems="center" spacing={2}>
<Stack direction="column" alignItems="left" spacing={2}>
<ul css={styles.featureList}>
<li css={styles.feature}>
<FeatureIcon /> High availability & workspace proxies
@@ -60,16 +60,18 @@ export const PopoverPaywall: FC<PopoverPaywallProps> = ({
<FeatureIcon /> Unlimited Git & external auth integrations
</li>
</ul>
<Button
href={docs("/enterprise")}
target="_blank"
rel="noreferrer"
startIcon={<span css={{ fontSize: 22 }}>&rarr;</span>}
variant="outlined"
color="neutral"
>
Learn about Premium
</Button>
<div css={styles.learnButton}>
<Button
href={docs("/enterprise")}
target="_blank"
rel="noreferrer"
startIcon={<span css={{ fontSize: 22 }}>&rarr;</span>}
variant="outlined"
color="neutral"
>
Learn about Premium
</Button>
</div>
</Stack>
</div>
);
@@ -80,7 +82,7 @@ const FeatureIcon: FC = () => {
<TaskAltIcon
css={[
(theme) => ({
color: theme.branding.border,
color: theme.branding.premium.border,
}),
]}
/>
@@ -92,7 +94,7 @@ const styles = {
display: "flex",
flexDirection: "row",
alignItems: "center",
maxWidth: 600,
maxWidth: 770,
padding: "24px 36px",
borderRadius: 8,
gap: 18,
@@ -106,7 +108,7 @@ const styles = {
description: (theme) => ({
marginTop: 8,
fontFamily: "inherit",
maxWidth: 420,
maxWidth: 360,
lineHeight: "160%",
color: theme.palette.text.secondary,
fontSize: 14,
@@ -121,10 +123,13 @@ const styles = {
listStyle: "none",
margin: 0,
marginRight: 8,
padding: "0 12px",
padding: "0 0 0 24px",
fontSize: 13,
fontWeight: 500,
},
learnButton: {
padding: "0 28px",
},
feature: {
display: "flex",
alignItems: "center",
@@ -3,6 +3,7 @@ import { appearanceConfigKey, updateAppearance } from "api/queries/appearance";
import type { UpdateAppearanceConfig } from "api/typesGenerated";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQueryClient } from "react-query";
@@ -15,6 +16,7 @@ import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView";
// the command line would be a significantly worse user experience.
const AppearanceSettingsPage: FC = () => {
const { appearance, entitlements } = useDashboard();
const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility();
const queryClient = useQueryClient();
const updateAppearanceMutation = useMutation(updateAppearance(queryClient));
@@ -46,6 +48,7 @@ const AppearanceSettingsPage: FC = () => {
isEntitled={
entitlements.features.appearance.entitlement !== "not_entitled"
}
isPremium={hasPremiumLicense}
/>
</>
);
@@ -4,8 +4,7 @@ import TextField from "@mui/material/TextField";
import type { UpdateAppearanceConfig } from "api/typesGenerated";
import {
Badges,
DisabledBadge,
EntitledBadge,
EnterpriseBadge,
PremiumBadge,
} from "components/Badges/Badges";
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
@@ -24,6 +23,7 @@ import { AnnouncementBannerSettings } from "./AnnouncementBannerSettings";
export type AppearanceSettingsPageViewProps = {
appearance: UpdateAppearanceConfig;
isEntitled: boolean;
isPremium: boolean;
onSaveAppearance: (
newConfig: Partial<UpdateAppearanceConfig>,
) => Promise<void>;
@@ -31,7 +31,7 @@ export type AppearanceSettingsPageViewProps = {
export const AppearanceSettingsPageView: FC<
AppearanceSettingsPageViewProps
> = ({ appearance, isEntitled, onSaveAppearance }) => {
> = ({ appearance, isEntitled, isPremium, onSaveAppearance }) => {
const applicationNameForm = useFormik<{
application_name: string;
}>({
@@ -60,13 +60,17 @@ export const AppearanceSettingsPageView: FC<
/>
<Badges>
{isEntitled ? <EntitledBadge /> : <DisabledBadge />}
<Popover mode="hover">
<PopoverTrigger>
<span>
<PremiumBadge />
</span>
</PopoverTrigger>
{isEntitled && !isPremium ? (
<EnterpriseBadge />
) : (
<PopoverTrigger>
<span>
<PremiumBadge />
</span>
</PopoverTrigger>
)}
<PopoverContent css={{ transform: "translateY(-28px)" }}>
<PopoverPaywall
message="Appearance"
@@ -1,5 +1,6 @@
import { Loader } from "components/Loader/Loader";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
@@ -9,6 +10,7 @@ import { ObservabilitySettingsPageView } from "./ObservabilitySettingsPageView";
const ObservabilitySettingsPage: FC = () => {
const { deploymentValues } = useDeploySettings();
const { entitlements } = useDashboard();
const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility();
return (
<>
@@ -20,6 +22,7 @@ const ObservabilitySettingsPage: FC = () => {
<ObservabilitySettingsPageView
options={deploymentValues.options}
featureAuditLogEnabled={entitlements.features.audit_log.enabled}
isPremium={hasPremiumLicense}
/>
) : (
<Loader />
@@ -1,10 +1,15 @@
import type { SerpentOption } from "api/typesGenerated";
import {
Badges,
DisabledBadge,
EnabledBadge,
EnterpriseBadge,
PremiumBadge,
} from "components/Badges/Badges";
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
@@ -15,11 +20,12 @@ import OptionsTable from "../OptionsTable";
export type ObservabilitySettingsPageViewProps = {
options: SerpentOption[];
featureAuditLogEnabled: boolean;
isPremium: boolean;
};
export const ObservabilitySettingsPageView: FC<
ObservabilitySettingsPageViewProps
> = ({ options, featureAuditLogEnabled }) => {
> = ({ options, featureAuditLogEnabled, isPremium }) => {
return (
<>
<Stack direction="column" spacing={6}>
@@ -33,8 +39,25 @@ export const ObservabilitySettingsPageView: FC<
/>
<Badges>
{featureAuditLogEnabled ? <EnabledBadge /> : <DisabledBadge />}
<PremiumBadge />
<Popover mode="hover">
{featureAuditLogEnabled && !isPremium ? (
<EnterpriseBadge />
) : (
<PopoverTrigger>
<span>
<PremiumBadge />
</span>
</PopoverTrigger>
)}
<PopoverContent css={{ transform: "translateY(-28px)" }}>
<PopoverPaywall
message="Observability"
description="With a Premium license, you can monitor your application with logs and metrics."
documentationLink="https://coder.com/docs/admin/appearance"
/>
</PopoverContent>
</Popover>
</Badges>
</div>
@@ -2,12 +2,8 @@ import TextField from "@mui/material/TextField";
import { isApiValidationError } from "api/errors";
import type { CreateOrganizationRequest } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import {
Badges,
DisabledBadge,
EntitledBadge,
PremiumBadge,
} from "components/Badges/Badges";
import { Badges, PremiumBadge } from "components/Badges/Badges";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import {
FormFields,
FormFooter,
@@ -15,6 +11,7 @@ import {
HorizontalForm,
} from "components/Form/Form";
import { IconField } from "components/IconField/IconField";
import { Paywall } from "components/Paywall/Paywall";
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
import {
Popover,
@@ -22,6 +19,7 @@ import {
PopoverTrigger,
} from "components/Popover/Popover";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useFormik } from "formik";
import type { FC } from "react";
import { docs } from "utils/docs";
@@ -67,79 +65,99 @@ export const CreateOrganizationPageView: FC<
const getFieldHelpers = getFormHelpers(form, error);
return (
<div>
<SettingsHeader
title="New Organization"
description="Organize your deployment into multiple platform teams."
/>
<Stack>
<div>
<SettingsHeader
title="New Organization"
description="Organize your deployment into multiple platform teams."
/>
<Badges>
{isEntitled ? <EntitledBadge /> : <DisabledBadge />}
<Popover mode="hover">
<PopoverTrigger>
<span>
<PremiumBadge />
</span>
</PopoverTrigger>
<PopoverContent css={{ transform: "translateY(-28px)" }}>
<PopoverPaywall
message="Organizations"
description="Organizations allow you to run a Coder deployment with multiple platform teams, all with unique use cases, templates, and even underlying infrastructure."
documentationLink={docs("/guides/using-organizations")}
/>
</PopoverContent>
</Popover>
</Badges>
{Boolean(error) && !isApiValidationError(error) && (
<div css={{ marginBottom: 32 }}>
<ErrorAlert error={error} />
</div>
)}
{Boolean(error) && !isApiValidationError(error) && (
<div css={{ marginBottom: 32 }}>
<ErrorAlert error={error} />
</div>
)}
<Badges>
<Popover mode="hover">
{isEntitled && (
<PopoverTrigger>
<span>
<PremiumBadge />
</span>
</PopoverTrigger>
)}
<HorizontalForm
onSubmit={form.handleSubmit}
aria-label="Organization settings form"
>
<FormSection
title="General info"
description="The name and description of the organization."
>
<fieldset
disabled={form.isSubmitting}
css={{ border: "unset", padding: 0, margin: 0, width: "100%" }}
<PopoverContent css={{ transform: "translateY(-28px)" }}>
<PopoverPaywall
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("/guides/using-organizations")}
/>
</PopoverContent>
</Popover>
</Badges>
</div>
<ChooseOne>
<Cond condition={!isEntitled}>
<Paywall
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("/guides/using-organizations")}
/>
</Cond>
<Cond>
<HorizontalForm
onSubmit={form.handleSubmit}
aria-label="Organization settings form"
>
<FormFields>
<TextField
{...getFieldHelpers("name")}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label="Name"
/>
<TextField
{...getFieldHelpers("display_name")}
fullWidth
label="Display name"
/>
<TextField
{...getFieldHelpers("description")}
multiline
fullWidth
label="Description"
rows={2}
/>
<IconField
{...getFieldHelpers("icon")}
onChange={onChangeTrimmed(form)}
fullWidth
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
</fieldset>
</FormSection>
<FormFooter isLoading={form.isSubmitting} />
</HorizontalForm>
</div>
<FormSection
title="General info"
description="The name and description of the organization."
>
<fieldset
disabled={form.isSubmitting}
css={{
border: "unset",
padding: 0,
margin: 0,
width: "100%",
}}
>
<FormFields>
<TextField
{...getFieldHelpers("name")}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label="Name"
/>
<TextField
{...getFieldHelpers("display_name")}
fullWidth
label="Display name"
/>
<TextField
{...getFieldHelpers("description")}
multiline
fullWidth
label="Description"
rows={2}
/>
<IconField
{...getFieldHelpers("icon")}
onChange={onChangeTrimmed(form)}
fullWidth
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
</fieldset>
</FormSection>
<FormFooter isLoading={form.isSubmitting} />
</HorizontalForm>
</Cond>
</ChooseOne>
</Stack>
);
};
@@ -49,7 +49,7 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
{!isCustomRolesEnabled && (
<Paywall
message="Custom Roles"
description="Custom Roles are available in the Premium plan and require an upgraded license. Please contact your Customer Success team for more information."
description="Create custom roles to grant users a tailored set of granular permissions."
documentationLink={docs("/admin/groups")}
/>
)}
@@ -165,7 +165,7 @@ export const GroupPage: FC = () => {
<TableRow>
<TableCell width="59%">User</TableCell>
<TableCell width="40">Status</TableCell>
<TableCell width="1%"></TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
@@ -89,7 +89,7 @@ export const OrganizationMembersPageView: FC<
<TableColumnHelpTooltip variant="groups" />
</Stack>
</TableCell>
<TableCell width="1%"></TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
@@ -213,7 +213,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
css={{ marginTop: 16 }}
>
<PremiumBadge />
<span>Premium license required to enabled.</span>
<span>Premium license required to be enabled.</span>
</Stack>
)}
</StackLabelHelperText>
+12 -4
View File
@@ -1,6 +1,14 @@
export interface Branding {
background: string;
divider: string;
border: string;
text: string;
enterprise: {
background: string;
divider: string;
border: string;
text: string;
};
premium: {
background: string;
divider: string;
border: string;
text: string;
};
}
+12 -4
View File
@@ -2,8 +2,16 @@ import type { Branding } from "../branding";
import colors from "../tailwindColors";
export default {
background: colors.violet[950],
divider: colors.violet[900],
border: colors.violet[400],
text: colors.violet[50],
enterprise: {
background: colors.blue[950],
divider: colors.blue[900],
border: colors.blue[400],
text: colors.blue[50],
},
premium: {
background: colors.violet[950],
divider: colors.violet[900],
border: colors.violet[400],
text: colors.violet[50],
},
} satisfies Branding;
+12 -4
View File
@@ -2,8 +2,16 @@ import type { Branding } from "../branding";
import colors from "../tailwindColors";
export default {
background: colors.violet[950],
divider: colors.violet[900],
border: colors.violet[400],
text: colors.violet[50],
enterprise: {
background: colors.blue[950],
divider: colors.blue[900],
border: colors.blue[400],
text: colors.blue[50],
},
premium: {
background: colors.violet[950],
divider: colors.violet[900],
border: colors.violet[400],
text: colors.violet[50],
},
} satisfies Branding;
+12 -4
View File
@@ -2,8 +2,16 @@ import type { Branding } from "../branding";
import colors from "../tailwindColors";
export default {
background: colors.violet[100],
divider: colors.violet[300],
border: colors.violet[600],
text: colors.violet[950],
enterprise: {
background: colors.blue[100],
divider: colors.blue[300],
border: colors.blue[600],
text: colors.blue[950],
},
premium: {
background: colors.violet[100],
divider: colors.violet[300],
border: colors.violet[600],
text: colors.violet[950],
},
} satisfies Branding;