Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 253 additions & 0 deletions .cursor/rules/studio-ui.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
---
description: How to generate pages and interfaces in Studio, a web interface for managing Supabase projects
globs:
alwaysApply: true
---

## Project Structure

- Next.js app using pages router
- Pages go in @apps/studio/pages
- Project related pages go in @apps/studio/pages/projects/[ref]
- Organization related pages go in @apps/studio/pages/org/[slug]
- Studio specific components go in @apps/studio/components
- Studio specific generic UI components go in @apps/studio/components/ui
- Studio specific components related to individual pages go in @apps/studio/components/interfaces e.g. @apps/studio/components/interfaces/Auth
- Generic helper functions go in @apps/studio/lib
- Generic hooks go in @apps/studio/hooks

## Component system

Our primitive component system is in @packages/ui and is based off shadcn/ui components. These components can be shared across all @apps e.g. studio and docs. Do not introduce new ui components unless asked to.

- UI components are imported from this package across apps e.g. import { Button, Badge } from 'ui'
- Some components have a _Shadcn_ namespace appended to component name e.g. import { Input*Shadcn* } from 'ui'
- We should be using _Shadcn_ components where possible
- Before composing interfaces, read @packages/ui/index.tsx file for a full list of available components

## Styling

We use Tailwind for styling.

- You should never use tailwind classes for colours and instead use classes we've defined ourselves
- Backgrounds // most of the time you will not need to define a background
- 'bg' used for main app surface background
- 'bg-muted' for elevating content // you can use Card instead
- 'bg-warning' for highlighting information that needs to be acted on
- 'bg-destructive' for highlighting issues
- Text
- 'text-foreground' for primary text like headings
- 'text-foreground-light' for body text
- 'text-foreground-lighter' for subtle text
- 'text-warning' for calling out information that needs action
- 'text-destructive' for calling out when something went wrong
- When needing to apply typography styles, read @apps/studio/styles/typography.scss and use one of the available classes instead of hard coding classes e.g. use "heading-default" instead of "text-sm font-medium"

## Page structure

When creating a new page follow these steps:

- Create the page in @apps/studio/pages
- Use the PageLayout component that has the following props

```jsx
export interface NavigationItem {
id?: string
label: string
href?: string
icon?: ReactNode
onClick?: () => void
badge?: string
active?: boolean
}

interface PageLayoutProps {
children?: ReactNode
title?: string | ReactNode
subtitle?: string | ReactNode
icon?: ReactNode
breadcrumbs?: Array<{
label?: string
href?: string
element?: ReactNode
}>
primaryActions?: ReactNode
secondaryActions?: ReactNode
navigationItems?: NavigationItem[]
className?: string
size?: 'default' | 'full' | 'large' | 'small'
isCompact?: boolean
}
```

- If a page has page related actions, add them to primary and secondary action props e.g. Users page has "Create new user" action
- If a page is within an existing section (e.g. Auth), you should use the related layout component e.g. AuthLayout
- Create a new component in @apps/studio/components/interfaces for the contents of the page
- Use ScaffoldContainer if the page should be center aligned in a container
- Use ScaffoldSection, ScaffoldSectionTitle, ScaffoldSectionDescription if the page has multiple sections

### Page example

```jsx
import { MyPageComponent } from 'components/interfaces/MyPage/MyPageComponent'
import AuthLayout from './AuthLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer } from 'components/layouts/Scaffold'
import type { NextPageWithLayout } from 'types'

const MyPage: NextPageWithLayout = () => {
return (
<ScaffoldContainer>
<MyPageComponent />
</ScaffoldContainer>
)
}

MyPage.getLayout = (page) => (
<DefaultLayout>
<AuthLayout>{page}</AuthLayout>
</DefaultLayout>
)

export default MyPage

export const MyPageComponent = () => (
<ScaffoldSection isFullWidth>
<div>
<ScaffoldSectionTitle>My page section</ScaffoldSectionTitle>
<ScaffoldSectionDescription>A brief description of the purpose of the page</ScaffoldSectionDescription>
</div>
// Content goes here
</ScaffoldSection>
)
```

## Forms

- Build forms with `react-hook-form` + `zod`.
- Use our `_Shadcn_` form primitives from `ui` and prefer `FormItemLayout` with layout="flex-row-reverse" for most controls (see `apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx`).
- Keep imports from `ui` with `_Shadcn_` suffixes.
- Forms should generally be wrapped in a Card unless specified

### Example (single field)

```tsx
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'

import { Button, Form_Shadcn_, FormField_Shadcn_, FormControl_Shadcn_, Input_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'

const profileSchema = z.object({
username: z.string().min(2, 'Username must be at least 2 characters'),
})

export function ProfileForm() {
const form = useForm<z.infer<typeof profileSchema>>({
resolver: zodResolver(profileSchema),
defaultValues: { username: '' },
mode: 'onSubmit',
reValidateMode: 'onBlur',
})

function onSubmit(values: z.infer<typeof profileSchema>) {
// handle values
}

return (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardContent className="space-y-6">
<FormField_Shadcn_
control={form.control}
name="username"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Username"
description="This is your public display name."
>
<FormControl_Shadcn_>
<Input_Shadcn_ placeholder="shadcn" autoComplete="off" {...field} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end">
<Button type="primary" htmlType="submit">
Submit
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
)
}
```

## Cards

- Use cards when needing to group related pieces of information
- Cards can have sections with CardContent
- Use CardFooter for actions
- Only use CardHeader and CardTitle if the card content has not been described by the surrounding content e.g. Page title or ScaffoldSectionTitle
- Use CardHeader and CardTitle when you are using multiple Cards to group related pieces of content e.g. Primary branch, Persistent branches, Preview branches

