fix: use multi-org settings layout even if not licensed (#14215)

* fix: only check flag for organization settings

I added checks against the license but actually what we want is for
these views to become the default even when not licensed (once the
experimental flag is removed).

* Move deployment settings header to components

This will let us use it in the org settings pages, for a consistent
look.

* Add premium badge

* Use settings header on org pages

* Add license badges to create org page

I am not sure if there is maybe a better place for this, but maybe this
is good enough.

* Change create org form description text

It says "change", but there is nothing to change yet since this is a new
organization.

* Consistently capitalize org menu items and headings

Also, remove the "organizations" prefix since it seems redundant.
This commit is contained in:
Asher
2024-08-08 23:29:37 -08:00
committed by GitHub
parent 9a47ea1279
commit abbcffe181
31 changed files with 302 additions and 238 deletions
+17
View File
@@ -123,6 +123,23 @@ export const EnterpriseBadge: FC = () => {
); );
}; };
export const PremiumBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
backgroundColor: theme.roles.info.background,
border: `1px solid ${theme.roles.info.outline}`,
color: theme.roles.info.text,
}),
]}
>
Premium
</span>
);
};
export const PreviewBadge: FC = () => { export const PreviewBadge: FC = () => {
return ( return (
<span <span
@@ -9,7 +9,7 @@ const meta: Meta<typeof PopoverPaywall> = {
export default meta; export default meta;
type Story = StoryObj<typeof PopoverPaywall>; type Story = StoryObj<typeof PopoverPaywall>;
const Example: Story = { export const Enterprise: Story = {
args: { args: {
message: "Black Lotus", message: "Black Lotus",
description: description:
@@ -17,4 +17,11 @@ const Example: Story = {
}, },
}; };
export { Example as PopoverPaywall }; export const Premium: 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.",
licenseType: "premium",
},
};
+10 -3
View File
@@ -3,7 +3,7 @@ import TaskAltIcon from "@mui/icons-material/TaskAlt";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import type { FC, ReactNode } from "react"; import type { FC, ReactNode } from "react";
import { EnterpriseBadge } from "components/Badges/Badges"; import { EnterpriseBadge, PremiumBadge } from "components/Badges/Badges";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
@@ -11,19 +11,21 @@ export interface PopoverPaywallProps {
message: string; message: string;
description?: ReactNode; description?: ReactNode;
documentationLink?: string; documentationLink?: string;
licenseType?: "enterprise" | "premium";
} }
export const PopoverPaywall: FC<PopoverPaywallProps> = ({ export const PopoverPaywall: FC<PopoverPaywallProps> = ({
message, message,
description, description,
documentationLink, documentationLink,
licenseType = "enterprise",
}) => { }) => {
return ( return (
<div css={styles.root}> <div css={styles.root}>
<div> <div>
<Stack direction="row" alignItems="center" css={{ marginBottom: 18 }}> <Stack direction="row" alignItems="center" css={{ marginBottom: 18 }}>
<h5 css={styles.title}>{message}</h5> <h5 css={styles.title}>{message}</h5>
<EnterpriseBadge /> {licenseType === "premium" ? <PremiumBadge /> : <EnterpriseBadge />}
</Stack> </Stack>
{description && <p css={styles.description}>{description}</p>} {description && <p css={styles.description}>{description}</p>}
@@ -51,6 +53,11 @@ export const PopoverPaywall: FC<PopoverPaywallProps> = ({
<li css={styles.feature}> <li css={styles.feature}>
<FeatureIcon /> Audit logs <FeatureIcon /> Audit logs
</li> </li>
{licenseType === "premium" && (
<li css={styles.feature}>
<FeatureIcon /> Organizations
</li>
)}
</ul> </ul>
<Button <Button
href={docs("/enterprise")} href={docs("/enterprise")}
@@ -60,7 +67,7 @@ export const PopoverPaywall: FC<PopoverPaywallProps> = ({
variant="outlined" variant="outlined"
color="neutral" color="neutral"
> >
Learn about Enterprise Learn about {licenseType === "premium" ? "Premium" : "Enterprise"}
</Button> </Button>
</Stack> </Stack>
</div> </div>
@@ -11,7 +11,7 @@ interface HeaderProps {
docsHref?: string; docsHref?: string;
} }
export const Header: FC<HeaderProps> = ({ export const SettingsHeader: FC<HeaderProps> = ({
title, title,
description, description,
docsHref, docsHref,
@@ -20,7 +20,6 @@ export const Navbar: FC = () => {
const canViewDeployment = Boolean(permissions.viewDeploymentValues); const canViewDeployment = Boolean(permissions.viewDeploymentValues);
const canViewOrganizations = const canViewOrganizations =
Boolean(permissions.editAnyOrganization) && Boolean(permissions.editAnyOrganization) &&
featureVisibility.multiple_organizations &&
experiments.includes("multi-organization"); experiments.includes("multi-organization");
const canViewAllUsers = Boolean(permissions.viewAllUsers); const canViewAllUsers = Boolean(permissions.viewAllUsers);
const proxyContextValue = useProxy(); const proxyContextValue = useProxy();
@@ -16,9 +16,9 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "components/Popover/Popover"; } from "components/Popover/Popover";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { getFormHelpers } from "utils/formUtils"; import { getFormHelpers } from "utils/formUtils";
import { Fieldset } from "../Fieldset"; import { Fieldset } from "../Fieldset";
import { Header } from "../Header";
import { AnnouncementBannerSettings } from "./AnnouncementBannerSettings"; import { AnnouncementBannerSettings } from "./AnnouncementBannerSettings";
export type AppearanceSettingsPageViewProps = { export type AppearanceSettingsPageViewProps = {
@@ -54,7 +54,7 @@ export const AppearanceSettingsPageView: FC<
return ( return (
<> <>
<Header <SettingsHeader
title="Appearance" title="Appearance"
description="Customize the look and feel of your Coder deployment." description="Customize the look and feel of your Coder deployment."
/> />
@@ -9,7 +9,6 @@ import { Stack } from "components/Stack/Stack";
import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useAuthenticated } from "contexts/auth/RequireAuth";
import { RequirePermission } from "contexts/auth/RequirePermission"; import { RequirePermission } from "contexts/auth/RequirePermission";
import { useDashboard } from "modules/dashboard/useDashboard"; import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout"; import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout";
import { Sidebar } from "./Sidebar"; import { Sidebar } from "./Sidebar";
@@ -34,9 +33,7 @@ export const useDeploySettings = (): DeploySettingsContextValue => {
export const DeploySettingsLayout: FC = () => { export const DeploySettingsLayout: FC = () => {
const { experiments } = useDashboard(); const { experiments } = useDashboard();
const feats = useFeatureVisibility(); const canViewOrganizations = experiments.includes("multi-organization");
const canViewOrganizations =
feats.multiple_organizations && experiments.includes("multi-organization");
return canViewOrganizations ? ( return canViewOrganizations ? (
<ManagementSettingsLayout /> <ManagementSettingsLayout />
@@ -9,8 +9,8 @@ import type { FC } from "react";
import type { DeploymentValues, ExternalAuthConfig } from "api/typesGenerated"; import type { DeploymentValues, ExternalAuthConfig } from "api/typesGenerated";
import { Alert } from "components/Alert/Alert"; import { Alert } from "components/Alert/Alert";
import { EnterpriseBadge } from "components/Badges/Badges"; import { EnterpriseBadge } from "components/Badges/Badges";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import { Header } from "../Header";
export type ExternalAuthSettingsPageViewProps = { export type ExternalAuthSettingsPageViewProps = {
config: DeploymentValues; config: DeploymentValues;
@@ -21,7 +21,7 @@ export const ExternalAuthSettingsPageView: FC<
> = ({ config }) => { > = ({ config }) => {
return ( return (
<> <>
<Header <SettingsHeader
title="External Authentication" title="External Authentication"
description="Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and OpenID Connect to authenticate developers with external services." description="Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and OpenID Connect to authenticate developers with external services."
docsHref={docs("/admin/external-auth")} docsHref={docs("/admin/external-auth")}
@@ -11,11 +11,11 @@ import {
ActiveUsersTitle, ActiveUsersTitle,
} from "components/ActiveUserChart/ActiveUserChart"; } from "components/ActiveUserChart/ActiveUserChart";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { useDeploymentOptions } from "utils/deployOptions"; import { useDeploymentOptions } from "utils/deployOptions";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import { Alert } from "../../../components/Alert/Alert"; import { Alert } from "../../../components/Alert/Alert";
import { Header } from "../Header";
import OptionsTable from "../OptionsTable"; import OptionsTable from "../OptionsTable";
import { ChartSection } from "./ChartSection"; import { ChartSection } from "./ChartSection";
@@ -38,7 +38,7 @@ export const GeneralSettingsPageView: FC<GeneralSettingsPageViewProps> = ({
}) => { }) => {
return ( return (
<> <>
<Header <SettingsHeader
title="General" title="General"
description="Information about your Coder deployment." description="Information about your Coder deployment."
docsHref={docs("/admin/configure")} docsHref={docs("/admin/configure")}
@@ -6,9 +6,9 @@ import { Link as RouterLink } from "react-router-dom";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import { FileUpload } from "components/FileUpload/FileUpload"; import { FileUpload } from "components/FileUpload/FileUpload";
import { displayError } from "components/GlobalSnackbar/utils"; import { displayError } from "components/GlobalSnackbar/utils";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { Fieldset } from "../Fieldset"; import { Fieldset } from "../Fieldset";
import { Header } from "../Header";
import { DividerWithText } from "./DividerWithText"; import { DividerWithText } from "./DividerWithText";
type AddNewLicenseProps = { type AddNewLicenseProps = {
@@ -50,7 +50,7 @@ export const AddNewLicensePageView: FC<AddNewLicenseProps> = ({
direction="row" direction="row"
justifyContent="space-between" justifyContent="space-between"
> >
<Header <SettingsHeader
title="Add a license" title="Add a license"
description="Get access to high availability, RBAC, quotas, and more." description="Get access to high availability, RBAC, quotas, and more."
/> />
@@ -10,9 +10,9 @@ import type { FC } from "react";
import Confetti from "react-confetti"; import Confetti from "react-confetti";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { GetLicensesResponse } from "api/api"; import type { GetLicensesResponse } from "api/api";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { useWindowSize } from "hooks/useWindowSize"; import { useWindowSize } from "hooks/useWindowSize";
import { Header } from "../Header";
import { LicenseCard } from "./LicenseCard"; import { LicenseCard } from "./LicenseCard";
type Props = { type Props = {
@@ -55,7 +55,7 @@ const LicensesSettingsPageView: FC<Props> = ({
direction="row" direction="row"
justifyContent="space-between" justifyContent="space-between"
> >
<Header <SettingsHeader
title="Licenses" title="Licenses"
description="Manage licenses to unlock Enterprise features." description="Manage licenses to unlock Enterprise features."
/> />
@@ -1,13 +1,13 @@
import type { FC } from "react"; import type { FC } from "react";
import type { SerpentOption } from "api/typesGenerated"; import type { SerpentOption } from "api/typesGenerated";
import { Badges, EnabledBadge, DisabledBadge } from "components/Badges/Badges"; import { Badges, EnabledBadge, DisabledBadge } from "components/Badges/Badges";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { import {
deploymentGroupHasParent, deploymentGroupHasParent,
useDeploymentOptions, useDeploymentOptions,
} from "utils/deployOptions"; } from "utils/deployOptions";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import { Header } from "../Header";
import OptionsTable from "../OptionsTable"; import OptionsTable from "../OptionsTable";
export type NetworkSettingsPageViewProps = { export type NetworkSettingsPageViewProps = {
@@ -19,7 +19,7 @@ export const NetworkSettingsPageView: FC<NetworkSettingsPageViewProps> = ({
}) => ( }) => (
<Stack direction="column" spacing={6}> <Stack direction="column" spacing={6}>
<div> <div>
<Header <SettingsHeader
title="Network" title="Network"
description="Configure your deployment connectivity." description="Configure your deployment connectivity."
docsHref={docs("/networking")} docsHref={docs("/networking")}
@@ -32,7 +32,7 @@ export const NetworkSettingsPageView: FC<NetworkSettingsPageViewProps> = ({
</div> </div>
<div> <div>
<Header <SettingsHeader
title="Port Forwarding" title="Port Forwarding"
secondary secondary
description="Port forwarding lets developers securely access processes on their Coder workspace from a local machine." description="Port forwarding lets developers securely access processes on their Coder workspace from a local machine."
@@ -4,8 +4,8 @@ import type { FC } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type * as TypesGen from "api/typesGenerated"; import type * as TypesGen from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { Header } from "../Header";
import { OAuth2AppForm } from "./OAuth2AppForm"; import { OAuth2AppForm } from "./OAuth2AppForm";
type CreateOAuth2AppProps = { type CreateOAuth2AppProps = {
@@ -26,7 +26,7 @@ export const CreateOAuth2AppPageView: FC<CreateOAuth2AppProps> = ({
direction="row" direction="row"
justifyContent="space-between" justifyContent="space-between"
> >
<Header <SettingsHeader
title="Add an OAuth2 application" title="Add an OAuth2 application"
description="Configure an application to use Coder as an OAuth2 provider." description="Configure an application to use Coder as an OAuth2 provider."
/> />
@@ -20,10 +20,10 @@ import { CopyableValue } from "components/CopyableValue/CopyableValue";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { TableLoader } from "components/TableLoader/TableLoader"; import { TableLoader } from "components/TableLoader/TableLoader";
import { createDayString } from "utils/createDayString"; import { createDayString } from "utils/createDayString";
import { Header } from "../Header";
import { OAuth2AppForm } from "./OAuth2AppForm"; import { OAuth2AppForm } from "./OAuth2AppForm";
export type MutatingResource = { export type MutatingResource = {
@@ -75,7 +75,7 @@ export const EditOAuth2AppPageView: FC<EditOAuth2AppProps> = ({
direction="row" direction="row"
justifyContent="space-between" justifyContent="space-between"
> >
<Header <SettingsHeader
title="Edit OAuth2 application" title="Edit OAuth2 application"
description="Configure an application to use Coder as an OAuth2 provider." description="Configure an application to use Coder as an OAuth2 provider."
/> />
@@ -14,10 +14,10 @@ import type * as TypesGen from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Avatar } from "components/Avatar/Avatar"; import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/AvatarData/AvatarData"; import { AvatarData } from "components/AvatarData/AvatarData";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { TableLoader } from "components/TableLoader/TableLoader"; import { TableLoader } from "components/TableLoader/TableLoader";
import { useClickableTableRow } from "hooks/useClickableTableRow"; import { useClickableTableRow } from "hooks/useClickableTableRow";
import { Header } from "../Header";
type OAuth2AppsSettingsProps = { type OAuth2AppsSettingsProps = {
apps?: TypesGen.OAuth2ProviderApp[]; apps?: TypesGen.OAuth2ProviderApp[];
@@ -38,7 +38,7 @@ const OAuth2AppsSettingsPageView: FC<OAuth2AppsSettingsProps> = ({
justifyContent="space-between" justifyContent="space-between"
> >
<div> <div>
<Header <SettingsHeader
title="OAuth2 Applications" title="OAuth2 Applications"
description="Configure applications to use Coder as an OAuth2 provider." description="Configure applications to use Coder as an OAuth2 provider."
/> />
@@ -6,10 +6,10 @@ import {
EnabledBadge, EnabledBadge,
EnterpriseBadge, EnterpriseBadge,
} from "components/Badges/Badges"; } from "components/Badges/Badges";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { deploymentGroupHasParent } from "utils/deployOptions"; import { deploymentGroupHasParent } from "utils/deployOptions";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import { Header } from "../Header";
import OptionsTable from "../OptionsTable"; import OptionsTable from "../OptionsTable";
export type ObservabilitySettingsPageViewProps = { export type ObservabilitySettingsPageViewProps = {
@@ -24,8 +24,8 @@ export const ObservabilitySettingsPageView: FC<
<> <>
<Stack direction="column" spacing={6}> <Stack direction="column" spacing={6}>
<div> <div>
<Header title="Observability" /> <SettingsHeader title="Observability" />
<Header <SettingsHeader
title="Audit Logging" title="Audit Logging"
secondary secondary
description="Allow auditors to monitor user operations in your deployment." description="Allow auditors to monitor user operations in your deployment."
@@ -39,7 +39,7 @@ export const ObservabilitySettingsPageView: FC<
</div> </div>
<div> <div>
<Header <SettingsHeader
title="Monitoring" title="Monitoring"
secondary secondary
description="Monitoring your Coder application with logs and metrics." description="Monitoring your Coder application with logs and metrics."
@@ -6,13 +6,13 @@ import {
EnabledBadge, EnabledBadge,
EnterpriseBadge, EnterpriseBadge,
} from "components/Badges/Badges"; } from "components/Badges/Badges";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { import {
deploymentGroupHasParent, deploymentGroupHasParent,
useDeploymentOptions, useDeploymentOptions,
} from "utils/deployOptions"; } from "utils/deployOptions";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import { Header } from "../Header";
import OptionsTable from "../OptionsTable"; import OptionsTable from "../OptionsTable";
export type SecuritySettingsPageViewProps = { export type SecuritySettingsPageViewProps = {
@@ -31,7 +31,7 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
return ( return (
<Stack direction="column" spacing={6}> <Stack direction="column" spacing={6}>
<div> <div>
<Header <SettingsHeader
title="Security" title="Security"
description="Ensure your Coder deployment is secure." description="Ensure your Coder deployment is secure."
/> />
@@ -47,7 +47,7 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
</div> </div>
<div> <div>
<Header <SettingsHeader
title="Browser Only Connections" title="Browser Only Connections"
secondary secondary
description="Block all workspace access via SSH, port forward, and other non-browser connections." description="Block all workspace access via SSH, port forward, and other non-browser connections."
@@ -62,7 +62,7 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
{tlsOptions.length > 0 && ( {tlsOptions.length > 0 && (
<div> <div>
<Header <SettingsHeader
title="TLS" title="TLS"
secondary secondary
description="Ensure TLS is properly configured for your Coder deployment." description="Ensure TLS is properly configured for your Coder deployment."
@@ -1,12 +1,12 @@
import type { SerpentOption } from "api/typesGenerated"; import type { SerpentOption } from "api/typesGenerated";
import { Badges, DisabledBadge, EnabledBadge } from "components/Badges/Badges"; import { Badges, DisabledBadge, EnabledBadge } from "components/Badges/Badges";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { import {
deploymentGroupHasParent, deploymentGroupHasParent,
useDeploymentOptions, useDeploymentOptions,
} from "utils/deployOptions"; } from "utils/deployOptions";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import { Header } from "../Header";
import OptionsTable from "../OptionsTable"; import OptionsTable from "../OptionsTable";
export type UserAuthSettingsPageViewProps = { export type UserAuthSettingsPageViewProps = {
@@ -27,9 +27,9 @@ export const UserAuthSettingsPageView = ({
<> <>
<Stack direction="column" spacing={6}> <Stack direction="column" spacing={6}>
<div> <div>
<Header title="User Authentication" /> <SettingsHeader title="User Authentication" />
<Header <SettingsHeader
title="Login with OpenID Connect" title="Login with OpenID Connect"
secondary secondary
description="Set up authentication to login with OpenID Connect." description="Set up authentication to login with OpenID Connect."
@@ -48,7 +48,7 @@ export const UserAuthSettingsPageView = ({
</div> </div>
<div> <div>
<Header <SettingsHeader
title="Login with GitHub" title="Login with GitHub"
secondary secondary
description="Set up authentication to login with GitHub." description="Set up authentication to login with GitHub."
@@ -3,10 +3,12 @@ import { useMutation, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { createOrganization } from "api/queries/organizations"; import { createOrganization } from "api/queries/organizations";
import { displaySuccess } from "components/GlobalSnackbar/utils"; import { displaySuccess } from "components/GlobalSnackbar/utils";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { CreateOrganizationPageView } from "./CreateOrganizationPageView"; import { CreateOrganizationPageView } from "./CreateOrganizationPageView";
const CreateOrganizationPage: FC = () => { const CreateOrganizationPage: FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const feats = useFeatureVisibility();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const createOrganizationMutation = useMutation( const createOrganizationMutation = useMutation(
@@ -18,6 +20,7 @@ const CreateOrganizationPage: FC = () => {
return ( return (
<CreateOrganizationPageView <CreateOrganizationPageView
error={error} error={error}
isEntitled={feats.multiple_organizations}
onSubmit={async (values) => { onSubmit={async (values) => {
await createOrganizationMutation.mutateAsync(values); await createOrganizationMutation.mutateAsync(values);
displaySuccess("Organization created."); displaySuccess("Organization created.");
@@ -5,6 +5,9 @@ import { CreateOrganizationPageView } from "./CreateOrganizationPageView";
const meta: Meta<typeof CreateOrganizationPageView> = { const meta: Meta<typeof CreateOrganizationPageView> = {
title: "pages/CreateOrganizationPageView", title: "pages/CreateOrganizationPageView",
component: CreateOrganizationPageView, component: CreateOrganizationPageView,
args: {
isEntitled: true,
},
}; };
export default meta; export default meta;
@@ -12,6 +15,12 @@ type Story = StoryObj<typeof CreateOrganizationPageView>;
export const Example: Story = {}; export const Example: Story = {};
export const NotEntitled: Story = {
args: {
isEntitled: false,
},
};
export const Error: Story = { export const Error: Story = {
args: { error: "Oh no!" }, args: { error: "Oh no!" },
}; };
@@ -5,6 +5,12 @@ import * as Yup from "yup";
import { isApiValidationError } from "api/errors"; import { isApiValidationError } from "api/errors";
import type { CreateOrganizationRequest } from "api/typesGenerated"; import type { CreateOrganizationRequest } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import {
Badges,
DisabledBadge,
PremiumBadge,
EntitledBadge,
} from "components/Badges/Badges";
import { import {
FormFields, FormFields,
FormSection, FormSection,
@@ -12,7 +18,14 @@ import {
FormFooter, FormFooter,
} from "components/Form/Form"; } from "components/Form/Form";
import { IconField } from "components/IconField/IconField"; import { IconField } from "components/IconField/IconField";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { docs } from "utils/docs";
import { import {
getFormHelpers, getFormHelpers,
nameValidator, nameValidator,
@@ -35,11 +48,12 @@ const validationSchema = Yup.object({
interface CreateOrganizationPageViewProps { interface CreateOrganizationPageViewProps {
error: unknown; error: unknown;
onSubmit: (values: CreateOrganizationRequest) => Promise<void>; onSubmit: (values: CreateOrganizationRequest) => Promise<void>;
isEntitled: boolean;
} }
export const CreateOrganizationPageView: FC< export const CreateOrganizationPageView: FC<
CreateOrganizationPageViewProps CreateOrganizationPageViewProps
> = ({ error, onSubmit }) => { > = ({ error, onSubmit, isEntitled }) => {
const form = useFormik<CreateOrganizationRequest>({ const form = useFormik<CreateOrganizationRequest>({
initialValues: { initialValues: {
name: "", name: "",
@@ -54,9 +68,30 @@ export const CreateOrganizationPageView: FC<
return ( return (
<div> <div>
<PageHeader> <SettingsHeader
<PageHeaderTitle>Organization settings</PageHeaderTitle> title="New Organization"
</PageHeader> 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."
// TODO: No documentation link yet.
documentationLink={docs("/admin")}
licenseType="premium"
/>
</PopoverContent>
</Popover>
</Badges>
{Boolean(error) && !isApiValidationError(error) && ( {Boolean(error) && !isApiValidationError(error) && (
<div css={{ marginBottom: 32 }}> <div css={{ marginBottom: 32 }}>
@@ -70,7 +105,7 @@ export const CreateOrganizationPageView: FC<
> >
<FormSection <FormSection
title="General info" title="General info"
description="Change the name or description of the organization." description="The name and description of the organization."
> >
<fieldset <fieldset
disabled={form.isSubmitting} disabled={form.isSubmitting}
@@ -28,11 +28,8 @@ import type {
} from "api/typesGenerated"; } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import { FormFields, FormFooter, VerticalForm } from "components/Form/Form"; import { FormFields, FormFooter, VerticalForm } from "components/Form/Form";
import { import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
PageHeader, import { Stack } from "components/Stack/Stack";
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import { getFormHelpers, nameValidator } from "utils/formUtils"; import { getFormHelpers, nameValidator } from "utils/formUtils";
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -77,37 +74,37 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
return ( return (
<> <>
<PageHeader <Stack
actions={ alignItems="baseline"
canAssignOrgRole && ( direction="row"
<> justifyContent="space-between"
<Button
onClick={() => {
navigate(`/organizations/${organizationName}/roles`);
}}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
form.handleSubmit();
}}
>
{role !== undefined ? "Save" : "Create Role"}
</Button>
</>
)
}
> >
<PageHeaderTitle> <SettingsHeader
{role ? "Edit" : "Create"} custom role title={`${role ? "Edit" : "Create"} Custom Role`}
</PageHeaderTitle> description="Set a name and permissions for this role."
<PageHeaderSubtitle> />
{"Set a name and permissions for this role."} {canAssignOrgRole && (
</PageHeaderSubtitle> <Stack direction="row" spacing={2}>
</PageHeader> <Button
onClick={() => {
navigate(`/organizations/${organizationName}/roles`);
}}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
form.handleSubmit();
}}
>
{role !== undefined ? "Save" : "Create Role"}
</Button>
</Stack>
)}
</Stack>
<VerticalForm onSubmit={form.handleSubmit}> <VerticalForm onSubmit={form.handleSubmit}>
<FormFields> <FormFields>
{Boolean(error) && !isApiValidationError(error) && ( {Boolean(error) && !isApiValidationError(error) && (
@@ -9,7 +9,8 @@ import { organizationPermissions } from "api/queries/organizations";
import { organizationRoles } from "api/queries/roles"; import { organizationRoles } from "api/queries/roles";
import { displayError } from "components/GlobalSnackbar/utils"; import { displayError } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { useOrganizationSettings } from "../ManagementSettingsLayout"; import { useOrganizationSettings } from "../ManagementSettingsLayout";
@@ -50,23 +51,21 @@ export const CustomRolesPage: FC = () => {
<title>{pageTitle("Custom Roles")}</title> <title>{pageTitle("Custom Roles")}</title>
</Helmet> </Helmet>
<PageHeader <Stack
actions={ alignItems="baseline"
<> direction="row"
{permissions.assignOrgRole && isCustomRolesEnabled && ( justifyContent="space-between"
<Button
component={RouterLink}
startIcon={<AddIcon />}
to="create"
>
Create custom role
</Button>
)}
</>
}
> >
<PageHeaderTitle>Custom Roles</PageHeaderTitle> <SettingsHeader
</PageHeader> title="Custom Roles"
description="Manage custom roles for this organization."
/>
{permissions.assignOrgRole && isCustomRolesEnabled && (
<Button component={RouterLink} startIcon={<AddIcon />} to="create">
Create custom role
</Button>
)}
</Stack>
<CustomRolesPageView <CustomRolesPageView
roles={filteredRoleData} roles={filteredRoleData}
@@ -13,7 +13,7 @@ import {
HorizontalForm, HorizontalForm,
} from "components/Form/Form"; } from "components/Form/Form";
import { IconField } from "components/IconField/IconField"; import { IconField } from "components/IconField/IconField";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; import { getFormHelpers, onChangeTrimmed } from "utils/formUtils";
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -47,9 +47,11 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
return ( return (
<> <>
<PageHeader css={{ paddingTop: 8 }}> <SettingsHeader
<PageHeaderTitle>Create a group</PageHeaderTitle> title="New Group"
</PageHeader> description="Create a group in this organization."
/>
<HorizontalForm onSubmit={form.handleSubmit}> <HorizontalForm onSubmit={form.handleSubmit}>
<FormSection <FormSection
title="Group settings" title="Group settings"
@@ -30,7 +30,6 @@ import { EmptyState } from "components/EmptyState/EmptyState";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { LastSeen } from "components/LastSeen/LastSeen"; import { LastSeen } from "components/LastSeen/LastSeen";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { Margins } from "components/Margins/Margins";
import { import {
MoreMenu, MoreMenu,
MoreMenuContent, MoreMenuContent,
@@ -38,7 +37,7 @@ import {
MoreMenuTrigger, MoreMenuTrigger,
ThreeDotsButton, ThreeDotsButton,
} from "components/MoreMenu/MoreMenu"; } from "components/MoreMenu/MoreMenu";
import { ResourcePageHeader } from "components/PageHeader/PageHeader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { import {
PaginationStatus, PaginationStatus,
@@ -98,111 +97,113 @@ export const GroupPage: FC = () => {
<> <>
{helmet} {helmet}
<Margins> <Stack
<ResourcePageHeader alignItems="baseline"
displayName={groupData?.display_name} direction="row"
name={groupData?.name} justifyContent="space-between"
actions={ >
canUpdateGroup && ( <SettingsHeader
<> title={groupData?.display_name || groupData?.name}
<Button description="Manage members for this group."
startIcon={<SettingsOutlined />}
to="settings"
component={RouterLink}
>
Settings
</Button>
<Button
disabled={groupData?.id === groupData?.organization_id}
onClick={() => {
setIsDeletingGroup(true);
}}
startIcon={<DeleteOutline />}
css={styles.removeButton}
>
Delete&hellip;
</Button>
</>
)
}
/> />
{canUpdateGroup && (
<Stack spacing={1}> <Stack direction="row" spacing={2}>
{canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( <Button
<AddGroupMember startIcon={<SettingsOutlined />}
isLoading={addMemberMutation.isLoading} to="settings"
onSubmit={async (user, reset) => { component={RouterLink}
try { >
await addMemberMutation.mutateAsync({ Settings
groupId, </Button>
userId: user.id, <Button
}); disabled={groupData?.id === groupData?.organization_id}
reset(); onClick={() => {
await groupQuery.refetch(); setIsDeletingGroup(true);
} catch (error) {
displayError(getErrorMessage(error, "Failed to add member."));
}
}} }}
/> startIcon={<DeleteOutline />}
)} css={styles.removeButton}
<TableToolbar> >
<PaginationStatus Delete&hellip;
isLoading={Boolean(isLoading)} </Button>
showing={groupData?.members.length ?? 0} </Stack>
total={groupData?.members.length ?? 0} )}
label="members" </Stack>
/>
</TableToolbar>
<TableContainer> <Stack spacing={1}>
<Table> {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && (
<TableHead> <AddGroupMember
isLoading={addMemberMutation.isLoading}
onSubmit={async (user, reset) => {
try {
await addMemberMutation.mutateAsync({
groupId,
userId: user.id,
});
reset();
await groupQuery.refetch();
} catch (error) {
displayError(getErrorMessage(error, "Failed to add member."));
}
}}
/>
)}
<TableToolbar>
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={groupData?.members.length ?? 0}
total={groupData?.members.length ?? 0}
label="members"
/>
</TableToolbar>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="59%">User</TableCell>
<TableCell width="40">Status</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{groupData?.members.length === 0 ? (
<TableRow> <TableRow>
<TableCell width="59%">User</TableCell> <TableCell colSpan={999}>
<TableCell width="40">Status</TableCell> <EmptyState
<TableCell width="1%"></TableCell> message="No members yet"
</TableRow> description="Add a member using the controls above"
</TableHead>
<TableBody>
{groupData?.members.length === 0 ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No members yet"
description="Add a member using the controls above"
/>
</TableCell>
</TableRow>
) : (
groupData?.members.map((member) => (
<GroupMemberRow
member={member}
group={groupData}
key={member.id}
canUpdate={canUpdateGroup}
onRemove={async () => {
try {
await removeMemberMutation.mutateAsync({
groupId: groupData.id,
userId: member.id,
});
await groupQuery.refetch();
displaySuccess("Member removed successfully.");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to remove member."),
);
}
}}
/> />
)) </TableCell>
)} </TableRow>
</TableBody> ) : (
</Table> groupData?.members.map((member) => (
</TableContainer> <GroupMemberRow
</Stack> member={member}
</Margins> group={groupData}
key={member.id}
canUpdate={canUpdateGroup}
onRemove={async () => {
try {
await removeMemberMutation.mutateAsync({
groupId: groupData.id,
userId: member.id,
});
await groupQuery.refetch();
displaySuccess("Member removed successfully.");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to remove member."),
);
}
}}
/>
))
)}
</TableBody>
</Table>
</TableContainer>
</Stack>
{groupQuery.data && ( {groupQuery.data && (
<DeleteDialog <DeleteDialog
@@ -11,7 +11,8 @@ import type { Organization } from "api/typesGenerated";
import { EmptyState } from "components/EmptyState/EmptyState"; import { EmptyState } from "components/EmptyState/EmptyState";
import { displayError } from "components/GlobalSnackbar/utils"; import { displayError } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { useOrganizationSettings } from "../ManagementSettingsLayout"; import { useOrganizationSettings } from "../ManagementSettingsLayout";
@@ -73,23 +74,21 @@ export const GroupsPage: FC = () => {
<title>{pageTitle("Groups")}</title> <title>{pageTitle("Groups")}</title>
</Helmet> </Helmet>
<PageHeader <Stack
actions={ alignItems="baseline"
<> direction="row"
{permissions.createGroup && feats.template_rbac && ( justifyContent="space-between"
<Button
component={RouterLink}
startIcon={<GroupAdd />}
to="create"
>
Create group
</Button>
)}
</>
}
> >
<PageHeaderTitle>Groups</PageHeaderTitle> <SettingsHeader
</PageHeader> title="Groups"
description="Manage groups for this organization."
/>
{permissions.createGroup && feats.template_rbac && (
<Button component={RouterLink} startIcon={<GroupAdd />} to="create">
Create group
</Button>
)}
</Stack>
<GroupsPageView <GroupsPageView
groups={groupsQuery.data} groups={groupsQuery.data}
@@ -24,7 +24,7 @@ import {
MoreMenuItem, MoreMenuItem,
ThreeDotsButton, ThreeDotsButton,
} from "components/MoreMenu/MoreMenu"; } from "components/MoreMenu/MoreMenu";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { UserAvatar } from "components/UserAvatar/UserAvatar";
@@ -52,9 +52,7 @@ export const OrganizationMembersPageView: FC<
> = (props) => { > = (props) => {
return ( return (
<div> <div>
<PageHeader> <SettingsHeader title="Members" />
<PageHeaderTitle>Organization members</PageHeaderTitle>
</PageHeader>
<Stack> <Stack>
{Boolean(props.error) && <ErrorAlert error={props.error} />} {Boolean(props.error) && <ErrorAlert error={props.error} />}
@@ -18,7 +18,7 @@ import {
FormFooter, FormFooter,
} from "components/Form/Form"; } from "components/Form/Form";
import { IconField } from "components/IconField/IconField"; import { IconField } from "components/IconField/IconField";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { import {
getFormHelpers, getFormHelpers,
nameValidator, nameValidator,
@@ -67,9 +67,7 @@ export const OrganizationSettingsPageView: FC<
return ( return (
<div> <div>
<PageHeader> <SettingsHeader title="Settings" />
<PageHeaderTitle>Organization settings</PageHeaderTitle>
</PageHeader>
{Boolean(error) && !isApiValidationError(error) && ( {Boolean(error) && !isApiValidationError(error) && (
<div css={{ marginBottom: 32 }}> <div css={{ marginBottom: 32 }}>
@@ -234,7 +234,7 @@ const OrganizationSettingsNavigation: FC<
<Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}> <Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}>
{organization.permissions.editOrganization && ( {organization.permissions.editOrganization && (
<SidebarNavSubItem end href={urlForSubpage(organization.name)}> <SidebarNavSubItem end href={urlForSubpage(organization.name)}>
Organization settings Settings
</SidebarNavSubItem> </SidebarNavSubItem>
)} )}
{organization.permissions.editMembers && ( {organization.permissions.editMembers && (
+1 -2
View File
@@ -25,8 +25,7 @@ export const UsersLayout: FC = () => {
const location = useLocation(); const location = useLocation();
const activeTab = location.pathname.endsWith("groups") ? "groups" : "users"; const activeTab = location.pathname.endsWith("groups") ? "groups" : "users";
const canViewOrganizations = const canViewOrganizations = experiments.includes("multi-organization");
feats.multiple_organizations && experiments.includes("multi-organization");
return ( return (
<> <>
+1 -4
View File
@@ -29,7 +29,6 @@ import { isNonInitialPage } from "components/PaginationWidget/utils";
import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useAuthenticated } from "contexts/auth/RequireAuth";
import { usePaginatedQuery } from "hooks/usePaginatedQuery"; import { usePaginatedQuery } from "hooks/usePaginatedQuery";
import { useDashboard } from "modules/dashboard/useDashboard"; import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { generateRandomString } from "utils/random"; import { generateRandomString } from "utils/random";
import { ResetPasswordDialog } from "./ResetPasswordDialog"; import { ResetPasswordDialog } from "./ResetPasswordDialog";
@@ -43,7 +42,6 @@ const UsersPage: FC = () => {
const searchParamsResult = useSearchParams(); const searchParamsResult = useSearchParams();
const { entitlements, experiments } = useDashboard(); const { entitlements, experiments } = useDashboard();
const [searchParams] = searchParamsResult; const [searchParams] = searchParamsResult;
const feats = useFeatureVisibility();
const groupsByUserIdQuery = useQuery(groupsByUserId("default")); const groupsByUserIdQuery = useQuery(groupsByUserId("default"));
const authMethodsQuery = useQuery(authMethods()); const authMethodsQuery = useQuery(authMethods());
@@ -104,8 +102,7 @@ const UsersPage: FC = () => {
authMethodsQuery.isLoading || authMethodsQuery.isLoading ||
groupsByUserIdQuery.isLoading; groupsByUserIdQuery.isLoading;
const canViewOrganizations = const canViewOrganizations = experiments.includes("multi-organization");
feats.multiple_organizations && experiments.includes("multi-organization");
if (canViewOrganizations && location.pathname !== "/deployment/users") { if (canViewOrganizations && location.pathname !== "/deployment/users") {
return <Navigate to={`/deployment/users${location.search}`} replace />; return <Navigate to={`/deployment/users${location.search}`} replace />;
} }