Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import type { UpdateAppearanceConfig } from "#/api/typesGenerated";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { pageTitle } from "#/utils/page";
import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView";
Expand All @@ -20,7 +19,6 @@ 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));
const { permissions } = useAuthenticated();
Expand Down Expand Up @@ -61,8 +59,7 @@ const AppearanceSettingsPage: FC = () => {
isEntitled={
entitlements.features.appearance.entitlement !== "not_entitled"
}
isPremium={hasPremiumLicense}
permissions={permissions}
canViewPremium={permissions.viewAllLicenses}
/>
</RequirePermission>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, screen, userEvent, within } from "storybook/test";
import { expect, within } from "storybook/test";
import { MockPermissions } from "#/testHelpers/entities";
import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView";

Expand All @@ -24,7 +24,7 @@ const meta: Meta<typeof AppearanceSettingsPageView> = {
],
},
isEntitled: false,
permissions: MockPermissions,
canViewPremium: MockPermissions.viewAllLicenses,
},
};

Expand All @@ -35,11 +35,17 @@ export const Entitled: Story = {
args: {
isEntitled: true,
},
play: async () => {
// The badge is passive: hovering it must not surface a paywall.
await userEvent.hover(screen.getByText("Enterprise"));
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await expect(
screen.queryByRole("link", { name: "Learn more about premium" }),
canvas.getByRole("form", { name: "Appearance settings" }),
).toBeVisible();
await expect(
canvas.getByRole("heading", { name: "Announcement Banners" }),
).toBeVisible();
await expect(
canvas.queryByRole("link", { name: "Learn about Premium" }),
).not.toBeInTheDocument();
},
};
Expand All @@ -48,14 +54,20 @@ export const NotEntitled: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

const cta = canvas.getByRole("link", { name: "Start trial for free" });
const cta = canvas.getByRole("link", { name: "Learn about Premium" });
await expect(cta).toHaveAttribute("href", "/deployment/premium");
await expect(
canvas.queryByRole("form", { name: "Appearance settings" }),
).not.toBeInTheDocument();
await expect(
canvas.queryByRole("heading", { name: "Announcement Banners" }),
).not.toBeInTheDocument();
},
};