## Sheets

- Use a sheet when needing to reveal more complicated forms or information relating to an object and context switching away to a new page would be disruptive e.g. we list auth providers, clicking an auth provider opens a sheet with information about that provider and a form to enable, user can close sheet to go back to providers list

## Tables

- Use the generic ui table components for most tables
- Tables are generally contained witin a card
- If a table has associated actions, they should go above on right hand side
- If a table has associated search or filters, they should go above on left hand side
- If a table is the main content of a page, and it does not have search or filters, you can add table actions to primary and secondary actions of PageLayout
- If a table is the main content of a page section, and it does not have search or filters, you can add table actions to the right of ScaffoldSectionTitle
- For simple lists of objects you can use ResourceList with ResourceListItem instead

### Table example

```jsx
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from 'ui'

;<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>
```

## Alerts

- Use Admonition component to alert users of important actions or restrictions in place
- Place the Admonition either at the top of the contents of the page (below page title) or at the top of the related ScaffoldSection , below ScaffoldTitle
- Use sparingly

### Alert example

```jsx
<Admonition
type="note"
title="No authentication logs available for this user"
description="Auth events such as logging in will be shown here"
/>
```
31 changes: 21 additions & 10 deletions apps/docs/content/guides/auth/social-login/auth-google.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -540,26 +540,37 @@ Future<void> _nativeGoogleSignIn() async {
/// iOS Client ID that you registered with Google Cloud.
const iosClientId = 'my-ios.apps.googleusercontent.com';

final GoogleSignIn googleSignIn = GoogleSignIn(
clientId: iosClientId,
final scopes = ['email', 'profile'];
final googleSignIn = GoogleSignIn.instance;

await googleSignIn.initialize(
serverClientId: webClientId,
clientId: iosClientId,
);
final googleUser = await googleSignIn.signIn();
final googleAuth = await googleUser!.authentication;
final accessToken = googleAuth.accessToken;
final idToken = googleAuth.idToken;

if (accessToken == null) {
throw 'No Access Token found.';
final googleUser = await googleSignIn.attemptLightweightAuthentication();
// or await googleSignIn.authenticate(); which will return a GoogleSignInAccount or throw an exception

if (googleUser == null) {
throw AuthException('Failed to sign in with Google.');
}

/// Authorization is required to obtain the access token with the appropriate scopes for Supabase authentication,
/// while also granting permission to access user information.
final authorization =
await googleUser.authorizationClient.authorizationForScopes(scopes) ??
await googleUser.authorizationClient.authorizeScopes(scopes);

final idToken = googleUser.authentication.idToken;

if (idToken == null) {
throw 'No ID Token found.';
throw AuthException('No ID Token found.');
}

await supabase.auth.signInWithIdToken(
provider: OAuthProvider.google,
idToken: idToken,
accessToken: accessToken,
accessToken: authorization.accessToken,
);
}
...
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/content/guides/platform/project-transfer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ Target organization - the organization you want to move the project to

- You need to be the owner of the source organization.
- You need to be at least a member of the target organization you want to move the project to.
- Projects with support tier add-ons cannot be transferred at this point. [Open a support ticket](/dashboard/support/new?category=billing&subject=Transfer%20project).
- No active GitHub integration connection
- No project-scoped roles pointing to the project (Team/Enterprise plan)
- No log drains configured
- Target organization is not managed by Vercel Marketplace (currently unsupported)

## Usage-billing and project add-ons

Expand Down
4 changes: 2 additions & 2 deletions apps/studio/components/grid/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { fetchAllTableRows, useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { RoleImpersonationState } from 'lib/role-impersonation'
Expand Down Expand Up @@ -84,7 +84,7 @@ const DefaultHeader = () => {

const snap = useTableEditorTableStateSnapshot()
const tableEditorSnap = useTableEditorStateSnapshot()
const { can: canCreateColumns } = useAsyncCheckProjectPermissions(
const { can: canCreateColumns } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'columns'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useParams } from 'common/hooks'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import { useAPIKeyDeleteMutation } from 'data/api-keys/api-key-delete-mutation'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'

interface APIKeyDeleteDialogProps {
Expand All @@ -19,7 +19,7 @@ export const APIKeyDeleteDialog = ({ apiKey, lastSeen }: APIKeyDeleteDialogProps
const { ref: projectRef } = useParams()
const [isOpen, setIsOpen] = useState(false)

const { can: canDeleteAPIKeys } = useAsyncCheckProjectPermissions(
const { can: canDeleteAPIKeys } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
Expand Down
8 changes: 5 additions & 3 deletions apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import CopyButton from 'components/ui/CopyButton'
import { useAPIKeyIdQuery } from 'data/api-keys/[id]/api-key-id-query'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { apiKeysKeys } from 'data/api-keys/keys'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'

export function ApiKeyPill({
Expand All @@ -28,8 +28,10 @@ export function ApiKeyPill({
const isSecret = apiKey.type === 'secret'

// Permission check for revealing/copying secret API keys
const { can: canManageSecretKeys, isLoading: isLoadingPermission } =
useAsyncCheckProjectPermissions(PermissionAction.READ, 'service_api_keys')
const { can: canManageSecretKeys, isLoading: isLoadingPermission } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)

// This query only runs when show=true (enabled: show)
// It fetches the fully revealed API key when needed
Expand Down
Loading
Loading