diff --git a/apps/studio/components/interfaces/Auth/Users/DeleteUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/DeleteUserModal.tsx
new file mode 100644
index 00000000000..ff513fb397f
--- /dev/null
+++ b/apps/studio/components/interfaces/Auth/Users/DeleteUserModal.tsx
@@ -0,0 +1,61 @@
+import { useParams } from 'common'
+import { toast } from 'sonner'
+
+import { useUserDeleteMutation } from 'data/auth/user-delete-mutation'
+import { User } from 'data/auth/users-infinite-query'
+import { timeout } from 'lib/helpers'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+
+interface DeleteUserModalProps {
+ visible: boolean
+ selectedUser?: User
+ onClose: () => void
+ onDeleteSuccess?: () => void
+}
+
+export const DeleteUserModal = ({
+ visible,
+ selectedUser,
+ onClose,
+ onDeleteSuccess,
+}: DeleteUserModalProps) => {
+ const { ref: projectRef } = useParams()
+
+ const { mutate: deleteUser, isLoading: isDeleting } = useUserDeleteMutation({
+ onSuccess: () => {
+ toast.success(`Successfully deleted ${selectedUser?.email}`)
+ onDeleteSuccess?.()
+ },
+ })
+
+ const handleDeleteUser = async () => {
+ await timeout(200)
+ if (!projectRef) return console.error('Project ref is required')
+ if (selectedUser?.id === undefined) {
+ return toast.error(`Failed to delete user: User ID not found`)
+ }
+ deleteUser({ projectRef, userId: selectedUser.id })
+ }
+
+ return (
+ onClose()}
+ onConfirm={() => handleDeleteUser()}
+ alert={{
+ title: 'Deleting a user is irreversible',
+ description:
+ 'This will remove the selected the user from the project and all associated data.',
+ }}
+ >
+
+ This is permanent! Are you sure you want to delete the user{' '}
+ {selectedUser?.email ?? selectedUser?.phone ?? 'this user'}?
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx
index 958d95fe0c3..d8807b64234 100644
--- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx
+++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx
@@ -10,7 +10,6 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import CopyButton from 'components/ui/CopyButton'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useUserDeleteMFAFactorsMutation } from 'data/auth/user-delete-mfa-factors-mutation'
-import { useUserDeleteMutation } from 'data/auth/user-delete-mutation'
import { useUserResetPasswordMutation } from 'data/auth/user-reset-password-mutation'
import { useUserSendMagicLinkMutation } from 'data/auth/user-send-magic-link-mutation'
import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation'
@@ -24,6 +23,7 @@ import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation'
import { BanUserModal } from './BanUserModal'
+import { DeleteUserModal } from './DeleteUserModal'
import { UserHeader } from './UserHeader'
import { PANEL_PADDING } from './UserPanel'
import { providerIconMap } from './Users.utils'
@@ -109,13 +109,6 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
toast.error(`Failed to send OTP: ${err.message}`)
},
})
- const { mutate: deleteUser } = useUserDeleteMutation({
- onSuccess: () => {
- toast.success(`Successfully deleted ${user?.email}`)
- setIsDeleteModalOpen(false)
- onDeleteSuccess()
- },
- })
const { mutate: deleteUserMFAFactors } = useUserDeleteMFAFactorsMutation({
onSuccess: () => {
toast.success("Successfully deleted the user's factors")
@@ -129,15 +122,6 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
},
})
- const handleDelete = async () => {
- await timeout(200)
- if (!projectRef) return console.error('Project ref is required')
- if (user.id === undefined) {
- return toast.error(`Failed to delete user: User ID not found`)
- }
- deleteUser({ projectRef, userId: user.id })
- }
-
const handleDeleteFactors = async () => {
await timeout(200)
if (!projectRef) return console.error('Project ref is required')
@@ -179,7 +163,7 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
)}
-
- setIsDeleteModalOpen(false)}
- onConfirm={() => handleDelete()}
- alert={{
- title: 'Deleting a user is irreversible',
- description: 'This will remove the user from the project and all associated data.',
+ selectedUser={user}
+ onClose={() => setIsDeleteModalOpen(false)}
+ onDeleteSuccess={() => {
+ setIsDeleteModalOpen(false)
+ onDeleteSuccess()
}}
- >
-
- This is permanent! Are you sure you want to delete the user{' '}
- {user.email ?? user.phone ?? 'this user'}?
-
-
+ />
-
{property}
+
{property}
{typeof value === 'boolean' ? (
{value ? (
@@ -481,7 +457,7 @@ export const RowData = ({ property, value }: { property: string; value?: string
) : (
-
{!value ? '-' : value}
+
{!value ? '-' : value}
{!!value && (
void
+ onSelectDeleteUser: (user: User) => void
}) => {
const columnOrder = config.map((c) => c.id) ?? USERS_TABLE_COLUMNS.map((c) => c.id)
@@ -319,42 +330,70 @@ export const formatUserColumns = ({
}
return (
-
- {/* [Joshen] Not convinced this is the ideal way to display the icons, but for now */}
- {col.id === 'providers' &&
- row.provider_icons.map((icon: string, idx: number) => {
- const provider = row.providers[idx]
- return (
-
-

-
- )
- })}
- {col.id === 'last_sign_in_at' && !isConfirmed ? (
-
Waiting for verification
- ) : (
-
- {formattedValue === null ? '-' : formattedValue}
-
- )}
-
+
+
+
+ {/* [Joshen] Not convinced this is the ideal way to display the icons, but for now */}
+ {col.id === 'providers' &&
+ row.provider_icons.map((icon: string, idx: number) => {
+ const provider = row.providers[idx]
+ return (
+
+

+
+ )
+ })}
+ {col.id === 'last_sign_in_at' && !isConfirmed ? (
+
Waiting for verification
+ ) : (
+
+ {formattedValue === null ? '-' : formattedValue}
+
+ )}
+
+
+ e.stopPropagation()}>
+ e.stopPropagation()}
+ onSelect={() => {
+ const value = col.id === 'providers' ? row.providers.join(', ') : formattedValue
+ copyToClipboard(value)
+ }}
+ >
+
+ Copy {col.id === 'id' ? col.name : col.name.toLowerCase()}
+
+
+ e.stopPropagation()}
+ onSelect={() => {
+ if (user) onSelectDeleteUser(user)
+ }}
+ >
+
+ Delete user
+
+
+
)
},
}
diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx
index f8dfb7a3afd..dca0bf82095 100644
--- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx
+++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx
@@ -16,7 +16,7 @@ import { FormHeader } from 'components/ui/Forms/FormHeader'
import { authKeys } from 'data/auth/keys'
import { useUserDeleteMutation } from 'data/auth/user-delete-mutation'
import { useUsersCountQuery } from 'data/auth/users-count-query'
-import { useUsersInfiniteQuery } from 'data/auth/users-infinite-query'
+import { User, useUsersInfiniteQuery } from 'data/auth/users-infinite-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import {
@@ -44,6 +44,7 @@ import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import AddUserDropdown from './AddUserDropdown'
+import { DeleteUserModal } from './DeleteUserModal'
import { UserPanel } from './UserPanel'
import { MAX_BULK_DELETE, PROVIDER_FILTER_OPTIONS } from './Users.constants'
import { formatUserColumns, formatUsersData, isAtBottom } from './Users.utils'
@@ -83,13 +84,16 @@ export const UsersV2 = () => {
const [search, setSearch] = useState('')
const [filter, setFilter] = useState('all')
const [filterKeywords, setFilterKeywords] = useState('')
- const [selectedUsers, setSelectedUsers] = useState>(new Set([]))
const [selectedColumns, setSelectedColumns] = useState([])
const [selectedProviders, setSelectedProviders] = useState([])
- const [selectedUser, setSelectedUser] = useState()
const [sortByValue, setSortByValue] = useState('created_at:desc')
+
+ const [selectedUser, setSelectedUser] = useState()
+ const [selectedUsers, setSelectedUsers] = useState>(new Set([]))
+ const [selectedUserToDelete, setSelectedUserToDelete] = useState()
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [isDeletingUsers, setIsDeletingUsers] = useState(false)
+
const [
columnConfiguration,
setColumnConfiguration,
@@ -143,7 +147,7 @@ export const UsersV2 = () => {
const totalUsers = countData ?? 0
const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages])
// [Joshen] Only relevant for when selecting one user only
- const selectedUserToDelete = users.find((u) => u.id === [...selectedUsers][0])
+ const selectedUserFromCheckbox = users.find((u) => u.id === [...selectedUsers][0])
const handleScroll = (event: UIEvent) => {
const isScrollingHorizontally = xScroll.current !== event.currentTarget.scrollLeft
@@ -199,6 +203,11 @@ export const UsersV2 = () => {
500
)
+ const onSelectDeleteUser = (user: User) => {
+ setSelectedUsers(new Set([user.id]))
+ setShowDeleteModal(true)
+ }
+
const handleDeleteUsers = async () => {
if (!projectRef) return console.error('Project ref is required')
const userIds = [...selectedUsers]
@@ -237,6 +246,7 @@ export const UsersV2 = () => {
users: users ?? [],
visibleColumns: selectedColumns,
setSortByValue,
+ onSelectDeleteUser,
})
setColumns(columns)
if (columns.length < USERS_TABLE_COLUMNS.length) {
@@ -377,6 +387,7 @@ export const UsersV2 = () => {
users: users ?? [],
visibleColumns: value,
setSortByValue,
+ onSelectDeleteUser,
})
setSelectedColumns(value)
@@ -580,12 +591,20 @@ export const UsersV2 = () => {
{selectedUsers.size === 1 ? (
{' '}
- {selectedUserToDelete?.email ?? selectedUserToDelete?.phone ?? 'this user'}
+ {selectedUserFromCheckbox?.email ?? selectedUserFromCheckbox?.phone ?? 'this user'}
) : null}
?
+
+ {/* [Joshen] For deleting via context menu, the dialog above is dependent on the selectedUsers state */}
+ setSelectedUserToDelete(undefined)}
+ onDeleteSuccess={() => setSelectedUserToDelete(undefined)}
+ />
>
)
}