mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08: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 = () => {
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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();
|
||||||
|
|||||||
+2
-2
@@ -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 />
|
||||||
|
|||||||
+2
-2
@@ -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."
|
||||||
|
|||||||
+2
-2
@@ -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."
|
||||||
/>
|
/>
|
||||||
|
|||||||
+2
-2
@@ -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."
|
||||||
/>
|
/>
|
||||||
|
|||||||
+4
-4
@@ -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…
|
|
||||||
</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…
|
||||||
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 && (
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user