Skip to content

Commit ad9ee3d

Browse files
committed
Add frontend
1 parent baf8f79 commit ad9ee3d

File tree

8 files changed

+138
-4
lines changed

8 files changed

+138
-4
lines changed

coderd/users_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,16 @@ func TestDeleteUser(t *testing.T) {
266266
_, another := coderdtest.CreateAnotherUserWithUser(t, api, user.OrganizationID)
267267
err := api.DeleteUser(context.Background(), another.ID)
268268
require.NoError(t, err)
269+
// Attempt to create a user with the same email and username, and delete them again.
270+
another, err = api.CreateUser(context.Background(), codersdk.CreateUserRequest{
271+
Email: another.Email,
272+
Username: another.Username,
273+
Password: "testing",
274+
OrganizationID: user.OrganizationID,
275+
})
276+
require.NoError(t, err)
277+
err = api.DeleteUser(context.Background(), another.ID)
278+
require.NoError(t, err)
269279
})
270280
t.Run("NoPermission", func(t *testing.T) {
271281
t.Parallel()

site/src/api/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
295295
return response.data
296296
}
297297

298+
export const deleteUser = async (userId: TypesGen.User["id"]): Promise<undefined> => {
299+
return await axios.delete(`/api/v2/users/${userId}`)
300+
}
301+
298302
// API definition:
299303
// https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53
300304
export const hasFirstUser = async (): Promise<boolean> => {

site/src/components/UsersTable/UsersTable.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ describe("AuditPage", () => {
1010
render(
1111
<UsersTable
1212
onSuspendUser={() => jest.fn()}
13+
onDeleteUser={() => jest.fn()}
14+
onListWorkspaces={() => jest.fn()}
1315
onActivateUser={() => jest.fn()}
1416
onResetUserPassword={() => jest.fn()}
1517
onUpdateUserRoles={() => jest.fn()}

site/src/components/UsersTable/UsersTable.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface UsersTableProps {
2424
isLoading?: boolean
2525
onSuspendUser: (user: TypesGen.User) => void
2626
onActivateUser: (user: TypesGen.User) => void
27+
onDeleteUser: (user: TypesGen.User) => void
28+
onListWorkspaces: (user: TypesGen.User) => void
2729
onResetUserPassword: (user: TypesGen.User) => void
2830
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
2931
}
@@ -32,6 +34,8 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
3234
users,
3335
roles,
3436
onSuspendUser,
37+
onDeleteUser,
38+
onListWorkspaces,
3539
onActivateUser,
3640
onResetUserPassword,
3741
onUpdateUserRoles,
@@ -64,6 +68,8 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
6468
canEditUsers={canEditUsers}
6569
isUpdatingUserRoles={isUpdatingUserRoles}
6670
onActivateUser={onActivateUser}
71+
onDeleteUser={onDeleteUser}
72+
onListWorkspaces={onListWorkspaces}
6773
onResetUserPassword={onResetUserPassword}
6874
onSuspendUser={onSuspendUser}
6975
onUpdateUserRoles={onUpdateUserRoles}

site/src/components/UsersTable/UsersTableBody.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
1414
export const Language = {
1515
emptyMessage: "No users found",
1616
suspendMenuItem: "Suspend",
17+
deleteMenuItem: "Delete",
18+
listWorkspacesMenuItem: "View Workspaces",
1719
activateMenuItem: "Activate",
1820
resetPasswordMenuItem: "Reset password",
1921
}
@@ -25,6 +27,8 @@ interface UsersTableBodyProps {
2527
canEditUsers?: boolean
2628
isLoading?: boolean
2729
onSuspendUser: (user: TypesGen.User) => void
30+
onDeleteUser: (user: TypesGen.User) => void
31+
onListWorkspaces: (user: TypesGen.User) => void
2832
onActivateUser: (user: TypesGen.User) => void
2933
onResetUserPassword: (user: TypesGen.User) => void
3034
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
@@ -34,6 +38,8 @@ export const UsersTableBody: FC<React.PropsWithChildren<UsersTableBodyProps>> =
3438
users,
3539
roles,
3640
onSuspendUser,
41+
onDeleteUser,
42+
onListWorkspaces,
3743
onActivateUser,
3844
onResetUserPassword,
3945
onUpdateUserRoles,
@@ -130,10 +136,20 @@ export const UsersTableBody: FC<React.PropsWithChildren<UsersTableBodyProps>> =
130136
onClick: onActivateUser,
131137
},
132138
]
133-
).concat({
134-
label: Language.resetPasswordMenuItem,
135-
onClick: onResetUserPassword,
136-
})
139+
).concat(
140+
{
141+
label: Language.deleteMenuItem,
142+
onClick: onDeleteUser,
143+
},
144+
{
145+
label: Language.listWorkspacesMenuItem,
146+
onClick: onListWorkspaces,
147+
},
148+
{
149+
label: Language.resetPasswordMenuItem,
150+
onClick: onResetUserPassword,
151+
},
152+
)
137153
}
138154
/>
139155
</TableCell>

site/src/pages/UsersPage/UsersPage.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { XServiceContext } from "../../xServices/StateContext"
1111
import { UsersPageView } from "./UsersPageView"
1212

1313
export const Language = {
14+
deleteDialogTitle: "Delete user",
15+
deleteDialogAction: "Delete",
16+
deleteDialogMessagePrefix: "Do you want to delete the user",
1417
suspendDialogTitle: "Suspend user",
1518
suspendDialogAction: "Suspend",
1619
suspendDialogMessagePrefix: "Do you want to suspend the user",
@@ -25,6 +28,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
2528
const {
2629
users,
2730
getUsersError,
31+
userIdToDelete,
2832
userIdToSuspend,
2933
userIdToActivate,
3034
userIdToResetPassword,
@@ -33,6 +37,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
3337
const navigate = useNavigate()
3438
const [searchParams, setSearchParams] = useSearchParams()
3539
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
40+
const userToBeDeleted = users?.find((u) => u.id === userIdToDelete)
3641
const userToBeActivated = users?.find((u) => u.id === userIdToActivate)
3742
const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword)
3843

@@ -84,6 +89,12 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
8489
openUserCreationDialog={() => {
8590
navigate("/users/create")
8691
}}
92+
onListWorkspaces={(user) => {
93+
navigate("/workspaces?filter=" + encodeURIComponent(`owner:${user.username}`))
94+
}}
95+
onDeleteUser={(user) => {
96+
usersSend({ type: "DELETE_USER", userId: user.id })
97+
}}
8798
onSuspendUser={(user) => {
8899
usersSend({ type: "SUSPEND_USER", userId: user.id })
89100
}}
@@ -112,6 +123,26 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
112123
}}
113124
/>
114125

