Files
supabase__supabase/apps/studio/hooks/misc/useCheckPermissions.ts
Charis 82d7d347c4 fix(studio): eliminate per-row query duplication on org team page (#50238)
## 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>
2026-09-11 14:02:28 +08:00

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 }
}