mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
* update deps + image codemod (studio) * update next links (studio) * update deps * update links (ui) * remove next-transpile-modules * move next-themes dependency * chore: update ConfirmDialog * chore: remove old ConfirmModal js file. migrated to TS * dependency wrangling * remove empty page * update next links (www) * First run bump react-data-grid-v7 beta 4 * fix package-lock.json * more deps wrangling * update recharts * update sentry options * fix some broken things in www * studio fixes * fix graphiql * fix studio build * fix menu hydration * small build error * update turbo * fix www typescript errors * docs image codemod * links codemod docs * fix docs typescript errors * move useConsent to ui to prevent circular deps * Fix links * Fix homepage * Fix links * move studio/ to apps/ * Revert "move studio/ to apps/" This reverts commit1b0a985fcb. * disable outputFileTracingRoot * remove outputFileTracingRoot * fix homepage product cards * fix PrivacySettings links * Fix links * Fix the build for www. * Minor fixes for JWTGenerator. * Fix the docs and ui tests. * Revert codehike back to 0.8.3 * remove ConfirmAlert() * reenable babel because mobx hates me * fix blog image and comparison page avatar * Fix svg errors * update image synthax * Fix code hike * Move the button in a div so that it doesn't inherit its parent height and make the button look weird. * When components are defined in a component, they get recreated on each render. This makes them unstable in certain cases and causes infinite rerenders. * Replace the next/head usage with next/script. * Chore/upgrade next 13 fix table editor (#18431) * fix table editor styling and fix row deletion logic * Fix deleting selected rows from header, and fix checkboxes not clearing up * Fix deleting all rows when filter applied, and fix deleting all rows * Fix grid size styling issue * Fix TS error * Hydration errors * studio org pages fixes * fix more studio links * audit logs fixes * dropdown icon styling fixes * fix some images in www * upgrade to next 14 * try new sentry wrapper for api * see if this is even invoked * Revert "see if this is even invoked" This reverts commit86c3973ffa. * Revert "try new sentry wrapper for api" This reverts commitf67623ebad. * Revert "upgrade to next 14" This reverts commita24dd6131e. * chore: allow node version 19/20 * Try to fix the LogTable so that it renders with the newer "react-data-grid" version. * Fix type errors in the log renderer code. * Fix the replication screen. * Add the CSS for the GraphiQL. * Fix SQL editor results rendering * Lint * Fix SQL editor results height issue * Fix auth RLS not invalidating RQ when toggling RLS * Fix database tables new/edit column regressed * Fix migrations page empty state if migrations schema not yet created * Fix API side panel docs temp remove postgrest text for column description PK and FK * Fix + improve timeout handling in SQL editor --------- Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com> Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com> Co-authored-by: Terry Sutton <saltcod@gmail.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com> Co-authored-by: Kevin Grüneberg <k.grueneberg1994@gmail.com>
390 lines
17 KiB
TypeScript
390 lines
17 KiB
TypeScript
import * as Tooltip from '@radix-ui/react-tooltip'
|
|
import { observer } from 'mobx-react-lite'
|
|
import Image from 'next/legacy/image'
|
|
import { Fragment, useState } from 'react'
|
|
|
|
import { useParams } from 'common/hooks'
|
|
import Table from 'components/to-be-cleaned/Table'
|
|
import AlertError from 'components/ui/AlertError'
|
|
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
|
import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query'
|
|
import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
|
|
import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query'
|
|
import { usePermissionsQuery } from 'data/permissions/permissions-query'
|
|
import { useSelectedOrganization, useStore } from 'hooks'
|
|
import { useProfile } from 'lib/profile'
|
|
import { Member } from 'types'
|
|
import { Badge, Button, IconAlertCircle, IconLoader, IconUser, Listbox, Loading, Modal } from 'ui'
|
|
import { getUserDisplayName, isInviteExpired } from '../Organization.utils'
|
|
import MemberActions from './MemberActions'
|
|
import RolesHelperModal from './RolesHelperModal/RolesHelperModal'
|
|
import { useGetRolesManagementPermissions } from './TeamSettings.utils'
|
|
|
|
interface SelectedMember extends Member {
|
|
oldRoleId: number
|
|
newRoleId: number
|
|
}
|
|
|
|
export interface MembersViewProps {
|
|
searchString: string
|
|
}
|
|
|
|
const MembersView = ({ searchString }: MembersViewProps) => {
|
|
const { ui } = useStore()
|
|
const { slug } = useParams()
|
|
const selectedOrganization = useSelectedOrganization()
|
|
|
|
const { profile } = useProfile()
|
|
const { data: permissions } = usePermissionsQuery()
|
|
const {
|
|
data: detailData,
|
|
error: detailError,
|
|
isLoading: isLoadingOrgDetails,
|
|
isError: isErrorOrgDetails,
|
|
isSuccess: isSuccessOrgDetails,
|
|
} = useOrganizationDetailQuery({ slug })
|
|
const {
|
|
data: rolesData,
|
|
error: rolesError,
|
|
isLoading: isLoadingRoles,
|
|
isError: isErrorRoles,
|
|
} = useOrganizationRolesQuery({ slug })
|
|
const { mutate: updateOrganizationMember, isLoading } = useOrganizationMemberUpdateMutation({
|
|
onSuccess() {
|
|
setUserRoleChangeModalVisible(false)
|
|
ui.setNotification({
|
|
category: 'success',
|
|
message: `Successfully updated role for ${getUserDisplayName(selectedMember)}`,
|
|
})
|
|
},
|
|
onError(error) {
|
|
ui.setNotification({
|
|
category: 'error',
|
|
message: `Failed to update role for ${getUserDisplayName(selectedMember)}: ${
|
|
error.message
|
|
}`,
|
|
})
|
|
},
|
|
})
|
|
|
|
const roles = rolesData?.roles ?? []
|
|
const members = detailData?.members ?? []
|
|
const { rolesAddable, rolesRemovable } = useGetRolesManagementPermissions(
|
|
selectedOrganization?.id,
|
|
roles,
|
|
permissions ?? []
|
|
)
|
|
|
|
const [selectedMember, setSelectedMember] = useState<SelectedMember>()
|
|
const [userRoleChangeModalVisible, setUserRoleChangeModalVisible] = useState(false)
|
|
|
|
const filteredMembers = (
|
|
!searchString
|
|
? members
|
|
: members.filter((x: any) => {
|
|
if (x.invited_at) {
|
|
return x.primary_email.includes(searchString)
|
|
}
|
|
if (x.id || x.gotrue_id) {
|
|
return x.username.includes(searchString) || x.primary_email.includes(searchString)
|
|
}
|
|
})
|
|
)
|
|
.slice()
|
|
.sort((a: any, b: any) => a.username.localeCompare(b.username))
|
|
|
|
const getRoleNameById = (id: number | undefined) => {
|
|
if (!roles) return id
|
|
return roles.find((x: any) => x.id === id)?.name
|
|
}
|
|
|
|
const handleRoleChange = async () => {
|
|
if (!selectedMember) return
|
|
|
|
const { gotrue_id, newRoleId } = selectedMember
|
|
if (!slug) return console.error('slug is required')
|
|
if (!gotrue_id) return console.error('gotrue_id is required')
|
|
updateOrganizationMember({ slug, gotrueId: gotrue_id, roleId: newRoleId })
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{isLoadingOrgDetails && <GenericSkeletonLoader />}
|
|
|
|
{isErrorOrgDetails && (
|
|
<AlertError error={detailError} subject="Failed to retrieve organization members" />
|
|
)}
|
|
|
|
{isErrorRoles && (
|
|
<AlertError error={rolesError} subject="Failed to retrieve organization roles" />
|
|
)}
|
|
|
|
{isSuccessOrgDetails && (
|
|
<div className="rounded w-full">
|
|
<Loading active={!filteredMembers}>
|
|
<Table
|
|
head={[
|
|
<Table.th key="header-user">User</Table.th>,
|
|
<Table.th key="header-status"></Table.th>,
|
|
<Table.th key="header-role" className="flex items-center space-x-2">
|
|
<span>Role</span>
|
|
<RolesHelperModal />
|
|
</Table.th>,
|
|
<Table.th key="header-action"></Table.th>,
|
|
]}
|
|
body={[
|
|
...filteredMembers.map((x: Member, i: number) => {
|
|
const [memberRoleId] = x.role_ids ?? []
|
|
const role = (roles || []).find((role) => role.id === memberRoleId)
|
|
const memberIsUser = x.primary_email == profile?.primary_email
|
|
const memberIsPendingInvite = !!x.invited_id
|
|
const canRemoveRole = rolesRemovable.includes(memberRoleId)
|
|
const disableRoleEdit = !canRemoveRole || memberIsUser || memberIsPendingInvite
|
|
const isEmailUser = x.username === x.primary_email
|
|
|
|
const validateSelectedRoleToChange = (roleId: any) => {
|
|
if (!role || role.id === roleId) return
|
|
|
|
const selectedRole = (roles || []).find((role) => role.id === roleId)
|
|
const canAddRole = rolesAddable.includes(selectedRole?.id ?? -1)
|
|
|
|
if (!canAddRole) {
|
|
return ui.setNotification({
|
|
category: 'error',
|
|
duration: 4000,
|
|
message: `You do not have permission to update this team member to ${
|
|
selectedRole!.name
|
|
}`,
|
|
})
|
|
}
|
|
|
|
setUserRoleChangeModalVisible(true)
|
|
setSelectedMember({ ...x, oldRoleId: role.id, newRoleId: roleId })
|
|
}
|
|
|
|
return (
|
|
<Fragment key={`member-row-${i}`}>
|
|
<Table.tr>
|
|
<Table.td>
|
|
<div className="flex items-center space-x-4">
|
|
<div>
|
|
{x.invited_id ? (
|
|
<span className="flex p-2 border-2 rounded-full border-scale-700">
|
|
<IconUser size={20} strokeWidth={2} />
|
|
</span>
|
|
) : isEmailUser ? (
|
|
<div className="w-[40px] h-[40px] bg-scale-300 border border-scale-400 rounded-full text-foreground-lighter flex items-center justify-center">
|
|
<IconUser strokeWidth={1.5} />
|
|
</div>
|
|
) : (
|
|
<Image
|
|
alt={x.username}
|
|
src={`https://github.com/${x.username}.png?size=80`}
|
|
width="40"
|
|
height="40"
|
|
className="border rounded-full"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<p className="text-foreground">{getUserDisplayName(x)}</p>
|
|
{x.invited_id === undefined && (
|
|
<p className="text-foreground-light">{x.primary_email}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Table.td>
|
|
|
|
<Table.td>
|
|
{x.invited_id && x.invited_at && (
|
|
<Badge color={isInviteExpired(x.invited_at) ? 'red' : 'yellow'}>
|
|
{isInviteExpired(x.invited_at) ? 'Expired' : 'Invited'}
|
|
</Badge>
|
|
)}
|
|
</Table.td>
|
|
|
|
<Table.td>
|
|
{isLoadingRoles ? (
|
|
<div className="w-[140px]">
|
|
<IconLoader className="animate-spin" size={16} strokeWidth={1.5} />
|
|
</div>
|
|
) : role !== undefined ? (
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger className="w-[140px]" asChild>
|
|
<div>
|
|
<Listbox
|
|
className={disableRoleEdit ? 'pointer-events-none' : ''}
|
|
disabled={disableRoleEdit}
|
|
value={role.id}
|
|
onChange={validateSelectedRoleToChange}
|
|
>
|
|
{roles.map((r: any) => (
|
|
<Listbox.Option
|
|
key={r.id}
|
|
value={r.id}
|
|
label={r.name}
|
|
disabled={disableRoleEdit}
|
|
className="w-36"
|
|
>
|
|
{r.name}
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox>
|
|
</div>
|
|
</Tooltip.Trigger>
|
|
{memberIsPendingInvite ? (
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
|
'border border-scale-200 ', //border
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-foreground">
|
|
Role can only be changed after the user has accepted the
|
|
invite
|
|
</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
) : !memberIsUser && !canRemoveRole ? (
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
|
'border border-scale-200 ', //border
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-foreground">
|
|
You need additional permissions to manage this team member
|
|
</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
) : (
|
|
<></>
|
|
)}
|
|
</Tooltip.Root>
|
|
) : (
|
|
<div className="flex items-center space-x-2">
|
|
<p className="text-sm text-foreground-light">Invalid role</p>
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger>
|
|
<IconAlertCircle size={16} strokeWidth={1.5} />
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
|
'border border-scale-200 ', //border
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-foreground">
|
|
This user has an invalid role, please reach out to us via
|
|
support
|
|
</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
</div>
|
|
)}
|
|
</Table.td>
|
|
<Table.td>
|
|
{!memberIsUser && <MemberActions member={x} roles={roles} />}
|
|
</Table.td>
|
|
</Table.tr>
|
|
</Fragment>
|
|
)
|
|
}),
|
|
...(searchString.length > 0 && filteredMembers.length === 0
|
|
? [
|
|
<Table.tr
|
|
key="no-results"
|
|
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
|
|
>
|
|
<Table.td colSpan={12}>
|
|
<div className="flex items-center space-x-3 opacity-75">
|
|
<IconAlertCircle size={16} strokeWidth={2} />
|
|
<p className="text-foreground-light">
|
|
No users matched the search query "{searchString}"
|
|
</p>
|
|
</div>
|
|
</Table.td>
|
|
</Table.tr>,
|
|
]
|
|
: []),
|
|
<Table.tr
|
|
key="footer"
|
|
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
|
|
>
|
|
<Table.td colSpan={4}>
|
|
<p className="text-foreground-light">
|
|
{searchString ? `${filteredMembers.length} of ` : ''}
|
|
{members.length || '0'} {members.length == 1 ? 'user' : 'users'}
|
|
</p>
|
|
</Table.td>
|
|
</Table.tr>,
|
|
]}
|
|
/>
|
|
</Loading>
|
|
</div>
|
|
)}
|
|
|
|
<Modal
|
|
hideFooter
|
|
size="medium"
|
|
visible={userRoleChangeModalVisible}
|
|
onCancel={() => setUserRoleChangeModalVisible(false)}
|
|
header="Change role of member"
|
|
>
|
|
<div className="flex flex-col gap-2 py-4">
|
|
<Modal.Content>
|
|
<p className="text-sm text-foreground-light">
|
|
You are changing the role of{' '}
|
|
<span className="text-foreground">{getUserDisplayName(selectedMember)}</span> from{' '}
|
|
<span className="text-foreground">{getRoleNameById(selectedMember?.oldRoleId)}</span>{' '}
|
|
to{' '}
|
|
<span className="text-foreground">{getRoleNameById(selectedMember?.newRoleId)}</span>
|
|
</p>
|
|
<p className="mt-3 text-sm text-foreground">
|
|
By changing the role of this member their permissions will change.
|
|
</p>
|
|
</Modal.Content>
|
|
<Modal.Separator />
|
|
<Modal.Content>
|
|
<div className="flex gap-3">
|
|
<Button
|
|
type="default"
|
|
block
|
|
size="medium"
|
|
onClick={() => setUserRoleChangeModalVisible(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
block
|
|
type="warning"
|
|
size="medium"
|
|
disabled={isLoading}
|
|
loading={isLoading}
|
|
onClick={() => handleRoleChange()}
|
|
>
|
|
Confirm
|
|
</Button>
|
|
</div>
|
|
</Modal.Content>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default observer(MembersView)
|