diff --git a/apps/studio/hooks/misc/useCheckPermissions.ts b/apps/studio/hooks/misc/useCheckPermissions.ts index 3c9e5597d81c9..429377a5e8610 100644 --- a/apps/studio/hooks/misc/useCheckPermissions.ts +++ b/apps/studio/hooks/misc/useCheckPermissions.ts @@ -6,8 +6,8 @@ import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { IS_PLATFORM } from 'lib/constants' import type { Permission } from 'types' -import { useSelectedOrganization } from './useSelectedOrganization' -import { useSelectedProject } from './useSelectedProject' +import { useSelectedOrganizationQuery } from './useSelectedOrganization' +import { useSelectedProjectQuery } from './useSelectedProject' const toRegexpString = (actionOrResource: string) => `^${actionOrResource.replace('.', '\\.').replace('%', '.*')}$` @@ -80,30 +80,50 @@ export function useGetProjectPermissions( projectRefOverride?: string, enabled = true ) { - const { data, isLoading, isSuccess } = usePermissionsQuery({ + const { + data, + isLoading: isLoadingPermissions, + isSuccess: isSuccessPermissions, + } = usePermissionsQuery({ enabled: permissionsOverride === undefined && enabled, }) - const permissions = permissionsOverride === undefined ? data : permissionsOverride - const organizationResult = useSelectedOrganization({ - enabled: organizationSlugOverride === undefined && enabled, + const organizationsQueryEnabled = organizationSlugOverride === undefined && enabled + const { + data: organizationData, + isLoading: isLoadingOrganization, + isSuccess: isSuccessOrganization, + } = useSelectedOrganizationQuery({ + enabled: organizationsQueryEnabled, }) - const organization = - organizationSlugOverride === undefined ? organizationResult : { slug: organizationSlugOverride } + organizationSlugOverride === undefined ? organizationData : { slug: organizationSlugOverride } const organizationSlug = organization?.slug - const projectResult = useSelectedProject({ - enabled: projectRefOverride === undefined && enabled, + const projectsQueryEnabled = projectRefOverride === undefined && enabled + const { + data: projectData, + isLoading: isLoadingProject, + isSuccess: isSuccessProject, + } = useSelectedProjectQuery({ + enabled: projectsQueryEnabled, }) - const project = - projectRefOverride === undefined || projectResult?.parent_project_ref - ? projectResult + projectRefOverride === undefined || projectData?.parent_project_ref + ? projectData : { ref: projectRefOverride, parent_project_ref: undefined } const projectRef = project?.parent_project_ref ? project.parent_project_ref : project?.ref + const isLoading = + isLoadingPermissions || + (organizationsQueryEnabled && isLoadingOrganization) || + (projectsQueryEnabled && isLoadingProject) + const isSuccess = + isSuccessPermissions && + (!organizationsQueryEnabled || isSuccessOrganization) && + (!projectsQueryEnabled || isSuccessProject) + return { permissions, organizationSlug, @@ -196,22 +216,33 @@ export function useAsyncCheckProjectPermissions( isSuccess: isPermissionsSuccess, } = useGetProjectPermissions(permissions, organizationSlug, projectRef, isLoggedIn) - if (!isLoggedIn) + if (!isLoggedIn) { return { isLoading: true, isSuccess: false, can: false, } - if (!IS_PLATFORM) + } + if (!IS_PLATFORM) { return { isLoading: false, isSuccess: true, can: true, } + } + + const can = doPermissionsCheck( + allPermissions, + action, + resource, + data, + _organizationSlug, + _projectRef + ) return { isLoading: isPermissionsLoading, isSuccess: isPermissionsSuccess, - can: doPermissionsCheck(allPermissions, action, resource, data, _organizationSlug, _projectRef), + can, } } diff --git a/apps/studio/hooks/misc/useSelectedOrganization.ts b/apps/studio/hooks/misc/useSelectedOrganization.ts index 312f7820481f5..e6c789d5700a1 100644 --- a/apps/studio/hooks/misc/useSelectedOrganization.ts +++ b/apps/studio/hooks/misc/useSelectedOrganization.ts @@ -2,8 +2,20 @@ import { useIsLoggedIn, useParams } from 'common' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useMemo } from 'react' -import { useProjectByRef } from './useSelectedProject' +import { useProjectByRef, useProjectByRefQuery } from './useSelectedProject' +/** + * @deprecated Use useSelectedOrganizationQuery instead for access to loading states etc + * + * Example migration: + * ``` + * // Old: + * const organization = useSelectedOrganization(ref) + * + * // New: + * const { data: organization } = useSelectedOrganizationQuery(ref) + * ``` + */ export function useSelectedOrganization({ enabled = true } = {}) { const isLoggedIn = useIsLoggedIn() @@ -20,3 +32,21 @@ export function useSelectedOrganization({ enabled = true } = {}) { }) }, [data, selectedProject, slug]) } + +export function useSelectedOrganizationQuery({ enabled = true } = {}) { + const isLoggedIn = useIsLoggedIn() + + const { ref, slug } = useParams() + const { data: selectedProject } = useProjectByRefQuery(ref) + + return useOrganizationsQuery({ + enabled: isLoggedIn && enabled, + select: (data) => { + return data.find((org) => { + if (slug !== undefined) return org.slug === slug + if (selectedProject !== undefined) return org.id === selectedProject.organization_id + return undefined + }) + }, + }) +} diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts index e16af5c23a7f8..a84a2df9b46ba 100644 --- a/apps/studio/hooks/misc/useSelectedProject.ts +++ b/apps/studio/hooks/misc/useSelectedProject.ts @@ -5,6 +5,18 @@ import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { ProjectInfo, useProjectsQuery } from 'data/projects/projects-query' import { PROVIDERS } from 'lib/constants' +/** + * @deprecated Use useSelectedProjectQuery instead for access to loading states etc + * + * Example migration: + * ``` + * // Old: + * const project = useSelectedProject() + * + * // New: + * const { data: project } = useSelectedProjectQuery() + * ``` + */ export function useSelectedProject({ enabled = true } = {}) { const { ref } = useParams() const { data } = useProjectDetailQuery({ ref }, { enabled }) @@ -15,6 +27,32 @@ export function useSelectedProject({ enabled = true } = {}) { ) } +export function useSelectedProjectQuery({ enabled = true } = {}) { + const { ref } = useParams() + + return useProjectDetailQuery( + { ref }, + { + enabled, + select: (data) => { + return { ...data, parentRef: data.parent_project_ref ?? data.ref } + }, + } + ) +} + +/** + * @deprecated Use useProjectByRefQuery instead for access to loading states etc + * + * Example migration: + * ``` + * // Old: + * const project = useProjectByRef(ref) + * + * // New: + * const { data: project } = useProjectByRefQuery(ref) + * ``` + */ export function useProjectByRef( ref?: string ): Omit | undefined { @@ -34,6 +72,28 @@ export function useProjectByRef( }, [project, projects, ref]) } +export function useProjectByRefQuery(ref?: string) { + const isLoggedIn = useIsLoggedIn() + + const projectQuery = useProjectDetailQuery({ ref }, { enabled: isLoggedIn }) + + // [Alaister]: This is here for the purpose of improving performance. + // Chances are, the user will already have the list of projects in the cache. + // We can't exclusively rely on this method, as useProjectsQuery does not return branch projects. + const projectsQuery = useProjectsQuery({ + enabled: isLoggedIn, + select: (data) => { + return data.find((project) => project.ref === ref) + }, + }) + + if (projectQuery.isSuccess) { + return projectQuery + } + + return projectsQuery +} + export const useIsAwsCloudProvider = () => { const project = useSelectedProject() const isAws = project?.cloud_provider === PROVIDERS.AWS.id diff --git a/apps/studio/package.json b/apps/studio/package.json index 1a32863c0aff3..c485159f62263 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -39,7 +39,7 @@ "@graphiql/react": "^0.19.4", "@graphiql/toolkit": "^0.9.1", "@gregnr/postgres-meta": "^0.82.0-dev.2", - "@hcaptcha/react-hcaptcha": "^1.11.1", + "@hcaptcha/react-hcaptcha": "^1.12.0", "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.1.3", "@hookform/resolvers": "^3.1.1", diff --git a/apps/www/package.json b/apps/www/package.json index a984168021b50..3a0ac4dd7bf6b 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -19,7 +19,7 @@ "dependencies": { "@code-hike/mdx": "^0.9.0", "@codesandbox/sandpack-react": "^2.20.0", - "@hcaptcha/react-hcaptcha": "^1.11.1", + "@hcaptcha/react-hcaptcha": "^1.12.0", "@heroicons/react": "^1.0.6", "@mdx-js/react": "^2.3.0", "@next/bundle-analyzer": "15.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb44843cad96c..fb38e65784442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,7 +189,7 @@ importers: version: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -694,8 +694,8 @@ importers: specifier: ^0.82.0-dev.2 version: 0.82.0-dev.2(encoding@0.1.13)(supports-color@8.1.1) '@hcaptcha/react-hcaptcha': - specifier: ^1.11.1 - version: 1.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.12.0 + version: 1.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@headlessui/react': specifier: ^1.7.17 version: 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1308,7 +1308,7 @@ importers: version: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1464,8 +1464,8 @@ importers: specifier: ^2.20.0 version: 2.20.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@hcaptcha/react-hcaptcha': - specifier: ^1.11.1 - version: 1.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.12.0 + version: 1.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': specifier: ^1.0.6 version: 1.0.6(react@18.3.1) @@ -4030,11 +4030,11 @@ packages: '@har-sdk/openapi-sampler@2.2.0': resolution: {integrity: sha512-WcuGKCsFQzMlJOaeKFxMUx89EULvTvZ5GvPAeAhC4yUKz7khhN34YkuLsbn4z2BLLtrJ0oxduF5+Wa2kB3tw8w==} - '@hcaptcha/loader@1.2.4': - resolution: {integrity: sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw==} + '@hcaptcha/loader@2.0.0': + resolution: {integrity: sha512-fFQH6ApU/zCCl6Y1bnbsxsp1Er/lKX+qlgljrpWDeFcenpEtoP68hExlKSXECospzKLeSWcr06cbTjlR/x3IJA==} - '@hcaptcha/react-hcaptcha@1.11.1': - resolution: {integrity: sha512-g6TwatNIzBtOR3RM4mxzvTUQGs5T9HMN+4fcNGHn7wUVThvmazThUs0vImI836bSkGpJS8n0rOYvv1UZ47q8Vw==} + '@hcaptcha/react-hcaptcha@1.12.0': + resolution: {integrity: sha512-QiHnQQ52k8SJJSHkc3cq4TlYzag7oPd4f5ZqnjVSe4fJDSlZaOQFtu5F5AYisVslwaitdDELPVLRsRJxiiI0Aw==} peerDependencies: react: '>= 16.3.0' react-dom: '>= 16.3.0' @@ -21877,12 +21877,12 @@ snapshots: randexp: 0.5.3 tslib: 2.6.2 - '@hcaptcha/loader@1.2.4': {} + '@hcaptcha/loader@2.0.0': {} - '@hcaptcha/react-hcaptcha@1.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@hcaptcha/react-hcaptcha@1.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.10 - '@hcaptcha/loader': 1.2.4 + '@hcaptcha/loader': 2.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -34975,7 +34975,7 @@ snapshots: neo-async@2.6.2: {} - next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1): + next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1): dependencies: '@contentlayer2/core': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3