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