diff --git a/site/e2e/tests/organizationGroups.spec.ts b/site/e2e/tests/organizationGroups.spec.ts index 662752b81f..7f07a03d82 100644 --- a/site/e2e/tests/organizationGroups.spec.ts +++ b/site/e2e/tests/organizationGroups.spec.ts @@ -112,7 +112,7 @@ test("change quota settings", async ({ page }) => { await login(page, orgUserAdmin); await page.goto(`/organizations/${org.name}/groups/${group.name}`); - await page.getByRole("link", { name: "Settings", exact: true }).click(); + await page.getByRole("link", { name: "Group settings" }).click(); await expectUrl(page).toHavePathName( `/organizations/${org.name}/groups/${group.name}/settings`, ); @@ -127,6 +127,6 @@ test("change quota settings", async ({ page }) => { ); // ...and that setting should persist if we go back - await page.getByRole("link", { name: "Settings", exact: true }).click(); + await page.getByRole("link", { name: "Group settings" }).click(); await expect(page.getByLabel("Quota Allowance")).toHaveValue("100"); }); diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index d21563db37..9e12ae8abf 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -127,7 +127,7 @@ export const createGroup = (queryClient: QueryClient, organization: string) => { }; }; -export const patchGroup = (queryClient: QueryClient) => { +export const patchGroup = (queryClient: QueryClient, organization: string) => { return { mutationFn: ({ groupId, @@ -135,40 +135,46 @@ export const patchGroup = (queryClient: QueryClient) => { }: PatchGroupRequest & { groupId: string }) => API.patchGroup(groupId, request), onSuccess: async (updatedGroup: Group) => - invalidateGroup(queryClient, "default", updatedGroup.id), + invalidateGroup(queryClient, organization, updatedGroup.name), }; }; -export const deleteGroup = (queryClient: QueryClient) => { +export const deleteGroup = (queryClient: QueryClient, organization: string) => { return { - mutationFn: API.deleteGroup, - onSuccess: async (_: unknown, groupId: string) => - invalidateGroup(queryClient, "default", groupId), + mutationFn: ({ groupId }: { groupId: string; groupName: string }) => + API.deleteGroup(groupId), + onSuccess: async ( + _: unknown, + { groupName }: { groupId: string; groupName: string }, + ) => invalidateGroup(queryClient, organization, groupName), }; }; -export const addMember = (queryClient: QueryClient) => { +export const addMember = (queryClient: QueryClient, organization: string) => { return { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => API.addMember(groupId, userId), onSuccess: async (updatedGroup: Group) => - invalidateGroup(queryClient, "default", updatedGroup.id), + invalidateGroup(queryClient, organization, updatedGroup.name), }; }; -export const removeMember = (queryClient: QueryClient) => { +export const removeMember = ( + queryClient: QueryClient, + organization: string, +) => { return { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => API.removeMember(groupId, userId), onSuccess: async (updatedGroup: Group) => - invalidateGroup(queryClient, "default", updatedGroup.id), + invalidateGroup(queryClient, organization, updatedGroup.name), }; }; const invalidateGroup = ( queryClient: QueryClient, organization: string, - groupId: string, + groupName: string, ) => Promise.all([ queryClient.invalidateQueries({ queryKey: groupsQueryKey }), @@ -176,7 +182,7 @@ const invalidateGroup = ( queryKey: getGroupsByOrganizationQueryKey(organization), }), queryClient.invalidateQueries({ - queryKey: getGroupQueryKey(organization, groupId), + queryKey: getGroupQueryKey(organization, groupName), }), ]); diff --git a/site/src/components/PageHeader/PageHeader.tsx b/site/src/components/PageHeader/PageHeader.tsx index 96366114dc..29930cdc41 100644 --- a/site/src/components/PageHeader/PageHeader.tsx +++ b/site/src/components/PageHeader/PageHeader.tsx @@ -61,23 +61,3 @@ export const PageHeaderCaption: FC = ({ children }) => { ); }; - -interface ResourcePageHeaderProps extends Omit { - displayName?: string; - name: string; -} - -export const ResourcePageHeader: FC = ({ - displayName, - name, - ...props -}) => { - const title = displayName || name; - - return ( - - {title} - {name !== title && {name}} - - ); -}; diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index 4fdf78eadb..764c91906a 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -1,20 +1,10 @@ -import TextField from "@mui/material/TextField"; import { isApiValidationError } from "api/errors"; import type { CreateGroupRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; -import { - FormFields, - FormFooter, - FormSection, - HorizontalForm, -} from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; -import { - SettingsHeader, - SettingsHeaderDescription, - SettingsHeaderTitle, -} from "components/SettingsHeader/SettingsHeader"; +import { Input } from "components/Input/Input"; +import { Label } from "components/Label/Label"; import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; import type { FC } from "react"; @@ -54,42 +44,84 @@ export const CreateGroupPageView: FC = ({ }); const getFieldHelpers = getFormHelpers(form, error); const onCancel = () => navigate(-1); + const nameField = getFieldHelpers("name"); + const displayNameField = getFieldHelpers("display_name", { + helperText: "Keep empty to default to the name.", + }); return ( - <> - - New Group - - Create a group in this organization. - - +
+
+

+ New Group +

+
- - - +
+
+
+

+ Group settings +

+

+ Set a name and avatar for this group. +

+
+
{Boolean(error) && !isApiValidationError(error) && ( )} - - +
+ + + {nameField.helperText && ( + + {nameField.helperText} + + )} +
+
+ + + {displayNameField.helperText && ( + + {displayNameField.helperText} + + )} +
= ({ label="Avatar URL" onPickEmoji={(value) => form.setFieldValue("avatar_url", value)} /> - - +
+
- +
- - - - +
+ +
); }; diff --git a/site/src/pages/GroupsPage/GroupMembersPage.tsx b/site/src/pages/GroupsPage/GroupMembersPage.tsx new file mode 100644 index 0000000000..61f56b3c28 --- /dev/null +++ b/site/src/pages/GroupsPage/GroupMembersPage.tsx @@ -0,0 +1,266 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import { getErrorDetail, getErrorMessage } from "api/errors"; +import { addMember, removeMember } from "api/queries/groups"; +import type { + Group, + OrganizationMemberWithUserData, + ReducedUser, +} from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { AvatarData } from "components/Avatar/AvatarData"; +import { Button } from "components/Button/Button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { LastSeen } from "components/LastSeen/LastSeen"; +import { Spinner } from "components/Spinner/Spinner"; +import { Stack } from "components/Stack/Stack"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; +import { + PaginationStatus, + TableToolbar, +} from "components/TableToolbar/TableToolbar"; +import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { EllipsisVertical, UserPlusIcon } from "lucide-react"; +import { isEveryoneGroup } from "modules/groups"; +import { type FC, useState } from "react"; +import { useMutation, useQueryClient } from "react-query"; +import { useOutletContext } from "react-router"; +import { toast } from "sonner"; +import type { GroupPageOutletContext } from "./GroupPage"; + +const GroupMembersPage: FC = () => { + const { + group: groupData, + organization, + permissions, + groupQuery, + } = useOutletContext(); + const queryClient = useQueryClient(); + const addMemberMutation = useMutation(addMember(queryClient, organization)); + const removeMemberMutation = useMutation( + removeMember(queryClient, organization), + ); + const canUpdateGroup = permissions ? permissions.canUpdateGroup : false; + const groupId = groupData.id; + + return ( +
+ {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( + { + try { + await addMemberMutation.mutateAsync({ + groupId, + userId: member.user_id, + }); + reset(); + await groupQuery.refetch(); + } catch (error) { + toast.error(getErrorMessage(error, "Failed to add member."), { + description: getErrorDetail(error), + }); + } + }} + /> + )} + + + + + + + + User + Status + + + + + + {groupData?.members.length === 0 ? ( + + + + + + ) : ( + groupData?.members.map((member) => ( + { + const mutation = removeMemberMutation.mutateAsync( + { + groupId: groupData.id, + userId: member.id, + }, + { + onSuccess: () => { + groupQuery.refetch(); + }, + }, + ); + toast.promise(mutation, { + loading: `Removing member "${member.username}" from "${groupData.name}"...`, + success: `Member "${member.username}" has been removed from "${groupData.name}" successfully.`, + error: (error) => ({ + message: `Failed to remove member "${member.username}" from "${groupData.name}".`, + description: getErrorDetail(error), + }), + }); + }} + /> + )) + )} + +
+
+ ); +}; + +interface AddGroupMemberProps { + isLoading: boolean; + onSubmit: (user: OrganizationMemberWithUserData, reset: () => void) => void; + organizationId: string; +} + +const AddGroupMember: FC = ({ + isLoading, + onSubmit, + organizationId, +}) => { + const [selectedUser, setSelectedUser] = + useState(null); + + const resetValues = () => { + setSelectedUser(null); + }; + + return ( +
{ + e.preventDefault(); + + if (selectedUser) { + onSubmit(selectedUser, resetValues); + } + }} + > + + { + setSelectedUser(newValue); + }} + /> + + + +
+ ); +}; + +interface GroupMemberRowProps { + member: ReducedUser; + group: Group; + canUpdate: boolean; + onRemove: () => void; +} + +const GroupMemberRow: FC = ({ + member, + group, + canUpdate, + onRemove, +}) => { + return ( + + + + } + title={member.username} + subtitle={member.email} + /> + + +
{member.status}
+ +
+ + {canUpdate && ( + + + + + + + Remove + + + + )} + +
+ ); +}; + +const styles = { + autoComplete: { + width: 300, + }, + status: { + textTransform: "capitalize", + }, + suspended: (theme) => ({ + color: theme.palette.text.secondary, + }), +} satisfies Record>; + +export default GroupMembersPage; diff --git a/site/src/pages/GroupsPage/GroupPage.stories.tsx b/site/src/pages/GroupsPage/GroupPage.stories.tsx index 4367461fcd..80675c91b6 100644 --- a/site/src/pages/GroupsPage/GroupPage.stories.tsx +++ b/site/src/pages/GroupsPage/GroupPage.stories.tsx @@ -9,7 +9,11 @@ import { API } from "api/api"; import { getGroupQueryKey, groupPermissionsKey } from "api/queries/groups"; import { organizationMembersKey } from "api/queries/organizations"; import { spyOn, userEvent, within } from "storybook/test"; -import { reactRouterParameters } from "storybook-addon-remix-react-router"; +import { + reactRouterOutlet, + reactRouterParameters, +} from "storybook-addon-remix-react-router"; +import GroupMembersPage from "./GroupMembersPage"; import GroupPage from "./GroupPage"; const meta: Meta = { @@ -23,7 +27,10 @@ const meta: Meta = { groupName: MockGroup.name, }, }, - routing: { path: "/organizations/:organization/groups/:groupName" }, + routing: reactRouterOutlet( + { path: "/organizations/:organization/groups/:groupName" }, + , + ), }), }, }; diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index 322c3e5ac9..a608ba6a19 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -1,69 +1,36 @@ -import type { Interpolation, Theme } from "@emotion/react"; import { getErrorDetail, getErrorMessage } from "api/errors"; -import { - addMember, - deleteGroup, - group, - groupPermissions, - removeMember, -} from "api/queries/groups"; -import type { - Group, - OrganizationMemberWithUserData, - ReducedUser, -} from "api/typesGenerated"; +import { deleteGroup, group, groupPermissions } from "api/queries/groups"; +import type { Group } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Avatar } from "components/Avatar/Avatar"; -import { AvatarData } from "components/Avatar/AvatarData"; import { Button } from "components/Button/Button"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "components/DropdownMenu/DropdownMenu"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { LastSeen } from "components/LastSeen/LastSeen"; import { Loader } from "components/Loader/Loader"; import { SettingsHeader, SettingsHeaderDescription, SettingsHeaderTitle, } from "components/SettingsHeader/SettingsHeader"; -import { Spinner } from "components/Spinner/Spinner"; -import { Stack } from "components/Stack/Stack"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "components/Table/Table"; -import { - PaginationStatus, - TableToolbar, -} from "components/TableToolbar/TableToolbar"; -import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; -import { - EllipsisVertical, - SettingsIcon, - TrashIcon, - UserPlusIcon, -} from "lucide-react"; -import { isEveryoneGroup } from "modules/groups"; +import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; +import { TrashIcon } from "lucide-react"; import { type FC, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Link as RouterLink, useNavigate, useParams } from "react-router"; +import { Outlet, useLocation, useNavigate, useParams } from "react-router"; import { toast } from "sonner"; import { pageTitle } from "utils/page"; +export type GroupPageOutletContext = { + group: Group; + permissions: { canUpdateGroup: boolean }; + organization: string; + groupQuery: ReturnType; +}; + const GroupPage: FC = () => { const { organization = "default", groupName } = useParams() as { organization?: string; groupName: string; }; + const location = useLocation(); const queryClient = useQueryClient(); const navigate = useNavigate(); const groupQuery = useQuery(group(organization, groupName)); @@ -72,9 +39,9 @@ const GroupPage: FC = () => { ...groupPermissions(groupData?.id ?? ""), enabled: !!groupData, }); - const addMemberMutation = useMutation(addMember(queryClient)); - const removeMemberMutation = useMutation(removeMember(queryClient)); - const deleteGroupMutation = useMutation(deleteGroup(queryClient)); + const deleteGroupMutation = useMutation( + deleteGroup(queryClient, organization), + ); const [isDeletingGroup, setIsDeletingGroup] = useState(false); const isLoading = groupQuery.isLoading || !groupData || !permissions; const canUpdateGroup = permissions ? permissions.canUpdateGroup : false; @@ -97,7 +64,11 @@ const GroupPage: FC = () => { ); } + const groupId = groupData.id; + const activeTab = location.pathname.endsWith("/settings") + ? "settings" + : "members"; return ( <> @@ -106,7 +77,7 @@ const GroupPage: FC = () => {
- {groupData?.display_name || groupData?.name || "Unknown Group"} + {groupData.display_name || groupData.name || "Unknown Group"} Manage members for this group. @@ -114,109 +85,42 @@ const GroupPage: FC = () => { {canUpdateGroup && ( - - - - + )}
- -
- {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( - { - try { - await addMemberMutation.mutateAsync({ - groupId, - userId: member.user_id, - }); - reset(); - await groupQuery.refetch(); - } catch (error) { - toast.error(getErrorMessage(error, "Failed to add member."), { - description: getErrorDetail(error), - }); - } - }} - /> +
+ {canUpdateGroup && ( + + + + Group members + + + Group settings + + + )} - - - - - - - User - Status - - - - - - {groupData?.members.length === 0 ? ( - - - - - - ) : ( - groupData?.members.map((member) => ( - { - const mutation = removeMemberMutation.mutateAsync( - { - groupId: groupData.id, - userId: member.id, - }, - { - onSuccess: () => { - groupQuery.refetch(); - }, - }, - ); - toast.promise(mutation, { - loading: `Removing member "${member.username}" from "${groupData.name}"...`, - success: `Member "${member.username}" has been removed from "${groupData.name}" successfully.`, - error: (error) => ({ - message: `Failed to remove member "${member.username}" from "${groupData.name}".`, - description: getErrorDetail(error), - }), - }); - }} - /> - )) - )} - -
+
{groupQuery.data && ( @@ -227,7 +131,10 @@ const GroupPage: FC = () => { entity="group" onConfirm={async () => { try { - await deleteGroupMutation.mutateAsync(groupId); + await deleteGroupMutation.mutateAsync({ + groupId, + groupName: groupData.name, + }); toast.success( `Group "${groupQuery.data.name}" deleted successfully.`, ); @@ -253,125 +160,4 @@ const GroupPage: FC = () => { ); }; -interface AddGroupMemberProps { - isLoading: boolean; - onSubmit: (user: OrganizationMemberWithUserData, reset: () => void) => void; - organizationId: string; -} - -const AddGroupMember: FC = ({ - isLoading, - onSubmit, - organizationId, -}) => { - const [selectedUser, setSelectedUser] = - useState(null); - - const resetValues = () => { - setSelectedUser(null); - }; - - return ( -
{ - e.preventDefault(); - - if (selectedUser) { - onSubmit(selectedUser, resetValues); - } - }} - > - - { - setSelectedUser(newValue); - }} - /> - - - -
- ); -}; - -interface GroupMemberRowProps { - member: ReducedUser; - group: Group; - canUpdate: boolean; - onRemove: () => void; -} - -const GroupMemberRow: FC = ({ - member, - group, - canUpdate, - onRemove, -}) => { - return ( - - - - } - title={member.username} - subtitle={member.email} - /> - - -
{member.status}
- -
- - {canUpdate && ( - - - - - - - Remove - - - - )} - -
- ); -}; - -const styles = { - autoComplete: { - width: 300, - }, - status: { - textTransform: "capitalize", - }, - suspended: (theme) => ({ - color: theme.palette.text.secondary, - }), -} satisfies Record>; - export default GroupPage; diff --git a/site/src/pages/GroupsPage/GroupSettingsPage.tsx b/site/src/pages/GroupsPage/GroupSettingsPage.tsx index 0098a9bff3..a57df48f6a 100644 --- a/site/src/pages/GroupsPage/GroupSettingsPage.tsx +++ b/site/src/pages/GroupsPage/GroupSettingsPage.tsx @@ -1,12 +1,10 @@ import { getErrorDetail, getErrorMessage } from "api/errors"; -import { group, patchGroup } from "api/queries/groups"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Loader } from "components/Loader/Loader"; +import { patchGroup } from "api/queries/groups"; import type { FC } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { useNavigate, useParams } from "react-router"; +import { useMutation, useQueryClient } from "react-query"; +import { useNavigate, useOutletContext, useParams } from "react-router"; import { toast } from "sonner"; -import { pageTitle } from "utils/page"; +import type { GroupPageOutletContext } from "./GroupPage"; import GroupSettingsPageView from "./GroupSettingsPageView"; const GroupSettingsPage: FC = () => { @@ -14,69 +12,46 @@ const GroupSettingsPage: FC = () => { organization?: string; groupName: string; }; + const { group: groupData } = useOutletContext(); const queryClient = useQueryClient(); - const groupQuery = useQuery(group(organization, groupName)); - const patchGroupMutation = useMutation(patchGroup(queryClient)); + const patchGroupMutation = useMutation(patchGroup(queryClient, organization)); const navigate = useNavigate(); - const navigateToGroup = () => { - navigate(`/organizations/${organization}/groups/${groupName}`); - }; - - const title = {pageTitle("Settings Group")}; - - if (groupQuery.error) { - return ; - } - - if (groupQuery.isLoading || !groupQuery.data) { - return ( - <> - {title} - - - ); - } - const groupId = groupQuery.data.id; - return ( - <> - {title} - - { - await patchGroupMutation.mutateAsync( - { - groupId, - ...data, - add_users: [], - remove_users: [], + navigate("..")} + onSubmit={async (data) => { + await patchGroupMutation.mutateAsync( + { + groupId: groupData.id, + ...data, + add_users: [], + remove_users: [], + }, + { + onSuccess: () => { + navigate(`/organizations/${organization}/groups/${data.name}`); }, - { - onSuccess: () => { - navigate(`../${data.name}`); - }, - onError: (error) => { - toast.error( - getErrorMessage( - error, - `Failed to update group "${groupName}".`, - ), - { - description: getErrorDetail(error), - }, - ); - }, + onError: (error) => { + toast.error( + getErrorMessage( + error, + `Failed to update group "${groupName}".`, + ), + { + description: getErrorDetail(error), + }, + ); }, - ); - }} - group={groupQuery.data} - formErrors={groupQuery.error} - isLoading={groupQuery.isLoading} - isUpdating={patchGroupMutation.isPending} - /> - + }, + ); + }} + group={groupData} + formErrors={undefined} + isLoading={false} + isUpdating={patchGroupMutation.isPending} + /> ); }; + export default GroupSettingsPage; diff --git a/site/src/pages/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/GroupsPage/GroupSettingsPageView.tsx index a07d1b4a10..e1cfc9ecd5 100644 --- a/site/src/pages/GroupsPage/GroupSettingsPageView.tsx +++ b/site/src/pages/GroupsPage/GroupSettingsPageView.tsx @@ -1,15 +1,8 @@ -import TextField from "@mui/material/TextField"; import type { Group } from "api/typesGenerated"; import { Button } from "components/Button/Button"; -import { - FormFields, - FormFooter, - FormSection, - HorizontalForm, -} from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; -import { Loader } from "components/Loader/Loader"; -import { ResourcePageHeader } from "components/PageHeader/PageHeader"; +import { Input } from "components/Input/Input"; +import { Label } from "components/Label/Label"; import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; import { isEveryoneGroup } from "modules/groups"; @@ -59,35 +52,75 @@ const UpdateGroupForm: FC = ({ onSubmit, }); const getFieldHelpers = getFormHelpers(form, errors); + const nameField = getFieldHelpers("name"); + const displayNameField = getFieldHelpers("display_name", { + helperText: "Keep empty to default to the name.", + }); + const quotaField = getFieldHelpers("quota_allowance", { + helperText: `This group gives ${form.values.quota_allowance} quota credits to each + of its members.`, + }); return ( - - - - +
+
+
+

+ General +

+
+
+
+ + + {nameField.helperText && ( + + {nameField.helperText} + + )} +
{!isEveryoneGroup(group) && ( <> - +
+ + + {displayNameField.helperText && ( + + {displayNameField.helperText} + + )} +
= ({ /> )} - - - - - - - +
+
+
+
+

+ Quotas +

+

+ You can use quotas to restrict how many resources a user can create. +

+
+
+
+ + + {quotaField.helperText && ( + + {quotaField.helperText} + + )} +
+
+
- +
@@ -127,8 +178,8 @@ const UpdateGroupForm: FC = ({ Save - - +
+ ); }; @@ -146,28 +197,16 @@ const GroupSettingsPageView: FC = ({ onSubmit, group, formErrors, - isLoading, isUpdating, }) => { - if (isLoading) { - return ; - } - return ( - <> - - - + ); }; diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 39fdf2e57a..6ea6c86a68 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -9,7 +9,6 @@ import { SettingsHeaderDescription, SettingsHeaderTitle, } from "components/SettingsHeader/SettingsHeader"; -import { Stack } from "components/Stack/Stack"; import { PlusIcon } from "lucide-react"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { RequirePermission } from "modules/permissions/RequirePermission"; @@ -80,11 +79,7 @@ const GroupsPage: FC = () => {
{title} - +
Groups @@ -101,7 +96,7 @@ const GroupsPage: FC = () => { )} - +
import("./pages/GroupsPage/CreateGroupPage"), ); const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage")); +const GroupMembersPage = lazy( + () => import("./pages/GroupsPage/GroupMembersPage"), +); const GroupSettingsPage = lazy( () => import("./pages/GroupsPage/GroupSettingsPage"), ); @@ -410,8 +413,10 @@ const groupsRouter = () => { } /> } /> - } /> - } /> + }> + } /> + } /> + );