export const NotEntitledWithoutLicenseAccess: Story = {
args: {
permissions: { ...MockPermissions, viewAllLicenses: false },
canViewPremium: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
Expand All @@ -64,7 +76,7 @@ export const NotEntitledWithoutLicenseAccess: Story = {
canvas.getByText(/contact your deployment administrator/i),
).toBeVisible();
await expect(
canvas.queryByRole("link", { name: "Start trial for free" }),
canvas.queryByRole("link", { name: "Learn about Premium" }),
).not.toBeInTheDocument();
},
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { useFormik } from "formik";
import type { FC } from "react";
import type { UpdateAppearanceConfig } from "#/api/typesGenerated";
import {
Badges,
EnterpriseBadge,
PremiumBadge,
} from "#/components/Badges/Badges";
import { Button } from "#/components/Button/Button";
import {
FormFields,
Expand All @@ -15,30 +10,28 @@ import {
} from "#/components/Form/Form";
import { FormField } from "#/components/FormField/FormField";
import { IconField } from "#/components/IconField/IconField";
import { PaywallSmall } from "#/components/Paywall/PaywallSmall";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import { Spinner } from "#/components/Spinner/Spinner";
import type { Permissions } from "#/modules/permissions";
import { getFormHelpers } from "#/utils/formUtils";
import { AnnouncementBannerSettings } from "./AnnouncementBannerSettings";

type AppearanceSettingsPageViewProps = {
appearance: UpdateAppearanceConfig;
isEntitled: boolean;
isPremium: boolean;
permissions: Permissions;
canViewPremium: boolean;
onSaveAppearance: (
newConfig: Partial<UpdateAppearanceConfig>,
) => Promise<void>;
};

export const AppearanceSettingsPageView: FC<
AppearanceSettingsPageViewProps
> = ({ appearance, isEntitled, isPremium, permissions, onSaveAppearance }) => {
> = ({ appearance, isEntitled, canViewPremium, onSaveAppearance }) => {
const form = useFormik<{
application_name: string;
logo_url: string;
Expand All @@ -51,89 +44,74 @@ export const AppearanceSettingsPageView: FC<
enableReinitialize: true,
});
const getFieldHelpers = getFormHelpers(form);
const fieldsDisabled = !isEntitled || form.isSubmitting;

return (
<div className="flex flex-col gap-12">
<div>
<SettingsHeader>
<SettingsHeaderTitle>Appearance</SettingsHeaderTitle>
<SettingsHeaderDescription>
Customize the look and feel of your Coder deployment.
</SettingsHeaderDescription>
</SettingsHeader>

<Badges>
{isEntitled ? (
isPremium ? (
<PremiumBadge />
) : (
<EnterpriseBadge />
)
) : (
<div className="mb-4">
<PaywallSmall
message="Appearance"
description="With a Premium license, you can customize the appearance and branding of your deployment."
canViewPremium={permissions.viewAllLicenses}
/>
</div>
)}
</Badges>
<div className="flex flex-col gap-8">
<SettingsHeader>
<SettingsHeaderTitle>Appearance</SettingsHeaderTitle>
<SettingsHeaderDescription>
Customize the look and feel of your Coder deployment.
</SettingsHeaderDescription>
</SettingsHeader>

<VerticalForm
onSubmit={form.handleSubmit}
aria-label="Appearance settings"
className="mt-8"
>
<FormSection
title="Branding"
description="Customize the application name and logo shown on the login page and in the dashboard."
{!isEntitled ? (
<PaywallPremium
message="Appearance"
description="With a Premium license, you can customize branding and announcement banners for your deployment."
canViewPremium={canViewPremium}
/>
) : (
<>
<VerticalForm
onSubmit={form.handleSubmit}
aria-label="Appearance settings"
>
<FormFields>
<FormField
field={getFieldHelpers("application_name", {
helperText: isEntitled
? 'Leave empty to use "Coder".'
: "This is an Enterprise only feature.",
})}
label="Application name"
placeholder="Coder"
disabled={fieldsDisabled}
/>
<FormSection
title="Branding"
description="Customize the application name and logo shown on the login page and in the dashboard."
>
<FormFields>
<FormField
field={getFieldHelpers("application_name", {
helperText: 'Leave empty to use "Coder".',
})}
label="Application name"
placeholder="Coder"
disabled={form.isSubmitting}
/>

<IconField
{...getFieldHelpers("logo_url", {
helperText: isEntitled
? "Leave empty to use the Coder logo. An image with transparency and an aspect ratio of 3:1 or less will look best."
: "This is an Enterprise only feature.",
})}
label="Logo URL"
placeholder="/icon/coder.svg"
disabled={fieldsDisabled}
onPickEmoji={(value) => {
void form.setFieldValue("logo_url", value);
}}
/>
</FormFields>
</FormSection>
<IconField
{...getFieldHelpers("logo_url", {
helperText:
"Leave empty to use the Coder logo. An image with transparency and an aspect ratio of 3:1 or less will look best.",
})}
label="Logo URL"
placeholder="/icon/coder.svg"
disabled={form.isSubmitting}
onPickEmoji={(value) => {
void form.setFieldValue("logo_url", value);
}}
/>
</FormFields>
</FormSection>

<FormFooter>
<Button type="submit" disabled={fieldsDisabled}>
<Spinner loading={form.isSubmitting} />
Save
</Button>
</FormFooter>
</VerticalForm>
</div>
<FormFooter>
<Button type="submit" disabled={form.isSubmitting}>
<Spinner loading={form.isSubmitting} />
Save
</Button>
</FormFooter>
</VerticalForm>

<AnnouncementBannerSettings
isEntitled={isEntitled}
announcementBanners={appearance.announcement_banners || []}
onSubmit={(announcementBanners) =>
onSaveAppearance({ announcement_banners: announcementBanners })
}
/>
<AnnouncementBannerSettings
isEntitled

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the now-unreachable entitlement branch

This component is now mounted only inside the parent’s isEntitled branch, and this call always passes true; repo-wide search confirms there are no other callers. Consequently, every !isEntitled path in AnnouncementBannerSettings, including its license guidance and disabled table state, is unreachable and leaves a misleading prop/API behind. Remove the prop and simplify those dead branches in accordance with the canonical FE3 contract.

AGENTS.md reference: site/AGENTS.md:L5-L7

Useful? React with 👍 / 👎.

announcementBanners={appearance.announcement_banners || []}
onSubmit={(announcementBanners) =>
onSaveAppearance({ announcement_banners: announcementBanners })
}
/>
</>
)}
</div>
);
};
Loading