mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
82d7d347c4
## Summary * Fixes extreme slowness (browser-crashing on filter) on `/org/[slug]/team` for orgs with 200+ members. * Root cause: `MemberRow`/`MemberActions` each independently subscribed to org-wide React Query data (roles, projects, permissions, feature flags) and rendered a hidden `UpdateRolesPanel` per row. Filtering caused hundreds of duplicate query observers to mount/unmount on every keystroke, each scheduling its own stale-timeout bookkeeping and blocking the main thread for multiple seconds. * Hoisted all org-wide data fetching (`useOrganizationRolesV2Query`, `useOrgProjectsInfiniteQuery`, `usePermissionsQuery`, `useSelectedOrganizationQuery`, `useIsFeatureEnabled`) to `MembersView` and passed the results down as props. * Replaced the per-row `useAsyncCheckPermissions` hook calls in `MemberActions` with the underlying pure `doPermissionsCheck` function memoized locally, removing their internal query subscriptions. * Simplified `useGetRolesManagementPermissions` to stop calling a query-fetching fallback hook that was unreachable given all current call sites already pass `permissions`/`orgSlug` directly. * Replaced 200 hidden per-row `UpdateRolesPanel` instances with a single shared instance owned by `MembersView`, opened via an `onManageAccess` callback. * Cached the regex built by `doPermissionsCheck`'s `toRegexpString` instead of rebuilding it on every permission check. Diagnosed from two Chrome performance traces of the team page while typing in the filter box (multi-second main-thread blocking tasks traced to React Query `QueryObserver` mount/unmount storms). ## Test plan - [X] `tsc --noEmit` clean (only one pre-existing, unrelated error in `packages/ui-patterns`) - [X] `eslint` clean on all changed files (only pre-existing warnings) - [X] `vitest run tests/components/Organization/TeamSettings` — 51 tests pass - [X] Manually verify filtering is smooth on an org with 200+ members <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Team Settings provides centralized member access and role management. * Members can update roles through the access-management panel. * **Improvements** * Permission checks now more accurately handle organization and project scopes, including wildcard patterns. * Member search is debounced for smoother filtering while typing. * Access-management actions use current organization members, roles, permissions, and feature settings. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
210 lines
6.5 KiB
TypeScript
210 lines
6.5 KiB
TypeScript
import { useIsLoggedIn, useParams } from 'common'
|
|
import jsonLogic from 'json-logic-js'
|
|
import { useMemo } from 'react'
|
|
|
|
import { useSelectedOrganizationQuery } from './useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from './useSelectedProject'
|
|
import { usePermissionsQuery } from '@/data/permissions/permissions-query'
|
|
import { IS_PLATFORM } from '@/lib/constants'
|
|
import type { Permission } from '@/types'
|
|
|
|
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
const regexpCache = new Map<string, RegExp>()
|
|
const getActionResourceRegexp = (actionOrResource: string) => {
|
|
let regexp = regexpCache.get(actionOrResource)
|
|
if (!regexp) {
|
|
const pattern = actionOrResource.split('%').map(escapeRegExp).join('.*')
|
|
regexp = new RegExp(`^${pattern}$`)
|
|
regexpCache.set(actionOrResource, regexp)
|
|
}
|
|
return regexp
|
|
}
|
|
|
|
function doPermissionConditionCheck(permissions: Permission[], data?: object) {
|
|
const isRestricted = permissions
|
|
.filter((permission) => permission.restrictive)
|
|
.some(
|
|
({ condition }: { condition: jsonLogic.RulesLogic }) =>
|
|
condition === null || jsonLogic.apply(condition, data)
|
|
)
|
|
if (isRestricted) return false
|
|
|
|
return permissions
|
|
.filter((permission) => !permission.restrictive)
|
|
.some(
|
|
({ condition }: { condition: jsonLogic.RulesLogic }) =>
|
|
condition === null || jsonLogic.apply(condition, data)
|
|
)
|
|
}
|
|
|
|
export function doPermissionsCheck(
|
|
permissions: Permission[] | undefined,
|
|
action: string,
|
|
resource: string,
|
|
data?: object,
|
|
organizationSlug?: string,
|
|
projectRef?: string | null
|
|
) {
|
|
if (!permissions || !Array.isArray(permissions)) {
|
|
return false
|
|
}
|
|
|
|
if (projectRef) {
|
|
const projectPermissions = permissions.filter(
|
|
(permission) =>
|
|
permission.organization_slug === organizationSlug &&
|
|
permission.actions.some((act) =>
|
|
action ? getActionResourceRegexp(act).test(action) : null
|
|
) &&
|
|
permission.resources.some((res) => getActionResourceRegexp(res).test(resource)) &&
|
|
permission.project_refs?.includes(projectRef)
|
|
)
|
|
if (projectPermissions.length > 0) {
|
|
return doPermissionConditionCheck(projectPermissions, { resource_name: resource, ...data })
|
|
}
|
|
}
|
|
|
|
const orgPermissions = permissions
|
|
// filter out org-level permission
|
|
.filter((permission) => !permission.project_refs || permission.project_refs.length === 0)
|
|
.filter(
|
|
(permission) =>
|
|
permission.organization_slug === organizationSlug &&
|
|
permission.actions.some((act) =>
|
|
action ? getActionResourceRegexp(act).test(action) : null
|
|
) &&
|
|
permission.resources.some((res) => getActionResourceRegexp(res).test(resource))
|
|
)
|
|
return doPermissionConditionCheck(orgPermissions, { resource_name: resource, ...data })
|
|
}
|
|
|
|
export function useGetPermissions(
|
|
permissionsOverride?: Permission[],
|
|
organizationSlugOverride?: string,
|
|
enabled = true
|
|
) {
|
|
return useGetProjectPermissions(permissionsOverride, organizationSlugOverride, undefined, enabled)
|
|
}
|
|
|
|
function useGetProjectPermissions(
|
|
permissionsOverride?: Permission[],
|
|
organizationSlugOverride?: string,
|
|
projectRefOverride?: string | null,
|
|
enabled = true
|
|
) {
|
|
const {
|
|
data,
|
|
isPending: isLoadingPermissions,
|
|
isSuccess: isSuccessPermissions,
|
|
} = usePermissionsQuery({
|
|
enabled: permissionsOverride === undefined && enabled,
|
|
})
|
|
const permissions = permissionsOverride === undefined ? data : permissionsOverride
|
|
|
|
const getOrganizationDataFromParamsSlug = organizationSlugOverride === undefined && enabled
|
|
const {
|
|
data: organizationData,
|
|
isPending: isLoadingOrganization,
|
|
isSuccess: isSuccessOrganization,
|
|
} = useSelectedOrganizationQuery({
|
|
enabled: getOrganizationDataFromParamsSlug,
|
|
})
|
|
const organization =
|
|
organizationSlugOverride === undefined ? organizationData : { slug: organizationSlugOverride }
|
|
const organizationSlug = organization?.slug
|
|
|
|
const { ref: urlProjectRef } = useParams()
|
|
const getProjectDataFromParamsRef = !!urlProjectRef && projectRefOverride === undefined && enabled
|
|
const {
|
|
data: projectData,
|
|
isPending: isLoadingProject,
|
|
isSuccess: isSuccessProject,
|
|
} = useSelectedProjectQuery({
|
|
enabled: getProjectDataFromParamsRef,
|
|
})
|
|
const project =
|
|
projectRefOverride === undefined || projectData?.parent_project_ref
|
|
? projectData
|
|
: { ref: projectRefOverride, parent_project_ref: undefined }
|
|
|
|
const projectRef =
|
|
projectRefOverride === null
|
|
? null
|
|
: project?.parent_project_ref
|
|
? project.parent_project_ref
|
|
: project?.ref
|
|
|
|
const isLoading =
|
|
isLoadingPermissions ||
|
|
(getOrganizationDataFromParamsSlug && isLoadingOrganization) ||
|
|
(getProjectDataFromParamsRef && isLoadingProject)
|
|
const isSuccess =
|
|
isSuccessPermissions &&
|
|
(!getOrganizationDataFromParamsSlug || isSuccessOrganization) &&
|
|
(!getProjectDataFromParamsRef || isSuccessProject)
|
|
|
|
return {
|
|
permissions,
|
|
organizationSlug,
|
|
projectRef,
|
|
isLoading,
|
|
isSuccess,
|
|
}
|
|
}
|
|
|
|
/** [Joshen] To be renamed to be useAsyncCheckPermissions, more generic as it covers both org and project perms */
|
|
// Useful when you want to avoid layout changes while waiting for permissions to load
|
|
export function useAsyncCheckPermissions(
|
|
action: string,
|
|
resource: string,
|
|
data?: object,
|
|
overrides?: {
|
|
organizationSlug?: string
|
|
projectRef?: string | null
|
|
permissions?: Permission[]
|
|
}
|
|
) {
|
|
const isLoggedIn = useIsLoggedIn()
|
|
const { organizationSlug, projectRef, permissions } = overrides ?? {}
|
|
|
|
const {
|
|
permissions: allPermissions,
|
|
organizationSlug: _organizationSlug,
|
|
projectRef: _projectRef,
|
|
isLoading: isPermissionsLoading,
|
|
isSuccess: isPermissionsSuccess,
|
|
} = useGetProjectPermissions(permissions, organizationSlug, projectRef, isLoggedIn)
|
|
|
|
const can = useMemo(() => {
|
|
if (!IS_PLATFORM) return true
|
|
if (!isLoggedIn) return false
|
|
if (!isPermissionsSuccess || !allPermissions) return false
|
|
|
|
return doPermissionsCheck(
|
|
allPermissions,
|
|
action,
|
|
resource,
|
|
data,
|
|
_organizationSlug,
|
|
_projectRef
|
|
)
|
|
}, [
|
|
isLoggedIn,
|
|
isPermissionsSuccess,
|
|
allPermissions,
|
|
action,
|
|
resource,
|
|
data,
|
|
_organizationSlug,
|
|
_projectRef,
|
|
])
|
|
|
|
// Derive loading/success consistently from the same branches
|
|
const isLoading = !IS_PLATFORM ? false : !isLoggedIn ? true : isPermissionsLoading
|
|
|
|
const isSuccess = !IS_PLATFORM ? true : !isLoggedIn ? false : isPermissionsSuccess
|
|
|
|
return { isLoading, isSuccess, can }
|
|
}
|