126+
<ConfirmDialog
127+
type="delete"
128+
hideCancel={false}
129+
open={usersState.matches("confirmUserDeletion")}
130+
confirmLoading={usersState.matches("deletingUser")}
131+
title={Language.deleteDialogTitle}
132+
confirmText={Language.deleteDialogAction}
133+
onConfirm={() => {
134+
usersSend("CONFIRM_USER_DELETE")
135+
}}
136+
onClose={() => {
137+
usersSend("CANCEL_USER_DELETE")
138+
}}
139+
description={
140+
<>
141+
{Language.deleteDialogMessagePrefix} <strong>{userToBeDeleted?.username}</strong>?
142+
</>
143+
}
144+
/>
145+
115146
<ConfirmDialog
116147
type="delete"
117148
hideCancel={false}

site/src/pages/UsersPage/UsersPageView.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface UsersPageViewProps {
2525
isLoading?: boolean
2626
openUserCreationDialog: () => void
2727
onSuspendUser: (user: TypesGen.User) => void
28+
onDeleteUser: (user: TypesGen.User) => void
29+
onListWorkspaces: (user: TypesGen.User) => void
2830
onActivateUser: (user: TypesGen.User) => void
2931
onResetUserPassword: (user: TypesGen.User) => void
3032
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
@@ -36,6 +38,8 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
3638
roles,
3739
openUserCreationDialog,
3840
onSuspendUser,
41+
onDeleteUser,
42+
onListWorkspaces,
3943
onActivateUser,
4044
onResetUserPassword,
4145
onUpdateUserRoles,
@@ -79,6 +83,8 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
7983
users={users}
8084
roles={roles}
8185
onSuspendUser={onSuspendUser}
86+
onDeleteUser={onDeleteUser}
87+
onListWorkspaces={onListWorkspaces}
8288
onActivateUser={onActivateUser}
8389
onResetUserPassword={onResetUserPassword}
8490
onUpdateUserRoles={onUpdateUserRoles}

site/src/xServices/users/usersXService.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const Language = {
1919
createUserError: "Error on creating the user.",
2020
suspendUserSuccess: "Successfully suspended the user.",
2121
suspendUserError: "Error suspending user.",
22+
deleteUserSuccess: "Successfully deleted the user.",
23+
deleteUserError: "Error deleting user.",
2224
activateUserSuccess: "Successfully activated the user.",
2325
activateUserError: "Error activating user.",
2426
resetUserPasswordSuccess: "Successfully updated the user password.",
@@ -37,6 +39,9 @@ export interface UsersContext {
3739
// Suspend user
3840
userIdToSuspend?: TypesGen.User["id"]
3941
suspendUserError?: Error | unknown
42+
// Delete user
43+
userIdToDelete?: TypesGen.User["id"]
44+
deleteUserError?: Error | unknown
4045
// Activate user
4146
userIdToActivate?: TypesGen.User["id"]
4247
activateUserError?: Error | unknown
@@ -57,6 +62,10 @@ export type UsersEvent =
5762
| { type: "SUSPEND_USER"; userId: TypesGen.User["id"] }
5863
| { type: "CONFIRM_USER_SUSPENSION" }
5964
| { type: "CANCEL_USER_SUSPENSION" }
65+
// Delete events
66+
| { type: "DELETE_USER"; userId: TypesGen.User["id"] }
67+
| { type: "CONFIRM_USER_DELETE" }
68+
| { type: "CANCEL_USER_DELETE" }
6069
// Activate events
6170
| { type: "ACTIVATE_USER"; userId: TypesGen.User["id"] }
6271
| { type: "CONFIRM_USER_ACTIVATION" }
@@ -84,6 +93,9 @@ export const usersMachine = createMachine(
8493
suspendUser: {
8594
data: TypesGen.User
8695
}
96+
deleteUser: {
97+
data: undefined
98+
}
8799
activateUser: {
88100
data: TypesGen.User
89101
}
@@ -110,6 +122,10 @@ export const usersMachine = createMachine(
110122
target: "confirmUserSuspension",
111123
actions: ["assignUserIdToSuspend"],
112124
},
125+
DELETE_USER: {
126+
target: "confirmUserDeletion",
127+
actions: ["assignUserIdToDelete"],
128+
},
113129
ACTIVATE_USER: {
114130
target: "confirmUserActivation",
115131
actions: ["assignUserIdToActivate"],
@@ -173,6 +189,12 @@ export const usersMachine = createMachine(
173189
CANCEL_USER_SUSPENSION: "idle",
174190
},
175191
},
192+
confirmUserDeletion: {
193+
on: {
194+
CONFIRM_USER_DELETE: "deletingUser",
195+
CANCEL_USER_DELETE: "idle",
196+
},
197+
},
176198
confirmUserActivation: {
177199
on: {
178200
CONFIRM_USER_ACTIVATION: "activatingUser",
@@ -195,6 +217,21 @@ export const usersMachine = createMachine(
195217
},
196218
},
197219
},
220+
deletingUser: {
221+
entry: "clearDeleteUserError",
222+
invoke: {
223+
src: "deleteUser",
224+
id: "deleteUser",
225+
onDone: {
226+
target: "gettingUsers",
227+
actions: ["displayDeleteSuccess"],
228+
},
229+
onError: {
230+
target: "idle",
231+
actions: ["assignDeleteUserError", "displayDeleteErrorMessage"],
232+
},
233+
},
234+
},
198235
activatingUser: {
199236
entry: "clearActivateUserError",
200237
invoke: {
@@ -271,6 +308,12 @@ export const usersMachine = createMachine(
271308

272309
return API.suspendUser(context.userIdToSuspend)
273310
},
311+
deleteUser: (context) => {
312+
if (!context.userIdToDelete) {
313+
throw new Error("userIdToDelete is undefined")
314+
}
315+
return API.deleteUser(context.userIdToDelete)
316+
},
274317
activateUser: (context) => {
275318
if (!context.userIdToActivate) {
276319
throw new Error("userIdToActivate is undefined")
@@ -316,6 +359,9 @@ export const usersMachine = createMachine(
316359
assignUserIdToSuspend: assign({
317360
userIdToSuspend: (_, event) => event.userId,
318361
}),
362+
assignUserIdToDelete: assign({
363+
userIdToDelete: (_, event) => event.userId,
364+
}),
319365
assignUserIdToActivate: assign({
320366
userIdToActivate: (_, event) => event.userId,
321367
}),
@@ -340,6 +386,9 @@ export const usersMachine = createMachine(
340386
assignSuspendUserError: assign({
341387
suspendUserError: (_, event) => event.data,
342388
}),
389+
assignDeleteUserError: assign({
390+
deleteUserError: (_, event) => event.data,
391+
}),
343392
assignActivateUserError: assign({
344393
activateUserError: (_, event) => event.data,
345394
}),
@@ -361,6 +410,9 @@ export const usersMachine = createMachine(
361410
clearSuspendUserError: assign({
362411
suspendUserError: (_) => undefined,
363412
}),
413+
clearDeleteUserError: assign({
414+
deleteUserError: (_) => undefined,
415+
}),
364416
clearActivateUserError: assign({
365417
activateUserError: (_) => undefined,
366418
}),
@@ -384,6 +436,13 @@ export const usersMachine = createMachine(
384436
const message = getErrorMessage(context.suspendUserError, Language.suspendUserError)
385437
displayError(message)
386438
},
439+
displayDeleteSuccess: () => {
440+
displaySuccess(Language.deleteUserSuccess)
441+
},
442+
displayDeleteErrorMessage: (context) => {
443+
const message = getErrorMessage(context.deleteUserError, Language.deleteUserError)
444+
displayError(message)
445+
},
387446
displayActivateSuccess: () => {
388447
displaySuccess(Language.activateUserSuccess)
389448
},

0 commit comments

Comments
 (0)