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

* fix: only check flag for organization settings

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

* Move deployment settings header to components

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

* Add premium badge

* Use settings header on org pages

* Add license badges to create org page

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

* Change create org form description text

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

* Consistently capitalize org menu items and headings

Also, remove the "organizations" prefix since it seems redundant.
This commit is contained in:
Asher
2024-08-08 23:29:37 -08:00
committed by GitHub
parent 9a47ea1279
commit abbcffe181
31 changed files with 302 additions and 238 deletions
+17
View File
@@ -123,6 +123,23 @@ export const EnterpriseBadge: FC = () => {
);
};
export const PremiumBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
backgroundColor: theme.roles.info.background,
border: `1px solid ${theme.roles.info.outline}`,
color: theme.roles.info.text,
}),
]}
>
Premium
</span>
);
};
export const PreviewBadge: FC = () => {
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",
},
};
+10 -3
View File
@@ -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>
@@ -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();
@@ -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 />
@@ -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."
@@ -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."
/>
@@ -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."
/>
@@ -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&hellip;
</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&hellip;
</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 && (
+1 -2
View File
@@ -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 (
<>
+1 -4
View File
@@ -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 />;
}