From f63b9c01a426d8bdaca0bc47ecd91ed04d02807a Mon Sep 17 00:00:00 2001 From: kemal Date: Mon, 24 Aug 2026 14:47:25 +0100 Subject: [PATCH] feat: display components for interstitial --- .../Consent/OverRoleAnnotation.test.tsx | 22 +++++++ .../OAuthApps/Consent/OverRoleAnnotation.tsx | 13 +++++ .../Consent/OverRoleAnnotation.utils.test.ts | 20 +++++++ .../Consent/OverRoleAnnotation.utils.ts | 10 ++++ .../OAuthApps/Consent/ScopeGroupCard.test.tsx | 58 +++++++++++++++++++ .../OAuthApps/Consent/ScopeGroupCard.tsx | 57 ++++++++++++++++++ .../Organization/OAuthApps/Consent/index.ts | 5 ++ 7 files changed, 185 insertions(+) create mode 100644 apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.test.tsx create mode 100644 apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.tsx create mode 100644 apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.test.ts create mode 100644 apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.ts create mode 100644 apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.test.tsx create mode 100644 apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.tsx create mode 100644 apps/studio/components/interfaces/Organization/OAuthApps/Consent/index.ts diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.test.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.test.tsx new file mode 100644 index 00000000000..4599c959ce8 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.test.tsx @@ -0,0 +1,22 @@ +import { screen } from '@testing-library/react' +import { describe, expect, test } from 'vitest' + +import { OverRoleAnnotation } from './OverRoleAnnotation' +import { customRender } from '@/tests/lib/custom-render' + +describe('OverRoleAnnotation', () => { + test('renders when the requested level exceeds the role', () => { + customRender() + expect(screen.getByText('Read-only for your role')).toBeInTheDocument() + }) + + test('renders nothing when the role covers the requested level', () => { + customRender() + expect(screen.queryByText('Read-only for your role')).not.toBeInTheDocument() + }) + + test('renders nothing for a role that can write', () => { + customRender() + expect(screen.queryByText('Read-only for your role')).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.tsx new file mode 100644 index 00000000000..a3d07034834 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.tsx @@ -0,0 +1,13 @@ +import { isScopeGroupOverRole } from './OverRoleAnnotation.utils' +import type { OAuthOrganizationRole, OAuthScopeLevel } from '@/data/oauth-apps/types' + +export interface OverRoleAnnotationProps { + level: OAuthScopeLevel + memberRole: OAuthOrganizationRole['role'] +} + +export const OverRoleAnnotation = ({ level, memberRole }: OverRoleAnnotationProps) => { + if (!isScopeGroupOverRole(level, memberRole)) return null + + return Read-only for your role +} diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.test.ts b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.test.ts new file mode 100644 index 00000000000..01c8e5c937a --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'vitest' + +import { isScopeGroupOverRole } from './OverRoleAnnotation.utils' + +describe('isScopeGroupOverRole', () => { + test('flags write and read_write levels for a Read-only role', () => { + expect(isScopeGroupOverRole('write', 'Read-only')).toBe(true) + expect(isScopeGroupOverRole('read_write', 'Read-only')).toBe(true) + }) + + test('does not flag a read level for a Read-only role', () => { + expect(isScopeGroupOverRole('read', 'Read-only')).toBe(false) + }) + + test('does not flag any level for roles that can write', () => { + expect(isScopeGroupOverRole('read_write', 'Developer')).toBe(false) + expect(isScopeGroupOverRole('write', 'Owner')).toBe(false) + expect(isScopeGroupOverRole('read', 'Developer')).toBe(false) + }) +}) diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.ts b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.ts new file mode 100644 index 00000000000..1d8daa72c51 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/OverRoleAnnotation.utils.ts @@ -0,0 +1,10 @@ +import type { OAuthOrganizationRole, OAuthScopeLevel } from '@/data/oauth-apps/types' + +// Role capability is binary for now: a "Read-only" role can't satisfy write scopes, and every +// other role (Developer, Owner, ...) is assumed to satisfy whatever the app requests. +export function isScopeGroupOverRole( + level: OAuthScopeLevel, + role: OAuthOrganizationRole['role'] +): boolean { + return role === 'Read-only' && level !== 'read' +} diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.test.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.test.tsx new file mode 100644 index 00000000000..9633485eab8 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.test.tsx @@ -0,0 +1,58 @@ +import { screen } from '@testing-library/react' +import { describe, expect, test } from 'vitest' + +import { ScopeGroupCard } from './ScopeGroupCard' +import type { OAuthScopeGroup } from '@/data/oauth-apps/types' +import { customRender } from '@/tests/lib/custom-render' + +const SCOPE_GROUPS: OAuthScopeGroup[] = [ + { + name: 'Project Settings, Action Runs, Logs, SQL Snippets', + level: 'read_write', + scopes: ['project_settings', 'action_runs', 'logs', 'sql_snippets'], + }, + { + name: 'Database Webhooks, Development Branches, Production Branches', + level: 'read', + scopes: ['database_webhooks', 'development_branches', 'production_branches'], + }, +] + +describe('ScopeGroupCard', () => { + test('interpolates the app name into the intro line', () => { + customRender( + + ) + + expect( + screen.getByText( + 'Authorizing Vercel grants it the following access permissions to the selected projects.' + ) + ).toBeInTheDocument() + }) + + test('renders READ and READ + WRITE badge labels', () => { + customRender( + + ) + + expect(screen.getByText('READ + WRITE')).toBeInTheDocument() + expect(screen.getByText('READ')).toBeInTheDocument() + }) + + test('shows the over-role annotation only for groups that exceed a Read-only role', () => { + customRender( + + ) + + expect(screen.getAllByText('Read-only for your role')).toHaveLength(1) + }) + + test('shows no over-role annotation for a role that can write', () => { + customRender( + + ) + + expect(screen.queryByText('Read-only for your role')).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.tsx new file mode 100644 index 00000000000..4881c3094b3 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.tsx @@ -0,0 +1,57 @@ +import { Badge, Card, CardContent } from 'ui' + +import { OverRoleAnnotation } from './OverRoleAnnotation' +import type { + OAuthOrganizationRole, + OAuthScopeGroup, + OAuthScopeLevel, +} from '@/data/oauth-apps/types' + +export interface ScopeGroupCardProps { + appName: string + scopeGroups: OAuthScopeGroup[] + memberRole: OAuthOrganizationRole['role'] +} + +export const ScopeGroupCard = ({ appName, scopeGroups, memberRole }: ScopeGroupCardProps) => { + return ( +
+
+

+ Permissions requested +

+

+ Authorizing {appName} grants it the following access permissions to the selected projects. +

+
+ + + +
+ {scopeGroups.map((scopeGroup) => ( +
+
+ + {getScopeLevelLabel(scopeGroup.level)} + + +
+

{scopeGroup.scopes.join(', ')}

+
+ ))} +
+
+
+
+ ) +} + +function getScopeLevelLabel(level: OAuthScopeLevel) { + if (level === 'read') return 'READ' + if (level === 'write') return 'WRITE' + return 'READ + WRITE' +} + +function getScopeLevelBadgeVariant(level: OAuthScopeLevel) { + return level === 'read' ? 'default' : 'warning' +} diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/Consent/index.ts b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/index.ts new file mode 100644 index 00000000000..d719bf886d7 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OAuthApps/Consent/index.ts @@ -0,0 +1,5 @@ +export { OverRoleAnnotation } from './OverRoleAnnotation' +export type { OverRoleAnnotationProps } from './OverRoleAnnotation' +export { isScopeGroupOverRole } from './OverRoleAnnotation.utils' +export { ScopeGroupCard } from './ScopeGroupCard' +export type { ScopeGroupCardProps } from './ScopeGroupCard'