mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
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:
committed by
GitHub
parent
aef400c2c5
commit
4dcf5ef323
@ -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 doesn’t match your Coder
|
using a provisioner version that doesn’t match your Coder
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user