diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 4bb1010f311e6..49cad287c8dfa 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -292,16 +292,22 @@ export const createTemplate = async ( * createGroup navigates to the /groups/create page and creates a group with a * random name. */ -export const createGroup = async (page: Page): Promise => { - await page.goto("/deployment/groups/create", { +export const createGroup = async ( + page: Page, + organization?: string, +): Promise => { + const prefix = organization + ? `/organizations/${organization}` + : "/deployment"; + await page.goto(`${prefix}/groups/create`, { waitUntil: "domcontentloaded", }); - await expectUrl(page).toHavePathName("/deployment/groups/create"); + await expectUrl(page).toHavePathName(`${prefix}/groups/create`); const name = randomName(); await page.getByLabel("Name", { exact: true }).fill(name); await page.getByRole("button", { name: /save/i }).click(); - await expectUrl(page).toHavePathName(`/deployment/groups/${name}`); + await expectUrl(page).toHavePathName(`${prefix}/groups/${name}`); return name; }; diff --git a/site/e2e/tests/groups/addMembers.spec.ts b/site/e2e/tests/groups/addMembers.spec.ts index 5b70e8910dc55..7f29f4a536385 100644 --- a/site/e2e/tests/groups/addMembers.spec.ts +++ b/site/e2e/tests/groups/addMembers.spec.ts @@ -5,6 +5,7 @@ import { getCurrentOrgId, setupApiCalls, } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -18,6 +19,7 @@ test.beforeEach(async ({ page }) => { test("add members", async ({ page, baseURL }) => { requiresLicense(); + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const group = await createGroup(orgId); const numberOfMembers = 3; @@ -25,7 +27,7 @@ test("add members", async ({ page, baseURL }) => { Array.from({ length: numberOfMembers }, () => createUser(orgId)), ); - await page.goto(`${baseURL}/groups/${group.name}`, { + await page.goto(`${baseURL}/organizations/${orgName}/groups/${group.name}`, { waitUntil: "domcontentloaded", }); await expect(page).toHaveTitle(`${group.display_name} - Coder`); diff --git a/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts b/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts index 049049265d5ae..b1ece8705e2c6 100644 --- a/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts +++ b/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; import { createUser, getCurrentOrgId, setupApiCalls } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -17,16 +18,20 @@ test(`Every user should be automatically added to the default '${DEFAULT_GROUP_N }) => { requiresLicense(); await setupApiCalls(page); + + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const numberOfMembers = 3; const users = await Promise.all( Array.from({ length: numberOfMembers }, () => createUser(orgId)), ); - await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); + await page.goto(`${baseURL}/organizations/${orgName}/groups`, { + waitUntil: "domcontentloaded", + }); await expect(page).toHaveTitle("Groups - Coder"); - const groupRow = page.getByRole("row", { name: DEFAULT_GROUP_NAME }); + const groupRow = page.getByText(DEFAULT_GROUP_NAME); await groupRow.click(); await expect(page).toHaveTitle(`${DEFAULT_GROUP_NAME} - Coder`); diff --git a/site/e2e/tests/groups/createGroup.spec.ts b/site/e2e/tests/groups/createGroup.spec.ts index 3ae7bbe2a317e..8df1cdbdcc9fb 100644 --- a/site/e2e/tests/groups/createGroup.spec.ts +++ b/site/e2e/tests/groups/createGroup.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "@playwright/test"; +import { defaultOrganizationName } from "../../constants"; import { randomName, requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -11,7 +12,11 @@ test.beforeEach(async ({ page }) => { test("create group", async ({ page, baseURL }) => { requiresLicense(); - await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); + const orgName = defaultOrganizationName; + + await page.goto(`${baseURL}/organizations/${orgName}/groups`, { + waitUntil: "domcontentloaded", + }); await expect(page).toHaveTitle("Groups - Coder"); await page.getByText("Create group").click(); diff --git a/site/e2e/tests/groups/removeGroup.spec.ts b/site/e2e/tests/groups/removeGroup.spec.ts index 06d13fd0dfccf..736b86f7d386d 100644 --- a/site/e2e/tests/groups/removeGroup.spec.ts +++ b/site/e2e/tests/groups/removeGroup.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; import { createGroup, getCurrentOrgId, setupApiCalls } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -13,10 +14,11 @@ test.beforeEach(async ({ page }) => { test("remove group", async ({ page, baseURL }) => { requiresLicense(); + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const group = await createGroup(orgId); - await page.goto(`${baseURL}/groups/${group.name}`, { + await page.goto(`${baseURL}/organizations/${orgName}/groups/${group.name}`, { waitUntil: "domcontentloaded", }); await expect(page).toHaveTitle(`${group.display_name} - Coder`); diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts index 3b5727cc42dba..81fb5ee4f4117 100644 --- a/site/e2e/tests/groups/removeMember.spec.ts +++ b/site/e2e/tests/groups/removeMember.spec.ts @@ -6,6 +6,7 @@ import { getCurrentOrgId, setupApiCalls, } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -19,6 +20,7 @@ test.beforeEach(async ({ page }) => { test("remove member", async ({ page, baseURL }) => { requiresLicense(); + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const [group, member] = await Promise.all([ createGroup(orgId), @@ -26,7 +28,7 @@ test("remove member", async ({ page, baseURL }) => { ]); await API.addMember(group.id, member.id); - await page.goto(`${baseURL}/groups/${group.name}`, { + await page.goto(`${baseURL}/organizations/${orgName}/groups/${group.name}`, { waitUntil: "domcontentloaded", }); await expect(page).toHaveTitle(`${group.display_name} - Coder`); diff --git a/site/e2e/tests/organizationGroups.spec.ts b/site/e2e/tests/organizationGroups.spec.ts index 2d0a41acafc02..dff12ab91c453 100644 --- a/site/e2e/tests/organizationGroups.spec.ts +++ b/site/e2e/tests/organizationGroups.spec.ts @@ -5,6 +5,7 @@ import { createUser, setupApiCalls, } from "../api"; +import { defaultOrganizationName } from "../constants"; import { expectUrl } from "../expectUrl"; import { login, randomName, requiresLicense } from "../helpers"; import { beforeCoderTest } from "../hooks"; @@ -15,6 +16,17 @@ test.beforeEach(async ({ page }) => { await setupApiCalls(page); }); +test("redirects", async ({ page }) => { + requiresLicense(); + + const orgName = defaultOrganizationName; + await page.goto("/groups"); + await expectUrl(page).toHavePathName(`/organizations/${orgName}/groups`); + + await page.goto("/deployment/groups"); + await expectUrl(page).toHavePathName(`/organizations/${orgName}/groups`); +}); + test("create group", async ({ page }) => { requiresLicense(); @@ -24,7 +36,7 @@ test("create group", async ({ page }) => { // Navigate to groups page await page.getByRole("link", { name: "Groups" }).click(); - await expect(page).toHaveTitle(`Groups - Org ${org.name} - Coder`); + await expect(page).toHaveTitle("Groups - Coder"); // Create a new group await page.getByText("Create group").click(); @@ -72,7 +84,7 @@ test("create group", async ({ page }) => { await expect(page.getByText("Group deleted successfully.")).toBeVisible(); await expectUrl(page).toHavePathName(`/organizations/${org.name}/groups`); - await expect(page).toHaveTitle(`Groups - Org ${org.name} - Coder`); + await expect(page).toHaveTitle("Groups - Coder"); }); test("change quota settings", async ({ page }) => { diff --git a/site/e2e/tests/updateTemplate.spec.ts b/site/e2e/tests/updateTemplate.spec.ts index b8f1192b461b5..33e85e40e3b6d 100644 --- a/site/e2e/tests/updateTemplate.spec.ts +++ b/site/e2e/tests/updateTemplate.spec.ts @@ -31,7 +31,7 @@ test("add and remove a group", async ({ page }) => { const orgName = defaultOrganizationName; const templateName = await createTemplate(page); - const groupName = await createGroup(page); + const groupName = await createGroup(page, orgName); await page.goto( `/templates/${orgName}/${templateName}/settings/permissions`, diff --git a/site/src/components/Icons/CoderIcon.tsx b/site/src/components/Icons/CoderIcon.tsx index 3615f43dc968d..7dd2a7625734d 100644 --- a/site/src/components/Icons/CoderIcon.tsx +++ b/site/src/components/Icons/CoderIcon.tsx @@ -17,7 +17,7 @@ export const CoderIcon: FC = ({ className, ...props }) => ( xmlns="http://www.w3.org/2000/svg" > Coder logo - + diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index 880ceecec2265..7e3b09d811b1b 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -3,7 +3,7 @@ import type { CSSObject, Interpolation, Theme } from "@emotion/react"; import { Stack } from "components/Stack/Stack"; import { type ClassName, useClassName } from "hooks/useClassName"; import type { ElementType, FC, ReactNode } from "react"; -import { Link, NavLink, useMatch } from "react-router-dom"; +import { Link, NavLink } from "react-router-dom"; import { cn } from "utils/cn"; interface SidebarProps { @@ -61,21 +61,16 @@ export const SettingsSidebarNavItem: FC = ({ href, end, }) => { - // 2025-01-10: useMatch is a workaround for a bug we encountered when you - // pass a render function to NavLink's className prop, and try to access - // NavLinks's isActive state value for the conditional styling. isActive - // wasn't always evaluating to true when it should be, but useMatch worked - const matchResult = useMatch(href); return ( + cn( + "relative text-sm text-content-secondary no-underline font-medium py-2 px-3 hover:bg-surface-secondary rounded-md transition ease-in-out duration-150", + isActive && "font-semibold text-content-primary", + ) + } > {children} diff --git a/site/src/modules/management/DeploymentSidebar.tsx b/site/src/modules/management/DeploymentSidebar.tsx index 1153ab226bda2..7600a075b97e3 100644 --- a/site/src/modules/management/DeploymentSidebar.tsx +++ b/site/src/modules/management/DeploymentSidebar.tsx @@ -1,4 +1,5 @@ import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; import { DeploymentSidebarView } from "./DeploymentSidebarView"; @@ -7,6 +8,15 @@ import { DeploymentSidebarView } from "./DeploymentSidebarView"; */ export const DeploymentSidebar: FC = () => { const { permissions } = useAuthenticated(); + const { entitlements, showOrganizations } = useDashboard(); + const hasPremiumLicense = + entitlements.features.multiple_organizations.enabled; - return ; + return ( + + ); }; diff --git a/site/src/modules/management/DeploymentSidebarView.tsx b/site/src/modules/management/DeploymentSidebarView.tsx index 052dcf8329b11..4783133a872bb 100644 --- a/site/src/modules/management/DeploymentSidebarView.tsx +++ b/site/src/modules/management/DeploymentSidebarView.tsx @@ -1,44 +1,18 @@ -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Sidebar as BaseSidebar, SettingsSidebarNavItem as SidebarNavItem, } from "components/Sidebar/Sidebar"; +import { Stack } from "components/Stack/Stack"; import type { Permissions } from "contexts/auth/permissions"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; +import { ArrowUpRight } from "lucide-react"; import type { FC } from "react"; -export interface OrganizationWithPermissions extends Organization { - permissions: AuthorizationResponse; -} - -interface DeploymentSidebarProps { - /** Site-wide permissions. */ - permissions: Permissions; -} - -/** - * A combined deployment settings and organization menu. - */ -export const DeploymentSidebarView: FC = ({ - permissions, -}) => { - const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility(); - - return ( - - - - ); -}; - -interface DeploymentSettingsNavigationProps { +interface DeploymentSidebarViewProps { /** Site-wide permissions. */ permissions: Permissions; - isPremium: boolean; + showOrganizations: boolean; + hasPremiumLicense: boolean; } /** @@ -48,12 +22,13 @@ interface DeploymentSettingsNavigationProps { * Menu items are shown based on the permissions. If organizations can be * viewed, groups are skipped since they will show under each org instead. */ -const DeploymentSettingsNavigation: FC = ({ +export const DeploymentSidebarView: FC = ({ permissions, - isPremium, + showOrganizations, + hasPremiumLicense, }) => { return ( -
+
{permissions.viewDeploymentValues && ( General @@ -100,7 +75,11 @@ const DeploymentSettingsNavigation: FC = ({ Users )} {permissions.viewAnyGroup && ( - Groups + + + Groups {showOrganizations && } + + )} {permissions.viewNotificationTemplate && ( @@ -115,10 +94,10 @@ const DeploymentSettingsNavigation: FC = ({ IdP Organization Sync )} - {!isPremium && ( + {!hasPremiumLicense && ( Premium )}
-
+ ); }; diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index d2d25cc4a41bd..11d692c0021da 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -60,10 +60,9 @@ const OrganizationSettingsLayout: FC = () => { const canViewOrganizationSettingsPage = permissions.viewDeploymentValues || permissions.editAnyOrganization; - const organization = - organizations && orgName - ? organizations.find((org) => org.name === orgName) - : undefined; + const organization = orgName + ? organizations.find((org) => org.name === orgName) + : undefined; return ( diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 17a9c097b9c62..ef805861d1543 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -60,7 +60,9 @@ export const OrganizationSidebarView: FC = ({ }; function urlForSubpage(organizationName: string, subpage = ""): string { - return `/organizations/${organizationName}/${subpage}`; + return [`/organizations/${organizationName}`, subpage] + .filter(Boolean) + .join("/"); } interface OrganizationsSettingsNavigationProps { diff --git a/site/src/pages/GroupsPage/CreateGroupPage.tsx b/site/src/pages/GroupsPage/CreateGroupPage.tsx index 92f480d8ab959..257a404a3b7ea 100644 --- a/site/src/pages/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPage.tsx @@ -2,14 +2,17 @@ import { createGroup } from "api/queries/groups"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQueryClient } from "react-query"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import CreateGroupPageView from "./CreateGroupPageView"; export const CreateGroupPage: FC = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); - const createGroupMutation = useMutation(createGroup(queryClient, "default")); + const { organization } = useParams() as { organization: string }; + const createGroupMutation = useMutation( + createGroup(queryClient, organization ?? "default"), + ); return ( <> @@ -19,7 +22,11 @@ export const CreateGroupPage: FC = () => { { const newGroup = await createGroupMutation.mutateAsync(data); - navigate(`/deployment/groups/${newGroup.name}`); + navigate( + organization + ? `/organizations/${organization}/groups/${newGroup.name}` + : `/deployment/groups/${newGroup.name}`, + ); }} error={createGroupMutation.error} isLoading={createGroupMutation.isLoading} diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx index 735c4160c9f67..ea8dfcc3f3e02 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx @@ -4,7 +4,7 @@ import { mockApiError } from "testHelpers/entities"; import { CreateGroupPageView } from "./CreateGroupPageView"; const meta: Meta = { - title: "pages/GroupsPage/CreateGroupPageView", + title: "pages/OrganizationGroupsPage/CreateGroupPageView", component: CreateGroupPageView, }; @@ -19,7 +19,15 @@ export const WithError: Story = { message: "A group named new-group already exists.", validations: [{ field: "name", detail: "Group names must be unique" }], }), - initialTouched: { name: true }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step("Enter name", async () => { + const input = canvas.getByLabelText("Name"); + await userEvent.type(input, "new-group"); + input.blur(); + }); }, }; diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index dd400459d0c2c..5557abd39dc1f 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -3,13 +3,16 @@ import { isApiValidationError } from "api/errors"; import type { CreateGroupRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; -import { FormFooter } from "components/Form/Form"; -import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { + FormFields, + FormFooter, + FormSection, + HorizontalForm, +} from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; -import { Margins } from "components/Margins/Margins"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Spinner } from "components/Spinner/Spinner"; -import { Stack } from "components/Stack/Stack"; -import { type FormikTouched, useFormik } from "formik"; +import { useFormik } from "formik"; import type { FC } from "react"; import { useNavigate } from "react-router-dom"; import { @@ -27,15 +30,12 @@ export type CreateGroupPageViewProps = { onSubmit: (data: CreateGroupRequest) => void; error?: unknown; isLoading: boolean; - // Helpful to show field errors on Storybook - initialTouched?: FormikTouched; }; export const CreateGroupPageView: FC = ({ onSubmit, error, isLoading, - initialTouched, }) => { const navigate = useNavigate(); const form = useFormik({ @@ -47,16 +47,23 @@ export const CreateGroupPageView: FC = ({ }, validationSchema, onSubmit, - initialTouched, }); const getFieldHelpers = getFormHelpers(form, error); - const onCancel = () => navigate("/deployment/groups"); + const onCancel = () => navigate(-1); return ( - - -
- + <> + + + + + {Boolean(error) && !isApiValidationError(error) && ( )} @@ -84,21 +91,21 @@ export const CreateGroupPageView: FC = ({ label="Avatar URL" onPickEmoji={(value) => form.setFieldValue("avatar_url", value)} /> - + + - - + + - - - -
-
+ + + + ); }; export default CreateGroupPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.stories.tsx b/site/src/pages/GroupsPage/GroupPage.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.stories.tsx rename to site/src/pages/GroupsPage/GroupPage.stories.tsx diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index 913101518c61e..6c226a1dba9ff 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -18,7 +18,11 @@ import { groupPermissions, removeMember, } from "api/queries/groups"; -import type { Group, ReducedUser, User } from "api/typesGenerated"; +import type { + Group, + OrganizationMemberWithUserData, + ReducedUser, +} from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; @@ -27,7 +31,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, @@ -35,17 +38,13 @@ import { MoreMenuTrigger, ThreeDotsButton, } from "components/MoreMenu/MoreMenu"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; import { PaginationStatus, TableToolbar, } from "components/TableToolbar/TableToolbar"; -import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; @@ -54,19 +53,19 @@ import { isEveryoneGroup } from "utils/groups"; import { pageTitle } from "utils/page"; export const GroupPage: FC = () => { - const { groupName } = useParams() as { + const { organization = "default", groupName } = useParams() as { + organization?: string; groupName: string; }; const queryClient = useQueryClient(); const navigate = useNavigate(); - const groupQuery = useQuery(group("default", groupName)); + const groupQuery = useQuery(group(organization, groupName)); const groupData = groupQuery.data; const { data: permissions } = useQuery( - groupData !== undefined - ? groupPermissions(groupData.id) - : { enabled: false }, + groupData ? groupPermissions(groupData.id) : { enabled: false }, ); const addMemberMutation = useMutation(addMember(queryClient)); + const removeMemberMutation = useMutation(removeMember(queryClient)); const deleteGroupMutation = useMutation(deleteGroup(queryClient)); const [isDeletingGroup, setIsDeletingGroup] = useState(false); const isLoading = groupQuery.isLoading || !groupData || !permissions; @@ -100,106 +99,115 @@ export const GroupPage: FC = () => { <> {helmet} - - - - - - ) - } - > - - {groupData?.display_name || groupData?.name} - - - {/* Show the name if it differs from the display name. */} - {groupData?.display_name && - groupData?.display_name !== groupData?.name - ? groupData?.name - : ""}{" "} - - - - - {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( - { - try { - await addMemberMutation.mutateAsync({ - groupId, - userId: user.id, - }); - reset(); - await groupQuery.refetch(); - } catch (error) { - displayError(getErrorMessage(error, "Failed to add member.")); - } + + + {canUpdateGroup && ( + + + + + )} + - - - - - User - Status - - - + + {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( + { + try { + await addMemberMutation.mutateAsync({ + groupId, + userId: member.user_id, + }); + reset(); + await groupQuery.refetch(); + } catch (error) { + displayError(getErrorMessage(error, "Failed to add member.")); + } + }} + /> + )} + + + + + +
+ + + User + Status + + + - - {groupData?.members.length === 0 ? ( - - - - - - ) : ( - groupData?.members.map((member) => ( - + {groupData?.members.length === 0 ? ( + + + - )) - )} - -
-
-
-
+ + + ) : ( + groupData?.members.map((member) => ( + { + 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."), + ); + } + }} + /> + )) + )} + + + + {groupQuery.data && ( { try { await deleteGroupMutation.mutateAsync(groupId); displaySuccess("Group deleted successfully."); - navigate("/deployment/groups"); + navigate(".."); } catch (error) { displayError(getErrorMessage(error, "Failed to delete group.")); } @@ -227,11 +235,17 @@ export const GroupPage: FC = () => { interface AddGroupMemberProps { isLoading: boolean; - onSubmit: (user: User, reset: () => void) => void; + onSubmit: (user: OrganizationMemberWithUserData, reset: () => void) => void; + organizationId: string; } -const AddGroupMember: FC = ({ isLoading, onSubmit }) => { - const [selectedUser, setSelectedUser] = useState(null); +const AddGroupMember: FC = ({ + isLoading, + onSubmit, + organizationId, +}) => { + const [selectedUser, setSelectedUser] = + useState(null); const resetValues = () => { setSelectedUser(null); @@ -248,9 +262,10 @@ const AddGroupMember: FC = ({ isLoading, onSubmit }) => { }} > - { setSelectedUser(newValue); }} @@ -274,16 +289,15 @@ interface GroupMemberRowProps { member: ReducedUser; group: Group; canUpdate: boolean; + onRemove: () => void; } const GroupMemberRow: FC = ({ member, group, canUpdate, + onRemove, }) => { - const queryClient = useQueryClient(); - const removeMemberMutation = useMutation(removeMember(queryClient)); - return ( @@ -309,19 +323,7 @@ const GroupMemberRow: FC = ({ { - try { - await removeMemberMutation.mutateAsync({ - groupId: group.id, - userId: member.id, - }); - displaySuccess("Member removed successfully."); - } catch (error) { - displayError( - getErrorMessage(error, "Failed to remove member."), - ); - } - }} + onClick={onRemove} disabled={group.id === group.organization_id} > Remove diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx b/site/src/pages/GroupsPage/GroupSettingsPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx rename to site/src/pages/GroupsPage/GroupSettingsPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx b/site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx rename to site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/GroupsPage/GroupSettingsPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx rename to site/src/pages/GroupsPage/GroupSettingsPageView.tsx diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 6313b8e450c9e..5e33e232227ef 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -1,18 +1,29 @@ +import GroupAdd from "@mui/icons-material/GroupAddOutlined"; import { getErrorMessage } from "api/errors"; import { groupsByOrganization } from "api/queries/groups"; +import { organizationPermissions } from "api/queries/organizations"; +import { Button } from "components/Button/Button"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError } from "components/GlobalSnackbar/utils"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { Loader } from "components/Loader/Loader"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; +import { Stack } from "components/Stack/Stack"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { type FC, useEffect } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; +import { Link as RouterLink } from "react-router-dom"; import { pageTitle } from "utils/page"; +import { useGroupsSettings } from "./GroupsPageProvider"; import GroupsPageView from "./GroupsPageView"; export const GroupsPage: FC = () => { - const { permissions } = useAuthenticated(); - const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); - const groupsQuery = useQuery(groupsByOrganization("default")); + const { template_rbac: groupsEnabled } = useFeatureVisibility(); + const { organization, showOrganizations } = useGroupsSettings(); + const groupsQuery = useQuery( + organization ? groupsByOrganization(organization.name) : { enabled: false }, + ); + const permissionsQuery = useQuery(organizationPermissions(organization?.id)); useEffect(() => { if (groupsQuery.error) { @@ -22,16 +33,52 @@ export const GroupsPage: FC = () => { } }, [groupsQuery.error]); + useEffect(() => { + if (permissionsQuery.error) { + displayError( + getErrorMessage(permissionsQuery.error, "Unable to load permissions."), + ); + } + }, [permissionsQuery.error]); + + if (!organization) { + return ; + } + + const permissions = permissionsQuery.data; + if (!permissions) { + return ; + } + return ( <> {pageTitle("Groups")} + + + {groupsEnabled && permissions.createGroup && ( + + )} + + ); diff --git a/site/src/pages/GroupsPage/GroupsPageProvider.tsx b/site/src/pages/GroupsPage/GroupsPageProvider.tsx new file mode 100644 index 0000000000000..85ccd763be10a --- /dev/null +++ b/site/src/pages/GroupsPage/GroupsPageProvider.tsx @@ -0,0 +1,64 @@ +import type { AuthorizationResponse, Organization } from "api/typesGenerated"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { RequirePermission } from "contexts/auth/RequirePermission"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import { + type FC, + type PropsWithChildren, + createContext, + useContext, +} from "react"; +import { Navigate, Outlet, useParams } from "react-router-dom"; + +export const GroupsPageContext = createContext< + OrganizationSettingsValue | undefined +>(undefined); + +type OrganizationSettingsValue = Readonly<{ + organization?: Organization; + showOrganizations: boolean; +}>; + +export const useGroupsSettings = (): OrganizationSettingsValue => { + const context = useContext(GroupsPageContext); + if (!context) { + throw new Error( + "useGroupsSettings should be used inside of GroupsPageContext", + ); + } + + return context; +}; + +const GroupsPageProvider: FC = () => { + const { organizations, showOrganizations } = useDashboard(); + const { organization: orgName } = useParams() as { + organization?: string; + }; + + const organization = orgName + ? organizations.find((org) => org.name === orgName) + : getOrganizationByDefault(organizations); + + if ( + location.pathname.startsWith("/deployment/groups") && + showOrganizations && + organization + ) { + return ( + + ); + } + + return ( + + + + ); +}; + +export default GroupsPageProvider; + +const getOrganizationByDefault = (organizations: readonly Organization[]) => { + return organizations.find((org) => org.is_default); +}; diff --git a/site/src/pages/GroupsPage/GroupsPageView.stories.tsx b/site/src/pages/GroupsPage/GroupsPageView.stories.tsx index a179a830e4652..466ee2b149524 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.stories.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.stories.tsx @@ -3,7 +3,7 @@ import { MockGroup } from "testHelpers/entities"; import { GroupsPageView } from "./GroupsPageView"; const meta: Meta = { - title: "pages/GroupsPage", + title: "pages/OrganizationGroupsPage", component: GroupsPageView, }; @@ -14,7 +14,7 @@ export const NotEnabled: Story = { args: { groups: [MockGroup], canCreateGroup: true, - isTemplateRBACEnabled: false, + groupsEnabled: false, }, }; @@ -22,7 +22,7 @@ export const WithGroups: Story = { args: { groups: [MockGroup], canCreateGroup: true, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; @@ -30,7 +30,7 @@ export const WithDisplayGroup: Story = { args: { groups: [{ ...MockGroup, name: "front-end" }], canCreateGroup: true, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; @@ -38,7 +38,7 @@ export const EmptyGroup: Story = { args: { groups: [], canCreateGroup: false, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; @@ -46,6 +46,6 @@ export const EmptyGroupWithPermission: Story = { args: { groups: [], canCreateGroup: true, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx index bd2d2ef981419..22ccd35515064 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.tsx @@ -2,7 +2,6 @@ import type { Interpolation, Theme } from "@emotion/react"; import AddOutlined from "@mui/icons-material/AddOutlined"; import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import AvatarGroup from "@mui/material/AvatarGroup"; -import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; @@ -14,6 +13,7 @@ import type { Group } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; +import { Button } from "components/Button/Button"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Paywall } from "components/Paywall/Paywall"; @@ -21,6 +21,7 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { useClickableTableRow } from "hooks"; import type { FC } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; @@ -28,25 +29,24 @@ import { docs } from "utils/docs"; export type GroupsPageViewProps = { groups: Group[] | undefined; canCreateGroup: boolean; - isTemplateRBACEnabled: boolean; + groupsEnabled: boolean; }; export const GroupsPageView: FC = ({ groups, canCreateGroup, - isTemplateRBACEnabled, + groupsEnabled, }) => { const isLoading = Boolean(groups === undefined); const isEmpty = Boolean(groups && groups.length === 0); - const navigate = useNavigate(); return ( <> - + @@ -78,13 +78,11 @@ export const GroupsPageView: FC = ({ } cta={ canCreateGroup && ( - ) } @@ -94,63 +92,9 @@ export const GroupsPageView: FC = ({ - {groups?.map((group) => { - const groupPageLink = `/deployment/groups/${group.name}`; - - return ( - { - navigate(groupPageLink); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - navigate(groupPageLink); - } - }} - css={styles.clickableTableRow} - > - - - } - title={group.display_name || group.name} - subtitle={`${group.members.length} members`} - /> - - - - {group.members.length === 0 && "-"} - - {group.members.map((member) => ( - - ))} - - - - -
- -
-
-
- ); - })} + {groups?.map((group) => ( + + ))}
@@ -162,7 +106,58 @@ export const GroupsPageView: FC = ({ ); }; -const TableLoader = () => { +interface GroupRowProps { + group: Group; +} + +const GroupRow: FC = ({ group }) => { + const navigate = useNavigate(); + const rowProps = useClickableTableRow({ + onClick: () => navigate(group.name), + }); + + return ( + + + + } + title={group.display_name || group.name} + subtitle={`${group.members.length} members`} + /> + + + + {group.members.length === 0 && "-"} + + {group.members.map((member) => ( + + ))} + + + + +
+ +
+
+
+ ); +}; + +const TableLoader: FC = () => { return ( @@ -183,21 +178,6 @@ const TableLoader = () => { }; const styles = { - clickableTableRow: (theme) => ({ - cursor: "pointer", - - "&:hover td": { - backgroundColor: theme.palette.action.hover, - }, - - "&:focus": { - outline: `1px solid ${theme.palette.primary.main}`, - }, - - "& .MuiTableCell-root:last-child": { - paddingRight: "16px !important", - }, - }), arrowRight: (theme) => ({ color: theme.palette.text.secondary, width: 20, diff --git a/site/src/pages/GroupsPage/SettingsGroupPage.tsx b/site/src/pages/GroupsPage/SettingsGroupPage.tsx deleted file mode 100644 index 5b44d5c99457f..0000000000000 --- a/site/src/pages/GroupsPage/SettingsGroupPage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { getErrorMessage } from "api/errors"; -import { group, patchGroup } from "api/queries/groups"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; -import type { FC } from "react"; -import { Helmet } from "react-helmet-async"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { useNavigate, useParams } from "react-router-dom"; -import { pageTitle } from "utils/page"; -import SettingsGroupPageView from "./SettingsGroupPageView"; - -export const SettingsGroupPage: FC = () => { - const { groupName } = useParams() as { groupName: string }; - const queryClient = useQueryClient(); - const groupQuery = useQuery(group("default", groupName)); - const patchGroupMutation = useMutation(patchGroup(queryClient)); - const navigate = useNavigate(); - - const navigateToGroup = () => { - navigate(`/deployment/groups/${groupName}`); - }; - - const helmet = ( - - {pageTitle("Settings Group")} - - ); - - if (groupQuery.error) { - return ; - } - - if (groupQuery.isLoading || !groupQuery.data) { - return ( - <> - {helmet} - - - ); - } - - const groupId = groupQuery.data.id; - - return ( - <> - {helmet} - - { - try { - await patchGroupMutation.mutateAsync({ - groupId, - ...data, - add_users: [], - remove_users: [], - }); - navigate(`/deployment/groups/${data.name}`, { replace: true }); - } catch (error) { - displayError(getErrorMessage(error, "Failed to update group")); - } - }} - group={groupQuery.data} - formErrors={groupQuery.error} - isLoading={groupQuery.isLoading} - isUpdating={patchGroupMutation.isLoading} - /> - - ); -}; -export default SettingsGroupPage; diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx deleted file mode 100644 index 78f4ead3ef6d0..0000000000000 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { action } from "@storybook/addon-actions"; -import type { Meta, StoryObj } from "@storybook/react"; -import { MockGroup } from "testHelpers/entities"; -import { SettingsGroupPageView } from "./SettingsGroupPageView"; - -const meta: Meta = { - title: "pages/GroupsPage/SettingsGroupPageView", - component: SettingsGroupPageView, - args: { - onCancel: action("onCancel"), - group: MockGroup, - isLoading: false, - }, -}; - -export default meta; -type Story = StoryObj; - -const Example: Story = {}; - -export { Example as SettingsGroupPageView }; diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx deleted file mode 100644 index 3877cabc0beb6..0000000000000 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import TextField from "@mui/material/TextField"; -import type { Group } from "api/typesGenerated"; -import { Button } from "components/Button/Button"; -import { FormFooter } from "components/Form/Form"; -import { FullPageForm } from "components/FullPageForm/FullPageForm"; -import { IconField } from "components/IconField/IconField"; -import { Loader } from "components/Loader/Loader"; -import { Margins } from "components/Margins/Margins"; -import { Spinner } from "components/Spinner/Spinner"; -import { Stack } from "components/Stack/Stack"; -import { useFormik } from "formik"; -import type { FC } from "react"; -import { - getFormHelpers, - nameValidator, - onChangeTrimmed, -} from "utils/formUtils"; -import { isEveryoneGroup } from "utils/groups"; -import * as Yup from "yup"; - -type FormData = { - name: string; - display_name: string; - avatar_url: string; - quota_allowance: number; -}; - -const validationSchema = Yup.object({ - name: nameValidator("Name"), - quota_allowance: Yup.number().required().min(0).integer(), -}); - -interface UpdateGroupFormProps { - group: Group; - errors: unknown; - onSubmit: (data: FormData) => void; - onCancel: () => void; - isLoading: boolean; -} - -const UpdateGroupForm: FC = ({ - group, - errors, - onSubmit, - onCancel, - isLoading, -}) => { - const form = useFormik({ - initialValues: { - name: group.name, - display_name: group.display_name, - avatar_url: group.avatar_url, - quota_allowance: group.quota_allowance, - }, - validationSchema, - onSubmit, - }); - const getFieldHelpers = getFormHelpers(form, errors); - - return ( - -
- - - {isEveryoneGroup(group) ? ( - <> - ) : ( - <> - - form.setFieldValue("avatar_url", value)} - /> - - )} - - - - - - - - -
-
- ); -}; - -export type SettingsGroupPageViewProps = { - onCancel: () => void; - onSubmit: (data: FormData) => void; - group: Group | undefined; - formErrors: unknown; - isLoading: boolean; - isUpdating: boolean; -}; - -export const SettingsGroupPageView: FC = ({ - onCancel, - onSubmit, - group, - formErrors, - isLoading, - isUpdating, -}) => { - if (isLoading) { - return ; - } - - return ( - - - - ); -}; - -export default SettingsGroupPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx deleted file mode 100644 index 257a404a3b7ea..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { createGroup } from "api/queries/groups"; -import type { FC } from "react"; -import { Helmet } from "react-helmet-async"; -import { useMutation, useQueryClient } from "react-query"; -import { useNavigate, useParams } from "react-router-dom"; -import { pageTitle } from "utils/page"; -import CreateGroupPageView from "./CreateGroupPageView"; - -export const CreateGroupPage: FC = () => { - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const { organization } = useParams() as { organization: string }; - const createGroupMutation = useMutation( - createGroup(queryClient, organization ?? "default"), - ); - - return ( - <> - - {pageTitle("Create Group")} - - { - const newGroup = await createGroupMutation.mutateAsync(data); - navigate( - organization - ? `/organizations/${organization}/groups/${newGroup.name}` - : `/deployment/groups/${newGroup.name}`, - ); - }} - error={createGroupMutation.error} - isLoading={createGroupMutation.isLoading} - /> - - ); -}; -export default CreateGroupPage; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx deleted file mode 100644 index ea8dfcc3f3e02..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { userEvent, within } from "@storybook/test"; -import { mockApiError } from "testHelpers/entities"; -import { CreateGroupPageView } from "./CreateGroupPageView"; - -const meta: Meta = { - title: "pages/OrganizationGroupsPage/CreateGroupPageView", - component: CreateGroupPageView, -}; - -export default meta; -type Story = StoryObj; - -export const Example: Story = {}; - -export const WithError: Story = { - args: { - error: mockApiError({ - message: "A group named new-group already exists.", - validations: [{ field: "name", detail: "Group names must be unique" }], - }), - }, - play: async ({ canvasElement, step }) => { - const canvas = within(canvasElement); - - await step("Enter name", async () => { - const input = canvas.getByLabelText("Name"); - await userEvent.type(input, "new-group"); - input.blur(); - }); - }, -}; - -export const InvalidName: Story = { - play: async ({ canvasElement }) => { - const user = userEvent.setup(); - const body = within(canvasElement.ownerDocument.body); - const input = await body.findByLabelText("Name"); - await user.type(input, "$om3 !nv@lid Name"); - input.blur(); - }, -}; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx deleted file mode 100644 index 5557abd39dc1f..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx +++ /dev/null @@ -1,111 +0,0 @@ -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 } from "components/SettingsHeader/SettingsHeader"; -import { Spinner } from "components/Spinner/Spinner"; -import { useFormik } from "formik"; -import type { FC } from "react"; -import { useNavigate } from "react-router-dom"; -import { - getFormHelpers, - nameValidator, - onChangeTrimmed, -} from "utils/formUtils"; -import * as Yup from "yup"; - -const validationSchema = Yup.object({ - name: nameValidator("Name"), -}); - -export type CreateGroupPageViewProps = { - onSubmit: (data: CreateGroupRequest) => void; - error?: unknown; - isLoading: boolean; -}; - -export const CreateGroupPageView: FC = ({ - onSubmit, - error, - isLoading, -}) => { - const navigate = useNavigate(); - const form = useFormik({ - initialValues: { - name: "", - display_name: "", - avatar_url: "", - quota_allowance: 0, - }, - validationSchema, - onSubmit, - }); - const getFieldHelpers = getFormHelpers(form, error); - const onCancel = () => navigate(-1); - - return ( - <> - - - - - - {Boolean(error) && !isApiValidationError(error) && ( - - )} - - - - form.setFieldValue("avatar_url", value)} - /> - - - - - - - - - - - ); -}; -export default CreateGroupPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx deleted file mode 100644 index 6c226a1dba9ff..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx +++ /dev/null @@ -1,357 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import DeleteOutline from "@mui/icons-material/DeleteOutline"; -import PersonAdd from "@mui/icons-material/PersonAdd"; -import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; -import LoadingButton from "@mui/lab/LoadingButton"; -import Button from "@mui/material/Button"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import { getErrorMessage } from "api/errors"; -import { - addMember, - deleteGroup, - group, - groupPermissions, - removeMember, -} from "api/queries/groups"; -import type { - Group, - OrganizationMemberWithUserData, - ReducedUser, -} from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Avatar } from "components/Avatar/Avatar"; -import { AvatarData } from "components/Avatar/AvatarData"; -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; -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 { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; -import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; -import { Stack } from "components/Stack/Stack"; -import { - PaginationStatus, - TableToolbar, -} from "components/TableToolbar/TableToolbar"; -import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; -import { type FC, useState } from "react"; -import { Helmet } from "react-helmet-async"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Link as RouterLink, useNavigate, useParams } from "react-router-dom"; -import { isEveryoneGroup } from "utils/groups"; -import { pageTitle } from "utils/page"; - -export const GroupPage: FC = () => { - const { organization = "default", groupName } = useParams() as { - organization?: string; - groupName: string; - }; - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const groupQuery = useQuery(group(organization, groupName)); - const groupData = groupQuery.data; - const { data: permissions } = useQuery( - groupData ? groupPermissions(groupData.id) : { enabled: false }, - ); - const addMemberMutation = useMutation(addMember(queryClient)); - const removeMemberMutation = useMutation(removeMember(queryClient)); - const deleteGroupMutation = useMutation(deleteGroup(queryClient)); - const [isDeletingGroup, setIsDeletingGroup] = useState(false); - const isLoading = groupQuery.isLoading || !groupData || !permissions; - const canUpdateGroup = permissions ? permissions.canUpdateGroup : false; - - const helmet = ( - - - {pageTitle( - (groupData?.display_name || groupData?.name) ?? "Loading...", - )} - - - ); - - if (groupQuery.error) { - return ; - } - - if (isLoading) { - return ( - <> - {helmet} - - - ); - } - const groupId = groupData.id; - - return ( - <> - {helmet} - - - - {canUpdateGroup && ( - - - - - )} - - - - {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( - { - try { - await addMemberMutation.mutateAsync({ - groupId, - userId: member.user_id, - }); - reset(); - await groupQuery.refetch(); - } catch (error) { - displayError(getErrorMessage(error, "Failed to add member.")); - } - }} - /> - )} - - - - - - - - - User - Status - - - - - - {groupData?.members.length === 0 ? ( - - - - - - ) : ( - groupData?.members.map((member) => ( - { - 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."), - ); - } - }} - /> - )) - )} - -
-
-
- - {groupQuery.data && ( - { - try { - await deleteGroupMutation.mutateAsync(groupId); - displaySuccess("Group deleted successfully."); - navigate(".."); - } catch (error) { - displayError(getErrorMessage(error, "Failed to delete group.")); - } - }} - onCancel={() => { - setIsDeletingGroup(false); - }} - /> - )} - - ); -}; - -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); - }} - /> - - } - loading={isLoading} - > - Add user - - -
- ); -}; - -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, - }, - removeButton: (theme) => ({ - color: theme.palette.error.main, - "&:hover": { - backgroundColor: "transparent", - }, - }), - status: { - textTransform: "capitalize", - }, - suspended: (theme) => ({ - color: theme.palette.text.secondary, - }), -} satisfies Record>; - -export default GroupPage; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx deleted file mode 100644 index 0e31af80e359a..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import GroupAdd from "@mui/icons-material/GroupAddOutlined"; -import Button from "@mui/material/Button"; -import { getErrorMessage } from "api/errors"; -import { groupsByOrganization } from "api/queries/groups"; -import { organizationPermissions } from "api/queries/organizations"; -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 { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; -import { Stack } from "components/Stack/Stack"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; -import { type FC, useEffect } from "react"; -import { Helmet } from "react-helmet-async"; -import { useQuery } from "react-query"; -import { Navigate, Link as RouterLink, useParams } from "react-router-dom"; -import { pageTitle } from "utils/page"; -import GroupsPageView from "./GroupsPageView"; - -export const GroupsPage: FC = () => { - const feats = useFeatureVisibility(); - const { organization: organizationName } = useParams() as { - organization: string; - }; - const groupsQuery = useQuery(groupsByOrganization(organizationName)); - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - - useEffect(() => { - if (groupsQuery.error) { - displayError( - getErrorMessage(groupsQuery.error, "Unable to load groups."), - ); - } - }, [groupsQuery.error]); - - useEffect(() => { - if (permissionsQuery.error) { - displayError( - getErrorMessage(permissionsQuery.error, "Unable to load permissions."), - ); - } - }, [permissionsQuery.error]); - - if (!organizations) { - return ; - } - - if (!organizationName) { - const defaultName = getOrganizationNameByDefault(organizations); - if (defaultName) { - return ; - } - // We expect there to always be a default organization. - throw new Error("No default organization found"); - } - - if (!organization) { - return ; - } - - const permissions = permissionsQuery.data; - if (!permissions) { - return ; - } - - return ( - <> - - - {pageTitle("Groups", organization.display_name || organization.name)} - - - - - - {permissions.createGroup && feats.template_rbac && ( - - )} - - - - - ); -}; - -export default GroupsPage; - -export const getOrganizationNameByDefault = ( - organizations: readonly Organization[], -) => { - return organizations.find((org) => org.is_default)?.name; -}; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx deleted file mode 100644 index 8198243ca2de5..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { MockGroup } from "testHelpers/entities"; -import { GroupsPageView } from "./GroupsPageView"; - -const meta: Meta = { - title: "pages/OrganizationGroupsPage", - component: GroupsPageView, -}; - -export default meta; -type Story = StoryObj; - -export const NotEnabled: Story = { - args: { - groups: [MockGroup], - canCreateGroup: true, - isTemplateRBACEnabled: false, - }, -}; - -export const WithGroups: Story = { - args: { - groups: [MockGroup], - canCreateGroup: true, - isTemplateRBACEnabled: true, - }, -}; - -export const WithDisplayGroup: Story = { - args: { - groups: [{ ...MockGroup, name: "front-end" }], - canCreateGroup: true, - isTemplateRBACEnabled: true, - }, -}; - -export const EmptyGroup: Story = { - args: { - groups: [], - canCreateGroup: false, - isTemplateRBACEnabled: true, - }, -}; - -export const EmptyGroupWithPermission: Story = { - args: { - groups: [], - canCreateGroup: true, - isTemplateRBACEnabled: true, - }, -}; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx deleted file mode 100644 index fe109d0ea5718..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import AddOutlined from "@mui/icons-material/AddOutlined"; -import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; -import AvatarGroup from "@mui/material/AvatarGroup"; -import Button from "@mui/material/Button"; -import Skeleton from "@mui/material/Skeleton"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import type { Group } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { AvatarData } from "components/Avatar/AvatarData"; -import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { Paywall } from "components/Paywall/Paywall"; -import { - TableLoaderSkeleton, - TableRowSkeleton, -} from "components/TableLoader/TableLoader"; -import { useClickableTableRow } from "hooks"; -import type { FC } from "react"; -import { Link as RouterLink, useNavigate } from "react-router-dom"; -import { docs } from "utils/docs"; - -export type GroupsPageViewProps = { - groups: Group[] | undefined; - canCreateGroup: boolean; - isTemplateRBACEnabled: boolean; -}; - -export const GroupsPageView: FC = ({ - groups, - canCreateGroup, - isTemplateRBACEnabled, -}) => { - const isLoading = Boolean(groups === undefined); - const isEmpty = Boolean(groups && groups.length === 0); - - return ( - <> - - - - - - - - - - Name - Users - - - - - - - - - - - - - } - variant="contained" - > - Create group - - ) - } - /> - - - - - - {groups?.map((group) => ( - - ))} - - - -
-
-
-
- - ); -}; - -interface GroupRowProps { - group: Group; -} - -const GroupRow: FC = ({ group }) => { - const navigate = useNavigate(); - const rowProps = useClickableTableRow({ - onClick: () => navigate(group.name), - }); - - return ( - - - - } - title={group.display_name || group.name} - subtitle={`${group.members.length} members`} - /> - - - - {group.members.length === 0 && "-"} - - {group.members.map((member) => ( - - ))} - - - - -
- -
-
-
- ); -}; - -const TableLoader: FC = () => { - return ( - - - -
- -
-
- - - - - - -
-
- ); -}; - -const styles = { - arrowRight: (theme) => ({ - color: theme.palette.text.secondary, - width: 20, - height: 20, - }), - arrowCell: { - display: "flex", - }, -} satisfies Record>; - -export default GroupsPageView; diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx rename to site/src/pages/OrganizationSettingsPage/CreateOrganizationPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx rename to site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.tsx diff --git a/site/src/pages/ManagementSettingsPage/Horizontal.tsx b/site/src/pages/OrganizationSettingsPage/Horizontal.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/Horizontal.tsx rename to site/src/pages/OrganizationSettingsPage/Horizontal.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpMappingTable.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpMappingTable.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpPillList.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpPillList.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.stories.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.stories.tsx rename to site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx rename to site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx diff --git a/site/src/pages/ManagementSettingsPage/UserTable/TableColumnHelpTooltip.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/TableColumnHelpTooltip.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/UserTable/TableColumnHelpTooltip.tsx rename to site/src/pages/OrganizationSettingsPage/UserTable/TableColumnHelpTooltip.tsx diff --git a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx rename to site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx diff --git a/site/src/pages/UsersPage/UsersLayout.tsx b/site/src/pages/UsersPage/UsersLayout.tsx deleted file mode 100644 index c0400d23b8cea..0000000000000 --- a/site/src/pages/UsersPage/UsersLayout.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import GroupAdd from "@mui/icons-material/GroupAddOutlined"; -import Button from "@mui/material/Button"; -import { Loader } from "components/Loader/Loader"; -import { Margins } from "components/Margins/Margins"; -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { type FC, Suspense } from "react"; -import { Outlet, Link as RouterLink } from "react-router-dom"; - -export const UsersLayout: FC = () => { - const { permissions } = useAuthenticated(); - const feats = useFeatureVisibility(); - - return ( - <> - - - {permissions.createGroup && feats.template_rbac && ( - - )} - - } - > - Groups - - - - - }> - - - - - ); -}; diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index e4337f9242216..7ee8e19c899ab 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -108,10 +108,6 @@ const UsersPage: FC = ({ defaultNewPassword }) => { authMethodsQuery.isLoading || groupsByUserIdQuery.isLoading; - if (location.pathname === "/users") { - return ; - } - return ( <> diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index 029d7fc4f12d7..81334b709d251 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -1,11 +1,12 @@ import type { GroupsByUserId } from "api/queries/groups"; import type * as TypesGen from "api/typesGenerated"; import { Button } from "components/Button/Button"; -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { PaginationContainer, type PaginationResult, } from "components/PaginationWidget/PaginationContainer"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; +import { Stack } from "components/Stack/Stack"; import { UserPlusIcon } from "lucide-react"; import type { ComponentProps, FC } from "react"; import { Link as RouterLink } from "react-router-dom"; @@ -67,21 +68,24 @@ export const UsersPageView: FC = ({ }) => { return ( <> - - - - Create user - - - ) - } + - Users - + + {canCreateUser && ( + + )} +
diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx index b075c295d61fa..1f47dd10d3291 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx @@ -8,7 +8,7 @@ import type { GroupsByUserId } from "api/queries/groups"; import type * as TypesGen from "api/typesGenerated"; import { Stack } from "components/Stack/Stack"; import type { FC } from "react"; -import { TableColumnHelpTooltip } from "../../ManagementSettingsPage/UserTable/TableColumnHelpTooltip"; +import { TableColumnHelpTooltip } from "../../OrganizationSettingsPage/UserTable/TableColumnHelpTooltip"; import { UsersTableBody } from "./UsersTableBody"; export const Language = { diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index 08a8aa99b182d..44b2baf69e798 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -30,7 +30,7 @@ import { import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import type { FC } from "react"; -import { UserRoleCell } from "../../ManagementSettingsPage/UserTable/UserRoleCell"; +import { UserRoleCell } from "../../OrganizationSettingsPage/UserTable/UserRoleCell"; import { UserGroupsCell } from "./UserGroupsCell"; dayjs.extend(relativeTime); diff --git a/site/src/router.tsx b/site/src/router.tsx index bb95fc1eb393a..acaf417cecbcd 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -19,7 +19,6 @@ import { TemplateLayout } from "./pages/TemplatePage/TemplateLayout"; import { TemplateSettingsLayout } from "./pages/TemplateSettingsPage/TemplateSettingsLayout"; import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"; import UserSettingsLayout from "./pages/UserSettingsPage/Layout"; -import { UsersLayout } from "./pages/UsersPage/UsersLayout"; import UsersPage from "./pages/UsersPage/UsersPage"; import { WorkspaceSettingsLayout } from "./pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; import WorkspacesPage from "./pages/WorkspacesPage/WorkspacesPage"; @@ -98,13 +97,6 @@ const TemplateSummaryPage = lazy( const CreateWorkspacePage = lazy( () => import("./pages/CreateWorkspacePage/CreateWorkspacePage"), ); -const CreateGroupPage = lazy( - () => import("./pages/GroupsPage/CreateGroupPage"), -); -const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage")); -const SettingsGroupPage = lazy( - () => import("./pages/GroupsPage/SettingsGroupPage"), -); const GeneralSettingsPage = lazy( () => import( @@ -237,39 +229,40 @@ const AddNewLicensePage = lazy( ), ); const CreateOrganizationPage = lazy( - () => import("./pages/ManagementSettingsPage/CreateOrganizationPage"), + () => import("./pages/OrganizationSettingsPage/CreateOrganizationPage"), ); const OrganizationSettingsPage = lazy( - () => import("./pages/ManagementSettingsPage/OrganizationSettingsPage"), -); -const OrganizationGroupsPage = lazy( - () => import("./pages/ManagementSettingsPage/GroupsPage/GroupsPage"), + () => import("./pages/OrganizationSettingsPage/OrganizationSettingsPage"), ); -const CreateOrganizationGroupPage = lazy( - () => import("./pages/ManagementSettingsPage/GroupsPage/CreateGroupPage"), +const GroupsPageProvider = lazy( + () => import("./pages/GroupsPage/GroupsPageProvider"), ); -const OrganizationGroupPage = lazy( - () => import("./pages/ManagementSettingsPage/GroupsPage/GroupPage"), +const GroupsPage = lazy(() => import("./pages/GroupsPage/GroupsPage")); +const CreateGroupPage = lazy( + () => import("./pages/GroupsPage/CreateGroupPage"), ); -const OrganizationGroupSettingsPage = lazy( - () => import("./pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage"), +const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage")); +const GroupSettingsPage = lazy( + () => import("./pages/GroupsPage/GroupSettingsPage"), ); const OrganizationMembersPage = lazy( - () => import("./pages/ManagementSettingsPage/OrganizationMembersPage"), + () => import("./pages/OrganizationSettingsPage/OrganizationMembersPage"), ); const OrganizationCustomRolesPage = lazy( () => - import("./pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage"), + import("./pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage"), ); const OrganizationIdPSyncPage = lazy( - () => import("./pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage"), + () => import("./pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage"), ); const CreateEditRolePage = lazy( () => - import("./pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage"), + import( + "./pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage" + ), ); const OrganizationProvisionersPage = lazy( - () => import("./pages/ManagementSettingsPage/OrganizationProvisionersPage"), + () => import("./pages/OrganizationSettingsPage/OrganizationProvisionersPage"), ); const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), @@ -281,7 +274,6 @@ const TemplateInsightsPage = lazy( const PremiumPage = lazy( () => import("./pages/DeploymentSettingsPage/PremiumPage/PremiumPage"), ); -const GroupsPage = lazy(() => import("./pages/GroupsPage/GroupsPage")); const IconsPage = lazy(() => import("./pages/IconsPage/IconsPage")); const AccessURLPage = lazy(() => import("./pages/HealthPage/AccessURLPage")); const DatabasePage = lazy(() => import("./pages/HealthPage/DatabasePage")); @@ -353,17 +345,16 @@ const templateRouter = () => { ); }; -const organizationGroupsRouter = () => { +const groupsRouter = () => { return ( - } /> + }> + } /> - } /> - } /> - } - /> + } /> + } /> + } /> + ); }; @@ -405,23 +396,15 @@ export const router = createBrowserRouter( {templateRouter()} - - }> - } /> - - - } /> - - - - }> - } /> - + } + /> - } /> - } /> - } /> - + } + /> } /> @@ -433,7 +416,7 @@ export const router = createBrowserRouter( }> } /> - {organizationGroupsRouter()} + {groupsRouter()} } /> } /> @@ -488,18 +471,8 @@ export const router = createBrowserRouter( } /> } /> - - }> - } /> - - } /> - } /> - } - /> - + {groupsRouter()} }> diff --git a/site/vite.config.mts b/site/vite.config.mts index 9da0221016cb1..4deaac0dd5365 100644 --- a/site/vite.config.mts +++ b/site/vite.config.mts @@ -79,6 +79,10 @@ export default defineConfig({ target: process.env.CODER_HOST || "http://localhost:3000", secure: process.env.NODE_ENV === "production", }, + "/healthz": { + target: process.env.CODER_HOST || "http://localhost:3000", + secure: process.env.NODE_ENV === "production", + }, }, }, resolve: { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy