Skip to content

Commit 876a7c7

Browse files
committed
Move groups to users page with tabs
1 parent d08bd75 commit 876a7c7

File tree

7 files changed

+205
-127
lines changed

7 files changed

+205
-127
lines changed

site/src/AppRouter.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FeatureNames } from "api/types"
33
import { FullScreenLoader } from "components/Loader/FullScreenLoader"
44
import { RequirePermission } from "components/RequirePermission/RequirePermission"
55
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"
6+
import { UsersLayout } from "components/UsersLayout/UsersLayout"
67
import IndexPage from "pages"
78
import AuditPage from "pages/AuditPage/AuditPage"
89
import GroupsPage from "pages/GroupsPage/GroupsPage"
@@ -142,7 +143,9 @@ export const AppRouter: FC = () => {
142143
index
143144
element={
144145
<AuthAndFrame>
145-
<UsersPage />
146+
<UsersLayout>
147+
<UsersPage />
148+
</UsersLayout>
146149
</AuthAndFrame>
147150
}
148151
/>
@@ -161,7 +164,9 @@ export const AppRouter: FC = () => {
161164
index
162165
element={
163166
<AuthAndFrame>
164-
<GroupsPage />
167+
<UsersLayout>
168+
<GroupsPage />
169+
</UsersLayout>
165170
</AuthAndFrame>
166171
}
167172
/>

site/src/components/NavbarView/NavbarView.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@ const NavItems: React.FC<
5555
{Language.users}
5656
</NavLink>
5757
</ListItem>
58-
<ListItem button className={styles.item}>
59-
<NavLink className={styles.link} to="/groups">
60-
Groups
61-
</NavLink>
62-
</ListItem>
6358
{canViewAuditLog && (
6459
<ListItem button className={styles.item}>
6560
<NavLink className={styles.link} to="/audit">
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Button from "@material-ui/core/Button"
2+
import Link from "@material-ui/core/Link"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import GroupAdd from "@material-ui/icons/GroupAddOutlined"
5+
import PersonAdd from "@material-ui/icons/PersonAddOutlined"
6+
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
7+
import { usePermissions } from "hooks/usePermissions"
8+
import { FC, PropsWithChildren } from "react"
9+
import { Link as RouterLink, NavLink, useNavigate } from "react-router-dom"
10+
import { combineClasses } from "util/combineClasses"
11+
import { Margins } from "../../components/Margins/Margins"
12+
import { Stack } from "../../components/Stack/Stack"
13+
14+
export const UsersLayout: FC<PropsWithChildren> = ({ children }) => {
15+
const styles = useStyles()
16+
const { createUser: canCreateUser } = usePermissions()
17+
const navigate = useNavigate()
18+
19+
return (
20+
<>
21+
<Margins>
22+
<PageHeader
23+
actions={
24+
canCreateUser ? (
25+
<>
26+
<Button
27+
onClick={() => {
28+
navigate("/users/create")
29+
}}
30+
startIcon={<PersonAdd />}
31+
>
32+
Create user
33+
</Button>
34+
<Link underline="none" component={RouterLink} to="/groups/create">
35+
<Button startIcon={<GroupAdd />}>Create group</Button>
36+
</Link>
37+
</>
38+
) : undefined
39+
}
40+
>
41+
<PageHeaderTitle>Users</PageHeaderTitle>
42+
</PageHeader>
43+
</Margins>
44+
45+
<div className={styles.tabs}>
46+
<Margins>
47+
<Stack direction="row" spacing={0.25}>
48+
<NavLink
49+
end
50+
to="/users"
51+
className={({ isActive }) =>
52+
combineClasses([styles.tabItem, isActive ? styles.tabItemActive : undefined])
53+
}
54+
>
55+
Users
56+
</NavLink>
57+
<NavLink
58+
to="/groups"
59+
className={({ isActive }) =>
60+
combineClasses([styles.tabItem, isActive ? styles.tabItemActive : undefined])
61+
}
62+
>
63+
Groups
64+
</NavLink>
65+
</Stack>
66+
</Margins>
67+
</div>
68+
69+
<Margins>{children}</Margins>
70+
</>
71+
)
72+
}
73+
74+
export const useStyles = makeStyles((theme) => {
75+
return {
76+
tabs: {
77+
borderBottom: `1px solid ${theme.palette.divider}`,
78+
marginBottom: theme.spacing(5),
79+
},
80+
81+
tabItem: {
82+
textDecoration: "none",
83+
color: theme.palette.text.secondary,
84+
fontSize: 14,
85+
display: "block",
86+
padding: theme.spacing(0, 2, 2),
87+
88+
"&:hover": {
89+
color: theme.palette.text.primary,
90+
},
91+
},
92+
93+
tabItemActive: {
94+
color: theme.palette.text.primary,
95+
position: "relative",
96+
97+
"&:before": {
98+
content: `""`,
99+
left: 0,
100+
bottom: 0,
101+
height: 2,
102+
width: "100%",
103+
background: theme.palette.secondary.dark,
104+
position: "absolute",
105+
},
106+
},
107+
}
108+
})

site/src/hooks/usePermissions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useActor } from "@xstate/react"
2+
import { useContext } from "react"
3+
import { AuthContext } from "xServices/auth/authXService"
4+
import { XServiceContext } from "xServices/StateContext"
5+
6+
export const usePermissions = (): NonNullable<AuthContext["permissions"]> => {
7+
const xServices = useContext(XServiceContext)
8+
const [authState, _] = useActor(xServices.authXService)
9+
const { permissions } = authState.context
10+
if (!permissions) {
11+
throw new Error("Permissions are not loaded yet.")
12+
}
13+
return permissions
14+
}

site/src/pages/GroupsPage/GroupsPage.tsx

Lines changed: 71 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import AvatarGroup from "@material-ui/lab/AvatarGroup"
1313
import { useMachine } from "@xstate/react"
1414
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
1515
import { EmptyState } from "components/EmptyState/EmptyState"
16-
import { Margins } from "components/Margins/Margins"
17-
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
1816
import { TableCellLink } from "components/TableCellLink/TableCellLink"
1917
import { TableLoader } from "components/TableLoader/TableLoader"
2018
import { UserAvatar } from "components/UserAvatar/UserAvatar"
@@ -25,14 +23,6 @@ import { Link as RouterLink, useNavigate } from "react-router-dom"
2523
import { pageTitle } from "util/page"
2624
import { groupsMachine } from "xServices/groups/groupsXService"
2725

28-
const CreateGroupButton: React.FC = () => {
29-
return (
30-
<Link underline="none" component={RouterLink} to="/groups/create">
31-
<Button startIcon={<AddCircleOutline />}>Create group</Button>
32-
</Link>
33-
)
34-
}
35-
3626
export const GroupsPage: React.FC = () => {
3727
const organizationId = useOrganizationId()
3828
const [state] = useMachine(groupsMachine, {
@@ -51,83 +41,82 @@ export const GroupsPage: React.FC = () => {
5141
<Helmet>
5242
<title>{pageTitle("Groups")}</title>
5343
</Helmet>
54-
<Margins>
55-
<PageHeader actions={<CreateGroupButton />}>
56-
<PageHeaderTitle>Groups</PageHeaderTitle>
57-
</PageHeader>
58-
<TableContainer>
59-
<Table>
60-
<TableHead>
61-
<TableRow>
62-
<TableCell width="50%">Name</TableCell>
63-
<TableCell width="49%">Users</TableCell>
64-
<TableCell width="1%"></TableCell>
65-
</TableRow>
66-
</TableHead>
67-
<TableBody>
68-
<ChooseOne>
69-
<Cond condition={isLoading}>
70-
<TableLoader />
71-
</Cond>
44+
<TableContainer>
45+
<Table>
46+
<TableHead>
47+
<TableRow>
48+
<TableCell width="50%">Name</TableCell>
49+
<TableCell width="49%">Users</TableCell>
50+
<TableCell width="1%"></TableCell>
51+
</TableRow>
52+
</TableHead>
53+
<TableBody>
54+
<ChooseOne>
55+
<Cond condition={isLoading}>
56+
<TableLoader />
57+
</Cond>
7258

73-
<Cond condition={isEmpty}>
74-
<TableRow>
75-
<TableCell colSpan={999}>
76-
<EmptyState
77-
message="No groups yet"
78-
description="Create your first group"
79-
cta={<CreateGroupButton />}
80-
/>
81-
</TableCell>
82-
</TableRow>
83-
</Cond>
59+
<Cond condition={isEmpty}>
60+
<TableRow>
61+
<TableCell colSpan={999}>
62+
<EmptyState
63+
message="No groups yet"
64+
description="Create your first group"
65+
cta={
66+
<Link underline="none" component={RouterLink} to="/groups/create">
67+
<Button startIcon={<AddCircleOutline />}>Create group</Button>
68+
</Link>
69+
}
70+
/>
71+
</TableCell>
72+
</TableRow>
73+
</Cond>
8474

85-
<Cond condition={!isEmpty}>
86-
{groups?.map((group) => {
87-
const groupPageLink = `/groups/${group.id}`
75+
<Cond condition={!isEmpty}>
76+
{groups?.map((group) => {
77+
const groupPageLink = `/groups/${group.id}`
8878

89-
return (
90-
<TableRow
91-
key={group.id}
92-
hover
93-
data-testid={`group-${group.id}`}
94-
tabIndex={0}
95-
onKeyDown={(event) => {
96-
if (event.key === "Enter") {
97-
navigate(groupPageLink)
98-
}
99-
}}
100-
className={styles.clickableTableRow}
101-
>
102-
<TableCellLink to={groupPageLink}>{group.name}</TableCellLink>
79+
return (
80+
<TableRow
81+
key={group.id}
82+
hover
83+
data-testid={`group-${group.id}`}
84+
tabIndex={0}
85+
onKeyDown={(event) => {
86+
if (event.key === "Enter") {
87+
navigate(groupPageLink)
88+
}
89+
}}
90+
className={styles.clickableTableRow}
91+
>
92+
<TableCellLink to={groupPageLink}>{group.name}</TableCellLink>
10393

104-
<TableCell>
105-
{group.members.length === 0 && "No members"}
106-
<AvatarGroup>
107-
{group.members.map((member) => (
108-
<UserAvatar
109-
key={member.username}
110-
username={member.username}
111-
avatarURL={member.avatar_url}
112-
/>
113-
))}
114-
</AvatarGroup>
115-
</TableCell>
94+
<TableCell>
95+
{group.members.length === 0 && "No members"}
96+
<AvatarGroup>
97+
{group.members.map((member) => (
98+
<UserAvatar
99+
key={member.username}
100+
username={member.username}
101+
avatarURL={member.avatar_url}
102+
/>
103+
))}
104+
</AvatarGroup>
105+
</TableCell>
116106

117-
<TableCellLink to={groupPageLink}>
118-
<div className={styles.arrowCell}>
119-
<KeyboardArrowRight className={styles.arrowRight} />
120-
</div>
121-
</TableCellLink>
122-
</TableRow>
123-
)
124-
})}
125-
</Cond>
126-
</ChooseOne>
127-
</TableBody>
128-
</Table>
129-
</TableContainer>
130-
</Margins>
107+
<TableCellLink to={groupPageLink}>
108+
<div className={styles.arrowCell}>
109+
<KeyboardArrowRight className={styles.arrowRight} />
110+
</div>
111+
</TableCellLink>
112+
</TableRow>
113+
)
114+
})}
115+
</Cond>
116+
</ChooseOne>
117+
</TableBody>
118+
</Table>
119+
</TableContainer>
131120
</>
132121
)
133122
}

site/src/pages/UsersPage/UsersPage.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useActor, useMachine } from "@xstate/react"
22
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
3+
import { usePermissions } from "hooks/usePermissions"
34
import { FC, ReactNode, useContext, useEffect } from "react"
45
import { Helmet } from "react-helmet-async"
56
import { useNavigate } from "react-router"
@@ -44,23 +45,15 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
4445
const userToBeDeleted = users?.find((u) => u.id === userIdToDelete)
4546
const userToBeActivated = users?.find((u) => u.id === userIdToActivate)
4647
const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword)
47-
48-
const [authState, _] = useActor(xServices.authXService)
49-
const { permissions } = authState.context
50-
const canEditUsers = permissions && permissions.updateUsers
51-
const canCreateUser = permissions && permissions.createUser
52-
48+
const { updateUsers: canEditUsers } = usePermissions()
5349
const [rolesState, rolesSend] = useActor(xServices.siteRolesXService)
5450
const { roles } = rolesState.context
5551

5652
// Is loading if
57-
// - permissions are loading or
5853
// - users are loading or
5954
// - the user can edit the users but the roles are loading
6055
const isLoading =
61-
authState.matches("gettingPermissions") ||
62-
usersState.matches("gettingUsers") ||
63-
(canEditUsers && rolesState.matches("gettingRoles"))
56+
usersState.matches("gettingUsers") || (canEditUsers && rolesState.matches("gettingRoles"))
6457

6558
// Fetch users on component mount
6659
useEffect(() => {
@@ -88,9 +81,6 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
8881
<UsersPageView
8982
roles={roles}
9083
users={users}
91-
openUserCreationDialog={() => {
92-
navigate("/users/create")
93-
}}
9484
onListWorkspaces={(user) => {
9585
navigate("/workspaces?filter=" + encodeURIComponent(`owner:${user.username}`))
9686
}}
@@ -117,7 +107,6 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
117107
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
118108
isLoading={isLoading}
119109
canEditUsers={canEditUsers}
120-
canCreateUser={canCreateUser}
121110
filter={usersState.context.filter}
122111
onFilter={(query) => {
123112
searchParams.set("filter", query)

0 commit comments

Comments
 (0)