From 6f5952e80568045be38950834bd7bf19a0508a7d Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 12 Jul 2024 19:07:09 +0000 Subject: [PATCH 01/12] =?UTF-8?q?=F0=9F=AB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/api/api.ts | 4 +- site/src/api/queries/groups.ts | 40 +- site/src/pages/GroupsPage/CreateGroupPage.tsx | 9 +- site/src/pages/GroupsPage/GroupPage.tsx | 7 +- .../pages/GroupsPage/SettingsGroupPage.tsx | 12 +- .../GroupsPage/CreateGroupPage.tsx | 33 ++ .../CreateGroupPageView.stories.tsx | 23 ++ .../GroupsPage/CreateGroupPageView.tsx | 85 +++++ .../GroupsPage/GroupPage.tsx | 361 ++++++++++++++++++ .../GroupsPage/GroupsPage.tsx | 65 ++++ .../GroupsPage/GroupsPageView.stories.tsx | 51 +++ .../GroupsPage/GroupsPageView.tsx | 208 ++++++++++ .../GroupsPage/SettingsGroupPage.tsx | 74 ++++ .../SettingsGroupPageView.stories.tsx | 21 + .../GroupsPage/SettingsGroupPageView.tsx | 148 +++++++ .../pages/ManagementSettingsPage/Sidebar.tsx | 6 +- site/src/router.tsx | 29 +- 17 files changed, 1137 insertions(+), 39 deletions(-) create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.stories.tsx create mode 100644 site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 75a476adcf559..b903ac271ff27 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1455,9 +1455,9 @@ class ApiMethods { return response.data; }; - getGroup = async (groupName: string): Promise => { + getGroup = async (orgId: string, groupName: string): Promise => { const response = await this.axios.get( - `/api/v2/organizations/default/groups/${groupName}`, + `/api/v2/organizations/${orgId}/groups/${groupName}`, ); return response.data; }; diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index feeeb2335b16b..ac5a179f1766e 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -9,7 +9,11 @@ import type { const GROUPS_QUERY_KEY = ["groups"]; type GroupSortOrder = "asc" | "desc"; -const getGroupQueryKey = (groupName: string) => ["group", groupName]; +const getGroupQueryKey = (organizationId: string, groupName: string) => [ + organizationId, + "group", + groupName, +]; export const groups = (organizationId: string) => { return { @@ -18,10 +22,10 @@ export const groups = (organizationId: string) => { } satisfies UseQueryOptions; }; -export const group = (groupName: string) => { +export const group = (organizationId: string, groupName: string) => { return { - queryKey: getGroupQueryKey(groupName), - queryFn: () => API.getGroup(groupName), + queryKey: getGroupQueryKey(organizationId, groupName), + queryFn: () => API.getGroup(organizationId, groupName), }; }; @@ -69,7 +73,7 @@ export function groupsForUser(organizationId: string, userId: string) { export const groupPermissions = (groupId: string) => { return { - queryKey: [...getGroupQueryKey(groupId), "permissions"], + queryKey: ["group", groupId, "permissions"], queryFn: () => API.checkAuthorization({ checks: { @@ -85,12 +89,12 @@ export const groupPermissions = (groupId: string) => { }; }; -export const createGroup = (queryClient: QueryClient) => { +export const createGroup = ( + queryClient: QueryClient, + organizationId: string, +) => { return { - mutationFn: ({ - organizationId, - ...request - }: CreateGroupRequest & { organizationId: string }) => + mutationFn: (request: CreateGroupRequest) => API.createGroup(organizationId, request), onSuccess: async () => { await queryClient.invalidateQueries(GROUPS_QUERY_KEY); @@ -106,7 +110,7 @@ export const patchGroup = (queryClient: QueryClient) => { }: PatchGroupRequest & { groupId: string }) => API.patchGroup(groupId, request), onSuccess: async (updatedGroup: Group) => - invalidateGroup(queryClient, updatedGroup.id), + invalidateGroup(queryClient, "default", updatedGroup.id), }; }; @@ -114,7 +118,7 @@ export const deleteGroup = (queryClient: QueryClient) => { return { mutationFn: API.deleteGroup, onSuccess: async (_: void, groupId: string) => - invalidateGroup(queryClient, groupId), + invalidateGroup(queryClient, "default", groupId), }; }; @@ -123,7 +127,7 @@ export const addMember = (queryClient: QueryClient) => { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => API.addMember(groupId, userId), onSuccess: async (updatedGroup: Group) => - invalidateGroup(queryClient, updatedGroup.id), + invalidateGroup(queryClient, "default", updatedGroup.id), }; }; @@ -132,14 +136,18 @@ export const removeMember = (queryClient: QueryClient) => { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => API.removeMember(groupId, userId), onSuccess: async (updatedGroup: Group) => - invalidateGroup(queryClient, updatedGroup.id), + invalidateGroup(queryClient, "default", updatedGroup.id), }; }; -export const invalidateGroup = (queryClient: QueryClient, groupId: string) => +export const invalidateGroup = ( + queryClient: QueryClient, + orgId: string, + groupId: string, +) => Promise.all([ queryClient.invalidateQueries(GROUPS_QUERY_KEY), - queryClient.invalidateQueries(getGroupQueryKey(groupId)), + queryClient.invalidateQueries(getGroupQueryKey(orgId, groupId)), ]); export function sortGroupsByName( diff --git a/site/src/pages/GroupsPage/CreateGroupPage.tsx b/site/src/pages/GroupsPage/CreateGroupPage.tsx index 16b75cb7bbb0d..11ab7371eef37 100644 --- a/site/src/pages/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPage.tsx @@ -3,15 +3,13 @@ import { Helmet } from "react-helmet-async"; import { useMutation, useQueryClient } from "react-query"; import { useNavigate } from "react-router-dom"; import { createGroup } from "api/queries/groups"; -import { useDashboard } from "modules/dashboard/useDashboard"; import { pageTitle } from "utils/page"; import CreateGroupPageView from "./CreateGroupPageView"; export const CreateGroupPage: FC = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); - const { organizationId } = useDashboard(); - const createGroupMutation = useMutation(createGroup(queryClient)); + const createGroupMutation = useMutation(createGroup(queryClient, "default")); return ( <> @@ -20,10 +18,7 @@ export const CreateGroupPage: FC = () => { { - const newGroup = await createGroupMutation.mutateAsync({ - organizationId, - ...data, - }); + const newGroup = await createGroupMutation.mutateAsync(data); navigate(`/groups/${newGroup.name}`); }} error={createGroupMutation.error} diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index b36f1f9c1cde0..0256205ad6183 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -54,10 +54,13 @@ import { isEveryoneGroup } from "utils/groups"; import { pageTitle } from "utils/page"; export const GroupPage: FC = () => { - const { groupName } = useParams() as { groupName: string }; + const { groupName, organization } = useParams() as { + organization: string; + groupName: string; + }; const queryClient = useQueryClient(); const navigate = useNavigate(); - const groupQuery = useQuery(group(groupName)); + const groupQuery = useQuery(group(organization, groupName)); const groupData = groupQuery.data; const { data: permissions } = useQuery( groupData !== undefined diff --git a/site/src/pages/GroupsPage/SettingsGroupPage.tsx b/site/src/pages/GroupsPage/SettingsGroupPage.tsx index efb7fadbce29e..66088a074c958 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPage.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPage.tsx @@ -13,8 +13,7 @@ import SettingsGroupPageView from "./SettingsGroupPageView"; export const SettingsGroupPage: FC = () => { const { groupName } = useParams() as { groupName: string }; const queryClient = useQueryClient(); - const groupQuery = useQuery(group(groupName)); - const { data: groupData, isLoading, error } = useQuery(group(groupName)); + const groupQuery = useQuery(group("default", groupName)); const patchGroupMutation = useMutation(patchGroup(queryClient)); const navigate = useNavigate(); @@ -28,11 +27,11 @@ export const SettingsGroupPage: FC = () => { ); - if (error) { - return ; + if (groupQuery.error) { + return ; } - if (isLoading || !groupData) { + if (groupQuery.isLoading || !groupQuery.data) { return ( <> {helmet} @@ -40,7 +39,8 @@ export const SettingsGroupPage: FC = () => { ); } - const groupId = groupData.id; + + const groupId = groupQuery.data.id; return ( <> diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx new file mode 100644 index 0000000000000..c32dcd7745af2 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx @@ -0,0 +1,33 @@ +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 { createGroup } from "api/queries/groups"; +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), + ); + + return ( + <> + + {pageTitle("Create Group")} + + { + const newGroup = await createGroupMutation.mutateAsync(data); + navigate(`/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 new file mode 100644 index 0000000000000..1d6a214d9abe3 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { mockApiError } from "testHelpers/entities"; +import { CreateGroupPageView } from "./CreateGroupPageView"; + +const meta: Meta = { + title: "pages/GroupsPage/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" }], + }), + initialTouched: { name: true }, + }, +}; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx new file mode 100644 index 0000000000000..4cfc62e6dea90 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx @@ -0,0 +1,85 @@ +import TextField from "@mui/material/TextField"; +import { type FormikTouched, useFormik } from "formik"; +import type { FC } from "react"; +import { useNavigate } from "react-router-dom"; +import * as Yup from "yup"; +import { isApiValidationError } from "api/errors"; +import type { CreateGroupRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { FormFooter } from "components/FormFooter/FormFooter"; +import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { IconField } from "components/IconField/IconField"; +import { Margins } from "components/Margins/Margins"; +import { Stack } from "components/Stack/Stack"; +import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; + +const validationSchema = Yup.object({ + name: Yup.string().required().label("Name"), +}); + +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({ + initialValues: { + name: "", + display_name: "", + avatar_url: "", + quota_allowance: 0, + }, + validationSchema, + onSubmit, + initialTouched, + }); + 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 new file mode 100644 index 0000000000000..aaa192b63e39f --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx @@ -0,0 +1,361 @@ +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 { 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 { getErrorMessage } from "api/errors"; +import { + addMember, + deleteGroup, + group, + groupPermissions, + removeMember, +} from "api/queries/groups"; +import type { Group, ReducedUser, User } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { AvatarData } from "components/AvatarData/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 { Margins } from "components/Margins/Margins"; +import { + MoreMenu, + MoreMenuContent, + MoreMenuItem, + MoreMenuTrigger, + ThreeDotsButton, +} from "components/MoreMenu/MoreMenu"; +import { + PageHeader, + PageHeaderSubtitle, + PageHeaderTitle, +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { + PaginationStatus, + TableToolbar, +} from "components/TableToolbar/TableToolbar"; +import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import { isEveryoneGroup } from "utils/groups"; +import { pageTitle } from "utils/page"; + +export const GroupPage: FC = () => { + const { organization, 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 !== undefined + ? groupPermissions(groupData.id) + : { enabled: false }, + ); + const addMemberMutation = useMutation(addMember(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} + + + + + + + ) + } + > + + {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.")); + } + }} + /> + )} + + + + + + + + + User + Status + + + + + + {groupData?.members.length === 0 ? ( + + + + + + ) : ( + groupData?.members.map((member) => ( + + )) + )} + +
+
+
+
+ + {groupQuery.data && ( + { + try { + await deleteGroupMutation.mutateAsync(groupId); + displaySuccess("Group deleted successfully."); + navigate("/groups"); + } catch (error) { + displayError(getErrorMessage(error, "Failed to delete group.")); + } + }} + onCancel={() => { + setIsDeletingGroup(false); + }} + /> + )} + + ); +}; + +interface AddGroupMemberProps { + isLoading: boolean; + onSubmit: (user: User, reset: () => void) => void; +} + +const AddGroupMember: FC = ({ isLoading, onSubmit }) => { + 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; +} + +const GroupMemberRow: FC = ({ + member, + group, + canUpdate, +}) => { + const queryClient = useQueryClient(); + const removeMemberMutation = useMutation(removeMember(queryClient)); + + return ( + + + + } + title={member.username} + subtitle={member.email} + /> + + +
{member.status}
+ +
+ + {canUpdate && ( + + + + + + { + try { + await removeMemberMutation.mutateAsync({ + groupId: group.id, + userId: member.id, + }); + displaySuccess("Member removed successfully."); + } catch (error) { + displayError( + getErrorMessage(error, "Failed to remove member."), + ); + } + }} + disabled={group.id === group.organization_id} + > + 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 new file mode 100644 index 0000000000000..370f77c007915 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx @@ -0,0 +1,65 @@ +import GroupAdd from "@mui/icons-material/GroupAddOutlined"; +import Button from "@mui/material/Button"; +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 { getErrorMessage } from "api/errors"; +import { groups } from "api/queries/groups"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; +import { pageTitle } from "utils/page"; +import GroupsPageView from "./GroupsPageView"; + +export const GroupsPage: FC = () => { + const { permissions } = useAuthenticated(); + const { organizationId } = useDashboard(); + const { createGroup: canCreateGroup } = permissions; + const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); + const groupsQuery = useQuery(groups(organizationId)); + + useEffect(() => { + if (groupsQuery.error) { + displayError( + getErrorMessage(groupsQuery.error, "Error on loading groups."), + ); + } + }, [groupsQuery.error]); + + return ( + <> + + {pageTitle("Groups")} + + + + {canCreateGroup && isTemplateRBACEnabled && ( + + )} + + } + > + Groups + + + + + ); +}; + +export default GroupsPage; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx new file mode 100644 index 0000000000000..705ddd07fb376 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { MockGroup } from "testHelpers/entities"; +import { GroupsPageView } from "./GroupsPageView"; + +const meta: Meta = { + title: "pages/GroupsPage", + 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 new file mode 100644 index 0000000000000..a8ee18bc86e6a --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx @@ -0,0 +1,208 @@ +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 { FC } from "react"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import type { Group } from "api/typesGenerated"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { GroupAvatar } from "components/GroupAvatar/GroupAvatar"; +import { Paywall } from "components/Paywall/Paywall"; +import { + TableLoaderSkeleton, + TableRowSkeleton, +} from "components/TableLoader/TableLoader"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; +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); + const navigate = useNavigate(); + + return ( + <> + + + + + + + + + + Name + Users + + + + + + + + + + + + + } + variant="contained" + > + Create group + + ) + } + /> + + + + + + {groups?.map((group) => ( + { + navigate(group.name); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + navigate(group.name); + } + }} + css={styles.clickableTableRow} + > + + + } + title={group.display_name || group.name} + subtitle={`${group.members.length} members`} + /> + + + + {group.members.length === 0 && "-"} + + {group.members.map((member) => ( + + ))} + + + + +
+ +
+
+
+ ))} +
+
+
+
+
+
+
+ + ); +}; + +const TableLoader = () => { + return ( + + + +
+ +
+
+ + + + + + +
+
+ ); +}; + +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, + height: 20, + }), + arrowCell: { + display: "flex", + }, +} satisfies Record>; + +export default GroupsPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage.tsx new file mode 100644 index 0000000000000..e26e1c663d7a0 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage.tsx @@ -0,0 +1,74 @@ +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 { 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 { pageTitle } from "utils/page"; +import SettingsGroupPageView from "./SettingsGroupPageView"; + +export const SettingsGroupPage: FC = () => { + const { organization, groupName } = useParams() as { + organization: string; + groupName: string; + }; + const queryClient = useQueryClient(); + const groupQuery = useQuery(group(organization, groupName)); + const patchGroupMutation = useMutation(patchGroup(queryClient)); + const navigate = useNavigate(); + + const navigateToGroup = () => { + navigate(`/organizations/${organization}/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(`/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/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.stories.tsx new file mode 100644 index 0000000000000..c715c82d74110 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.stories.tsx @@ -0,0 +1,21 @@ +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/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.tsx new file mode 100644 index 0000000000000..6998268ef74fb --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.tsx @@ -0,0 +1,148 @@ +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import type { FC } from "react"; +import * as Yup from "yup"; +import type { Group } from "api/typesGenerated"; +import { FormFooter } from "components/FormFooter/FormFooter"; +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 { Stack } from "components/Stack/Stack"; +import { + getFormHelpers, + nameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import { isEveryoneGroup } from "utils/groups"; + +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/Sidebar.tsx b/site/src/pages/ManagementSettingsPage/Sidebar.tsx index dde4ef35664bd..46ac9338c0b69 100644 --- a/site/src/pages/ManagementSettingsPage/Sidebar.tsx +++ b/site/src/pages/ManagementSettingsPage/Sidebar.tsx @@ -115,7 +115,7 @@ export const OrganizationSettingsNavigation: FC< {active && ( - + Organization settings = ({ interface SidebarNavSubItemProps { children?: ReactNode; href: string; + end?: boolean; } export const SidebarNavSubItem: FC = ({ children, href, + end, }) => { const link = useClassName(classNames.subLink, []); const activeLink = useClassName(classNames.activeSubLink, []); return ( cx([link, isActive && activeLink])} > diff --git a/site/src/router.tsx b/site/src/router.tsx index d6aa16523e9f3..0a126ba33f9f6 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -227,6 +227,18 @@ const CreateOrganizationPage = lazy( const OrganizationSettingsPage = lazy( () => import("./pages/ManagementSettingsPage/OrganizationSettingsPage"), ); +const OrganizationGroupsPage = lazy( + () => import("./pages/ManagementSettingsPage/GroupsPage/GroupsPage"), +); +const CreateOrganizationGroupPage = lazy( + () => import("./pages/ManagementSettingsPage/GroupsPage/CreateGroupPage"), +); +const OrganizationGroupPage = lazy( + () => import("./pages/ManagementSettingsPage/GroupsPage/GroupPage"), +); +const OrganizationGroupSettingsPage = lazy( + () => import("./pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage"), +); const OrganizationSettingsPlaceholder = lazy( () => import("./pages/ManagementSettingsPage/OrganizationSettingsPlaceholder"), @@ -352,10 +364,19 @@ export const router = createBrowserRouter( path="members" element={} /> - } - /> + + } /> + + } + /> + } /> + } + /> + } From 46cb3c7c60e4c5cdd6cf19de082b0c53a1fa8fbc Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 17 Jul 2024 18:36:39 +0000 Subject: [PATCH 02/12] fix a bunch of stuff --- .../GroupsPage/CreateGroupPage.tsx | 2 +- .../ManagementSettingsPage/GroupsPage/GroupPage.tsx | 2 +- .../{SettingsGroupPage.tsx => GroupSettingsPage.tsx} | 10 +++++----- ...stories.tsx => GroupSettingsPageView.stories.tsx} | 12 ++++++------ ...gsGroupPageView.tsx => GroupSettingsPageView.tsx} | 9 +++------ .../ManagementSettingsPage/GroupsPage/GroupsPage.tsx | 5 +++-- site/src/pages/ManagementSettingsPage/Sidebar.tsx | 8 -------- site/src/router.tsx | 2 +- 8 files changed, 20 insertions(+), 30 deletions(-) rename site/src/pages/ManagementSettingsPage/GroupsPage/{SettingsGroupPage.tsx => GroupSettingsPage.tsx} (89%) rename site/src/pages/ManagementSettingsPage/GroupsPage/{SettingsGroupPageView.stories.tsx => GroupSettingsPageView.stories.tsx} (50%) rename site/src/pages/ManagementSettingsPage/GroupsPage/{SettingsGroupPageView.tsx => GroupSettingsPageView.tsx} (94%) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx index c32dcd7745af2..a51d67d63ce34 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx @@ -22,7 +22,7 @@ export const CreateGroupPage: FC = () => { { const newGroup = await createGroupMutation.mutateAsync(data); - navigate(`/groups/${newGroup.name}`); + navigate(`/organizations/${organization}/groups/${newGroup.name}`); }} error={createGroupMutation.error} isLoading={createGroupMutation.isLoading} diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx index aaa192b63e39f..cea47881c5e74 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx @@ -212,7 +212,7 @@ export const GroupPage: FC = () => { try { await deleteGroupMutation.mutateAsync(groupId); displaySuccess("Group deleted successfully."); - navigate("/groups"); + navigate(".."); } catch (error) { displayError(getErrorMessage(error, "Failed to delete group.")); } diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx similarity index 89% rename from site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage.tsx rename to site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx index e26e1c663d7a0..ca9b836c4ba5c 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx @@ -8,9 +8,9 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; import { pageTitle } from "utils/page"; -import SettingsGroupPageView from "./SettingsGroupPageView"; +import GroupSettingsPageView from "./GroupSettingsPageView"; -export const SettingsGroupPage: FC = () => { +export const GroupSettingsPage: FC = () => { const { organization, groupName } = useParams() as { organization: string; groupName: string; @@ -48,7 +48,7 @@ export const SettingsGroupPage: FC = () => { <> {helmet} - { try { @@ -58,7 +58,7 @@ export const SettingsGroupPage: FC = () => { add_users: [], remove_users: [], }); - navigate(`/groups/${data.name}`, { replace: true }); + navigate(`../${data.name}`); } catch (error) { displayError(getErrorMessage(error, "Failed to update group")); } @@ -71,4 +71,4 @@ export const SettingsGroupPage: FC = () => { ); }; -export default SettingsGroupPage; +export default GroupSettingsPage; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx similarity index 50% rename from site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.stories.tsx rename to site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx index c715c82d74110..893c285d7ef1b 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx @@ -1,11 +1,11 @@ import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { MockGroup } from "testHelpers/entities"; -import { SettingsGroupPageView } from "./SettingsGroupPageView"; +import GroupSettingsPageView from "./GroupSettingsPageView"; -const meta: Meta = { - title: "pages/GroupsPage/SettingsGroupPageView", - component: SettingsGroupPageView, +const meta: Meta = { + title: "pages/GroupsPage/GroupSettingsPageView", + component: GroupSettingsPageView, args: { onCancel: action("onCancel"), group: MockGroup, @@ -14,8 +14,8 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; const Example: Story = {}; -export { Example as SettingsGroupPageView }; +export { Example as GroupSettingsPageView }; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx similarity index 94% rename from site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.tsx rename to site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx index 6998268ef74fb..67782005775f7 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/SettingsGroupPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx @@ -68,15 +68,12 @@ const UpdateGroupForm: FC = ({ label="Name" disabled={isEveryoneGroup(group)} /> - {isEveryoneGroup(group) ? ( - <> - ) : ( + {!isEveryoneGroup(group) && ( <> = ({ +const GroupSettingsPageView: FC = ({ onCancel, onSubmit, group, @@ -145,4 +142,4 @@ export const SettingsGroupPageView: FC = ({ ); }; -export default SettingsGroupPageView; +export default GroupSettingsPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx index 370f77c007915..bb03ff2278e23 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx @@ -13,13 +13,14 @@ import { useDashboard } from "modules/dashboard/useDashboard"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { pageTitle } from "utils/page"; import GroupsPageView from "./GroupsPageView"; +import { useOrganizationSettings } from "../ManagementSettingsLayout"; export const GroupsPage: FC = () => { const { permissions } = useAuthenticated(); - const { organizationId } = useDashboard(); + const { currentOrganizationId } = useOrganizationSettings(); const { createGroup: canCreateGroup } = permissions; const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); - const groupsQuery = useQuery(groups(organizationId)); + const groupsQuery = useQuery(groups(currentOrganizationId!)); useEffect(() => { if (groupsQuery.error) { diff --git a/site/src/pages/ManagementSettingsPage/Sidebar.tsx b/site/src/pages/ManagementSettingsPage/Sidebar.tsx index 46ac9338c0b69..8bf262f30d9cc 100644 --- a/site/src/pages/ManagementSettingsPage/Sidebar.tsx +++ b/site/src/pages/ManagementSettingsPage/Sidebar.tsx @@ -118,20 +118,12 @@ export const OrganizationSettingsNavigation: FC< Organization settings - - External authentication - Members Groups - - Metrics - diff --git a/site/src/router.tsx b/site/src/router.tsx index d380736791161..36525099574f9 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -237,7 +237,7 @@ const OrganizationGroupPage = lazy( () => import("./pages/ManagementSettingsPage/GroupsPage/GroupPage"), ); const OrganizationGroupSettingsPage = lazy( - () => import("./pages/ManagementSettingsPage/GroupsPage/SettingsGroupPage"), + () => import("./pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage"), ); const OrganizationMembersPage = lazy( () => import("./pages/ManagementSettingsPage/OrganizationMembersPage"), From 571bb5eb150c2e360a545e4608ba0db34af87198 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 17 Jul 2024 19:21:20 +0000 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GroupsPage/GroupSettingsPageView.tsx | 50 +++++++++++-------- .../pages/ManagementSettingsPage/Sidebar.tsx | 1 - site/src/pages/UsersPage/UsersLayout.tsx | 34 +++++++------ .../WorkspaceSettingsForm.tsx | 2 +- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx index 67782005775f7..2260acb579b04 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx @@ -3,12 +3,14 @@ import { useFormik } from "formik"; import type { FC } from "react"; import * as Yup from "yup"; import type { Group } from "api/typesGenerated"; -import { FormFooter } from "components/FormFooter/FormFooter"; -import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { + FormFields, + FormFooter, + FormSection, + HorizontalForm, +} from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; import { Loader } from "components/Loader/Loader"; -import { Margins } from "components/Margins/Margins"; -import { Stack } from "components/Stack/Stack"; import { getFormHelpers, nameValidator, @@ -56,9 +58,12 @@ const UpdateGroupForm: FC = ({ const getFieldHelpers = getFormHelpers(form, errors); return ( - -
- + + + = ({ /> )} + + + + = ({ type="number" label="Quota Allowance" /> - + + - - -
+ + ); }; @@ -130,15 +142,13 @@ const GroupSettingsPageView: FC = ({ } return ( - - - + ); }; diff --git a/site/src/pages/ManagementSettingsPage/Sidebar.tsx b/site/src/pages/ManagementSettingsPage/Sidebar.tsx index 8bf262f30d9cc..cc5fbb5d97e1c 100644 --- a/site/src/pages/ManagementSettingsPage/Sidebar.tsx +++ b/site/src/pages/ManagementSettingsPage/Sidebar.tsx @@ -78,7 +78,6 @@ const DeploymentSettingsNavigation: FC = () => { Observability
Users - Groups
)} diff --git a/site/src/pages/UsersPage/UsersLayout.tsx b/site/src/pages/UsersPage/UsersLayout.tsx index 8b3dc7858c41e..776cb212f011e 100644 --- a/site/src/pages/UsersPage/UsersLayout.tsx +++ b/site/src/pages/UsersPage/UsersLayout.tsx @@ -15,9 +15,11 @@ import { TAB_PADDING_Y, TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { USERS_LINK } from "modules/navigation"; +import { useDashboard } from "modules/dashboard/useDashboard"; export const UsersLayout: FC = () => { const { permissions } = useAuthenticated(); + const { experiments } = useDashboard(); const { createUser: canCreateUser, createGroup: canCreateGroup } = permissions; const navigate = useNavigate(); @@ -57,21 +59,23 @@ export const UsersLayout: FC = () => { - - - - - Users - - - Groups - - - - + {!experiments.includes("multi-organization") && ( + + + + + Users + + + Groups + + + + + )} }> diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx index 8ef7386f01534..1ead5d7863b38 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx @@ -78,7 +78,7 @@ export const WorkspaceSettingsForm: FC = ({ workspace.allow_renames ? form.values.name !== form.initialValues.name && "Depending on the template, renaming your workspace may be destructive" - : "Renaming your workspace can be destructive and has not been enabled for this deployment." + : "Renaming your workspace can be destructive and is disabled by the template." } /> From 9cdee442859bc8c661125c29748a1795ba59caf5 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 17 Jul 2024 19:34:57 +0000 Subject: [PATCH 04/12] u --- site/src/api/api.ts | 5 ++++- .../pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx | 3 +-- site/src/pages/UsersPage/UsersLayout.tsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 50d4c2d170c69..6233349dbe913 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1477,7 +1477,10 @@ class ApiMethods { return response.data; }; - getGroup = async (orgId: string, groupName: string): Promise => { + getGroup = async ( + orgId: string, + groupName: string, + ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${orgId}/groups/${groupName}`, ); diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx index bb03ff2278e23..91d727589d8b2 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx @@ -9,11 +9,10 @@ import { groups } from "api/queries/groups"; import { displayError } from "components/GlobalSnackbar/utils"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { useDashboard } from "modules/dashboard/useDashboard"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { pageTitle } from "utils/page"; -import GroupsPageView from "./GroupsPageView"; import { useOrganizationSettings } from "../ManagementSettingsLayout"; +import GroupsPageView from "./GroupsPageView"; export const GroupsPage: FC = () => { const { permissions } = useAuthenticated(); diff --git a/site/src/pages/UsersPage/UsersLayout.tsx b/site/src/pages/UsersPage/UsersLayout.tsx index 776cb212f011e..ac11e008e436d 100644 --- a/site/src/pages/UsersPage/UsersLayout.tsx +++ b/site/src/pages/UsersPage/UsersLayout.tsx @@ -13,9 +13,9 @@ import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { TAB_PADDING_Y, TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { useDashboard } from "modules/dashboard/useDashboard"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { USERS_LINK } from "modules/navigation"; -import { useDashboard } from "modules/dashboard/useDashboard"; export const UsersLayout: FC = () => { const { permissions } = useAuthenticated(); From 3399d2124b3ca3aaad7b2b7019e57dfb5ae01627 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 17 Jul 2024 19:41:31 +0000 Subject: [PATCH 05/12] iykyk --- .../GroupsPage/CreateGroupPageView.stories.tsx | 2 +- .../GroupsPage/GroupSettingsPageView.stories.tsx | 2 +- .../GroupsPage/GroupsPageView.stories.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx index 1d6a214d9abe3..7ca2e49bd12ea 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx @@ -3,7 +3,7 @@ import { mockApiError } from "testHelpers/entities"; import { CreateGroupPageView } from "./CreateGroupPageView"; const meta: Meta = { - title: "pages/GroupsPage/CreateGroupPageView", + title: "pages/OrganizationGroupsPage/CreateGroupPageView", component: CreateGroupPageView, }; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx index 893c285d7ef1b..071fa031dbd2a 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx @@ -4,7 +4,7 @@ import { MockGroup } from "testHelpers/entities"; import GroupSettingsPageView from "./GroupSettingsPageView"; const meta: Meta = { - title: "pages/GroupsPage/GroupSettingsPageView", + title: "pages/OrganizationGroupsPage/GroupSettingsPageView", component: GroupSettingsPageView, args: { onCancel: action("onCancel"), diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx index 705ddd07fb376..fabd8c7104154 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/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, }; From b252a50ba84fdd565f8b70eaaa64de57ef0c40d6 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 17 Jul 2024 23:07:20 +0000 Subject: [PATCH 06/12] do the thing here too --- .../GroupsPage/CreateGroupPageView.tsx | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx index 4cfc62e6dea90..0bb3d8c4b6b6a 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx @@ -6,12 +6,14 @@ import * as Yup from "yup"; import { isApiValidationError } from "api/errors"; import type { CreateGroupRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { FormFooter } from "components/FormFooter/FormFooter"; -import { FullPageForm } from "components/FullPageForm/FullPageForm"; import { IconField } from "components/IconField/IconField"; -import { Margins } from "components/Margins/Margins"; -import { Stack } from "components/Stack/Stack"; import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; +import { + FormFields, + FormFooter, + FormSection, + HorizontalForm, +} from "components/Form/Form"; const validationSchema = Yup.object({ name: Yup.string().required().label("Name"), @@ -47,39 +49,40 @@ export const CreateGroupPageView: FC = ({ const onCancel = () => navigate(-1); return ( - - -
- - {Boolean(error) && !isApiValidationError(error) && ( - - )} + + + + {Boolean(error) && !isApiValidationError(error) && ( + + )} - - - form.setFieldValue("avatar_url", value)} - /> - - - -
-
+ + + form.setFieldValue("avatar_url", value)} + /> + + + + ); }; export default CreateGroupPageView; From 5ef64853c0fde1870d2dd9ca838509330bf4431e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 18 Jul 2024 17:31:06 +0000 Subject: [PATCH 07/12] :^) --- .../ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx index 0bb3d8c4b6b6a..dafdc0c139ceb 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx @@ -6,14 +6,14 @@ import * as Yup from "yup"; import { isApiValidationError } from "api/errors"; import type { CreateGroupRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { IconField } from "components/IconField/IconField"; -import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; import { FormFields, FormFooter, FormSection, HorizontalForm, } from "components/Form/Form"; +import { IconField } from "components/IconField/IconField"; +import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; const validationSchema = Yup.object({ name: Yup.string().required().label("Name"), From 0c2ddac5ea74a6468425766dff9ee362f2714e81 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 18 Jul 2024 17:48:26 +0000 Subject: [PATCH 08/12] better --- .../GroupsPage/CreateGroupPageView.tsx | 72 ++++++----- .../GroupsPage/GroupSettingsPageView.tsx | 122 ++++++++++-------- 2 files changed, 104 insertions(+), 90 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx index dafdc0c139ceb..d88e123ec1d05 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx @@ -13,6 +13,7 @@ import { HorizontalForm, } from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; const validationSchema = Yup.object({ @@ -49,40 +50,45 @@ export const CreateGroupPageView: FC = ({ const onCancel = () => navigate(-1); return ( - - - - {Boolean(error) && !isApiValidationError(error) && ( - - )} + <> + + Create a group + + + + + {Boolean(error) && !isApiValidationError(error) && ( + + )} - - - form.setFieldValue("avatar_url", value)} - /> - - - - + + + form.setFieldValue("avatar_url", value)} + /> + + + + + ); }; export default CreateGroupPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx index 2260acb579b04..40ceaf579a219 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx @@ -11,6 +11,7 @@ import { } from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; import { Loader } from "components/Loader/Loader"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { getFormHelpers, nameValidator, @@ -58,65 +59,72 @@ const UpdateGroupForm: FC = ({ const getFieldHelpers = getFormHelpers(form, errors); return ( - - - - - {!isEveryoneGroup(group) && ( - <> - - form.setFieldValue("avatar_url", value)} - /> - - )} - - - - - + + {group.name} + + + + + + {!isEveryoneGroup(group) && ( + <> + + + form.setFieldValue("avatar_url", value) + } + /> + + )} + + + + + - - + })} + onChange={onChangeTrimmed(form)} + autoFocus + fullWidth + type="number" + label="Quota Allowance" + /> + + - - + + + ); }; From 91732b69b5b4df61b576320c4e9c8c700c27ec7e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 18 Jul 2024 17:56:18 +0000 Subject: [PATCH 09/12] oops --- .../ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx index 40ceaf579a219..97080f9ffc720 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx @@ -60,7 +60,7 @@ const UpdateGroupForm: FC = ({ return ( <> - + {group.name} From 560fd4fdbdd3cbade18b2be6526a622e06477ab7 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 19 Jul 2024 17:05:13 +0000 Subject: [PATCH 10/12] query refetching is cool --- site/src/api/api.ts | 12 +++---- site/src/api/queries/groups.ts | 4 +-- site/src/api/queries/organizations.ts | 9 ++--- .../GroupsPage/GroupPage.tsx | 34 ++++++++++--------- .../ManagementSettingsLayout.tsx | 2 +- .../OrganizationSettingsPage.tsx | 10 +++--- 6 files changed, 38 insertions(+), 33 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 9e3c3b3687425..b408e290e1273 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -515,19 +515,19 @@ class ApiMethods { }; updateOrganization = async ( - orgId: string, + organizationId: string, params: TypesGen.UpdateOrganizationRequest, ) => { const response = await this.axios.patch( - `/api/v2/organizations/${orgId}`, + `/api/v2/organizations/${organizationId}`, params, ); return response.data; }; - deleteOrganization = async (orgId: string) => { + deleteOrganization = async (organizationId: string) => { await this.axios.delete( - `/api/v2/organizations/${orgId}`, + `/api/v2/organizations/${organizationId}`, ); }; @@ -1486,11 +1486,11 @@ class ApiMethods { }; getGroup = async ( - orgId: string, + organizationId: string, groupName: string, ): Promise => { const response = await this.axios.get( - `/api/v2/organizations/${orgId}/groups/${groupName}`, + `/api/v2/organizations/${organizationId}/groups/${groupName}`, ); return response.data; }; diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index ac5a179f1766e..e532ebcd81d43 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -142,12 +142,12 @@ export const removeMember = (queryClient: QueryClient) => { export const invalidateGroup = ( queryClient: QueryClient, - orgId: string, + organizationId: string, groupId: string, ) => Promise.all([ queryClient.invalidateQueries(GROUPS_QUERY_KEY), - queryClient.invalidateQueries(getGroupQueryKey(orgId, groupId)), + queryClient.invalidateQueries(getGroupQueryKey(organizationId, groupId)), ]); export function sortGroupsByName( diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 171efaec104f4..1dc44a2a1c9a3 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -19,14 +19,14 @@ export const createOrganization = (queryClient: QueryClient) => { }; interface UpdateOrganizationVariables { - orgId: string; + organizationId: string; req: UpdateOrganizationRequest; } export const updateOrganization = (queryClient: QueryClient) => { return { mutationFn: (variables: UpdateOrganizationVariables) => - API.updateOrganization(variables.orgId, variables.req), + API.updateOrganization(variables.organizationId, variables.req), onSuccess: async () => { await queryClient.invalidateQueries(organizationsKey); @@ -36,7 +36,8 @@ export const updateOrganization = (queryClient: QueryClient) => { export const deleteOrganization = (queryClient: QueryClient) => { return { - mutationFn: (orgId: string) => API.deleteOrganization(orgId), + mutationFn: (organizationId: string) => + API.deleteOrganization(organizationId), onSuccess: async () => { await queryClient.invalidateQueries(meKey); @@ -79,7 +80,7 @@ export const removeOrganizationMember = ( }; }; -export const organizationsKey = ["organizations", "me"] as const; +export const organizationsKey = ["organizations"] as const; export const organizations = () => { return { diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx index cea47881c5e74..4f18408971e9e 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx @@ -68,6 +68,7 @@ export const GroupPage: FC = () => { : { 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; @@ -193,6 +194,20 @@ export const GroupPage: FC = () => { group={groupData} key={member.id} canUpdate={canUpdateGroup} + onRemove={async () => { + try { + await removeMemberMutation.mutateAsync({ + groupId: groupData.id, + userId: member.id, + }); + await groupQuery.refetch(); + displaySuccess("Member removed successfully."); + } catch (error) { + displayError( + getErrorMessage(error, "Failed to remove member."), + ); + } + }} /> )) )} @@ -275,16 +290,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 ( @@ -315,19 +329,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/ManagementSettingsLayout.tsx b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx index 35563f3eb13c3..ee21518840c35 100644 --- a/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx +++ b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx @@ -61,7 +61,7 @@ export const ManagementSettingsLayout: FC = () => { currentOrganizationId: !inOrganizationSettings ? undefined : !organization - ? organizationsQuery.data[0]?.id + ? "00000000-0000-0000-0000-000000000000" : organizationsQuery.data.find( (org) => org.name === organization, )?.id, diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx index 959d206c2e163..19831dc8cfbf6 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx @@ -37,10 +37,12 @@ const OrganizationSettingsPage: FC = () => { organization={org} error={error} onSubmit={async (values) => { - await updateOrganizationMutation.mutateAsync({ - orgId: org.id, - req: values, - }); + const updatedOrganization = + await updateOrganizationMutation.mutateAsync({ + organizationId: org.id, + req: values, + }); + navigate(`/organizations/${updatedOrganization.name}`); displaySuccess("Organization settings updated."); }} onDeleteOrganization={() => { From c6360096986950e4e2580297ea31a060eb97b41e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 19 Jul 2024 19:44:59 +0000 Subject: [PATCH 11/12] feedback --- enterprise/coderd/groups.go | 9 +- site/src/components/PageHeader/PageHeader.tsx | 20 +++ .../CreateGroupPageView.stories.tsx | 11 +- .../GroupsPage/CreateGroupPageView.tsx | 4 - .../GroupsPage/GroupPage.tsx | 18 +-- .../GroupsPage/GroupSettingsPageView.tsx | 144 +++++++++--------- .../GroupsPage/GroupsPageView.tsx | 121 +++++++-------- 7 files changed, 166 insertions(+), 161 deletions(-) diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index dffbc5200c767..0b027f21ff2e0 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -2,6 +2,7 @@ package coderd import ( "database/sql" + "errors" "fmt" "net/http" @@ -170,9 +171,9 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { OrganizationID: group.OrganizationID, UserID: uuid.MustParse(id), })) - if xerrors.Is(err, sql.ErrNoRows) { + if errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("User %q must be a member of organization %q", id, group.ID), + Message: fmt.Sprintf("User must be a member of organization %q", group.Name), }) return } @@ -364,7 +365,7 @@ func (api *API) group(rw http.ResponseWriter, r *http.Request) { ) users, err := api.Database.GetGroupMembersByGroupID(ctx, group.ID) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.InternalServerError(rw, err) return } @@ -391,7 +392,7 @@ func (api *API) groups(rw http.ResponseWriter, r *http.Request) { ) groups, err := api.Database.GetGroupsByOrganizationID(ctx, org.ID) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.InternalServerError(rw, err) return } diff --git a/site/src/components/PageHeader/PageHeader.tsx b/site/src/components/PageHeader/PageHeader.tsx index 036a209d03c6c..852b8616e620e 100644 --- a/site/src/components/PageHeader/PageHeader.tsx +++ b/site/src/components/PageHeader/PageHeader.tsx @@ -107,3 +107,23 @@ 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/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx index 7ca2e49bd12ea..1737d5fef91c3 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { mockApiError } from "testHelpers/entities"; import { CreateGroupPageView } from "./CreateGroupPageView"; +import { userEvent, within } from "@storybook/test"; const meta: Meta = { title: "pages/OrganizationGroupsPage/CreateGroupPageView", @@ -18,6 +19,14 @@ 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/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx index d88e123ec1d05..8ccb79f017bf0 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx @@ -24,15 +24,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({ @@ -44,7 +41,6 @@ export const CreateGroupPageView: FC = ({ }, validationSchema, onSubmit, - initialTouched, }); const getFieldHelpers = getFormHelpers(form, error); const onCancel = () => navigate(-1); diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx index 4f18408971e9e..084c83ace5607 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx @@ -42,6 +42,7 @@ import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, + ResourcePageHeader, } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; import { @@ -103,7 +104,9 @@ export const GroupPage: FC = () => { {helmet} - @@ -127,18 +130,7 @@ export const GroupPage: FC = () => { ) } - > - - {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) && ( diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx index 97080f9ffc720..2b71ad58224fe 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx @@ -11,7 +11,7 @@ import { } from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; import { Loader } from "components/Loader/Loader"; -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { ResourcePageHeader } from "components/PageHeader/PageHeader"; import { getFormHelpers, nameValidator, @@ -59,72 +59,65 @@ const UpdateGroupForm: FC = ({ const getFieldHelpers = getFormHelpers(form, errors); return ( - <> - - {group.name} - - - - - - {!isEveryoneGroup(group) && ( - <> - - - form.setFieldValue("avatar_url", value) - } - /> - - )} - - - - - + + + + {!isEveryoneGroup(group) && ( + <> + + form.setFieldValue("avatar_url", value)} + /> + + )} + + + + + - - + })} + onChange={onChangeTrimmed(form)} + autoFocus + fullWidth + type="number" + label="Quota Allowance" + /> + + - - - + + ); }; @@ -150,13 +143,20 @@ const GroupSettingsPageView: FC = ({ } return ( - + <> + + + ); }; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx index a8ee18bc86e6a..fb93e7dbddb59 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx @@ -25,6 +25,7 @@ import { } from "components/TableLoader/TableLoader"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { docs } from "utils/docs"; +import { useClickableTableRow } from "hooks"; export type GroupsPageViewProps = { groups: Group[] | undefined; @@ -96,57 +97,7 @@ export const GroupsPageView: FC = ({ {groups?.map((group) => ( - { - navigate(group.name); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - navigate(group.name); - } - }} - css={styles.clickableTableRow} - > - - - } - title={group.display_name || group.name} - subtitle={`${group.members.length} members`} - /> - - - - {group.members.length === 0 && "-"} - - {group.members.map((member) => ( - - ))} - - - - -
- -
-
-
+ ))}
@@ -159,7 +110,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 ( @@ -180,21 +182,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, From bdf596549514d98118401a8675a73fdef6765a04 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 19 Jul 2024 19:56:18 +0000 Subject: [PATCH 12/12] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GroupsPage/CreateGroupPageView.stories.tsx | 2 +- .../GroupsPage/CreateGroupPageView.tsx | 2 +- .../pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx | 7 +------ .../ManagementSettingsPage/GroupsPage/GroupsPageView.tsx | 3 +-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx index 1737d5fef91c3..a6b1783a26c0c 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import { mockApiError } from "testHelpers/entities"; import { CreateGroupPageView } from "./CreateGroupPageView"; -import { userEvent, within } from "@storybook/test"; const meta: Meta = { title: "pages/OrganizationGroupsPage/CreateGroupPageView", diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx index 8ccb79f017bf0..f932c9c379988 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx @@ -1,5 +1,5 @@ import TextField from "@mui/material/TextField"; -import { type FormikTouched, useFormik } from "formik"; +import { useFormik } from "formik"; import type { FC } from "react"; import { useNavigate } from "react-router-dom"; import * as Yup from "yup"; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx index 084c83ace5607..2ab2e0c2fd8b4 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx @@ -38,12 +38,7 @@ import { MoreMenuTrigger, ThreeDotsButton, } from "components/MoreMenu/MoreMenu"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, - ResourcePageHeader, -} from "components/PageHeader/PageHeader"; +import { ResourcePageHeader } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; import { PaginationStatus, diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx index fb93e7dbddb59..4789ad13550b5 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx @@ -24,8 +24,8 @@ import { TableRowSkeleton, } from "components/TableLoader/TableLoader"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; -import { docs } from "utils/docs"; import { useClickableTableRow } from "hooks"; +import { docs } from "utils/docs"; export type GroupsPageViewProps = { groups: Group[] | undefined; @@ -40,7 +40,6 @@ export const GroupsPageView: FC = ({ }) => { const isLoading = Boolean(groups === undefined); const isEmpty = Boolean(groups && groups.length === 0); - const navigate = useNavigate(); return ( <>