chore: add paywall to provisioners page (#14803)

* chore: add paywall to provisioners page

* きれい

* move some things into the page view

* I guess I'm not allowed to use proper nouns

* :|
This commit is contained in:
Kayla Washburn-Love
2024-09-25 16:54:49 -06:00
committed by GitHub
parent aef400c2c5
commit 4dcf5ef323
4 changed files with 84 additions and 56 deletions

View File

@ -30,7 +30,7 @@ import { ProvisionerTag } from "./ProvisionerTag";
type ProvisionerGroupType = "builtin" | "psk" | "key"; type ProvisionerGroupType = "builtin" | "psk" | "key";
interface ProvisionerGroupProps { interface ProvisionerGroupProps {
readonly buildInfo?: BuildInfoResponse; readonly buildInfo: BuildInfoResponse;
readonly keyName: string; readonly keyName: string;
readonly keyTags: Record<string, string>; readonly keyTags: Record<string, string>;
readonly type: ProvisionerGroupType; readonly type: ProvisionerGroupType;
@ -80,7 +80,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
let warnings = 0; let warnings = 0;
let provisionersWithWarnings = 0; let provisionersWithWarnings = 0;
const provisionersWithWarningInfo = provisioners.map((it) => { const provisionersWithWarningInfo = provisioners.map((it) => {
const outOfDate = Boolean(buildInfo) && it.version !== buildInfo?.version; const outOfDate = it.version !== buildInfo.version;
const warningCount = outOfDate ? 1 : 0; const warningCount = outOfDate ? 1 : 0;
warnings += warningCount; warnings += warningCount;
if (warnings > 0) { if (warnings > 0) {
@ -292,7 +292,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
}; };
interface ProvisionerVersionPopoverProps { interface ProvisionerVersionPopoverProps {
buildInfo?: BuildInfoResponse; buildInfo: BuildInfoResponse;
provisioner: ProvisionerDaemon; provisioner: ProvisionerDaemon;
} }
@ -304,11 +304,9 @@ const ProvisionerVersionPopover: FC<ProvisionerVersionPopoverProps> = ({
<Popover mode="hover"> <Popover mode="hover">
<PopoverTrigger> <PopoverTrigger>
<span> <span>
{buildInfo {provisioner.version === buildInfo.version
? provisioner.version === buildInfo.version ? "Up to date"
? "Up to date" : "Out of date"}
: "Out of date"
: provisioner.version}
</span> </span>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
@ -324,7 +322,7 @@ const ProvisionerVersionPopover: FC<ProvisionerVersionPopoverProps> = ({
<p css={styles.text}>{provisioner.version}</p> <p css={styles.text}>{provisioner.version}</p>
<h4 css={styles.versionPopoverTitle}>Protocol version</h4> <h4 css={styles.versionPopoverTitle}>Protocol version</h4>
<p css={styles.text}>{provisioner.api_version}</p> <p css={styles.text}>{provisioner.api_version}</p>
{provisioner.api_version !== buildInfo?.provisioner_api_version && ( {provisioner.api_version !== buildInfo.provisioner_api_version && (
<p css={[styles.text, { fontSize: 13 }]}> <p css={[styles.text, { fontSize: 13 }]}>
This provisioner is out of date. You may experience issues when This provisioner is out of date. You may experience issues when
using a provisioner version that doesnt match your Coder using a provisioner version that doesnt match your Coder

View File

@ -3,15 +3,17 @@ import {
organizationsPermissions, organizationsPermissions,
provisionerDaemonGroups, provisionerDaemonGroups,
} from "api/queries/organizations"; } from "api/queries/organizations";
import type { Organization, ProvisionerDaemon } from "api/typesGenerated"; import type { Organization } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState"; import { EmptyState } from "components/EmptyState/EmptyState";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { Paywall } from "components/Paywall/Paywall";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import NotFoundPage from "pages/404Page/404Page"; import { useDashboard } from "modules/dashboard/useDashboard";
import type { FC } from "react"; import type { FC } from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { docs } from "utils/docs";
import { useOrganizationSettings } from "./ManagementSettingsLayout"; import { useOrganizationSettings } from "./ManagementSettingsLayout";
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView"; import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
@ -20,6 +22,7 @@ const OrganizationProvisionersPage: FC = () => {
organization: string; organization: string;
}; };
const { organizations } = useOrganizationSettings(); const { organizations } = useOrganizationSettings();
const { entitlements } = useDashboard();
const { metadata } = useEmbeddedMetadata(); const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
@ -27,42 +30,18 @@ const OrganizationProvisionersPage: FC = () => {
const organization = organizations const organization = organizations
? getOrganizationByName(organizations, organizationName) ? getOrganizationByName(organizations, organizationName)
: undefined; : undefined;
const permissionsQuery = useQuery(
organizationsPermissions(organizations?.map((o) => o.id)),
);
const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName)); const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName));
if (!organization) { if (!organization) {
return <EmptyState message="Organization not found" />; return <EmptyState message="Organization not found" />;
} }
if (permissionsQuery.isLoading || provisionersQuery.isLoading) {
return <Loader />;
}
const permissions = permissionsQuery.data;
const provisioners = provisionersQuery.data;
const error = permissionsQuery.error || provisionersQuery.error;
if (error || !permissions || !provisioners) {
return <ErrorAlert error={error} />;
}
// The user may not be able to edit this org but they can still see it because
// they can edit members, etc. In this case they will be shown a read-only
// summary page instead of the settings form.
// Similarly, if the feature is not entitled then the user will not be able to
// edit the organization.
if (!permissions[organization.id]?.viewProvisioners) {
// This probably doesn't work with the layout................fix this pls
// Kayla, hey, yes you, you gotta fix this.
// Don't scroll past this. It's important. Fix it!!!
return <NotFoundPage />;
}
return ( return (
<OrganizationProvisionersPageView <OrganizationProvisionersPageView
showPaywall={!entitlements.features.multiple_organizations.enabled}
error={provisionersQuery.error}
buildInfo={buildInfoQuery.data} buildInfo={buildInfoQuery.data}
provisioners={provisioners} provisioners={provisionersQuery.data}
/> />
); );
}; };

View File

@ -9,6 +9,7 @@ import {
MockProvisionerPskKey, MockProvisionerPskKey,
MockProvisionerWithTags, MockProvisionerWithTags,
MockUserProvisioner, MockUserProvisioner,
mockApiError,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView"; import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
@ -112,3 +113,18 @@ export const Empty: Story = {
provisioners: [], provisioners: [],
}, },
}; };
export const WithError: Story = {
args: {
error: mockApiError({
message: "Fern is mad",
detail: "Frieren slept in and didn't get groceries",
}),
},
};
export const Paywall: Story = {
args: {
showPaywall: true,
},
};

View File

@ -5,7 +5,10 @@ import type {
ProvisionerKey, ProvisionerKey,
ProvisionerKeyDaemons, ProvisionerKeyDaemons,
} from "api/typesGenerated"; } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState"; import { EmptyState } from "components/EmptyState/EmptyState";
import { Loader } from "components/Loader/Loader";
import { Paywall } from "components/Paywall/Paywall";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup"; import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup";
@ -13,16 +16,62 @@ import type { FC } from "react";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
interface OrganizationProvisionersPageViewProps { interface OrganizationProvisionersPageViewProps {
/** Determines if the paywall will be shown or not */
showPaywall?: boolean;
/** An error to display instead of the page content */
error?: unknown;
/** Info about the version of coderd */ /** Info about the version of coderd */
buildInfo?: BuildInfoResponse; buildInfo?: BuildInfoResponse;
/** Groups of provisioners, along with their key information */ /** Groups of provisioners, along with their key information */
provisioners: readonly ProvisionerKeyDaemons[]; provisioners?: readonly ProvisionerKeyDaemons[];
} }
export const OrganizationProvisionersPageView: FC< export const OrganizationProvisionersPageView: FC<
OrganizationProvisionersPageViewProps OrganizationProvisionersPageViewProps
> = ({ buildInfo, provisioners }) => { > = ({ showPaywall, error, buildInfo, provisioners }) => {
return (
<div>
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<SettingsHeader title="Provisioners" />
{!showPaywall && (
<Button
endIcon={<OpenInNewIcon />}
target="_blank"
href={docs("/admin/provisioners")}
>
Create a provisioner
</Button>
)}
</Stack>
{showPaywall ? (
<Paywall
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("/")}
/>
) : error ? (
<ErrorAlert error={error} />
) : !buildInfo || !provisioners ? (
<Loader />
) : (
<ViewContent buildInfo={buildInfo} provisioners={provisioners} />
)}
</div>
);
};
type ViewContentProps = Required<
Pick<OrganizationProvisionersPageViewProps, "buildInfo" | "provisioners">
>;
const ViewContent: FC<ViewContentProps> = ({ buildInfo, provisioners }) => {
const isEmpty = provisioners.every((group) => group.daemons.length === 0); const isEmpty = provisioners.every((group) => group.daemons.length === 0);
const provisionerGroupsCount = provisioners.length; const provisionerGroupsCount = provisioners.length;
@ -32,21 +81,7 @@ export const OrganizationProvisionersPageView: FC<
); );
return ( return (
<div> <>
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<SettingsHeader title="Provisioners" />
<Button
endIcon={<OpenInNewIcon />}
target="_blank"
href={docs("/admin/provisioners")}
>
Create a provisioner
</Button>
</Stack>
{isEmpty ? ( {isEmpty ? (
<EmptyState <EmptyState
message="No provisioners" message="No provisioners"
@ -98,7 +133,7 @@ export const OrganizationProvisionersPageView: FC<
); );
})} })}
</Stack> </Stack>
</div> </>
); );
}; };