Merge branch 'master' into dnywh/chore/oauth-app-integrations

This commit is contained in:
Danny White authored and GitHub committed 2026-03-10 14:24:28 +11:00
commit 8d1aaa5b76
236 files changed
+112208 -55779

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@ permissions:
jobs:
eval:
name: Run evals
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-evals'))
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-evals') && github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
timeout-minutes: 20
@@ -0,0 +1,65 @@
name: Cleanup Braintrust preview scorers
on:
pull_request:
types: [closed]
permissions:
pull-requests: write
jobs:
cleanup-scorers:
name: Delete preview scorers
if: contains(github.event.pull_request.labels.*.name, 'preview-scorers') && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Delete preview scorers from staging
env:
BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }}
BRAINTRUST_STAGING_PROJECT_ID: ${{ secrets.BRAINTRUST_STAGING_PROJECT_ID }}
run: |
BRANCH_SLUG=$(echo "${GITHUB_HEAD_REF}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
readarray -t SLUGS < <(jq -r '.[].slug' apps/studio/evals/scorer-online-manifest.json)
for slug in "${SLUGS[@]}"; do
prefixed="${BRANCH_SLUG}-${slug}"
id=$(curl -s "https://api.braintrust.dev/v1/function?project_id=${BRAINTRUST_STAGING_PROJECT_ID}&slug=${prefixed}" \
-H "Authorization: Bearer ${BRAINTRUST_API_KEY}" | jq -r '.objects[0].id // empty')
if [ -n "$id" ]; then
curl -s -X DELETE "https://api.braintrust.dev/v1/function/${id}" \
-H "Authorization: Bearer ${BRAINTRUST_API_KEY}"
echo "Deleted ${prefixed}"
else
echo "Not found: ${prefixed} (already deleted or never deployed)"
fi
done
- name: Post cleanup comment
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const prNumber = context.payload.pull_request.number
const marker = '<!-- preview-scorers-bot -->'
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
})
const existing = comments.find(c => c.body?.includes(marker))
if (!existing) return
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: `${marker}\n### Braintrust Preview Scorers\n\nPreview scorers have been cleaned up.`,
})
@@ -0,0 +1,123 @@
name: Deploy Braintrust preview scorers
on:
pull_request:
types: [labeled, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
pull-requests: write
contents: read
jobs:
push-scorers:
name: Push preview scorers
if: contains(github.event.pull_request.labels.*.name, 'preview-scorers') && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
- name: Check for scorer file changes
id: changed
# On labeled events, always push. On synchronize, only push if scorer files changed.
run: |
if [[ "${{ github.event.action }}" == "synchronize" ]]; then
changed=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD | grep -E 'evals/scorer' || true)
if [ -z "$changed" ]; then
echo "No scorer files changed, skipping push"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Install pnpm
if: steps.changed.outputs.skip != 'true'
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
- name: Use Node.js
if: steps.changed.outputs.skip != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install Dependencies
if: steps.changed.outputs.skip != 'true'
run: pnpm install --frozen-lockfile
- name: Push scorers to staging
if: steps.changed.outputs.skip != 'true'
id: push
run: |
cd apps/studio && pnpm scorers:deploy
slugs=$(jq -r '[.[].slug] | join(",")' evals/scorer-online-manifest.json)
echo "slugs=$slugs" >> $GITHUB_OUTPUT
env:
BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }}
BRAINTRUST_PROJECT_ID: ${{ secrets.BRAINTRUST_STAGING_PROJECT_ID }}
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Post PR comment
if: steps.changed.outputs.skip != 'true'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
BRAINTRUST_STAGING_PROJECT_ID: ${{ secrets.BRAINTRUST_STAGING_PROJECT_ID }}
SCORER_SLUGS: ${{ steps.push.outputs.slugs }}
with:
script: |
const prNumber = context.payload.pull_request.number
const branch = process.env.GITHUB_HEAD_REF
const prefix = branch.replace(/[^a-z0-9-]/gi, '-').toLowerCase()
const stagingUrl = 'https://www.braintrust.dev/app/supabase.io/p/Assistant%20(Staging%20Scorers)/scorers'
const slugs = process.env.SCORER_SLUGS.split(',')
const slugList = slugs.map(s => `- \`${prefix}-${s}\``).join('\n')
const sha = context.sha.slice(0, 7)
const marker = '<!-- preview-scorers-bot -->'
const body = `${marker}
### Braintrust Preview Scorers
Deployed scorers to [Assistant (Staging Scorers)](${stagingUrl}):
${slugList}
Commit: ${sha}`
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
})
const existing = comments.find(c => c.body?.includes(marker))
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
})
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
})
}
@@ -0,0 +1,43 @@
name: Deploy Braintrust scorers
on:
push:
branches: [master]
paths:
- "apps/studio/evals/scorer.ts"
- "apps/studio/evals/scorer-online.ts"
- "apps/studio/evals/scorer-online-manifest.json"
permissions:
contents: read
jobs:
push:
name: Push scorers
runs-on: ubuntu-latest
timeout-minutes: 10
env:
BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }}
BRAINTRUST_PROJECT_ID: ${{ secrets.BRAINTRUST_PROJECT_ID }}
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Push scorers
run: cd apps/studio && pnpm scorers:deploy
+4
View File
@@ -78,6 +78,10 @@ jobs:
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-
- name: Reset supabase
if: steps.filter.outputs.studio == 'true'
run: rm -rf supabase && pnpm exec supabase init && mkdir supabase/functions
- name: 🚀 Run Playwright tests against Vercel Preview
if: steps.filter.outputs.studio == 'true'
id: playwright
@@ -359,7 +359,7 @@ const TerraformReferencePage = async () => {
return (
<GuideTemplate meta={meta} editLink={editLink}>
The Terraform Provider provices access to{' '}
The Terraform Provider provides access to{' '}
<Link
href="https://developer.hashicorp.com/terraform/language/resources"
rel="noopener noreferrer"
@@ -21,11 +21,11 @@ This means you can make 120 requests to Project A and 120 requests to Project B
### Rate limit response headers
Every API response includes rate limit information in the following headers:
Every API response includes rate limit information following official [HTTP specification headers](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers):
- `X-RateLimit-Limit` - The maximum number of requests allowed in the current time window
- `X-RateLimit-Remaining` - The number of requests remaining before you hit the rate limit
- `X-RateLimit-Reset` - The number of milliseconds remaining until your rate limit resets
- `X-RateLimit-Reset` - The number of seconds remaining until your rate limit resets
You can use these headers to monitor your usage and implement proactive rate limit handling before receiving a 429 response.
@@ -60,6 +60,6 @@ Some endpoints have stricter rate limits than the standard 120 requests per minu
### Best practices
- **Monitor rate limit headers** - Check the `X-RateLimit-Remaining` header to see how many requests you have left. When it approaches 0, slow down your requests to avoid hitting the limit.
- **Implement exponential backoff** - When you receive a 429 response, wait before retrying. You can use the `X-RateLimit-Reset` header (milliseconds) to determine exactly how long to wait.
- **Implement exponential backoff** - When you receive a 429 response, wait before retrying. You can use the `X-RateLimit-Reset` header (seconds) to determine exactly how long to wait.
- **Batch operations** - Where possible, combine multiple operations into fewer API calls to reduce your request count.
- **Be mindful of expensive endpoints** - Analytics, database context, and domain endpoints have stricter limits, so use them judiciously.
@@ -179,7 +179,7 @@ description = "Unauthorized access to Realtime channel."
description = "Realtime is currently restarting."
[UnableToProcessListenPayload]
description = "Payload sent in NOTIFY operation was JSON parsable."
description = "Payload sent in NOTIFY operation was not JSON parsable."
[UnableToListenToTenantDatabase]
description = "Unable to LISTEN for notifications against the Tenant Database."
@@ -9,19 +9,22 @@ description: 'Learn how to use Supabase in your Ionic Angular App.'
<Admonition type="note">
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/mhartington/supabase-ionic-angular).
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/ionic-angular-user-management).
</Admonition>
<$Partial path="project_setup.mdx" variables={{ "framework": "ionicangular", "tab": "mobiles" }} />
<$Partial
path="project_setup.mdx"
variables={{ "framework": "ionicangular", "tab": "mobiles" }}
/>
## Building the app
Let's start building the Angular app from scratch.
Start building the Angular app from scratch.
### Initialize an Ionic Angular app
We can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize
Use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize
an app called `supabase-ionic-angular`:
```bash
@@ -30,368 +33,81 @@ ionic start supabase-ionic-angular blank --type angular
cd supabase-ionic-angular
```
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
Install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js
```
And finally, we want to save the environment variables in the `src/environments/environment.ts` file.
All we need are the API URL and the key that you copied [earlier](#get-api-details).
These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
And finally, save the environment variables in the `src/environments/environment.ts` file.
All you need are the API URL and the key that you copied [earlier](#get-api-details).
These variables will be exposed on the browser, and that's fine as [Row Level Security](/docs/guides/auth#row-level-security) is enabled on the Database.
<$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/environments/environment.ts"
lines={[[1, -1]]}
meta="name=src/environments/environment.ts"
/>
```ts name=environment.ts
export const environment = {
production: false,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
}
```
Now that you have the API credentials in place, create a `SupabaseService` with `ionic g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API.
</$CodeTabs>
Now that we have the API credentials in place, let's create a `SupabaseService` with `ionic g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API.
<$CodeTabs>
```ts name=src/app/supabase.service.ts
import { Injectable } from '@angular/core'
import { LoadingController, ToastController } from '@ionic/angular'
import { AuthChangeEvent, createClient, Session, SupabaseClient } from '@supabase/supabase-js'
import { environment } from '../environments/environment'
export interface Profile {
username: string
website: string
avatar_url: string
}
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient
constructor(
private loadingCtrl: LoadingController,
private toastCtrl: ToastController
) {
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
}
get user() {
return this.supabase.auth.getUser().then(({ data }) => data?.user)
}
get session() {
return this.supabase.auth.getSession().then(({ data }) => data?.session)
}
get profile() {
return this.user
.then((user) => user?.id)
.then((id) =>
this.supabase.from('profiles').select(`username, website, avatar_url`).eq('id', id).single()
)
}
authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
return this.supabase.auth.onAuthStateChange(callback)
}
signIn(email: string) {
return this.supabase.auth.signInWithOtp({ email })
}
signOut() {
return this.supabase.auth.signOut()
}
async updateProfile(profile: Profile) {
const user = await this.user
const update = {
...profile,
id: user?.id,
updated_at: new Date(),
}
return this.supabase.from('profiles').upsert(update)
}
downLoadImage(path: string) {
return this.supabase.storage.from('avatars').download(path)
}
uploadAvatar(filePath: string, file: File) {
return this.supabase.storage.from('avatars').upload(filePath, file)
}
async createNotice(message: string) {
const toast = await this.toastCtrl.create({ message, duration: 5000 })
await toast.present()
}
createLoader() {
return this.loadingCtrl.create()
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/supabase.service.ts"
lines={[[1, -1]]}
meta="name=src/app/supabase.service.ts"
/>
### Set up a login route
Let's set up a route to manage logins and signups. We'll use Magic Links so users can sign in with their email without using passwords.
Set up a route to manage logins and signups. Use Magic Links so users can sign in with their email without using passwords.
Create a `LoginPage` with the `ionic g page login` Ionic CLI command.
<Admonition type="tip">
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/login/login.page.ts"
lines={[[1, -1]]}
meta="name=src/app/login/login.page.ts"
/>
This guide will show the template inline, but the example app will have `templateUrl`s
</Admonition>
<$CodeTabs>
```ts name=src/app/login/login.page.ts
import { Component, OnInit } from '@angular/core'
import { SupabaseService } from '../supabase.service'
@Component({
selector: 'app-login',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<h1>Supabase + Ionic Angular</h1>
<p>Sign in via magic link with your email below</p>
</div>
<ion-list inset="true">
<form (ngSubmit)="handleLogin($event)">
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input [(ngModel)]="email" name="email" autocomplete type="email"></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button type="submit" fill="clear">Login</ion-button>
</div>
</form>
</ion-list>
</ion-content>
`,
styleUrls: ['./login.page.scss'],
})
export class LoginPage {
email = ''
constructor(private readonly supabase: SupabaseService) {}
async handleLogin(event: any) {
event.preventDefault()
const loader = await this.supabase.createLoader()
await loader.present()
try {
const { error } = await this.supabase.signIn(this.email)
if (error) {
throw error
}
await loader.dismiss()
await this.supabase.createNotice('Check your email for the login link!')
} catch (error: any) {
await loader.dismiss()
await this.supabase.createNotice(error.error_description || error.message)
}
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/login/login.page.html"
lines={[[1, -1]]}
meta="name=src/app/login/login.page.html"
/>
### Account page
After a user is signed in, we can allow them to edit their profile details and manage their account.
After a user is signed in, allow them to edit their profile details and manage their account.
Create an `AccountComponent` with `ionic g page account` Ionic CLI command.
<$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/account/account.page.ts"
lines={[[1, -1]]}
meta="name=src/app/account/account.page.ts"
/>
```ts name=src/app/account.page.ts
import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { Profile, SupabaseService } from '../supabase.service'
@Component({
selector: 'app-account',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form>
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input type="email" name="email" [(ngModel)]="email" readonly></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Name</ion-label>
<ion-input type="text" name="username" [(ngModel)]="profile.username"></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Website</ion-label>
<ion-input type="url" name="website" [(ngModel)]="profile.website"></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button fill="clear" (click)="updateProfile()">Update Profile</ion-button>
</div>
</form>
<div class="ion-text-center">
<ion-button fill="clear" (click)="signOut()">Log Out</ion-button>
</div>
</ion-content>
`,
styleUrls: ['./account.page.scss'],
})
export class AccountPage implements OnInit {
profile: Profile = {
username: '',
avatar_url: '',
website: '',
}
email = ''
constructor(
private readonly supabase: SupabaseService,
private router: Router
) {}
ngOnInit() {
this.getEmail()
this.getProfile()
}
async getEmail() {
this.email = await this.supabase.user.then((user) => user?.email || '')
}
async getProfile() {
try {
const { data: profile, error, status } = await this.supabase.profile
if (error && status !== 406) {
throw error
}
if (profile) {
this.profile = profile
}
} catch (error: any) {
alert(error.message)
}
}
async updateProfile(avatar_url: string = '') {
const loader = await this.supabase.createLoader()
await loader.present()
try {
const { error } = await this.supabase.updateProfile({ ...this.profile, avatar_url })
if (error) {
throw error
}
await loader.dismiss()
await this.supabase.createNotice('Profile updated!')
} catch (error: any) {
await loader.dismiss()
await this.supabase.createNotice(error.message)
}
}
async signOut() {
console.log('testing?')
await this.supabase.signOut()
this.router.navigate(['/'], { replaceUrl: true })
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/account/account.page.html"
lines={[[1, -1]]}
meta="name=src/app/account/account.page.html"
/>
### Launch!
Now that we have all the components in place, let's update `AppComponent`:
Now that you have all the components in place, update `AppComponent`:
<$CodeTabs>
```ts name=src/app/app.component.ts
import { Component } from '@angular/core'
import { Router } from '@angular/router'
import { SupabaseService } from './supabase.service'
@Component({
selector: 'app-root',
template: `
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
`,
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(
private supabase: SupabaseService,
private router: Router
) {
this.supabase.authChanges((_, session) => {
console.log(session)
if (session?.user) {
this.router.navigate(['/account'])
}
})
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/app.component.ts"
lines={[[1, -1]]}
meta="name=src/app/app.component.ts"
/>
Then update the `AppRoutingModule`
<$CodeTabs>
```ts name=src/app/app-routing.module.ts"
import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () => import('./login/login.module').then((m) => m.LoginPageModule),
},
{
path: 'account',
loadChildren: () => import('./account/account.module').then((m) => m.AccountPageModule),
},
]
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules,
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/app-routing.module.ts"
lines={[[1, -1]]}
meta="name=src/app/app-routing.module.ts"
/>
Once that's done, run this in a terminal window:
@@ -399,7 +115,7 @@ Once that's done, run this in a terminal window:
ionic serve
```
And the browser will automatically open to show the app.
And the browser automatically opens to show the app.
![Supabase Angular](/docs/img/ionic-demos/ionic-angular.png)
@@ -419,178 +135,39 @@ npm install @ionic/pwa-elements @capacitor/camera
[Capacitor](https://capacitorjs.com) is a cross-platform native runtime from Ionic that enables web apps to be deployed through the app store and provides access to native device API.
Ionic PWA elements is a companion package that will polyfill certain browser APIs that provide no user interface with custom Ionic UI.
Ionic PWA elements is a companion package that polyfills certain browser APIs that provide no user interface with custom Ionic UI.
With those packages installed, we can update our `main.ts` to include an additional bootstrapping call for the Ionic PWA Elements.
With those packages installed, update `main.ts` to include an additional bootstrapping call for the Ionic PWA Elements.
<$CodeTabs>
```ts name=src/main.ts
import { enableProdMode } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'
import { environment } from './environments/environment'
import { defineCustomElements } from '@ionic/pwa-elements/loader'
defineCustomElements(window)
if (environment.production) {
enableProdMode()
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.log(err))
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/main.ts"
lines={[[1, -1]]}
meta="name=src/main.ts"
/>
Then create an `AvatarComponent` with this Ionic CLI command:
```bash
ionic g component avatar --module=/src/app/account/account.module.ts --create-module
ionic g component avatar --module=/src/app/account/account.module.ts --create-module
```
<$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/avatar/avatar.component.ts"
lines={[[1, -1]]}
meta="name=src/app/avatar/avatar.component.ts"
/>
```ts name=src/app/avatar.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
import { SupabaseService } from '../supabase.service'
import { Camera, CameraResultType } from '@capacitor/camera'
import { addIcons } from 'ionicons'
import { person } from 'ionicons/icons'
@Component({
selector: 'app-avatar',
template: `
<div class="avatar_wrapper" (click)="uploadAvatar()">
<img *ngIf="_avatarUrl; else noAvatar" [src]="_avatarUrl" />
<ng-template #noAvatar>
<ion-icon name="person" class="no-avatar"></ion-icon>
</ng-template>
</div>
`,
style: [
`
:host {
display: block;
margin: auto;
min-height: 150px;
}
:host .avatar_wrapper {
margin: 16px auto 16px;
border-radius: 50%;
overflow: hidden;
height: 150px;
aspect-ratio: 1;
background: var(--ion-color-step-50);
border: thick solid var(--ion-color-step-200);
}
:host .avatar_wrapper:hover {
cursor: pointer;
}
:host .avatar_wrapper ion-icon.no-avatar {
width: 100%;
height: 115%;
}
:host img {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}
`,
],
})
export class AvatarComponent {
_avatarUrl: SafeResourceUrl | undefined
uploading = false
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/avatar/avatar.component.html"
lines={[[1, -1]]}
meta="name=src/app/avatar/avatar.component.html"
/>
@Input()
set avatarUrl(url: string | undefined) {
if (url) {
this.downloadImage(url)
}
}
@Output() upload = new EventEmitter<string>()
constructor(
private readonly supabase: SupabaseService,
private readonly dom: DomSanitizer
) {
addIcons({ person })
}
async downloadImage(path: string) {
try {
const { data, error } = await this.supabase.downLoadImage(path)
if (error) {
throw error
}
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data!))
} catch (error: any) {
console.error('Error downloading image: ', error.message)
}
}
async uploadAvatar() {
const loader = await this.supabase.createLoader()
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
})
const file = await fetch(photo.dataUrl!)
.then((res) => res.blob())
.then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }))
const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`
await loader.present()
const { error } = await this.supabase.uploadAvatar(fileName, file)
if (error) {
throw error
}
this.upload.emit(fileName)
} catch (error: any) {
this.supabase.createNotice(error.message)
} finally {
loader.dismiss()
}
}
}
```
</$CodeTabs>
### Add the new widget
And then, we can add the widget on top of the `AccountComponent` HTML template:
<$CodeTabs>
```ts name=src/app/account.component.ts
template: `
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<app-avatar
[avatarUrl]="this.profile?.avatar_url"
(upload)="updateProfile($event)"
></app-avatar>
<!-- input fields -->
`
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/avatar/avatar.component.scss"
lines={[[1, -1]]}
meta="name=src/app/avatar/avatar.component.scss"
/>
At this stage, you have a fully functional application!
@@ -12,14 +12,6 @@ This guide refers to upgrading the Postgres version of your Supabase Project. Fo
You can upgrade your project using in-place upgrades or by pausing and restoring your project.
<ShowUntil date="2024-12-17">
<Admonition type="tip">
The Migrating and Upgrading guide has been divided into two sections. To migrate between Supabase projects, see [Migrating within Supabase](/docs/guides/platform/migrating-within-supabase).
</Admonition>
</ShowUntil>
## In-place upgrades
<Admonition type="note">
File diff suppressed because it is too large. Load diff
+3
View File
@@ -7,6 +7,9 @@ export function ShowUntil({ children, date }: { children: ReactNode; date: strin
if (isNaN(untilDate.getTime()) || currentDate < untilDate) {
return <>{children}</>
} else {
console.error(
`[docs/features/ui/ShowUntil]: Component for ${date} expired, please update remove this note.`
)
return null
}
}
+4 -15
View File
@@ -487,12 +487,11 @@
"name": "desired_instance_size",
"required": false,
"in": "query",
"description": "Desired instance size",
"description": "Desired instance size. Omit this field to always default to the smallest possible size.",
"schema": {
"example": "nano",
"type": "string",
"enum": [
"pico",
"nano",
"micro",
"small",
@@ -6434,6 +6433,7 @@
"region": {
"type": "string",
"description": "Region you want your server to reside in. Use region_selection instead.",
"example": "us-east-1",
"deprecated": true,
"enum": [
"us-east-1",
@@ -6514,9 +6514,10 @@
"description": "This field is deprecated and is ignored in this request"
},
"desired_instance_size": {
"description": "Desired instance size. Omit this field to always default to the smallest possible size.",
"example": "nano",
"type": "string",
"enum": [
"pico",
"nano",
"micro",
"small",
@@ -6543,18 +6544,6 @@
"format": "uri",
"description": "Template URL used to create the project from the CLI.",
"example": "https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone"
},
"release_channel": {
"type": "string",
"enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"],
"description": "Release channel. If not provided, GA will be used.",
"deprecated": true
},
"postgres_engine": {
"type": "string",
"enum": ["15", "17", "17-oriole"],
"description": "Postgres engine version. If not provided, the latest version will be used.",
"deprecated": true
}
},
"required": ["db_pass", "name", "organization_slug"],
File diff suppressed because it is too large. Load diff
@@ -23,7 +23,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 88,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L88"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L88"
}
],
"type": {
@@ -42,7 +42,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 89,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L89"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L89"
}
],
"type": {
@@ -61,7 +61,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 90,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L90"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L90"
}
],
"type": {
@@ -80,7 +80,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 91,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L91"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L91"
}
],
"type": {
@@ -99,7 +99,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 92,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L92"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L92"
}
],
"type": {
@@ -118,7 +118,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 93,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L93"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L93"
}
],
"type": {
@@ -137,7 +137,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 94,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L94"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L94"
}
],
"type": {
@@ -156,7 +156,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 95,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L95"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L95"
}
],
"type": {
@@ -175,7 +175,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 96,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L96"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L96"
}
],
"type": {
@@ -194,7 +194,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 97,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L97"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L97"
}
],
"type": {
@@ -213,7 +213,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 98,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L98"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L98"
}
],
"type": {
@@ -232,7 +232,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 99,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L99"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L99"
}
],
"type": {
@@ -251,7 +251,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 100,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L100"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L100"
}
],
"type": {
@@ -270,7 +270,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 101,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L101"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L101"
}
],
"type": {
@@ -289,7 +289,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 102,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L102"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L102"
}
],
"type": {
@@ -309,7 +309,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 87,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L87"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L87"
}
]
},
@@ -339,7 +339,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"signatures": [
@@ -373,7 +373,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"parameters": [
@@ -416,7 +416,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 42,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L42"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L42"
}
],
"type": {
@@ -645,7 +645,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 41,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L41"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L41"
}
],
"type": {
@@ -682,7 +682,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 43,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L43"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L43"
}
],
"type": {
@@ -705,7 +705,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 40,
"character": 7,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L40"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L40"
}
]
}
@@ -735,7 +735,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 19,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L19"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L19"
}
],
"type": {
@@ -964,7 +964,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 17,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L17"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L17"
}
],
"type": {
@@ -1000,7 +1000,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 18,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L18"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L18"
}
],
"type": {
@@ -1023,7 +1023,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L16"
}
],
"type": {
@@ -1042,7 +1042,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"signatures": [
@@ -1076,7 +1076,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"typeParameters": [
@@ -1175,7 +1175,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"signatures": [
@@ -1209,7 +1209,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"parameters": [
@@ -1260,7 +1260,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 15,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L15"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L15"
}
]
},
@@ -1301,7 +1301,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L32"
}
],
"signatures": [
@@ -1316,7 +1316,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L32"
}
],
"parameters": [
@@ -1387,7 +1387,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1411,7 +1411,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 30,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L30"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L30"
}
],
"extendedTypes": [
@@ -1480,7 +1480,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L50"
}
],
"signatures": [
@@ -1495,7 +1495,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L50"
}
],
"parameters": [
@@ -1543,7 +1543,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1572,7 +1572,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 49,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L49"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L49"
}
],
"extendedTypes": [
@@ -1621,7 +1621,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L82"
}
],
"signatures": [
@@ -1636,7 +1636,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L82"
}
],
"parameters": [
@@ -1684,7 +1684,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1713,7 +1713,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 81,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L81"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L81"
}
],
"extendedTypes": [
@@ -1762,7 +1762,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L66"
}
],
"signatures": [
@@ -1777,7 +1777,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L66"
}
],
"parameters": [
@@ -1825,7 +1825,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1854,7 +1854,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 65,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L65"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L65"
}
],
"extendedTypes": [
@@ -1877,7 +1877,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L105"
}
],
"type": {
@@ -1910,7 +1910,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 121,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L121"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L121"
}
],
"type": {
@@ -2019,7 +2019,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L109"
}
],
"type": {
@@ -2035,7 +2035,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L109"
}
],
"indexSignatures": [
@@ -2050,7 +2050,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 14,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L109"
}
],
"parameters": [
@@ -2096,7 +2096,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 113,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L113"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L113"
}
],
"type": {
@@ -2146,7 +2146,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 117,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L117"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L117"
}
],
"type": {
@@ -2177,7 +2177,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 132,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L132"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L132"
}
],
"type": {
@@ -2211,7 +2211,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 137,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L137"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L137"
}
],
"type": {
@@ -2231,7 +2231,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 36,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L105"
}
]
}
@@ -2248,7 +2248,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L16"
}
],
"typeParameters": [
@@ -23,7 +23,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 88,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L88"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L88"
}
],
"type": {
@@ -42,7 +42,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 89,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L89"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L89"
}
],
"type": {
@@ -61,7 +61,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 90,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L90"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L90"
}
],
"type": {
@@ -80,7 +80,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 91,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L91"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L91"
}
],
"type": {
@@ -99,7 +99,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 92,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L92"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L92"
}
],
"type": {
@@ -118,7 +118,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 93,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L93"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L93"
}
],
"type": {
@@ -137,7 +137,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 94,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L94"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L94"
}
],
"type": {
@@ -156,7 +156,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 95,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L95"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L95"
}
],
"type": {
@@ -175,7 +175,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 96,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L96"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L96"
}
],
"type": {
@@ -194,7 +194,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 97,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L97"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L97"
}
],
"type": {
@@ -213,7 +213,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 98,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L98"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L98"
}
],
"type": {
@@ -232,7 +232,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 99,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L99"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L99"
}
],
"type": {
@@ -251,7 +251,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 100,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L100"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L100"
}
],
"type": {
@@ -270,7 +270,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 101,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L101"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L101"
}
],
"type": {
@@ -289,7 +289,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 102,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L102"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L102"
}
],
"type": {
@@ -309,7 +309,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 87,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L87"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L87"
}
]
},
@@ -339,7 +339,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"signatures": [
@@ -373,7 +373,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"parameters": [
@@ -416,7 +416,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 42,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L42"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L42"
}
],
"type": {
@@ -645,7 +645,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 41,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L41"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L41"
}
],
"type": {
@@ -682,7 +682,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 43,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L43"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L43"
}
],
"type": {
@@ -705,7 +705,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 40,
"character": 7,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L40"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L40"
}
]
}
@@ -735,7 +735,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 19,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L19"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L19"
}
],
"type": {
@@ -964,7 +964,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 17,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L17"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L17"
}
],
"type": {
@@ -1000,7 +1000,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 18,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L18"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L18"
}
],
"type": {
@@ -1023,7 +1023,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L16"
}
],
"type": {
@@ -1042,7 +1042,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"signatures": [
@@ -1076,7 +1076,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"typeParameters": [
@@ -1175,7 +1175,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"signatures": [
@@ -1209,7 +1209,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"parameters": [
@@ -1260,7 +1260,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 15,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/FunctionsClient.ts#L15"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/FunctionsClient.ts#L15"
}
]
},
@@ -1301,7 +1301,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L32"
}
],
"signatures": [
@@ -1316,7 +1316,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L32"
}
],
"parameters": [
@@ -1387,7 +1387,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1411,7 +1411,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 30,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L30"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L30"
}
],
"extendedTypes": [
@@ -1480,7 +1480,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L50"
}
],
"signatures": [
@@ -1495,7 +1495,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L50"
}
],
"parameters": [
@@ -1543,7 +1543,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1572,7 +1572,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 49,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L49"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L49"
}
],
"extendedTypes": [
@@ -1621,7 +1621,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L82"
}
],
"signatures": [
@@ -1636,7 +1636,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L82"
}
],
"parameters": [
@@ -1684,7 +1684,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1713,7 +1713,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 81,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L81"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L81"
}
],
"extendedTypes": [
@@ -1762,7 +1762,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L66"
}
],
"signatures": [
@@ -1777,7 +1777,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L66"
}
],
"parameters": [
@@ -1825,7 +1825,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1854,7 +1854,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 65,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L65"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L65"
}
],
"extendedTypes": [
@@ -1877,7 +1877,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L105"
}
],
"type": {
@@ -1910,7 +1910,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 121,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L121"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L121"
}
],
"type": {
@@ -2019,7 +2019,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L109"
}
],
"type": {
@@ -2035,7 +2035,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L109"
}
],
"indexSignatures": [
@@ -2050,7 +2050,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 14,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L109"
}
],
"parameters": [
@@ -2096,7 +2096,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 113,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L113"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L113"
}
],
"type": {
@@ -2146,7 +2146,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 117,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L117"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L117"
}
],
"type": {
@@ -2177,7 +2177,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 132,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L132"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L132"
}
],
"type": {
@@ -2211,7 +2211,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 137,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L137"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L137"
}
],
"type": {
@@ -2231,7 +2231,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 36,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L105"
}
]
}
@@ -2248,7 +2248,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/8416b1100ff3f4c63b99641e99ac1e3b993c0ddc/packages/core/functions-js/src/types.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c338e2fc24ce9e085f87a2e8d6d39d6dc0a2d97f/packages/core/functions-js/src/types.ts#L16"
}
],
"typeParameters": [
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1078,6 +1078,7 @@
"region": {
"type": "string",
"description": "Region you want your server to reside in. Use region_selection instead.",
"example": "us-east-1",
"deprecated": true,
"enum": [
"us-east-1",
@@ -1166,9 +1167,10 @@
"description": "This field is deprecated and is ignored in this request"
},
"desired_instance_size": {
"description": "Desired instance size. Omit this field to always default to the smallest possible size.",
"example": "nano",
"type": "string",
"enum": [
"pico",
"nano",
"micro",
"small",
@@ -1195,18 +1197,6 @@
"format": "uri",
"description": "Template URL used to create the project from the CLI.",
"example": "https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone"
},
"release_channel": {
"type": "string",
"enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"],
"description": "Release channel. If not provided, GA will be used.",
"deprecated": true
},
"postgres_engine": {
"type": "string",
"enum": ["15", "17", "17-oriole"],
"description": "Postgres engine version. If not provided, the latest version will be used.",
"deprecated": true
}
},
"required": ["db_pass", "name", "organization_slug"],
@@ -1344,12 +1334,11 @@
"name": "desired_instance_size",
"required": false,
"in": "query",
"description": "Desired instance size",
"description": "Desired instance size. Omit this field to always default to the smallest possible size.",
"schema": {
"example": "nano",
"type": "string",
"enum": [
"pico",
"nano",
"micro",
"small",
@@ -21970,6 +21959,7 @@
"region": {
"type": "string",
"description": "Region you want your server to reside in. Use region_selection instead.",
"example": "us-east-1",
"deprecated": true,
"enum": [
"us-east-1",
@@ -22058,9 +22048,10 @@
"description": "This field is deprecated and is ignored in this request"
},
"desired_instance_size": {
"description": "Desired instance size. Omit this field to always default to the smallest possible size.",
"example": "nano",
"type": "string",
"enum": [
"pico",
"nano",
"micro",
"small",
@@ -22087,18 +22078,6 @@
"format": "uri",
"description": "Template URL used to create the project from the CLI.",
"example": "https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone"
},
"release_channel": {
"type": "string",
"enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"],
"description": "Release channel. If not provided, GA will be used.",
"deprecated": true
},
"postgres_engine": {
"type": "string",
"enum": ["15", "17", "17-oriole"],
"description": "Postgres engine version. If not provided, the latest version will be used.",
"deprecated": true
}
},
"required": ["db_pass", "name", "organization_slug"],
File diff suppressed because it is too large. Load diff
+7 -23
View File
@@ -1,12 +1,12 @@
{
"rules": {
"react-hooks/exhaustive-deps": 208,
"react-hooks/exhaustive-deps": 205,
"import/no-anonymous-default-export": 57,
"@tanstack/query/exhaustive-deps": 13,
"@typescript-eslint/no-explicit-any": 1207,
"@typescript-eslint/no-explicit-any": 1205,
"no-restricted-imports": 59,
"no-restricted-exports": 296,
"react/no-unstable-nested-components": 69
"no-restricted-exports": 282,
"react/no-unstable-nested-components": 68
},
"ruleFiles": {
"react-hooks/exhaustive-deps": {
@@ -139,7 +139,6 @@
"hooks/analytics/useProjectUsageStats.tsx": 1,
"hooks/analytics/useTimeseriesUnixToIso.ts": 2,
"hooks/branches/useEdgeFunctionsDiff.ts": 1,
"hooks/misc/useReportDateRange.ts": 1,
"hooks/misc/useSchemaQueryState.ts": 1,
"hooks/misc/useTrackExperimentExposure.ts": 1,
"hooks/misc/useUpgradePrompt.tsx": 1,
@@ -153,7 +152,6 @@
"pages/project/[ref]/auth/policies.tsx": 2,
"pages/project/[ref]/building.tsx": 1,
"pages/project/[ref]/editor/[id].tsx": 1,
"pages/project/[ref]/integrations/index.tsx": 1,
"pages/project/[ref]/settings/jwt/legacy.tsx": 1,
"pages/project/[ref]/sql/quickstarts.tsx": 1,
"pages/project/[ref]/sql/templates.tsx": 1,
@@ -274,7 +272,6 @@
"components/interfaces/Auth/Policies/Policies.utils.ts": 1,
"components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditor/index.tsx": 2,
"components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx": 2,
"components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditorPanel/RLSCodeEditor.tsx": 2,
"components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx": 2,
@@ -305,7 +302,7 @@
"components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx": 1,
"components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx": 3,
"components/interfaces/Database/Indexes/Indexes.tsx": 1,
"components/interfaces/Database/Migrations/Migrations.tsx": 2,
"components/interfaces/Database/Migrations/Migrations.tsx": 1,
"components/interfaces/Database/Privileges/PrivilegesTable.tsx": 1,
"components/interfaces/Database/Publications/PublicationsList.tsx": 5,
"components/interfaces/Database/Publications/PublicationsTableItem.tsx": 5,
@@ -422,7 +419,6 @@
"components/interfaces/SQLEditor/useAddDefinitions.ts": 1,
"components/interfaces/Settings/Addons/Addons.tsx": 1,
"components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx": 1,
"components/interfaces/Settings/Addons/IPv4SidePanel.tsx": 1,
"components/interfaces/Settings/Addons/PITRSidePanel.tsx": 1,
"components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx": 2,
"components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx": 1,
@@ -437,7 +433,7 @@
"components/interfaces/Settings/Logs/LogTable.tsx": 2,
"components/interfaces/Settings/Logs/Logs.constants.ts": 1,
"components/interfaces/Settings/Logs/Logs.types.ts": 1,
"components/interfaces/Settings/Logs/Logs.utils.ts": 11,
"components/interfaces/Settings/Logs/Logs.utils.ts": 12,
"components/interfaces/Settings/Logs/LogsQueryPanel.tsx": 2,
"components/interfaces/Settings/Logs/PreviewFilterPanel.tsx": 1,
"components/interfaces/Settings/Logs/PreviewFilterPanelWithUniversal.tsx": 2,
@@ -714,6 +710,7 @@
"pages/integrations/vercel/[slug]/deploy-button/new-project.tsx": 1,
"pages/integrations/vercel/[slug]/marketplace/choose-project.tsx": 1,
"pages/new/index.tsx": 1,
"pages/partners/stripe/fabric/login.tsx": 1,
"pages/project/[ref]/auth/templates/[templateId].tsx": 1,
"pages/project/[ref]/functions/[functionSlug]/index.tsx": 3,
"pages/project/[ref]/settings/log-drains.tsx": 1,
@@ -802,10 +799,7 @@
"components/interfaces/Auth/Policies/PolicyEditor/PolicyAllowedOperation.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditor/PolicyDefinition.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditor/PolicyEditorFooter.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditor/index.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModalTitle.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx": 1,
"components/interfaces/Auth/Policies/PolicyReview.tsx": 1,
"components/interfaces/Auth/Policies/PolicySelection.tsx": 1,
"components/interfaces/Auth/Policies/PolicyTemplates/TemplatePreview.tsx": 1,
"components/interfaces/Auth/Policies/PolicyTemplates/TemplatesList.tsx": 1,
@@ -871,7 +865,6 @@
"components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx": 1,
"components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx": 1,
"components/interfaces/Organization/BillingSettings/CreditBalance.tsx": 1,
"components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/MembersExceedLimitModal.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 1,
@@ -929,21 +922,13 @@
"components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx": 1,
"components/interfaces/Settings/Addons/IPv4SidePanel.tsx": 1,
"components/interfaces/Settings/Addons/PITRSidePanel.tsx": 1,
"components/interfaces/Settings/Database/BannedIPs.tsx": 1,
"components/interfaces/Settings/Database/DatabaseSettings/ConfirmDisableReadOnlyModal.tsx": 1,
"components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx": 1,
"components/interfaces/Settings/Database/DiskSizeConfiguration.tsx": 1,
"components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx": 1,
"components/interfaces/Settings/Database/NetworkRestrictions/AddRestrictionModal.tsx": 1,
"components/interfaces/Settings/Database/NetworkRestrictions/AllowAllModal.tsx": 1,
"components/interfaces/Settings/Database/NetworkRestrictions/DisallowAllModal.tsx": 1,
"components/interfaces/Settings/Database/NetworkRestrictions/RemoveRestrictionModal.tsx": 1,
"components/interfaces/Settings/Database/SSLConfiguration.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/DNSRecord.tsx": 1,
"components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx": 1,
"components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 1,
@@ -971,7 +956,6 @@
"components/interfaces/Settings/Logs/UpgradePrompt.tsx": 1,
"components/interfaces/SignIn/PasswordConditionsHelper.tsx": 1,
"components/interfaces/Storage/StorageBucketsError.tsx": 1,
"components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx": 1,
"components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx": 1,
"components/interfaces/Storage/StoragePolicies/StoragePoliciesPlaceholder.tsx": 1,
"components/interfaces/Storage/StoragePolicies/StoragePoliciesReview.tsx": 1,
@@ -0,0 +1,150 @@
import { describe, expect, test } from 'vitest'
import { buildFilterFromCellValue, isComplexValue } from './FilterPopoverNew.utils'
describe('isComplexValue', () => {
test('returns false for null', () => {
expect(isComplexValue(null)).toBe(false)
})
test('returns false for undefined', () => {
expect(isComplexValue(undefined)).toBe(false)
})
test('returns false for strings', () => {
expect(isComplexValue('hello')).toBe(false)
expect(isComplexValue('')).toBe(false)
})
test('returns false for numbers', () => {
expect(isComplexValue(42)).toBe(false)
expect(isComplexValue(0)).toBe(false)
expect(isComplexValue(-1)).toBe(false)
expect(isComplexValue(3.14)).toBe(false)
})
test('returns false for booleans', () => {
expect(isComplexValue(true)).toBe(false)
expect(isComplexValue(false)).toBe(false)
})
test('returns true for empty objects', () => {
expect(isComplexValue({})).toBe(true)
})
test('returns true for objects with properties', () => {
expect(isComplexValue({ key: 'value' })).toBe(true)
expect(isComplexValue({ nested: { key: 'value' } })).toBe(true)
})
test('returns true for empty arrays', () => {
expect(isComplexValue([])).toBe(true)
})
test('returns true for arrays with items', () => {
expect(isComplexValue([1, 2, 3])).toBe(true)
expect(isComplexValue(['a', 'b'])).toBe(true)
expect(isComplexValue([{ key: 'value' }])).toBe(true)
})
})
describe('buildFilterFromCellValue', () => {
test('creates filter with equals operator for string values', () => {
const filter = buildFilterFromCellValue('name', 'Alice')
expect(filter).toEqual({
column: 'name',
operator: '=',
value: 'Alice',
})
})
test('creates filter with equals operator for empty string', () => {
const filter = buildFilterFromCellValue('label', '')
expect(filter).toEqual({
column: 'label',
operator: '=',
value: '',
})
})
test('creates filter with equals operator for numbers', () => {
const filter = buildFilterFromCellValue('age', 42)
expect(filter).toEqual({
column: 'age',
operator: '=',
value: '42',
})
})
test('creates filter with equals operator for zero', () => {
const filter = buildFilterFromCellValue('count', 0)
expect(filter).toEqual({
column: 'count',
operator: '=',
value: '0',
})
})
test('creates filter with equals operator for negative numbers', () => {
const filter = buildFilterFromCellValue('balance', -100)
expect(filter).toEqual({
column: 'balance',
operator: '=',
value: '-100',
})
})
test('creates filter with equals operator for boolean true', () => {
const filter = buildFilterFromCellValue('active', true)
expect(filter).toEqual({
column: 'active',
operator: '=',
value: 'true',
})
})
test('creates filter with equals operator for boolean false', () => {
const filter = buildFilterFromCellValue('active', false)
expect(filter).toEqual({
column: 'active',
operator: '=',
value: 'false',
})
})
test('creates filter with is operator for null', () => {
const filter = buildFilterFromCellValue('name', null)
expect(filter).toEqual({
column: 'name',
operator: 'is',
value: 'null',
})
})
test('creates filter with is operator for undefined', () => {
const filter = buildFilterFromCellValue('name', undefined)
expect(filter).toEqual({
column: 'name',
operator: 'is',
value: 'null',
})
})
test('creates filter for date strings', () => {
const filter = buildFilterFromCellValue('created_at', '2024-01-01')
expect(filter).toEqual({
column: 'created_at',
operator: '=',
value: '2024-01-01',
})
})
test('creates filter for datetime strings', () => {
const filter = buildFilterFromCellValue('updated_at', '2024-01-01T12:00:00Z')
expect(filter).toEqual({
column: 'updated_at',
operator: '=',
value: '2024-01-01T12:00:00Z',
})
})
})
@@ -1,4 +1,4 @@
import type { SupaColumn } from 'components/grid/types'
import type { Filter, SupaColumn } from 'components/grid/types'
import {
isBoolColumn,
isDateColumn,
@@ -59,6 +59,19 @@ const BOOLEAN_OPERATORS: OperatorDefinition[] = [
{ value: 'is', label: 'Is', group: 'setNull' },
]
export function isComplexValue(value: unknown): boolean {
return value !== null && value !== undefined && typeof value === 'object'
}
export function buildFilterFromCellValue(columnKey: string, value: unknown): Filter {
const isNullish = value === null || value === undefined
return {
column: columnKey,
operator: isNullish ? 'is' : '=',
value: isNullish ? 'null' : String(value),
}
}
export function columnToFilterProperty(column: SupaColumn): FilterProperty {
// For enum columns, use the enum values as options
if (isEnumColumn(column.dataType) && column.enum) {
@@ -2,17 +2,21 @@ import { useQueryClient } from '@tanstack/react-query'
import { ROW_CONTEXT_MENU_ID } from 'components/grid/constants'
import type { SupaRow } from 'components/grid/types'
import { queueRowDeletesWithOptimisticUpdate } from 'components/grid/utils/queueOperationUtils'
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
useIsQueueOperationsEnabled,
useIsTableFilterBarEnabled,
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Copy, Edit, Trash } from 'lucide-react'
import { Copy, Edit, ListFilter, Trash } from 'lucide-react'
import { useCallback } from 'react'
import { Item, ItemParams, Menu } from 'react-contexify'
import { toast } from 'sonner'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { DialogSectionSeparator, copyToClipboard } from 'ui'
import { copyToClipboard, DialogSectionSeparator } from 'ui'
import { formatClipboardValue } from '../../utils/common'
import { buildFilterFromCellValue, isComplexValue } from '../header/filter/FilterPopoverNew.utils'
type RowContextMenuProps = {
rows: SupaRow[]
@@ -26,6 +30,7 @@ export const RowContextMenu = ({ rows }: RowContextMenuProps) => {
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const isTableFilterBarEnabled = useIsTableFilterBarEnabled()
function onDeleteRow(p: RowContextMenuItemProps) {
const rowIdx = p.props?.rowIdx
@@ -88,6 +93,49 @@ export const RowContextMenu = ({ rows }: RowContextMenuProps) => {
[rows]
)
const getRowAndColumn = useCallback(
(rowIdx: number | undefined | null) => {
if (!snap.selectedCellPosition || rowIdx === undefined || rowIdx === null) {
return null
}
const row = rows[rowIdx]
const column = snap.gridColumns[snap.selectedCellPosition.idx as number]
if (!row || !column) return null
return { row, column }
},
[rows, snap.selectedCellPosition, snap.gridColumns]
)
const onFilterByValue = useCallback(
(p: RowContextMenuItemProps) => {
const result = getRowAndColumn(p.props?.rowIdx)
if (!result) return
const { row, column } = result
const newFilter = buildFilterFromCellValue(column.key, row[column.key])
snap.setFilters([...snap.filters, newFilter])
const displayValue = newFilter.value === 'null' ? 'NULL' : newFilter.value
toast.success(`Filtering ${column.name} by ${displayValue}`)
},
[getRowAndColumn, snap]
)
const isFilterByValueHidden = useCallback(
({ props: itemProps }: { props?: { rowIdx: number } }) => {
if (!isTableFilterBarEnabled) return true
const result = getRowAndColumn(itemProps?.rowIdx)
if (!result) return true
return isComplexValue(result.row[result.column.key])
},
[isTableFilterBarEnabled, getRowAndColumn]
)
return (
<Menu id={ROW_CONTEXT_MENU_ID} animation={false} className="!min-w-36">
<Item onClick={onCopyCellContent}>
@@ -98,6 +146,10 @@ export const RowContextMenu = ({ rows }: RowContextMenuProps) => {
<Copy size={12} />
<span className="ml-2 text-xs">Copy row</span>
</Item>
<Item onClick={onFilterByValue} hidden={isFilterByValueHidden}>
<ListFilter size={12} />
<span className="ml-2 text-xs">Filter by value</span>
</Item>
{/* We can't just wrap this entire section in a fragment conditional
on snap.editable because of a bug in react-contexify. Only the
@@ -41,6 +41,8 @@ import {
getMetricValues,
} from './OverviewUsage.constants'
import { getStatusColor } from 'components/ui/DataTable/DataTable.utils'
import { ErrorCodeTooltip } from '../../Settings/Logs/ErrorCodeTooltip'
import { Service } from 'data/graphql/graphql'
import { AiIconAnimation } from 'ui'
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
@@ -398,9 +400,11 @@ export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsPr
header: 'Error code',
className: 'w-full',
render: (row) => (
<span className="line-clamp-1 font-mono uppercase text-xs inline-flex text-foreground-light">
{row.error_code}
</span>
<ErrorCodeTooltip errorCode={row.error_code} service={Service.Auth}>
<span className="line-clamp-1 font-mono uppercase text-xs inline-flex text-foreground-light cursor-default">
{row.error_code}
</span>
</ErrorCodeTooltip>
),
},
{
@@ -1,18 +1,17 @@
import type { PostgresPolicy } from '@supabase/postgres-meta'
import { ChevronDown, PanelLeftClose, PanelRightClose, X } from 'lucide-react'
import { useState } from 'react'
import {
cn,
Collapsible_Shadcn_,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
SheetClose,
SheetHeader,
SheetTitle,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
} from 'ui'
export const PolicyEditorPanelHeader = ({
@@ -119,6 +118,7 @@ export const PolicyEditorPanelHeader = ({
) : (
<PanelRightClose size={19} strokeWidth={1} />
)}
<span className="sr-only">{showTools ? 'Hide' : 'Show'} tools</span>
</button>
</TooltipTrigger>
<TooltipContent side="left">{showTools ? 'Hide' : 'Show'} tools</TooltipContent>
@@ -1,11 +1,5 @@
import { yupResolver } from '@hookform/resolvers/yup'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as yup from 'yup'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { InlineLink } from 'components/ui/InlineLink'
@@ -13,21 +7,27 @@ import NoPermission from 'components/ui/NoPermission'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Card,
CardContent,
CardFooter,
cn,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
PrePostTab,
Switch,
cn,
} from 'ui'
import { Admonition, PageSection, PageSectionContent } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as yup from 'yup'
import { urlRegex } from '../Auth.constants'
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
@@ -61,25 +61,34 @@ export const SmtpForm = () => {
)
const smtpSchema = yup.object({
SMTP_ADMIN_EMAIL: yup.string().when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema.email('Must be a valid email').required('Sender email address is required'),
otherwise: (schema) => schema,
}),
SMTP_SENDER_NAME: yup.string().when('ENABLE_SMTP', {
is: true,
then: (schema) => schema.required('Sender name is required'),
otherwise: (schema) => schema,
}),
SMTP_HOST: yup.string().when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema
.matches(urlRegex({ excludeSimpleDomains: false }), 'Must be a valid URL or IP address')
.required('Host URL is required.'),
otherwise: (schema) => schema,
}),
SMTP_ADMIN_EMAIL: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema.email('Must be a valid email').required('Sender email address is required'),
otherwise: (schema) => schema,
}),
SMTP_SENDER_NAME: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) => schema.required('Sender name is required'),
otherwise: (schema) => schema,
}),
SMTP_HOST: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema
.matches(urlRegex({ excludeSimpleDomains: false }), 'Must be a valid URL or IP address')
.required('Host URL is required.'),
otherwise: (schema) => schema,
}),
SMTP_PORT: yup.number().when('ENABLE_SMTP', {
is: true,
then: (schema) =>
@@ -98,12 +107,15 @@ export const SmtpForm = () => {
.max(32767, 'Must not be more than 32,767 an hour'),
otherwise: (schema) => schema,
}),
SMTP_USER: yup.string().when('ENABLE_SMTP', {
is: true,
then: (schema) => schema.required('SMTP Username is required'),
otherwise: (schema) => schema,
}),
SMTP_PASS: yup.string(),
SMTP_USER: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) => schema.required('SMTP Username is required'),
otherwise: (schema) => schema,
}),
SMTP_PASS: yup.string().trim(),
ENABLE_SMTP: yup.boolean().required(),
})
@@ -1,6 +1,7 @@
import { type OptimizedSearchColumns } from '@supabase/pg-meta'
import { BASE_PATH } from 'lib/constants'
import { PROVIDER_PHONE, PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation'
import { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
export type Filter = 'all' | 'verified' | 'unverified' | 'anonymous'
@@ -1,15 +1,15 @@
import { OptimizedSearchColumns } from '@supabase/pg-meta'
import { THRESHOLD_COUNT } from '@supabase/pg-meta/src/sql/studio/get-count-estimate'
import { keepPreviousData } from '@tanstack/react-query'
import { HelpCircle, Loader2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
import { useParams } from 'common'
import { formatEstimatedCount } from 'components/grid/components/footer/pagination/Pagination.utils'
import { useUsersCountQuery } from 'data/auth/users-count-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { HelpCircle, Loader2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import type { Filter, SpecificFilterColumn } from './Users.constants'
interface UsersFooterProps {
@@ -1,5 +1,5 @@
import pgMeta from '@supabase/pg-meta'
import type { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
import { USER_SEARCH_INDEXES } from '@supabase/pg-meta'
import type { OptimizedSearchColumns } from '@supabase/pg-meta'
import { keepPreviousData, useQueryClient } from '@tanstack/react-query'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
@@ -246,7 +246,7 @@ export const UsersV2 = () => {
const userSearchIndexesAreValidAndReady =
!isUserSearchIndexesError &&
!isUserSearchIndexesLoading &&
userSearchIndexes?.length === pgMeta.USER_SEARCH_INDEXES.length &&
userSearchIndexes?.length === USER_SEARCH_INDEXES.length &&
userSearchIndexes?.every((index) => index.is_valid && index.is_ready)
/**
@@ -226,7 +226,7 @@ export const SchemaGraph = () => {
<div className="flex items-center gap-x-2">
<ButtonTooltip
type="outline"
icon={copied ? <Check /> : <Copy />}
icon={copied ? <Check data-testid="copy-sql-ready" /> : <Copy />}
onClick={() => {
if (tables) {
copyToClipboard(tablesToSQL(tables))
@@ -66,13 +66,13 @@ export const HomePageActions = ({ slug: _slug, hideNewProject = false }: HomePag
}, [sortStorage, isSuccessSortStorage, setSort, slug])
return (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center justify-between gap-2 w-full">
<div className="flex flex-col gap-2 min-w-0 flex-1 basis-full md:basis-auto sm:flex-row sm:flex-wrap sm:items-center">
<Input
placeholder="Search for a project"
icon={<Search />}
size="tiny"
className="w-32 md:w-64"
className="w-full sm:w-32 md:w-64"
value={search}
onChange={(event) => setSearch(event.target.value)}
actions={[
@@ -89,32 +89,34 @@ export const HomePageActions = ({ slug: _slug, hideNewProject = false }: HomePag
]}
/>
<FilterPopover
name="Status"
title="Filter projects by status"
options={[
{ key: PROJECT_STATUS.ACTIVE_HEALTHY, label: 'Active' },
{ key: PROJECT_STATUS.INACTIVE, label: 'Paused' },
]}
activeOptions={filterStatus}
valueKey="key"
labelKey="label"
onSaveFilters={(options) => setFilterStatusStorage(options)}
/>
<div className="flex items-center gap-2">
<FilterPopover
name="Status"
title="Filter projects by status"
options={[
{ key: PROJECT_STATUS.ACTIVE_HEALTHY, label: 'Active' },
{ key: PROJECT_STATUS.INACTIVE, label: 'Paused' },
]}
activeOptions={filterStatus}
valueKey="key"
labelKey="label"
onSaveFilters={(options) => setFilterStatusStorage(options)}
/>
<SortDropdown
options={[
{ label: 'name', value: 'name' },
{ label: 'creation date', value: 'created' },
]}
value={sort}
setValue={(val) => setSortStorage(val as ProjectListSort)}
/>
<SortDropdown
options={[
{ label: 'name', value: 'name' },
{ label: 'creation date', value: 'created' },
]}
value={sort}
setValue={(val) => setSortStorage(val as ProjectListSort)}
/>
{isFetchingProjects && <Loader2 className="animate-spin" size={14} />}
{isFetchingProjects && <Loader2 className="animate-spin" size={14} />}
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-shrink-0">
{viewMode && setViewMode && (
<ToggleGroup
type="single"
@@ -1,5 +1,4 @@
import Panel from 'components/ui/Panel'
import { usePHFlag } from 'hooks/ui/useFlag'
import Link from 'next/link'
import { UseFormReturn } from 'react-hook-form'
import {
@@ -22,10 +21,6 @@ interface SecurityOptionsProps {
}
export const SecurityOptions = ({ form, layout = 'horizontal' }: SecurityOptionsProps) => {
const rlsExperimentVariant = usePHFlag<'control' | 'test' | false | undefined>(
'projectCreationEnableRlsEventTrigger'
)
const shouldShowEnableRlsEventTrigger = rlsExperimentVariant === 'test'
const dataApi = useWatch_Shadcn_({ control: form.control, name: 'dataApi' })
return (
@@ -65,32 +60,30 @@ export const SecurityOptions = ({ form, layout = 'horizontal' }: SecurityOptions
)}
/>
{shouldShowEnableRlsEventTrigger && (
<FormField_Shadcn_
name="enableRlsEventTrigger"
control={form.control}
render={({ field }) => (
<FormItem_Shadcn_ className="flex items-start gap-3">
<FormControl_Shadcn_>
<Checkbox_Shadcn_
checked={field.value}
disabled={field.disabled}
onCheckedChange={(value) => field.onChange(value === true)}
/>
</FormControl_Shadcn_>
<div className="space-y-1">
<FormLabel_Shadcn_ className="text-sm text-foreground">
Enable automatic RLS
</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-foreground-lighter">
Create an event trigger that automatically enables Row Level Security on all
new tables in the public schema.
</FormDescription_Shadcn_>
</div>
</FormItem_Shadcn_>
)}
/>
)}
<FormField_Shadcn_
name="enableRlsEventTrigger"
control={form.control}
render={({ field }) => (
<FormItem_Shadcn_ className="flex items-start gap-3">
<FormControl_Shadcn_>
<Checkbox_Shadcn_
checked={field.value}
disabled={field.disabled}
onCheckedChange={(value) => field.onChange(value === true)}
/>
</FormControl_Shadcn_>
<div className="space-y-1">
<FormLabel_Shadcn_ className="text-sm text-foreground">
Enable automatic RLS
</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-foreground-lighter">
Create an event trigger that automatically enables Row Level Security on all new
tables in the public schema.
</FormDescription_Shadcn_>
</div>
</FormItem_Shadcn_>
)}
/>
{!dataApi && (
<Admonition
@@ -1,7 +1,6 @@
import { useParams } from 'common'
import { COUNTRY_LAT_LON } from 'components/interfaces/ProjectCreation/ProjectCreation.constants'
import {
MAP_CHART_THEME,
buildCountsByIso2,
computeMarkerRadius,
extractIso2FromFeatureProps,
@@ -10,10 +9,11 @@ import {
isKnownCountryCode,
isMicroCountry,
iso2ToCountryName,
MAP_CHART_THEME,
} from 'components/interfaces/Reports/utils/geo'
import {
TextFormatter,
jsonSyntaxHighlight,
TextFormatter,
} from 'components/interfaces/Settings/Logs/LogsFormatters'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
@@ -28,20 +28,20 @@ import { Fragment, useRef, useState } from 'react'
import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps'
import type { ResponseError } from 'types'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Collapsible,
Collapsible_Shadcn_,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
WarningIcon,
} from 'ui'
import * as z from 'zod'
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
import { queryParamsToObject } from '../Reports.utils'
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
export const NetworkTrafficRenderer = (
props: ReportWidgetProps<{
@@ -453,10 +453,6 @@ export const RequestsByCountryMapRenderer = (
const iso2 = extractIso2FromFeatureProps(
(geo.properties || undefined) as Record<string, unknown> | undefined
)
// Skip Antarctica entirely (causes hover issues)
if ((title || '').toLowerCase() === 'antarctica') {
return null
}
const value = iso2 ? countsByIso2[iso2] || 0 : 0
const baseOpacity = getFillOpacity(value, max, theme)
const tooltipTitle = title
@@ -0,0 +1,144 @@
import Image from 'next/image'
import Link from 'next/link'
import { useState } from 'react'
import { useTheme } from 'next-themes'
import { ExternalLink } from 'lucide-react'
import { useErrorCodesQuery } from 'data/content-api/docs-error-codes-query'
import { Service } from 'data/graphql/graphql'
import { BASE_PATH } from 'lib/constants'
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { AiAssistantDropdown } from 'components/ui/AiAssistantDropdown'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
import {
cn,
DropdownMenuItem,
DropdownMenuSeparator,
HoverCard_Shadcn_,
HoverCardContent_Shadcn_,
HoverCardTrigger_Shadcn_,
InfoIcon,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
const SERVICE_DOCS_URLS: Partial<Record<Service, string>> = {
[Service.Auth]: 'https://supabase.com/docs/guides/auth/debugging/error-codes',
}
interface ErrorCodeTooltipProps {
errorCode: string
service?: Service
children: React.ReactNode
}
export const ErrorCodeTooltip = ({ errorCode, service, children }: ErrorCodeTooltipProps) => {
const [isOpen, setIsOpen] = useState(false)
const { resolvedTheme } = useTheme()
const snap = useAiAssistantStateSnapshot()
const { openSidebar } = useSidebarManagerSnapshot()
const { data, isPending } = useErrorCodesQuery({ code: errorCode, service }, { enabled: isOpen })
const errors = data?.errors?.nodes?.filter((e) => !!e.message) ?? []
const docsUrl =
errors.map((e) => SERVICE_DOCS_URLS[e.service]).find(Boolean) ??
(service ? SERVICE_DOCS_URLS[service] : undefined)
const buildPrompt = () => {
const description = errors[0]?.message
const servicePart = service ? ` in Supabase ${service}` : ''
const descriptionPart = description ? `\n\nError description: ${description}` : ''
return `I'm encountering error code \`${errorCode}\`${servicePart}.${descriptionPart}\n\nCan you explain what this error means and suggest steps to fix it?`
}
const handleOpenAssistant = () => {
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
snap.newChat({
name: `Fix error ${errorCode}`,
initialMessage: buildPrompt(),
})
}
return (
<HoverCard_Shadcn_ open={isOpen} onOpenChange={setIsOpen} openDelay={200} closeDelay={100}>
<HoverCardTrigger_Shadcn_ asChild>
<span className="inline-flex items-center gap-1 cursor-default">
{children}
<InfoIcon hideBackground className="w-3.5 h-3.5 shrink-0 fill-foreground-muted" />
</span>
</HoverCardTrigger_Shadcn_>
<HoverCardContent_Shadcn_ side="top" align="center" className="w-[360px] p-0 overflow-hidden">
<div className="flex flex-col">
<div className="px-4 pt-3 pb-2.5 border-b border-border">
<p className="font-mono text-xs uppercase text-foreground-light truncate max-w-[40ch]">
{errorCode}
</p>
</div>
<div className="px-4 py-3 space-y-2">
{isPending ? (
<div className="space-y-1.5">
<ShimmeringLoader className="w-full" />
<ShimmeringLoader className="w-4/5" />
<ShimmeringLoader className="w-3/5" />
</div>
) : errors.length === 0 ? (
<p className="text-sm text-foreground-lighter">
No description available for this error code.
</p>
) : (
<p className="text-sm text-foreground leading-relaxed">{errors[0].message}</p>
)}
</div>
<div
className={cn(
'border-t border-border px-4 py-2.5 flex items-center',
docsUrl ? 'justify-between' : 'justify-end'
)}
>
{docsUrl && (
<div className="flex items-center gap-1.5">
<Image
src={
resolvedTheme?.includes('dark')
? `${BASE_PATH}/img/supabase-dark.svg`
: `${BASE_PATH}/img/supabase-light.svg`
}
alt="Supabase"
height={14}
width={72}
/>
<span className="font-mono text-[11px] font-semibold text-brand tracking-wide">
DOCS
</span>
</div>
)}
<AiAssistantDropdown
label="Fix with AI"
buildPrompt={buildPrompt}
onOpenAssistant={handleOpenAssistant}
copyLabel="Copy Markdown"
showExternalAI
extraDropdownItems={
docsUrl ? (
<>
<DropdownMenuItem asChild className="gap-2">
<Link href={docsUrl} target="_blank" rel="noreferrer">
<ExternalLink size={14} />
Go to Docs
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
) : undefined
}
/>
</div>
</div>
</HoverCardContent_Shadcn_>
</HoverCard_Shadcn_>
)
}
@@ -1,8 +1,10 @@
import { Column } from 'react-data-grid'
import { Service } from 'data/graphql/graphql'
import type { LogData } from '../Logs.types'
import { ResponseCodeFormatter, RowLayout, TextFormatter } from '../LogsFormatters'
import { defaultRenderCell } from './DefaultPreviewColumnRenderer'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import { ErrorCodeTooltip } from '../ErrorCodeTooltip'
const columns: Column<LogData>[] = [
{
@@ -13,12 +15,24 @@ const columns: Column<LogData>[] = [
if (!props.row.status_code && !props.row.method && !props.row.path) {
return defaultRenderCell(props)
}
const error_code = props.row.metadata?.[0]?.response?.[0]?.headers?.[0]?.x_sb_error_code
const service =
typeof props.row.path === 'string' && props.row.path.startsWith('/auth/')
? Service.Auth
: undefined
return (
<RowLayout>
<TimestampInfo utcTimestamp={props.row.timestamp!} />
<ResponseCodeFormatter value={String(props.row.status_code)} />
<TextFormatter value={String(props.row.method)} />
<TextFormatter value={String(props.row.path)} />
{error_code && (
<ErrorCodeTooltip errorCode={String(error_code)} service={service}>
<span className="font-mono text-xs text-destructive-600 truncate shrink-0">
{error_code}
</span>
</ErrorCodeTooltip>
)}
</RowLayout>
)
},
@@ -15,6 +15,7 @@ import {
import { TimestampInfo } from 'ui-patterns'
import { ErrorCodeDialog } from '../ErrorCodeDialog'
import { ErrorCodeTooltip } from '../ErrorCodeTooltip'
import type { LogSearchCallback, PreviewLogData } from '../Logs.types'
import { ResponseCodeFormatter } from '../LogsFormatters'
@@ -60,6 +61,7 @@ const PropertyRow = ({
const isUserAgent = keyName === 'user_agent'
const isEventMessage = keyName === 'event_message'
const isPath = keyName === 'path'
const isErrorCode = keyName === 'error_code'
function getSearchPairs() {
if (isSearch && typeof value === 'string') {
@@ -163,6 +165,10 @@ const PropertyRow = ({
<div className="flex items-center gap-1 justify-end">
<ResponseCodeFormatter value={value} />
</div>
) : isErrorCode ? (
<ErrorCodeTooltip errorCode={String(value)} service={service}>
<div className="truncate">{value}</div>
</ErrorCodeTooltip>
) : (
<div className="truncate">{value}</div>
)}
@@ -6,8 +6,7 @@ import dayjs, { Dayjs } from 'dayjs'
import { get } from 'lodash'
import uniqBy from 'lodash/uniqBy'
import { useEffect } from 'react'
import logConstants from 'shared-data/logConstants'
import logConstants from 'shared-data/log-constants'
import { LogsTableName, SQL_FILTER_TEMPLATES } from './Logs.constants'
import type { Filters, LogData, LogsEndpointParams } from './Logs.types'
@@ -237,6 +237,11 @@ export const FileExplorerRow = ({
],
},
]),
{
name: 'Download',
icon: <Download size={14} strokeWidth={1} />,
onClick: () => downloadFile(itemWithColumnIndex),
},
...(canUpdateFiles
? [
{
@@ -249,11 +254,6 @@ export const FileExplorerRow = ({
icon: <Move size={14} strokeWidth={1} />,
onClick: () => setSelectedItemsToMove([itemWithColumnIndex]),
},
{
name: 'Download',
icon: <Download size={14} strokeWidth={1} />,
onClick: () => downloadFile(itemWithColumnIndex),
},
{ name: 'Separator', icon: undefined, onClick: undefined },
]
: []),
@@ -83,6 +83,10 @@ export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => {
</Item>
</Submenu>
)}
<Item onClick={({ props }) => onHandleClick('download', props.item)}>
<Download size={14} strokeWidth={1} />
<span className="ml-2 text-xs">Download</span>
</Item>
{canUpdateFiles && [
<Item key="rename-file" onClick={({ props }) => onHandleClick('rename', props.item)}>
<Edit size={14} strokeWidth={1} />
@@ -92,10 +96,6 @@ export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => {
<Move size={14} strokeWidth={1} />
<span className="ml-2 text-xs">Move</span>
</Item>,
<Item key="download-file" onClick={({ props }) => onHandleClick('download', props.item)}>
<Download size={14} strokeWidth={1} />
<span className="ml-2 text-xs">Download</span>
</Item>,
<Separator key="file-separator" />,
<Item key="delete-file" onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
<Trash2 size={14} strokeWidth={1} stroke="red" />
@@ -1,13 +1,13 @@
import { compact, get, isEmpty, uniqBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import type { Bucket } from 'data/storage/buckets-query'
import { useLatest } from 'hooks/misc/useLatest'
import { IS_PLATFORM } from 'lib/constants'
import { compact, get, isEmpty, uniqBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import { useSelectedBucket } from '../FilesBuckets/useSelectedBucket'
import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
import { ConfirmDeleteModal } from './ConfirmDeleteModal'
@@ -17,11 +17,12 @@ import { FileExplorerHeader } from './FileExplorerHeader'
import { FileExplorerHeaderSelection } from './FileExplorerHeaderSelection'
import { MoveItemsModal } from './MoveItemsModal'
import { PreviewPane } from './PreviewPane'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
export const StorageExplorer = () => {
const { ref, bucketId } = useParams()
const storageExplorerRef = useRef(null)
const {
bucketId,
projectRef,
view,
columns,
@@ -29,7 +30,6 @@ export const StorageExplorer = () => {
openedFolders,
selectedItemsToMove,
selectedBucket,
openBucket,
fetchFolderContents,
fetchMoreFolderContents,
fetchFoldersByPath,
@@ -43,7 +43,7 @@ export const StorageExplorer = () => {
setSelectedItemsToMove,
} = useStorageExplorerStateSnapshot()
useProjectStorageConfigQuery({ projectRef }, { enabled: IS_PLATFORM })
useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const { data: bucket, isLoading: isBucketQueryLoading } = useSelectedBucket()
// Detect when transitioning between buckets to avoid showing stale content from the previous bucket.
@@ -93,12 +93,7 @@ export const StorageExplorer = () => {
useEffect(() => {
if (bucket && projectRef) fetchContents(bucket)
}, [bucketId, bucket, projectRef, debouncedSearchString, fetchContents])
const openBucketRef = useLatest(openBucket)
useEffect(() => {
if (bucket && !!projectRef) openBucketRef.current(bucket)
}, [bucketId, bucket, projectRef, openBucketRef])
}, [bucket, projectRef, debouncedSearchString, selectedBucket.id, fetchContents])
/** Checkbox selection methods */
/** [Joshen] We'll only support checkbox selection for files ONLY */
@@ -10,7 +10,8 @@ const HeaderTitle: React.FC<Props> = ({ table, column }) => {
if (!column) {
return (
<>
Add new column to <code>{table.name}</code>
<span>Add new column to</span>
<code className="ml-1">{table.name}</code>
</>
)
}
@@ -7,6 +7,7 @@ import {
} from 'components/grid/utils/queueOperationUtils'
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { type GeneratedPolicy } from 'components/interfaces/Auth/Policies/Policies.utils'
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { databasePoliciesKeys } from 'data/database-policies/keys'
import { useDatabasePublicationCreateMutation } from 'data/database-publications/database-publications-create-mutation'
import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query'
@@ -28,9 +29,7 @@ import { RetrieveTableResult } from 'data/tables/table-retrieve-query'
import { getTables } from 'data/tables/tables-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { isValidExperimentVariant } from 'hooks/misc/useTableCreateGeneratePolicies'
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
import { usePHFlag } from 'hooks/ui/useFlag'
import { useUrlState } from 'hooks/ui/useUrlState'
import { useTrack } from 'lib/telemetry/track'
import { isEmpty, isUndefined, noop } from 'lodash'
@@ -41,7 +40,6 @@ import { useTableEditorStateSnapshot, type TableEditorState } from 'state/table-
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import type { Dictionary } from 'types'
import { SonnerProgress } from 'ui'
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { ColumnEditor } from './ColumnEditor/ColumnEditor'
import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types'
@@ -206,7 +204,6 @@ export const SidePanelEditor = ({
const getImpersonatedRoleState = useGetImpersonatedRoleState()
const isApiGrantTogglesEnabled = useDataApiGrantTogglesEnabled()
const generatePoliciesFlag = usePHFlag<string>('tableCreateGeneratePolicies')
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const [isEdited, setIsEdited] = useState<boolean>(false)
@@ -739,17 +736,6 @@ export const SidePanelEditor = ({
toast.success(`Table ${table.name} is good to go!`, { id: toastId })
}
// Track experiment conversion if user is in the experiment
if (isValidExperimentVariant(generatePoliciesFlag)) {
track('table_create_generate_policies_experiment_converted', {
experiment_id: 'tableCreateGeneratePolicies',
variant: generatePoliciesFlag,
has_rls_enabled: isRLSEnabled,
has_rls_policies: generatedPolicies.length > 0,
has_generated_policies: generatedPolicies.length > 0,
})
}
onTableCreated(table)
} else if (action === 'duplicate' && !!selectedTable) {
const tableToDuplicate = selectedTable
@@ -195,10 +195,7 @@ export const SpreadsheetImport = ({
header={
selectedTable !== undefined ? (
<>
Add data to{' '}
<code className="text-sm">
{selectedTable.schema}.{selectedTable.name}
</code>
Add data to <code className="text-sm ml-1">{selectedTable.name}</code>
</>
) : (
'Add content to new table'
@@ -1,105 +0,0 @@
import { X } from 'lucide-react'
import {
Badge,
Card,
cn,
HoverCard_Shadcn_,
HoverCardContent_Shadcn_,
HoverCardTrigger_Shadcn_,
SimpleCodeBlock,
} from 'ui'
export interface PolicyListItemData {
name: string
command?: string | null
sql?: string | null
isNew?: boolean
}
interface PolicyListItemProps {
policy: PolicyListItemData
onRemove?: () => void
}
/**
* A shared component for displaying policy items with hover preview
* Used in RLSManagement and policy creation toast notifications
*/
export const PolicyListItem = ({ policy, onRemove }: PolicyListItemProps) => {
return (
<HoverCard_Shadcn_ openDelay={200} closeDelay={100}>
<HoverCardTrigger_Shadcn_ asChild>
<div
className={cn(
'flex items-center justify-between text-sm text-foreground py-2 px-3 hover:bg-surface-100 transition-colors',
'border-b border-default last:border-b-0'
)}
>
<code className="text-xs truncate flex-1">{policy.name}</code>
<div className="flex items-center gap-2">
{policy.command && <Badge variant="default">{policy.command}</Badge>}
{policy.isNew && <Badge variant="success">New</Badge>}
{!!onRemove && policy.isNew && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onRemove()
}}
className="flex items-center justify-center hover:bg-surface-200 rounded p-0.5 transition-colors"
aria-label="Remove policy"
>
<X size={16} strokeWidth={1.5} className="text-foreground-lighter" />
</button>
)}
</div>
</div>
</HoverCardTrigger_Shadcn_>
<HoverCardContent_Shadcn_ className="w-96" side="left">
{policy.sql ? (
<SimpleCodeBlock className="language-sql" showCopy={false}>
{policy.sql}
</SimpleCodeBlock>
) : (
<p className="text-xs text-foreground-lighter">No definition available.</p>
)}
</HoverCardContent_Shadcn_>
</HoverCard_Shadcn_>
)
}
interface PolicyListProps {
disabled?: boolean
policies: PolicyListItemData[]
className?: string
onRemove?: (index: number) => void
}
/**
* A list of policies with hover previews
* Used in RLSManagement and toast notifications
*/
export const PolicyList = ({
disabled = false,
policies,
className,
onRemove,
}: PolicyListProps) => {
return (
<Card aria-disabled={disabled} className={cn(disabled && 'opacity-50 pointer-events-none')}>
<div
className={`rounded border border-default overflow-hidden bg-surface-100 ${className ?? ''}`}
>
{policies.map((policy, idx) => (
<PolicyListItem
key={`${policy.name}-${idx}`}
policy={policy}
onRemove={onRemove ? () => onRemove(idx) : undefined}
/>
))}
</div>
</Card>
)
}
@@ -1,242 +0,0 @@
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
type GeneratedPolicy,
generateStartingPoliciesForTable,
} from 'components/interfaces/Auth/Policies/Policies.utils'
import { AIOptInModal } from 'components/ui/AIAssistantPanel/AIOptInModal'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import {
ForeignKeyConstraint,
useForeignKeyConstraintsQuery,
} from 'data/database/foreign-key-constraints-query'
import { useOrgAiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useTrack } from 'lib/telemetry/track'
import { Button, Card, CardContent, cn } from 'ui'
import type { ForeignKey } from '../../ForeignKeySelector/ForeignKeySelector.types'
import { ColumnField } from '../../SidePanelEditor.types'
interface PolicyListEmptyStateProps {
schema: string
tableName?: string
columns: ColumnField[]
foreignKeyRelations: ForeignKey[]
isNewRecord: boolean
isRLSEnabled: boolean
isDuplicating: boolean
onGeneratedPoliciesChange?: (policies: GeneratedPolicy[]) => void
}
export const PolicyListEmptyState = ({
schema,
tableName,
columns,
foreignKeyRelations,
isNewRecord,
isRLSEnabled,
isDuplicating,
onGeneratedPoliciesChange,
}: PolicyListEmptyStateProps) => {
const track = useTrack()
const { data: project } = useSelectedProjectQuery()
const { includeSchemaMetadata } = useOrgAiOptInLevel()
const [isGenerating, setIsGenerating] = useState(false)
const [generateFailed, setGenerateFailed] = useState(false)
const [isOptInModalOpen, setIsOptInModalOpen] = useState(false)
// Fetch foreign key constraints for policy generation BFS traversal
const { data: schemaForeignKeys } = useForeignKeyConstraintsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: schema,
},
{
enabled: isNewRecord && !isDuplicating && !!schema,
}
)
const emptyStateMessage = useMemo(() => {
if (!generateFailed) {
return {
title: 'Generate starting policies',
description: (
<>
Starter policies are generated from your table relationships to{' '}
<code className="text-code-inline">auth.users</code> or, if none exist, using AI.
</>
),
}
}
if (!includeSchemaMetadata) {
return {
title: 'Unable to generate policies',
description: (
<>
<p>We couldn't detect any relationships to auth.users to suggest policies.</p>
<p>Enable schema metadata sharing to use our AI-assisted policy generator.</p>
</>
),
}
}
return {
title: 'We could not generate policies for this table',
description:
"Automatic policy generation wasn't possible for this table. Update the schema and try again, or add policies manually after creating the table.",
}
}, [generateFailed, includeSchemaMetadata])
const { title, description } = emptyStateMessage
const showPermissionButton = generateFailed && !includeSchemaMetadata
const allowPolicyGeneration = isNewRecord && !isDuplicating
const convertForeignKeysToConstraints = (fks: ForeignKey[]): ForeignKeyConstraint[] => {
if (!tableName || !schema) {
return []
}
return fks
.filter((fk) => fk.columns && fk.columns.length > 0) // Only include FKs with columns
.map((fk) => ({
id: typeof fk.id === 'number' ? fk.id : 0,
constraint_name: fk.name || '',
source_id: typeof fk.tableId === 'number' ? fk.tableId : 0,
source_schema: schema.trim(),
source_table: tableName.trim(),
source_columns: fk.columns.map((col: { source: string; target: string }) =>
col.source.trim()
),
target_id: 0,
target_schema: fk.schema.trim(),
target_table: fk.table.trim(),
target_columns: fk.columns.map((col: { source: string; target: string }) =>
col.target.trim()
),
deletion_action: fk.deletionAction || 'NO ACTION',
update_action: fk.updateAction || 'NO ACTION',
}))
}
const handleGeneratePolicies = async () => {
if (!project?.ref || !tableName || columns.length === 0) {
return toast.error(
'Unable to generate policies. Please ensure table name and columns are set.'
)
}
track('rls_generate_policies_clicked')
setIsGenerating(true)
try {
const trimmedTableName = tableName.trim()
const trimmedSchema = schema.trim()
const newTableForeignKeys = convertForeignKeysToConstraints(foreignKeyRelations)
const allForeignKeys = [
...newTableForeignKeys,
...(schemaForeignKeys ?? []).filter(
(existingFk) =>
!(
existingFk.source_schema === trimmedSchema &&
existingFk.source_table === trimmedTableName
)
),
]
const tableColumns = columns.map((col) => ({ name: col.name.trim() }))
const policies = await generateStartingPoliciesForTable({
table: { name: trimmedTableName, schema: trimmedSchema },
foreignKeyConstraints: allForeignKeys,
columns: tableColumns,
projectRef: project.ref,
connectionString: project.connectionString,
enableAi: includeSchemaMetadata,
})
if (policies.length === 0) {
setGenerateFailed(true)
} else {
setGenerateFailed(false)
onGeneratedPoliciesChange?.(policies)
}
} catch (error: any) {
console.error('Failed to generate policies:', error)
toast.error(error.message || 'Failed to generate policies')
} finally {
setIsGenerating(false)
}
}
if (allowPolicyGeneration) {
return (
<>
<Card>
<CardContent className="flex flex-col items-center justify-center py-8">
<div
className={cn(
'flex flex-col items-center gap-4 text-center',
!generateFailed && 'max-w-sm'
)}
>
<div className="flex flex-col gap-1">
<h4 className="text-sm text-foreground">{title}</h4>
<p className="text-sm text-foreground-lighter">{description}</p>
</div>
<div className="flex items-center gap-2">
<ButtonTooltip
type="default"
size="tiny"
onClick={handleGeneratePolicies}
loading={isGenerating}
disabled={isGenerating || !isRLSEnabled || !tableName || columns.length === 0}
tooltip={{
content: {
side: 'bottom',
text: !tableName
? 'Provide a name for your table first before generating policies'
: columns.length === 0
? 'Create at least one column first before generating policies'
: undefined,
},
}}
>
{isGenerating
? 'Generating policies...'
: generateFailed
? 'Try generating again'
: 'Generate policies'}
</ButtonTooltip>
{showPermissionButton && (
<Button
type="default"
size="tiny"
onClick={() => setIsOptInModalOpen(true)}
disabled={isGenerating}
>
Permission settings
</Button>
)}
</div>
</div>
</CardContent>
</Card>
<AIOptInModal visible={isOptInModalOpen} onCancel={() => setIsOptInModalOpen(false)} />
</>
)
}
return (
<Card>
<CardContent>
<p className="text-sm text-foreground-lighter">No policies exist for this table</p>
</CardContent>
</Card>
)
}
@@ -1,205 +0,0 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useMemo } from 'react'
import { type GeneratedPolicy } from 'components/interfaces/Auth/Policies/Policies.utils'
import { generatePolicyUpdateSQL } from 'components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
import { DocsButton } from 'components/ui/DocsButton'
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { Button, cn } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import type { ForeignKey } from '../../ForeignKeySelector/ForeignKeySelector.types'
import { TableField } from '../TableEditor.types'
import { PolicyList, type PolicyListItemData } from './PolicyList'
import { PolicyListEmptyState } from './PolicyListEmptyState'
import { ToggleRLSButton } from './ToggleRLSButton'
interface RLSManagementProps {
table?: PostgresTable
tableFields: TableField // Fields within the form
foreignKeyRelations?: ForeignKey[] // For new tables
isNewRecord: boolean
isDuplicating: boolean
isExposed?: boolean
generatedPolicies?: GeneratedPolicy[]
onRLSUpdate?: (isEnabled: boolean) => void
onGeneratedPoliciesChange?: (policies: GeneratedPolicy[]) => void
}
export const RLSManagement = ({
table,
tableFields,
foreignKeyRelations = [],
isNewRecord,
isDuplicating,
isExposed,
generatedPolicies = [],
onRLSUpdate,
onGeneratedPoliciesChange,
}: RLSManagementProps) => {
const track = useTrack()
const { data: project } = useSelectedProjectQuery()
const { selectedSchema } = useQuerySchemaState()
const { name: tableName, columns, isRLSEnabled } = tableFields
const schema = table?.schema ?? selectedSchema
const isExistingTable = !!table && !isNewRecord && !isDuplicating
const generatedPoliciesTargetTable = generatedPolicies[0]?.table
const policiesNotRelevantDueToTableNameChange =
isNewRecord && tableName !== generatedPoliciesTargetTable
const disablePoliciesList =
(isExistingTable && !isRLSEnabled) || policiesNotRelevantDueToTableNameChange
const { data: policies } = useDatabasePoliciesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
enabled: !isNewRecord && !isDuplicating,
}
)
const tablePolicies = (policies ?? []).filter(
(policy) => policy.schema === table?.schema && policy.table === table?.name
)
const existingPoliciesList = useMemo<PolicyListItemData[]>(
() =>
(tablePolicies ?? []).map((policy) => ({
name: policy.name,
command: policy.action ?? policy.command,
sql: generatePolicyUpdateSQL(policy),
isNew: false,
})),
[tablePolicies]
)
// Convert generated policies to PolicyListItemData format
const generatedPoliciesList = useMemo<PolicyListItemData[]>(
() =>
generatedPolicies.map((policy) => ({
name: policy.name,
command: policy.command,
sql: policy.sql,
isNew: true,
})),
[generatedPolicies]
)
const allPoliciesList = useMemo<PolicyListItemData[]>(
() => [...existingPoliciesList, ...generatedPoliciesList],
[existingPoliciesList, generatedPoliciesList]
)
const hasPolicies = allPoliciesList.length > 0
const handleRemoveGeneratedPolicy = (index: number) => {
// Find the index in generatedPoliciesList that corresponds to the allPoliciesList index
const generatedIndex = index - existingPoliciesList.length
if (generatedIndex >= 0 && generatedIndex < generatedPolicies.length) {
const updatedPolicies = generatedPolicies.filter((_, i) => i !== generatedIndex)
onGeneratedPoliciesChange?.(updatedPolicies)
// Track policy removal
if (project?.ref) track('rls_generated_policy_removed')
}
}
return (
<div className="flex flex-col gap-y-4">
<div className="flex items-center justify-between">
<div>
<h5>Policies</h5>
<p className="text-sm text-foreground-lighter">
Set rules around who can read and write data to this table
</p>
</div>
{!isNewRecord && (
<Button asChild type="default" icon={<ExternalLink />}>
<Link
target="_blank"
rel="noopener noreferrer"
href={`/project/${project?.ref}/auth/policies`}
>
Manage policies
</Link>
</Button>
)}
</div>
{generatedPolicies.length > 0 && policiesNotRelevantDueToTableNameChange && (
<Admonition
type="warning"
title="Update generated policies with new table name"
description={`These policies were previously generated for the table name "${generatedPoliciesTargetTable}"`}
actions={
<Button
type="default"
className="w-min mt-3"
onClick={() => {
const updatedPolicies = generatedPolicies.map((policy) => {
return {
...policy,
table: tableName,
name: policy.name.replaceAll(generatedPoliciesTargetTable, tableName),
sql: policy.sql.replaceAll(generatedPoliciesTargetTable, tableName),
}
})
onGeneratedPoliciesChange?.(updatedPolicies)
}}
>
Update policies
</Button>
}
/>
)}
<Admonition
// [Joshen] Using CSS to determine visibility here as the onSuccess within ToggleRLSButton doesn't get triggered
// if we dynamically render this component, since this admonition gets unmounted from the DOM once RLS is updated
className={cn(
'mb-0',
!isNewRecord && !isRLSEnabled && isExposed !== false ? 'block' : 'hidden'
)}
type="warning"
title="Row Level Security is currently disabled"
description="Your table is currently accessible by anyone on the internet. We recommend enabling RLS to restrict access."
actions={
<div className="flex items-center gap-x-2 mt-3">
<ToggleRLSButton table={table} isRLSEnabled={isRLSEnabled} onSuccess={onRLSUpdate} />
<DocsButton href={`${DOCS_URL}/guides/database/postgres/row-level-security`} />
</div>
}
/>
{!hasPolicies ? (
<PolicyListEmptyState
schema={schema}
tableName={tableName}
columns={columns}
foreignKeyRelations={foreignKeyRelations}
isNewRecord={isNewRecord}
isDuplicating={isDuplicating}
isRLSEnabled={isRLSEnabled}
onGeneratedPoliciesChange={(policies) => {
onGeneratedPoliciesChange?.(policies)
}}
/>
) : (
<PolicyList
policies={allPoliciesList}
disabled={disablePoliciesList}
className="border-0 rounded-none"
onRemove={isNewRecord && !isDuplicating ? handleRemoveGeneratedPolicy : undefined}
/>
)}
</div>
)
}
@@ -1,70 +0,0 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
interface ToggleRLSButtonProps {
table?: PostgresTable
isRLSEnabled?: boolean
onSuccess?: (value: boolean) => void
}
export const ToggleRLSButton = ({
table,
isRLSEnabled = false,
onSuccess,
}: ToggleRLSButtonProps) => {
const { data: project } = useSelectedProjectQuery()
const [showConfirmation, setShowConfirmation] = useState(false)
const action = isRLSEnabled ? 'Disable' : 'Enable'
const { mutate: updateTable, isPending } = useTableUpdateMutation()
const onConfirm = () => {
if (!project) return console.error('Project is required')
if (!table) return console.error('Table is missing')
updateTable(
{
id: table.id,
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: table.schema,
name: table.name,
payload: { rls_enabled: !isRLSEnabled },
},
{
onSuccess: () => {
toast.success(`Row Level Security has been ${action.toLowerCase()} for this table.`)
onSuccess?.(!isRLSEnabled)
setShowConfirmation(false)
},
onError: (error) => {
toast.error(`Failed to ${action.toLowerCase()} RLS: ${error.message}`)
},
}
)
}
return (
<>
<Button type="default" className="w-min" onClick={() => setShowConfirmation(true)}>
{action} RLS
</Button>
<ConfirmationModal
visible={showConfirmation}
loading={isPending}
title={`Confirm to ${action.toLowerCase()} Row Level Security`}
description={`Are you sure you want to ${action.toLowerCase()} Row Level Security for this table?`}
confirmLabel={`${action} RLS`}
onCancel={() => setShowConfirmation(false)}
onConfirm={onConfirm}
/>
</>
)
}
@@ -10,7 +10,6 @@ import { useChanged } from 'hooks/misc/useChanged'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useTableCreateGeneratePolicies } from 'hooks/misc/useTableCreateGeneratePolicies'
import { useUrlState } from 'hooks/ui/useUrlState'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { DOCS_URL } from 'lib/constants'
@@ -35,7 +34,6 @@ import ColumnManagement from './ColumnManagement'
import { ForeignKeysManagement } from './ForeignKeysManagement/ForeignKeysManagement'
import { HeaderTitle } from './HeaderTitle'
import { RLSDisableModalContent } from './RLSDisableModal'
import { RLSManagement } from './RLSManagement/RLSManagement'
import { DEFAULT_COLUMNS } from './TableEditor.constants'
import type { ImportContent, TableField } from './TableEditor.types'
import {
@@ -100,13 +98,6 @@ export const TableEditor = ({
const [isImportingSpreadsheet, setIsImportingSpreadsheet] = useState<boolean>(false)
const [rlsConfirmVisible, setRlsConfirmVisible] = useState<boolean>(false)
const [generatedPolicies, setGeneratedPolicies] = useState<GeneratedPolicy[]>([])
const { enabled: generatePoliciesEnabled } = useTableCreateGeneratePolicies({
isNewRecord,
projectInsertedAt: project?.inserted_at,
})
const { data: types } = useEnumeratedTypesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -233,7 +224,7 @@ export const TableEditor = ({
columns,
foreignKeyRelations: fkRelations,
resolve,
generatedPolicies,
generatedPolicies: [],
})
} else if (isDuplicating) {
const payload: SaveTablePayloadFor<'duplicate'> = {
@@ -283,7 +274,6 @@ export const TableEditor = ({
setErrors({})
setImportContent(undefined)
setIsDuplicateRows(false)
setGeneratedPolicies([])
if (isNewRecord) {
const tableFields = generateTableField()
if (templateData) {
@@ -339,7 +329,6 @@ export const TableEditor = ({
if (!tableFields) return null
const isApiAccessAndPoliciesSectionShown = isApiGrantTogglesEnabled || generatePoliciesEnabled
const isExposed = isApiGrantTogglesEnabled
? !!apiAccessToggleHandler.data?.schemaExposed &&
checkDataApiPrivilegesNonEmpty(apiAccessToggleHandler.data.privileges)
@@ -387,93 +376,89 @@ export const TableEditor = ({
<SidePanel.Separator />
{!generatePoliciesEnabled && (
<>
<SidePanel.Content className="space-y-10 py-6">
<Checkbox
id="enable-rls"
// @ts-ignore
label={
<div className="flex items-center space-x-2">
<span>Enable Row Level Security (RLS)</span>
<Badge>Recommended</Badge>
</div>
}
description="Restrict access to your table by enabling RLS and writing Postgres policies."
checked={tableFields.isRLSEnabled}
onChange={() => {
// if isEnabled, show confirm modal to turn off
// if not enabled, allow turning on without modal confirmation
tableFields.isRLSEnabled
? setRlsConfirmVisible(true)
: onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })
}}
size="medium"
<SidePanel.Content className="space-y-10 py-6">
<Checkbox
id="enable-rls"
// @ts-ignore
label={
<div className="flex items-center space-x-2">
<span>Enable Row Level Security (RLS)</span>
<Badge>Recommended</Badge>
</div>
}
description="Restrict access to your table by enabling RLS and writing Postgres policies."
checked={tableFields.isRLSEnabled}
onChange={() => {
// if isEnabled, show confirm modal to turn off
// if not enabled, allow turning on without modal confirmation
tableFields.isRLSEnabled
? setRlsConfirmVisible(true)
: onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })
}}
size="medium"
/>
{tableFields.isRLSEnabled ? (
<Admonition
type="default"
className="!mt-3"
title="Policies are required to query data"
description={
<>
You need to create an access policy before you can query data from this table.
Without a policy, querying this table will return an{' '}
<u className="text-foreground">empty array</u> of results.{' '}
{isNewRecord ? 'You can create policies after saving this table.' : ''}
</>
}
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
) : (
<Admonition
type="warning"
className="!mt-3"
title="You are allowing anonymous access to your table"
description={
<>
{tableFields.name ? `The table ${tableFields.name}` : 'Your table'} will be publicly
writable and readable
</>
}
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
)}
{tableFields.isRLSEnabled ? (
<Admonition
type="default"
className="!mt-3"
title="Policies are required to query data"
description={
<>
You need to create an access policy before you can query data from this table.
Without a policy, querying this table will return an{' '}
<u className="text-foreground">empty array</u> of results.{' '}
{isNewRecord ? 'You can create policies after saving this table.' : ''}
</>
}
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
) : (
<Admonition
type="warning"
className="!mt-3"
title="You are allowing anonymous access to your table"
description={
<>
{tableFields.name ? `The table ${tableFields.name}` : 'Your table'} will be
publicly writable and readable
</>
}
>
<DocsButton
abbrev={false}
className="mt-2"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
)}
{realtimeEnabled && (
<Checkbox
id="enable-realtime"
label="Enable Realtime"
description="Broadcast changes on this table to authorized subscribers"
checked={tableFields.isRealtimeEnabled}
onChange={() => {
track('realtime_toggle_table_clicked', {
newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled',
origin: 'tableSidePanel',
})
onUpdateField({
isRealtimeEnabled: !tableFields.isRealtimeEnabled,
})
}}
size="medium"
/>
)}
</SidePanel.Content>
{realtimeEnabled && (
<Checkbox
id="enable-realtime"
label="Enable Realtime"
description="Broadcast changes on this table to authorized subscribers"
checked={tableFields.isRealtimeEnabled}
onChange={() => {
track('realtime_toggle_table_clicked', {
newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled',
origin: 'tableSidePanel',
})
onUpdateField({
isRealtimeEnabled: !tableFields.isRealtimeEnabled,
})
}}
size="medium"
/>
)}
</SidePanel.Content>
<SidePanel.Separator />
</>
)}
<SidePanel.Separator />
<SidePanel.Content className="space-y-10 py-6">
{!isDuplicating && (
@@ -517,21 +502,19 @@ export const TableEditor = ({
closePanel={() => setIsImportingSpreadsheet(false)}
/>
{!generatePoliciesEnabled && (
<ConfirmationModal
visible={rlsConfirmVisible}
title="Turn off Row Level Security"
confirmLabel="Confirm"
size="medium"
onCancel={() => setRlsConfirmVisible(false)}
onConfirm={() => {
onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })
setRlsConfirmVisible(false)
}}
>
<RLSDisableModalContent />
</ConfirmationModal>
)}
<ConfirmationModal
visible={rlsConfirmVisible}
title="Turn off Row Level Security"
confirmLabel="Confirm"
size="medium"
onCancel={() => setRlsConfirmVisible(false)}
onConfirm={() => {
onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })
setRlsConfirmVisible(false)
}}
>
<RLSDisableModalContent />
</ConfirmationModal>
</SidePanel.Content>
{!isDuplicating && (
@@ -549,37 +532,19 @@ export const TableEditor = ({
</>
)}
{isApiAccessAndPoliciesSectionShown && (
{isApiGrantTogglesEnabled && (
<>
<SidePanel.Separator />
<SidePanel.Content className="py-6 space-y-6">
{isApiGrantTogglesEnabled && (
<ApiAccessToggle
projectRef={project?.ref}
schemaName={isNewRecord ? selectedSchema : table?.schema}
tableName={
isNewRecord || isDuplicating ? tableFields.name : tableFields.name || table?.name
}
isNewRecord={isNewRecord || isDuplicating}
handler={apiAccessToggleHandler}
/>
)}
{/* [Joshen] Temporarily hide this section if duplicating, as we aren't duplicating policies atm when duplicating tables */}
{/* We should do this thought, but let's do this in another PR as the current one is already quite big */}
{generatePoliciesEnabled && !isDuplicating && (
<RLSManagement
table={table}
tableFields={tableFields}
foreignKeyRelations={isNewRecord ? fkRelations : undefined}
isNewRecord={isNewRecord}
isDuplicating={isDuplicating}
isExposed={isExposed}
generatedPolicies={generatedPolicies}
onGeneratedPoliciesChange={setGeneratedPolicies}
onRLSUpdate={(value) => onUpdateField({ isRLSEnabled: value })}
/>
)}
<ApiAccessToggle
projectRef={project?.ref}
schemaName={isNewRecord ? selectedSchema : table?.schema}
tableName={
isNewRecord || isDuplicating ? tableFields.name : tableFields.name || table?.name
}
isNewRecord={isNewRecord || isDuplicating}
handler={apiAccessToggleHandler}
/>
</SidePanel.Content>
</>
)}
@@ -5,6 +5,7 @@ import { DataTableColumnLevelIndicator } from 'components/ui/DataTable/DataTable
import { DataTableColumnStatusCode } from 'components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { ColumnFilterSchema, ColumnSchema } from '../UnifiedLogs.schema'
import { STATUS_CODE_LABELS } from '../UnifiedLogs.constants'
import { AuthUserHoverCard } from './AuthUserHoverCard'
import { HoverCardTimestamp } from './HoverCardTimestamp'
import { LogTypeIcon } from './LogTypeIcon'
@@ -109,6 +110,11 @@ export function generateDynamicColumns(data: ColumnSchema[]): {
header: '',
cell: ({ row }) => {
const value = row.getValue<ColumnSchema['status']>('status')
const label =
value != null
? STATUS_CODE_LABELS[String(value) as keyof typeof STATUS_CODE_LABELS] ??
'Unknown status'
: null
return (
<div className="flex items-center gap-1">
{/* {row.original.auth_user && (
@@ -117,10 +123,24 @@ export function generateDynamicColumns(data: ColumnSchema[]): {
<AuthUserHoverCard authUser={row.original.auth_user} />
</div>
)} */}
<DataTableColumnStatusCode
value={value}
level={row.getValue<ColumnSchema['level']>('level')}
/>
{label ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<DataTableColumnStatusCode
value={value}
level={row.getValue<ColumnSchema['level']>('level')}
/>
</span>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
) : (
<DataTableColumnStatusCode
value={value}
level={row.getValue<ColumnSchema['level']>('level')}
/>
)}
</div>
)
},
@@ -17,13 +17,11 @@ export const ProjectContextProvider = ({
return (
<TableEditorStateContextProvider key={`table-editor-state-${projectRef}`}>
<TabsStateContextProvider key={`tabs-state-${projectRef}`}>
<StorageExplorerStateContextProvider key={`storage-explorer-state-${projectRef}`}>
<DatabaseSelectorStateContextProvider key={`database-selector-state-${projectRef}`}>
<RoleImpersonationStateContextProvider key={`role-impersonation-state-${projectRef}`}>
{children}
</RoleImpersonationStateContextProvider>
</DatabaseSelectorStateContextProvider>
</StorageExplorerStateContextProvider>
<DatabaseSelectorStateContextProvider key={`database-selector-state-${projectRef}`}>
<RoleImpersonationStateContextProvider key={`role-impersonation-state-${projectRef}`}>
{children}
</RoleImpersonationStateContextProvider>
</DatabaseSelectorStateContextProvider>
</TabsStateContextProvider>
</TableEditorStateContextProvider>
)
@@ -0,0 +1,195 @@
import { render, waitFor } from '@testing-library/react'
import type { ReactNode } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { STUDIO_PAGE_TITLE_SEPARATOR } from '@/lib/page-title'
const { mockRouter, mockSetSelectedDatabaseId, mockSetMobileMenuOpen } = vi.hoisted(() => ({
mockRouter: {
pathname: '/project/[ref]/observability/query-performance',
asPath: '/project/default/observability/query-performance',
push: vi.fn(),
replace: vi.fn(),
},
mockSetSelectedDatabaseId: vi.fn(),
mockSetMobileMenuOpen: vi.fn(),
}))
vi.mock('next/router', () => ({
useRouter: () => mockRouter,
}))
vi.mock('next/head', async () => {
const React = await import('react')
const Head = ({ children }: { children?: ReactNode }) => {
React.useEffect(() => {
const titleElement = React.Children.toArray(children).find(
(child) => React.isValidElement(child) && child.type === 'title'
)
if (!React.isValidElement(titleElement)) return
const titleText = React.Children.toArray(titleElement.props.children).join('')
document.title = titleText
}, [children])
return null
}
return { default: Head }
})
vi.mock('common', () => ({
useParams: () => ({ ref: 'default' }),
mergeRefs:
(..._refs: any[]) =>
(_value: unknown) => {},
}))
vi.mock('framer-motion', () => ({
AnimatePresence: ({ children }: { children: ReactNode }) => <>{children}</>,
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
}))
vi.mock('ui', () => ({
cn: (...classes: Array<string | false | null | undefined>) => classes.filter(Boolean).join(' '),
LogoLoader: () => <div data-testid="logo-loader" />,
ResizableHandle: (props: any) => <div {...props} />,
ResizablePanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
ResizablePanelGroup: ({ children, ...props }: any) => <div {...props}>{children}</div>,
useIsMobile: () => false,
usePanelRef: () => undefined,
}))
vi.mock('ui-patterns/MobileSheetNav/MobileSheetNav', () => ({
default: ({ children }: { children: ReactNode }) => <>{children}</>,
}))
vi.mock('../editors/EditorsLayout.hooks', () => ({
useEditorType: () => undefined,
}))
vi.mock('../MainScrollContainerContext', () => ({
useSetMainScrollContainer: () => () => {},
}))
vi.mock('./BuildingState', () => ({ default: () => null }))
vi.mock('./ConnectingState', () => ({ default: () => null }))
vi.mock('./LoadingState', () => ({ LoadingState: () => null }))
vi.mock('./PausedState/ProjectPausedState', () => ({ ProjectPausedState: () => null }))
vi.mock('./PauseFailedState', () => ({ default: () => null }))
vi.mock('./PausingState', () => ({ default: () => null }))
vi.mock('./ProductMenuBar', () => ({
default: ({ children }: { children: ReactNode }) => <>{children}</>,
}))
vi.mock('./ResizingState', () => ({ default: () => null }))
vi.mock('./RestartingState', () => ({ default: () => null }))
vi.mock('./RestoreFailedState', () => ({ default: () => null }))
vi.mock('./RestoringState', () => ({ default: () => null }))
vi.mock('./UpgradingState', () => ({ UpgradingState: () => null }))
vi.mock('@/components/interfaces/BranchManagement/CreateBranchModal', () => ({
CreateBranchModal: () => null,
}))
vi.mock('@/components/interfaces/ProjectAPIDocs/ProjectAPIDocs', () => ({
ProjectAPIDocs: () => null,
}))
vi.mock('@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner', () => ({
ResourceExhaustionWarningBanner: () => null,
}))
vi.mock('@/hooks/custom-content/useCustomContent', () => ({
useCustomContent: () => ({ appTitle: 'Supabase' }),
}))
vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
useSelectedOrganizationQuery: () => ({
data: { name: 'Organization 1', slug: 'org-1' },
}),
}))
vi.mock('@/hooks/misc/useSelectedProject', () => ({
useSelectedProjectQuery: () => ({
data: {
ref: 'default',
name: 'Project 1',
status: 'ACTIVE_HEALTHY',
postgrestStatus: 'ONLINE',
},
}),
}))
vi.mock('@/hooks/misc/withAuth', () => ({
withAuth: (Component: any) => Component,
}))
vi.mock('@/hooks/ui/useFlag', () => ({
usePHFlag: () => undefined,
}))
vi.mock('@/state/app-state', () => ({
useAppStateSnapshot: () => ({
mobileMenuOpen: false,
showSidebar: false,
setMobileMenuOpen: mockSetMobileMenuOpen,
}),
}))
vi.mock('@/state/database-selector', () => ({
useDatabaseSelectorStateSnapshot: () => ({
setSelectedDatabaseId: mockSetSelectedDatabaseId,
}),
}))
import { ProjectLayout } from './index'
describe('ProjectLayout title', () => {
beforeEach(() => {
mockRouter.pathname = '/project/[ref]/observability/query-performance'
mockRouter.asPath = '/project/default/observability/query-performance'
document.title = ''
})
afterEach(() => {
vi.clearAllMocks()
document.title = ''
})
it('sets a composed document title and deduplicates identical section/surface labels', async () => {
render(
<ProjectLayout title="Settings" product="Settings" isBlocking={false}>
<div>Page Content</div>
</ProjectLayout>
)
await waitFor(() => {
expect(document.title).toBe(
['Settings', 'Project 1', 'Organization 1', 'Supabase'].join(STUDIO_PAGE_TITLE_SEPARATOR)
)
})
})
it('prefers entity-first browserTitle metadata when provided', async () => {
render(
<ProjectLayout
title="Database"
product="Database"
browserTitle={{ entity: 'users', section: 'Tables' }}
isBlocking={false}
>
<div>Page Content</div>
</ProjectLayout>
)
await waitFor(() => {
expect(document.title).toBe(
['users', 'Tables', 'Database', 'Project 1', 'Organization 1', 'Supabase'].join(
STUDIO_PAGE_TITLE_SEPARATOR
)
)
})
})
})
@@ -38,6 +38,7 @@ import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { withAuth } from '@/hooks/misc/withAuth'
import { usePHFlag } from '@/hooks/ui/useFlag'
import { PROJECT_STATUS } from '@/lib/constants'
import { buildStudioPageTitle } from '@/lib/page-title'
import { useAppStateSnapshot } from '@/state/app-state'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
@@ -73,6 +74,13 @@ export interface ProjectLayoutProps {
isBlocking?: boolean
product?: string
productMenu?: ReactNode
browserTitle?: {
entity?: string
section?: string
surface?: string
override?: string
}
// Deprecated: use browserTitle.entity instead. Kept for backwards compatibility.
selectedTable?: string
resizableSidebar?: boolean
productMenuClassName?: string
@@ -86,6 +94,7 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
isBlocking = true,
product = '',
productMenu,
browserTitle,
children,
selectedTable,
resizableSidebar = false,
@@ -103,7 +112,7 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
const combinedRef = mergeRefs(ref, setMainScrollContainer)
const { appTitle } = useCustomContent(['app:title'])
const titleSuffix = appTitle || 'Supabase'
const brandTitle = appTitle || 'Supabase'
const isMobile = useIsMobile()
@@ -115,6 +124,17 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
const pageTitle =
browserTitle?.override ||
buildStudioPageTitle({
entity: browserTitle?.entity ?? selectedTable,
section: browserTitle?.section ?? title,
surface: browserTitle?.surface ?? product,
project: projectName,
org: organizationName,
brand: brandTitle,
}) ||
brandTitle
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
@@ -128,17 +148,7 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
return (
<>
<Head>
<title>
{title
? `${title} | ${titleSuffix}`
: selectedTable
? `${selectedTable} | ${projectName} | ${organizationName} | ${titleSuffix}`
: projectName
? `${projectName} | ${organizationName} | ${titleSuffix}`
: organizationName
? `${organizationName} | ${titleSuffix}`
: titleSuffix}
</title>
<title>{pageTitle}</title>
<meta name="description" content="Supabase Studio" />
</Head>
<div className="flex flex-row h-full w-full">
@@ -1,11 +1,10 @@
import Link from 'next/link'
import { PropsWithChildren } from 'react'
import { IS_PLATFORM, useParams } from 'common'
import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants'
import { useStorageV2Page } from 'components/interfaces/Storage/Storage.utils'
import { DocsButton } from 'components/ui/DocsButton'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { PropsWithChildren } from 'react'
import { NavMenu, NavMenuItem } from 'ui'
import {
PageHeader,
@@ -1,6 +1,9 @@
import { AiPromptCopiedEvent } from 'common/telemetry-constants'
import { BASE_PATH } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { Check, ChevronDown, Copy } from 'lucide-react'
import { useTheme } from 'next-themes'
import Image from 'next/image'
import { ComponentProps, ReactNode, useEffect, useState } from 'react'
import {
AiIconAnimation,
@@ -19,6 +22,24 @@ import {
type TelemetrySource = AiPromptCopiedEvent['properties']['source']
const EXTERNAL_AI_TOOLS = [
{
label: 'Open in ChatGPT',
url: 'https://chatgpt.com/',
promptParam: 'q',
icon: {
light: '/img/mcp-clients/openai-icon.svg',
dark: '/img/mcp-clients/openai-icon-dark.svg',
},
},
{
label: 'Open in Claude.ai',
url: 'https://claude.ai/new',
promptParam: 'q',
icon: { light: '/img/mcp-clients/claude-icon.svg', dark: '/img/mcp-clients/claude-icon.svg' },
},
]
export interface AiAssistantDropdownItem {
label: string
icon?: ReactNode
@@ -37,6 +58,9 @@ export interface AiAssistantDropdownProps {
loading?: boolean
className?: string
tooltip?: string
copyLabel?: string
showExternalAI?: boolean
extraDropdownItems?: ReactNode
additionalDropdownItems?: AiAssistantDropdownItem[]
}
@@ -52,9 +76,13 @@ export function AiAssistantDropdown({
loading = false,
className,
tooltip,
copyLabel = 'Copy prompt',
showExternalAI = false,
extraDropdownItems,
additionalDropdownItems,
}: AiAssistantDropdownProps) {
const track = useTrack()
const { resolvedTheme } = useTheme()
const [showCopied, setShowCopied] = useState(false)
const [isOpen, setIsOpen] = useState(false)
@@ -75,6 +103,15 @@ export function AiAssistantDropdown({
}
}
const handleOpenExternalAI = (tool: (typeof EXTERNAL_AI_TOOLS)[number]) => {
const prompt = buildPrompt()
window.open(
`${tool.url}?${tool.promptParam}=${encodeURIComponent(prompt)}`,
'_blank',
'noreferrer'
)
}
const handleOpenAssistant = () => {
onOpenAssistant()
}
@@ -105,10 +142,31 @@ export function AiAssistantDropdown({
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
{extraDropdownItems}
<DropdownMenuItem onClick={handleCopyPrompt} className="gap-2">
{showCopied ? <Check size={14} className="text-brand" /> : <Copy size={14} />}
{showCopied ? 'Copied!' : 'Copy prompt'}
{showCopied ? 'Copied!' : copyLabel}
</DropdownMenuItem>
{showExternalAI && (
<>
<DropdownMenuSeparator />
{EXTERNAL_AI_TOOLS.map((tool) => (
<DropdownMenuItem
key={tool.url}
className="gap-2"
onClick={() => handleOpenExternalAI(tool)}
>
<Image
src={`${BASE_PATH}${resolvedTheme?.includes('dark') ? tool.icon.dark : tool.icon.light}`}
alt={tool.label}
width={14}
height={14}
/>
{tool.label}
</DropdownMenuItem>
))}
</>
)}
{additionalDropdownItems && additionalDropdownItems.length > 0 && (
<>
<DropdownMenuSeparator />
@@ -1,8 +1,8 @@
import pgMeta from '@supabase/pg-meta'
import { getIndexWorkerStatusSQL } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, type ExecuteSqlError } from 'data/sql/execute-sql-query'
import { UseCustomQueryOptions } from 'types'
import { authKeys } from './keys'
type IndexWorkerStatusVariables = {
@@ -18,7 +18,7 @@ export async function getIndexWorkerStatus(
{ projectRef, connectionString }: IndexWorkerStatusVariables,
signal?: AbortSignal
): Promise<IndexWorkerStatusData> {
const sql = pgMeta.getIndexWorkerStatusSQL()
const sql = getIndexWorkerStatusSQL()
const { result } = await executeSql<IndexWorkerStatusData[]>(
{
+1 -1
View File
@@ -1,4 +1,4 @@
import type { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
import type { OptimizedSearchColumns } from '@supabase/pg-meta'
export const authKeys = {
user: (projectRef: string | undefined, userId?: string | null) =>
+2 -1
View File
@@ -1,8 +1,8 @@
import { getUserSQL } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, type ExecuteSqlError } from 'data/sql/execute-sql-query'
import { UseCustomQueryOptions } from 'types'
import { getUserSQL } from './auth.sql'
import { authKeys } from './keys'
import { User } from './users-infinite-query'
import { UUID_REGEX } from '@/lib/constants'
@@ -21,6 +21,7 @@ export async function getUser(
if (!UUID_REGEX.test(userId)) throw new Error('Invalid user ID format')
const sql = getUserSQL(userId)
const { result } = await executeSql(
{ projectRef, connectionString, sql, queryKey: [`user-${userId}`] },
signal
@@ -1,8 +1,8 @@
import pgMeta from '@supabase/pg-meta'
import { getIndexStatusesSQL } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, type ExecuteSqlError } from 'data/sql/execute-sql-query'
import { UseCustomQueryOptions } from 'types'
import { authKeys } from './keys'
type UsersIndexStatusesVariables = {
@@ -20,7 +20,7 @@ export async function getUserIndexStatuses(
{ projectRef, connectionString }: UsersIndexStatusesVariables,
signal?: AbortSignal
): Promise<UsersIndexStatusesData> {
const sql = pgMeta.getIndexStatusesSQL()
const sql = getIndexStatusesSQL()
const { result } = await executeSql<UsersIndexStatusesData>(
{
+3 -3
View File
@@ -1,9 +1,9 @@
import type { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
import { getUsersCountSQL } from '@supabase/pg-meta/src/sql/studio/get-users-count'
import type { OptimizedSearchColumns } from '@supabase/pg-meta'
import { getUsersCountSQL } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, type ExecuteSqlError } from 'data/sql/execute-sql-query'
import { UseCustomQueryOptions } from 'types'
import { authKeys } from './keys'
import { type Filter } from './users-infinite-query'
@@ -1,15 +1,11 @@
import {
getPaginatedUsersSQL,
UsersCursor,
} from '@supabase/pg-meta/src/sql/studio/get-users-paginated'
import { getPaginatedUsersSQL, OptimizedSearchColumns, UsersCursor } from '@supabase/pg-meta'
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
import { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
import type { components } from 'data/api'
import { executeSql, ExecuteSqlError } from 'data/sql/execute-sql-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
import { UseCustomInfiniteQueryOptions } from 'types'
import { authKeys } from './keys'
const USERS_PAGE_LIMIT = 50
@@ -40,6 +40,8 @@ export const useErrorCodesQuery = <TData = ErrorCodeDescriptionsData>(
queryKey: contentApiKeys.errorCodes(variables),
queryFn: ({ signal }) => getErrorCodeDescriptions(variables, signal),
enabled,
retry: false,
staleTime: Infinity,
...options,
})
}
+1 -3
View File
@@ -1,14 +1,12 @@
import { handleError } from 'data/fetchers'
import type { TypedDocumentString } from './graphql'
const CONTENT_API_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL!
export async function executeGraphQL<TResult, TVariables>(
query: TypedDocumentString<TResult, TVariables>,
{ variables, signal }: { variables?: TVariables; signal?: AbortSignal }
) {
try {
const response = await fetch(CONTENT_API_URL, {
const response = await fetch('/api/content/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -35,6 +35,33 @@ export const signBucketObject = async (
type SignBucketObjectData = Awaited<ReturnType<typeof signBucketObject>>
type SignBucketObjectsParams = {
projectRef: string
bucketId?: string
paths: Array<string>
expiresIn: number
}
export const signBucketObjects = async (
{ projectRef, bucketId, paths, expiresIn }: SignBucketObjectsParams,
signal?: AbortSignal
) => {
if (!bucketId) throw new Error('bucketId is required')
const { data, error } = await post('/platform/storage/{ref}/buckets/{id}/objects/sign-multi', {
params: {
path: {
ref: projectRef,
id: bucketId,
},
},
body: { path: paths, expiresIn },
signal,
})
if (error) handleError(error)
return data ?? []
}
export const useGetSignBucketObjectMutation = ({
onSuccess,
onError,
+6 -88
View File
@@ -1,21 +1,21 @@
import assert from 'node:assert'
import { openai } from '@ai-sdk/openai'
import { Eval } from 'braintrust'
import { generateAssistantResponse } from 'lib/ai/generate-assistant-response'
import { getMockTools } from 'lib/ai/tools/mock-tools'
import assert from 'node:assert'
import { dataset } from './dataset'
import { buildAssistantEvalOutput } from './output'
import {
completenessScorer,
concisenessScorer,
correctnessScorer,
docsFaithfulnessScorer,
goalCompletionScorer,
sqlIdentifierQuotingScorer,
sqlSyntaxScorer,
toolUsageScorer,
urlValidityScorer,
} from './scorer'
import { ToolSet, TypedToolCall, TypedToolResult } from 'ai'
import { sqlIdentifierQuotingScorer, sqlSyntaxScorer } from './scorer-wasm'
assert(process.env.BRAINTRUST_PROJECT_ID, 'BRAINTRUST_PROJECT_ID is not set')
assert(process.env.OPENAI_API_KEY, 'OPENAI_API_KEY is not set')
@@ -31,50 +31,10 @@ Eval('Assistant', {
tools: await getMockTools(input.mockTables ? { list_tables: input.mockTables } : undefined),
})
const finishReason = await result.finishReason
// `result.toolCalls` only shows the last step, instead aggregate tools across all steps
const steps = await result.steps
const [finishReason, steps] = await Promise.all([result.finishReason, result.steps])
const simplifiedSteps = steps.map((step) => ({
text: step.text,
toolCalls: step.toolCalls.map((call) => ({
toolName: call.toolName,
input: call.input,
})),
}))
const toolNames: string[] = []
const sqlQueries: string[] = []
const docs: string[] = []
for (const step of steps) {
for (const [i, toolCall] of step.toolCalls.entries()) {
toolNames.push(toolCall.toolName)
const toolResult = step.toolResults.at(i)
if (!toolResult) {
continue
}
const parsed = parseToolCall(toolCall, toolResult)
if (parsed.sqlQuery) {
sqlQueries.push(parsed.sqlQuery)
}
if (parsed.docs) {
docs.push(...parsed.docs)
}
}
}
return {
finishReason,
steps: simplifiedSteps,
toolNames,
sqlQueries,
docs,
}
return buildAssistantEvalOutput(finishReason, steps)
},
scores: [
toolUsageScorer,
@@ -88,45 +48,3 @@ Eval('Assistant', {
urlValidityScorer,
],
})
type ParsedToolCall = {
/** Query generated by `execute_sql` */
sqlQuery?: string
/** Docs text pulled in from `search_docs` */
docs?: string[]
}
/**
* Validate and extract relevant info from a tool call/result
*/
function parseToolCall(
toolCall: TypedToolCall<ToolSet>,
toolResult: TypedToolResult<ToolSet>
): ParsedToolCall {
switch (toolCall.toolName) {
case 'execute_sql': {
const sqlQuery = toolCall.input.sql
if (typeof sqlQuery !== 'string') {
return {}
}
return { sqlQuery }
}
case 'search_docs': {
const content = toolResult.output.content
if (!content || !Array.isArray(content)) {
return {}
}
const docs = content.map((item) => item?.text).filter((text) => typeof text === 'string')
if (docs.length === 0) {
return {}
}
return { docs }
}
}
return {}
}
+66
View File
@@ -0,0 +1,66 @@
import { type ToolSet, type TypedToolCall, type TypedToolResult } from 'ai'
import { type AssistantEvalOutput } from './scorer'
type Step = {
text: string
toolCalls: TypedToolCall<ToolSet>[]
toolResults: TypedToolResult<ToolSet>[]
}
type ParsedToolCall = {
/** Query generated by `execute_sql` */
sqlQuery?: string
/** Docs text pulled in from `search_docs` */
docs?: string[]
}
function parseToolCall(
toolCall: TypedToolCall<ToolSet>,
toolResult: TypedToolResult<ToolSet>
): ParsedToolCall {
switch (toolCall.toolName) {
case 'execute_sql': {
const sqlQuery = toolCall.input?.sql
if (typeof sqlQuery !== 'string') return {}
return { sqlQuery }
}
case 'search_docs': {
const content = toolResult.output?.content
if (!content || !Array.isArray(content)) return {}
const docs = content.map((item) => item?.text).filter((text) => typeof text === 'string')
if (docs.length === 0) return {}
return { docs }
}
}
return {}
}
export function buildAssistantEvalOutput(
finishReason: AssistantEvalOutput['finishReason'],
steps: Step[]
): AssistantEvalOutput {
const simplifiedSteps = steps.map((step) => ({
text: step.text,
toolCalls: step.toolCalls.map((call) => ({
toolName: call.toolName,
input: call.input,
})),
}))
const toolNames: string[] = []
const sqlQueries: string[] = []
const docs: string[] = []
for (const step of steps) {
for (const [i, toolCall] of step.toolCalls.entries()) {
toolNames.push(toolCall.toolName)
const toolResult = step.toolResults.at(i)
if (!toolResult) continue
const parsed = parseToolCall(toolCall, toolResult)
if (parsed.sqlQuery) sqlQueries.push(parsed.sqlQuery)
if (parsed.docs) docs.push(...parsed.docs)
}
}
return { finishReason, steps: simplifiedSteps, toolNames, sqlQueries, docs }
}
@@ -0,0 +1,7 @@
[
{ "slug": "goal-completion", "name": "Goal Completion" },
{ "slug": "conciseness", "name": "Conciseness" },
{ "slug": "completeness", "name": "Completeness" },
{ "slug": "docs-faithfulness", "name": "Docs Faithfulness" },
{ "slug": "url-validity", "name": "URL Validity" }
]
+57
View File
@@ -0,0 +1,57 @@
/**
* Entry point for `braintrust push` to deploy scorers to Braintrust.
*
* Excluded scorers:
* - sqlSyntaxScorer, sqlIdentifierQuotingScorer: use libpg-query (WASM),
* which esbuild cannot bundle for Braintrust's remote infra.
* - toolUsageScorer: requires expected.requiredTools, offline-eval-only.
* - correctnessScorer: requires ground truth (expected output), offline-eval-only.
*/
import braintrust, { type EvalScorer } from 'braintrust'
import {
completenessScorer,
concisenessScorer,
docsFaithfulnessScorer,
goalCompletionScorer,
urlValidityScorer,
type AssistantEvalInput,
type AssistantEvalOutput,
type Expected,
} from './scorer'
import manifest from './scorer-online-manifest.json'
const projectId = process.env.BRAINTRUST_PROJECT_ID
if (!projectId && process.env.IS_BRAINTRUST_PUSH)
throw new Error('BRAINTRUST_PROJECT_ID is not set')
// When running in CI, prefix scorers with the branch name to avoid collisions between PRs
// in the staging project. GITHUB_HEAD_REF is set on PR events, GITHUB_REF_NAME on push/dispatch.
const branch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME
const prefix = branch ? `${branch.replace(/[^a-z0-9-]/gi, '-').toLowerCase()}-` : ''
const prNumber = process.env.GITHUB_PR_NUMBER ? Number(process.env.GITHUB_PR_NUMBER) : undefined
const metadata = branch ? { gitBranch: branch, ...(prNumber && { prNumber }) } : undefined
const description = prNumber && branch ? `#${prNumber} · ${branch}` : branch
const handlers = {
'goal-completion': goalCompletionScorer,
conciseness: concisenessScorer,
completeness: completenessScorer,
'docs-faithfulness': docsFaithfulnessScorer,
'url-validity': urlValidityScorer,
} satisfies Record<string, EvalScorer<AssistantEvalInput, AssistantEvalOutput, Expected>>
// @ts-expect-error - Project ID is only required at build-time
const project = braintrust.projects.create({ id: projectId })
for (const { slug, name } of manifest) {
project.scorers.create({
slug: `${prefix}${slug}`,
name,
description,
handler: handlers[slug as keyof typeof handlers],
ifExists: 'replace',
metadata,
})
}
+80
View File
@@ -0,0 +1,80 @@
import { EvalScorer } from 'braintrust'
import { extractIdentifiers, isQuotedInSql, needsQuoting } from 'lib/sql-identifier-quoting'
import { parse } from 'libpg-query'
import { AssistantEvalInput, AssistantEvalOutput, Expected } from './scorer'
export const sqlSyntaxScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ output }) => {
if (output.sqlQueries === undefined || output.sqlQueries.length === 0) {
return null
}
const errors: string[] = []
let validQueries = 0
for (const sql of output.sqlQueries) {
try {
await parse(sql)
validQueries++
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
errors.push(`SQL syntax error: ${errorMessage}`)
}
}
return {
name: 'SQL Validity',
score: validQueries / output.sqlQueries.length,
metadata: errors.length > 0 ? { errors } : undefined,
}
}
export const sqlIdentifierQuotingScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ output }) => {
// Skip if no SQL queries
if (!output.sqlQueries?.length) {
return null
}
const errors: string[] = []
let totalNeedingQuotes = 0
let properlyQuoted = 0
for (const sql of output.sqlQueries) {
try {
const ast = await parse(sql)
const identifiers = extractIdentifiers(ast)
for (const identifier of identifiers) {
if (needsQuoting(identifier)) {
totalNeedingQuotes++
if (isQuotedInSql(sql, identifier)) {
properlyQuoted++
} else {
const sqlPreview = sql.length > 100 ? `${sql.substring(0, 100)}...` : sql
errors.push(
`Identifier "${identifier}" needs quoting but is not quoted in: ${sqlPreview}`
)
}
}
}
} catch {
// Skip invalid SQL - already handled by sqlSyntaxScorer
}
}
const score = totalNeedingQuotes === 0 ? 1 : properlyQuoted / totalNeedingQuotes
return {
name: 'SQL Identifier Quoting',
score,
metadata: errors.length > 0 ? { errors } : undefined,
}
}
+66 -116
View File
@@ -3,13 +3,10 @@ import { LLMClassifierFromTemplate } from 'autoevals'
import { EvalCase, EvalScorer } from 'braintrust'
import { stripIndent } from 'common-tags'
import { extractUrls } from 'lib/helpers'
import { extractIdentifiers } from 'lib/sql-identifier-quoting'
import { isQuotedInSql, needsQuoting } from 'lib/sql-identifier-quoting'
import { parse } from 'libpg-query'
const LLM_AS_A_JUDGE_MODEL = 'gpt-5.2-2025-12-11'
const LLM_AS_A_JUDGE_MODEL = 'gpt-5.2' // NOTE: `gpt-5.2-2025-12-11` snapshot not yet working with online scorers
type Input = {
export type AssistantEvalInput = {
prompt: string
mockTables?: Record<
string,
@@ -21,7 +18,7 @@ type Input = {
>
}
type Output = {
export type AssistantEvalOutput = {
finishReason: FinishReason
steps: Array<{ text: string; toolCalls: Array<{ toolName: string; input: unknown }> }>
toolNames: string[]
@@ -50,12 +47,12 @@ export type AssistantEvalCaseMetadata = {
description?: string
}
export type AssistantEvalCase = EvalCase<Input, Expected, AssistantEvalCaseMetadata>
export type AssistantEvalCase = EvalCase<AssistantEvalInput, Expected, AssistantEvalCaseMetadata>
/**
* Serialize steps into a string representation including text and tool calls
*/
function serializeSteps(steps: Output['steps']): string {
function serializeSteps(steps: AssistantEvalOutput['steps']): string {
return steps
.map((step) => {
const toolCalls = step.toolCalls
@@ -69,17 +66,18 @@ function serializeSteps(steps: Output['steps']): string {
/**
* Extract only the text content from steps, filtering out empty text
*/
function extractTextOnly(steps: Output['steps']): string {
function extractTextOnly(steps: AssistantEvalOutput['steps']): string {
return steps
.map((step) => step.text)
.filter((text) => text && text.trim().length > 0)
.join('\n')
}
export const toolUsageScorer: EvalScorer<Input, Output, Expected> = async ({
output,
expected,
}) => {
export const toolUsageScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ output, expected }) => {
if (!expected.requiredTools) return null
const presentCount = expected.requiredTools.filter((tool) =>
@@ -94,31 +92,6 @@ export const toolUsageScorer: EvalScorer<Input, Output, Expected> = async ({
}
}
export const sqlSyntaxScorer: EvalScorer<Input, Output, Expected> = async ({ output }) => {
if (output.sqlQueries === undefined || output.sqlQueries.length === 0) {
return null
}
const errors: string[] = []
let validQueries = 0
for (const sql of output.sqlQueries) {
try {
await parse(sql)
validQueries++
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
errors.push(`SQL syntax error: ${errorMessage}`)
}
}
return {
name: 'SQL Validity',
score: validQueries / output.sqlQueries.length,
metadata: errors.length > 0 ? { errors } : undefined,
}
}
const concisenessEvaluator = LLMClassifierFromTemplate<{ input: string }>({
name: 'Conciseness',
promptTemplate: stripIndent`
@@ -137,7 +110,11 @@ const concisenessEvaluator = LLMClassifierFromTemplate<{ input: string }>({
model: LLM_AS_A_JUDGE_MODEL,
})
export const concisenessScorer: EvalScorer<Input, Output, Expected> = async ({ input, output }) => {
export const concisenessScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ input, output }) => {
return await concisenessEvaluator({
input: input.prompt,
output: serializeSteps(output.steps),
@@ -161,10 +138,11 @@ const completenessEvaluator = LLMClassifierFromTemplate<{ input: string }>({
model: LLM_AS_A_JUDGE_MODEL,
})
export const completenessScorer: EvalScorer<Input, Output, Expected> = async ({
input,
output,
}) => {
export const completenessScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ input, output }) => {
return await completenessEvaluator({
input: input.prompt,
output: serializeSteps(output.steps),
@@ -189,10 +167,11 @@ const goalCompletionEvaluator = LLMClassifierFromTemplate<{ input: string }>({
model: LLM_AS_A_JUDGE_MODEL,
})
export const goalCompletionScorer: EvalScorer<Input, Output, Expected> = async ({
input,
output,
}) => {
export const goalCompletionScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ input, output }) => {
return await goalCompletionEvaluator({
input: input.prompt,
output: serializeSteps(output.steps),
@@ -220,7 +199,11 @@ const docsFaithfulnessEvaluator = LLMClassifierFromTemplate<{ docs: string }>({
model: LLM_AS_A_JUDGE_MODEL,
})
export const docsFaithfulnessScorer: EvalScorer<Input, Output, Expected> = async ({ output }) => {
export const docsFaithfulnessScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ output }) => {
// Skip scoring if no docs were retrieved
if (!output.docs || output.docs.length === 0) {
return null
@@ -261,11 +244,11 @@ const correctnessEvaluator = LLMClassifierFromTemplate<{ input: string; expected
model: LLM_AS_A_JUDGE_MODEL,
})
export const correctnessScorer: EvalScorer<Input, Output, Expected> = async ({
input,
output,
expected,
}) => {
export const correctnessScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ input, output, expected }) => {
// Skip scoring if no ground truth is provided
if (!expected.correctAnswer) {
return null
@@ -278,77 +261,44 @@ export const correctnessScorer: EvalScorer<Input, Output, Expected> = async ({
})
}
export const sqlIdentifierQuotingScorer: EvalScorer<Input, Output, Expected> = async ({
input,
output,
}) => {
// Skip if no SQL queries
if (!output.sqlQueries?.length) {
return null
}
const errors: string[] = []
let totalNeedingQuotes = 0
let properlyQuoted = 0
for (const sql of output.sqlQueries) {
try {
const ast = await parse(sql)
const identifiers = extractIdentifiers(ast)
for (const identifier of identifiers) {
if (needsQuoting(identifier)) {
totalNeedingQuotes++
if (isQuotedInSql(sql, identifier)) {
properlyQuoted++
} else {
const sqlPreview = sql.length > 100 ? `${sql.substring(0, 100)}...` : sql
errors.push(
`Identifier "${identifier}" needs quoting but is not quoted in: ${sqlPreview}`
)
}
}
}
} catch (error) {
// Skip invalid SQL - already handled by sqlSyntaxScorer
continue
}
}
const score = totalNeedingQuotes === 0 ? 1 : properlyQuoted / totalNeedingQuotes
return {
name: 'SQL Identifier Quoting',
score,
metadata: errors.length > 0 ? { errors } : undefined,
}
}
export const urlValidityScorer: EvalScorer<Input, Output, Expected> = async ({ output }) => {
export const urlValidityScorer: EvalScorer<
AssistantEvalInput,
AssistantEvalOutput,
Expected
> = async ({ output }) => {
const responseText = extractTextOnly(output.steps)
const urls = extractUrls(responseText, { excludeCodeBlocks: true, excludeTemplates: true })
const allUrls = extractUrls(responseText, { excludeCodeBlocks: true, excludeTemplates: true })
const urls = allUrls.filter((url) => {
try {
const { hostname } = new URL(url)
return hostname === 'supabase.com' || hostname.endsWith('.supabase.com')
} catch {
return false
}
})
// Skip if no URLs found
if (urls.length === 0) {
return null
}
const errors: string[] = []
let validUrls = 0
for (const url of urls) {
try {
const response = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(5000) })
if (response.ok) {
validUrls++
} else {
errors.push(`${url} returned ${response.status}`)
const results = await Promise.all(
urls.map(async (url) => {
try {
const response = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(5000) })
if (response.ok) {
return { valid: true }
}
return { valid: false, error: `${url} returned ${response.status}` }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return { valid: false, error: `${url} failed: ${errorMessage}` }
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
errors.push(`${url} failed: ${errorMessage}`)
}
}
})
)
const errors = results.flatMap((r) => (r.error ? [r.error] : []))
const validUrls = results.filter((r) => r.valid).length
const metadata = {
urls,
@@ -1,82 +0,0 @@
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { useTrackExperimentExposure } from 'hooks/misc/useTrackExperimentExposure'
import { usePHFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import { useMemo } from 'react'
dayjs.extend(utc)
export type TableCreateGeneratePoliciesVariant = 'control' | 'variation'
const VALID_VARIANTS: TableCreateGeneratePoliciesVariant[] = ['control', 'variation']
export function isValidExperimentVariant(
value: unknown
): value is TableCreateGeneratePoliciesVariant {
return (
typeof value === 'string' &&
VALID_VARIANTS.includes(value as TableCreateGeneratePoliciesVariant)
)
}
interface UseTableCreateGeneratePoliciesOptions {
/**
* Whether this is a new table being created
*/
isNewRecord?: boolean
/**
* Project creation timestamp
*/
projectInsertedAt?: string
}
interface UseTableCreateGeneratePoliciesResult {
/**
* Whether the generate policies feature is enabled
*/
enabled: boolean
}
/**
* Hook to manage the table create generate policies feature flag.
* Handles feature flag determination and exposure tracking.
*
* @param options Configuration for feature targeting
* @returns Feature state including whether it's enabled
*/
export function useTableCreateGeneratePolicies({
isNewRecord = false,
projectInsertedAt,
}: UseTableCreateGeneratePoliciesOptions): UseTableCreateGeneratePoliciesResult {
const tableCreateGeneratePoliciesFlag = usePHFlag<string>('tableCreateGeneratePolicies')
const enabled = useMemo(() => {
if (!IS_PLATFORM) return false
if (tableCreateGeneratePoliciesFlag !== 'variation') return false
return true
}, [tableCreateGeneratePoliciesFlag])
const daysSinceCreation = useMemo(() => {
if (!projectInsertedAt) return undefined
const insertedDate = dayjs.utc(projectInsertedAt)
if (!insertedDate.isValid()) return undefined
return dayjs.utc().diff(insertedDate, 'day')
}, [projectInsertedAt])
const shouldTrack =
IS_PLATFORM &&
isNewRecord &&
isValidExperimentVariant(tableCreateGeneratePoliciesFlag) &&
daysSinceCreation !== undefined
useTrackExperimentExposure(
'table_create_generate_policies',
shouldTrack ? tableCreateGeneratePoliciesFlag : undefined,
{ days_since_project_creation: daysSinceCreation }
)
return {
enabled,
}
}
@@ -8,8 +8,10 @@ import {
type ToolSet,
type UIMessage,
} from 'ai'
import { traced, wrapAISDK, type Span } from 'braintrust'
import { startSpan, traced, withCurrent, wrapAISDK, type Span } from 'braintrust'
import { source } from 'common-tags'
import { buildAssistantEvalOutput } from 'evals/output'
import type { AssistantEvalInput, AssistantEvalOutput } from 'evals/scorer'
import type { AiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi'
import { IS_TRACING_ENABLED } from 'lib/ai/braintrust-logger'
import {
@@ -66,10 +68,6 @@ export async function generateAssistantResponse({
const shouldTrace = IS_TRACING_ENABLED && !isHipaaEnabled
const run = async (span?: Span) => {
if (span) {
onSpanCreated?.(span.id)
}
// Only returns last 7 messages
// Filters out tools with invalid states
// Filters out tool outputs based on opt-in level using renderingToolOutputParser
@@ -144,24 +142,37 @@ export async function generateAssistantResponse({
const streamTextFn = shouldTrace ? tracedStreamText : ai.streamText
const streamTextArgs = {
return streamTextFn({
model,
stopWhen: stepCountIs(5),
messages: coreMessages,
...(providerOptions && { providerOptions }),
tools,
...(abortSignal && { abortSignal }),
onFinish: ({ steps }) => {
for (const step of steps) {
for (const toolCall of step.toolCalls) {
if (toolCall.toolName === 'rename_chat') {
const { newName } = toolCall.input as { newName: string }
span?.log({ metadata: { chatName: newName } })
...(span && {
onFinish: ({ steps, finishReason }) => {
for (const step of steps) {
for (const toolCall of step.toolCalls) {
if (toolCall.toolName === 'rename_chat') {
const { newName } = toolCall.input as { newName: string }
span.log({ metadata: { chatName: newName } })
}
}
}
}
},
} satisfies Parameters<typeof ai.streamText>[0]
span.log({
output: buildAssistantEvalOutput(finishReason, steps) satisfies AssistantEvalOutput,
})
span.end()
},
}),
} satisfies Parameters<typeof ai.streamText>[0])
}
if (shouldTrace) {
// startSpan instead of traced() so we control when the span closes — onFinish logs
// output to the span before we call span.end(), ensuring online scoring sees the output.
const span = startSpan({ name: 'generateAssistantResponse', type: 'function' })
onSpanCreated?.(span.id)
const lastUserMessage = rawMessages.findLast((m) => m.role === 'user')
const lastUserText = lastUserMessage?.parts
@@ -169,8 +180,8 @@ export async function generateAssistantResponse({
.map((p) => p.text)
.join('\n')
span?.log({
input: lastUserText,
span.log({
input: { prompt: lastUserText ?? '' } satisfies AssistantEvalInput,
metadata: {
projectRef,
chatId,
@@ -185,11 +196,7 @@ export async function generateAssistantResponse({
},
})
return streamTextFn(streamTextArgs)
}
if (shouldTrace) {
return traced(run, { type: 'function', name: 'generateAssistantResponse' })
return withCurrent(span, () => run(span))
}
return run()
+94
View File
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest'
import { buildStudioPageTitle, STUDIO_PAGE_TITLE_SEPARATOR } from './page-title'
describe('buildStudioPageTitle', () => {
it('builds a project-scoped title in most-specific-first order', () => {
expect(
buildStudioPageTitle({
surface: 'Database',
project: 'Acme Project',
org: 'Acme Org',
brand: 'Supabase',
})
).toBe(
`Database${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Acme Org${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
)
})
it('includes entity and section when provided', () => {
expect(
buildStudioPageTitle({
entity: 'users',
section: 'Tables',
surface: 'Database',
project: 'Acme Project',
org: 'Acme Org',
brand: 'Supabase',
})
).toBe(
`users${STUDIO_PAGE_TITLE_SEPARATOR}Tables${STUDIO_PAGE_TITLE_SEPARATOR}Database${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Acme Org${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
)
})
it('omits missing segments', () => {
expect(
buildStudioPageTitle({
section: 'Authentication',
project: 'Acme Project',
brand: 'Supabase',
})
).toBe(
`Authentication${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
)
})
it('deduplicates adjacent segments case-insensitively', () => {
expect(
buildStudioPageTitle({
section: 'Database',
surface: 'database',
project: 'Acme Project',
org: 'Acme Org',
brand: 'Supabase',
})
).toBe(
`Database${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Acme Org${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
)
})
it('normalizes whitespace in each segment', () => {
expect(
buildStudioPageTitle({
entity: ' hello world ',
surface: ' Edge Functions ',
brand: ' Supabase ',
})
).toBe(
`hello world${STUDIO_PAGE_TITLE_SEPARATOR}Edge Functions${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
)
})
it('truncates very long segments', () => {
const longName = 'x'.repeat(80)
expect(
buildStudioPageTitle({
entity: longName,
surface: 'Table Editor',
brand: 'Supabase',
})
).toBe(
`${'x'.repeat(59)}…${STUDIO_PAGE_TITLE_SEPARATOR}Table Editor${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
)
})
it('supports custom brand titles', () => {
expect(
buildStudioPageTitle({
surface: 'Settings',
brand: 'Supabase Studio',
})
).toBe(`Settings${STUDIO_PAGE_TITLE_SEPARATOR}Supabase Studio`)
})
})
+46
View File
@@ -0,0 +1,46 @@
export interface StudioPageTitleParts {
entity?: string
section?: string
surface?: string
project?: string
org?: string
brand?: string
}
export const STUDIO_PAGE_TITLE_SEPARATOR = ' | '
const MAX_SEGMENT_LENGTH = 60
const normalizeTitleSegment = (value?: string) => {
if (value === undefined) return undefined
const normalized = value.trim().replace(/\s+/g, ' ')
if (normalized.length === 0) return undefined
if (normalized.length <= MAX_SEGMENT_LENGTH) return normalized
return `${normalized.slice(0, MAX_SEGMENT_LENGTH - 1).trimEnd()}…`
}
export const buildStudioPageTitle = (parts: StudioPageTitleParts) => {
const orderedParts = [
parts.entity,
parts.section,
parts.surface,
parts.project,
parts.org,
parts.brand,
]
const segments: string[] = []
orderedParts.forEach((part) => {
const segment = normalizeTitleSegment(part)
if (!segment) return
const lastSegment = segments[segments.length - 1]
if (lastSegment !== undefined && lastSegment.toLowerCase() === segment.toLowerCase()) return
segments.push(segment)
})
return segments.join(STUDIO_PAGE_TITLE_SEPARATOR)
}
+3 -2
View File
@@ -24,7 +24,8 @@
"build:graphql-types:watch": "pnpm graphql-codegen --config scripts/codegen.ts --watch",
"evals:setup": "cp node_modules/libpg-query/wasm/libpg-query.wasm evals/libpg-query.wasm",
"evals:run": "braintrust eval --no-send-logs evals/assistant.eval.ts",
"evals:upload": "braintrust eval evals/assistant.eval.ts"
"evals:upload": "braintrust eval evals/assistant.eval.ts",
"scorers:deploy": "IS_BRAINTRUST_PUSH=true braintrust push evals/scorer-online.ts"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.0",
@@ -183,7 +184,7 @@
"@vitest/coverage-v8": "^3.2.0",
"@vitest/ui": "^3.2.0",
"api-types": "workspace:*",
"autoevals": "^0.0.131",
"autoevals": "^0.0.132",
"braintrust": "^3.0.0",
"common": "workspace:*",
"config": "workspace:*",
+26
View File
@@ -0,0 +1,26 @@
import { NextApiRequest, NextApiResponse } from 'next'
const CONTENT_API_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL!
// Default export needed by Next.js convention
// eslint-disable-next-line no-restricted-exports
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST'])
return res.status(405).json({ error: `Method ${req.method} Not Allowed` })
}
try {
const response = await fetch(CONTENT_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body),
})
const data = await response.json()
return res.status(response.status).json(data)
} catch (error) {
console.error('Content API proxy error:', error)
return res.status(500).json({ error: 'Failed to reach Content API' })
}
}
@@ -0,0 +1,46 @@
import { createClient } from '@supabase/supabase-js'
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
const wrappedHandler = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
switch (method) {
case 'POST':
return handlePost(req, res)
default:
res.setHeader('Allow', ['POST'])
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
}
}
const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
const { id } = req.query
const { path, expiresIn = 60 * 60 * 24 } = req.body
const { data, error } = await supabase.storage
.from(id as string)
.createSignedUrls(path, expiresIn)
if (error) {
return res.status(400).json({ error: { message: error.message } })
}
const parsed = new URL(process.env.SUPABASE_PUBLIC_URL!)
const remapped = (data ?? []).map((item) => {
if (!item.signedUrl) return item
const signedUrl = new URL(item.signedUrl)
signedUrl.protocol = parsed.protocol
signedUrl.host = parsed.host
signedUrl.port = parsed.port
return { ...item, signedUrl: signedUrl.href }
})
return res.status(201).json(remapped)
}
export default wrappedHandler
+3 -20
View File
@@ -11,8 +11,8 @@ import { DisabledWarningDueToIncident } from 'components/interfaces/ProjectCreat
import { FreeProjectLimitWarning } from 'components/interfaces/ProjectCreation/FreeProjectLimitWarning'
import { OrganizationSelector } from 'components/interfaces/ProjectCreation/OrganizationSelector'
import {
PostgresVersionSelector,
extractPostgresVersionDetails,
PostgresVersionSelector,
} from 'components/interfaces/ProjectCreation/PostgresVersionSelector'
import { sizes } from 'components/interfaces/ProjectCreation/ProjectCreation.constants'
import { FormSchema } from 'components/interfaces/ProjectCreation/ProjectCreation.schema'
@@ -44,7 +44,6 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useTrackExperimentExposure } from 'hooks/misc/useTrackExperimentExposure'
import { withAuth } from 'hooks/misc/withAuth'
import { usePHFlag } from 'hooks/ui/useFlag'
import { DOCS_URL, PROJECT_STATUS, PROVIDERS, useDefaultProvider } from 'lib/constants'
@@ -57,9 +56,9 @@ import { useForm } from 'react-hook-form'
import { AWS_REGIONS, type CloudProvider } from 'shared-data'
import { toast } from 'sonner'
import type { NextPageWithLayout } from 'types'
import { Button, FormField_Shadcn_, Form_Shadcn_, useWatch_Shadcn_ } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { Button, Form_Shadcn_, FormField_Shadcn_, useWatch_Shadcn_ } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { z } from 'zod'
const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small']
@@ -72,10 +71,6 @@ const Wizard: NextPageWithLayout = () => {
const { profile } = useProfile()
const { data: currentOrg } = useSelectedOrganizationQuery()
const rlsExperimentVariant = usePHFlag<'control' | 'test' | false | undefined>(
'projectCreationEnableRlsEventTrigger'
)
const shouldShowEnableRlsEventTrigger = rlsExperimentVariant === 'test'
const isFreePlan = currentOrg?.plan?.id === 'free'
const canChooseInstanceSize = !isFreePlan
@@ -250,9 +245,6 @@ const Wizard: NextPageWithLayout = () => {
{
instanceSize: form.getValues('instanceSize'),
enableRlsEventTrigger: form.getValues('enableRlsEventTrigger'),
...((rlsExperimentVariant === 'control' || rlsExperimentVariant === 'test') && {
rlsOptionVariant: rlsExperimentVariant,
}),
dataApiEnabled: form.getValues('dataApi'),
useOrioleDb: form.getValues('useOrioleDb'),
},
@@ -389,15 +381,6 @@ const Wizard: NextPageWithLayout = () => {
}
}, [instanceSize, watchedInstanceSize, form])
// Track exposure to RLS option experiment (only when explicitly assigned to a variant)
const shouldTrackRlsExposure =
!!currentOrg?.slug && (rlsExperimentVariant === 'control' || rlsExperimentVariant === 'test')
useTrackExperimentExposure(
'project_creation_rls_option',
shouldTrackRlsExposure ? rlsExperimentVariant : undefined
)
return (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmitWithComputeCostsConfirmation)}>
@@ -1,10 +1,3 @@
import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { DeleteBucketModal } from 'components/interfaces/Storage/DeleteBucketModal'
import { EditBucketModal } from 'components/interfaces/Storage/EditBucketModal'
@@ -16,6 +9,12 @@ import { useBucketPolicyCount } from 'components/interfaces/Storage/useBucketPol
import DefaultLayout from 'components/layouts/DefaultLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import StorageLayout from 'components/layouts/StorageLayout/StorageLayout'
import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import type { NextPageWithLayout } from 'types'
import {
Badge,
@@ -27,6 +26,8 @@ import {
DropdownMenuTrigger,
} from 'ui'
import { StorageExplorerStateContextProvider } from '@/state/storage-explorer'
const BucketPage: NextPageWithLayout = () => {
const router = useRouter()
const { bucketId, ref } = useParams()
@@ -57,11 +58,11 @@ const BucketPage: NextPageWithLayout = () => {
}, [isSuccess])
if (isError) {
return <StorageBucketsError error={error as any} />
return <StorageBucketsError error={error} />
}
return (
<>
<StorageExplorerStateContextProvider key={`storage-explorer-state-${ref}`}>
<PageLayout
size="full"
isCompact
@@ -163,7 +164,7 @@ const BucketPage: NextPageWithLayout = () => {
/>
</>
)}
</>
</StorageExplorerStateContextProvider>
)
}
+1
View File
@@ -26,6 +26,7 @@ const HOSTED_SUPPORTED_API_URLS = [
'/generate-attachment-url',
'/incident-status',
'/api/integrations/stripe-sync',
'/content/graphql',
]
export function proxy(request: NextRequest) {
+1 -1
View File
@@ -1 +1 @@
{"type":"Topology","arcs":[[[571881,341685],[0,0],[66,-22],[88,0],[132,0],[105,-2]],[[572272,341661],[10,-43],[11,-163],[32,-145],[-64,-85],[-27,-17],[-28,-1],[-66,31],[-37,104],[-108,116],[-45,114],[-69,113]],[[584842,293266],[29,3],[100,6],[87,25],[79,53],[123,157],[6,20],[4,30],[20,89],[8,19],[7,6],[128,72],[17,18],[42,87],[69,40],[30,25],[56,24],[53,72],[42,46],[1,0]],[[585743,294058],[5,-88],[-25,-124],[-3,-62],[10,-56],[16,-46],[23,-32],[28,-15],[35,5],[25,19],[49,59],[91,75],[26,43],[6,26],[9,71],[6,21],[20,16],[9,-9],[7,-22],[14,-24],[36,-38],[35,-47],[28,-58],[16,-72],[0,-79],[-33,-228],[4,-34],[8,-38],[3,-21],[2,-18],[-6,-35],[-16,-13],[-47,5],[-21,-3],[-30,-30],[-17,-32],[-17,-21],[-27,3],[-12,14],[-23,49],[-13,18],[-13,7],[-40,1],[-63,-21],[-31,-73],[-8,-107],[12,-126],[11,-63],[13,-40],[46,-77],[41,-48],[10,-24],[3,-23],[-4,-57],[2,-26],[43,-127],[9,-54],[21,-222]],[[586016,292157],[-1,-1],[-59,-78],[-55,-104],[-29,-149],[-14,-260],[-17,-48],[-184,-227],[-54,-85],[-170,-372],[-20,-129],[-26,-48],[-107,-111],[-72,-96],[-63,-110],[-92,-103],[-38,-77],[-31,-170],[-28,-47],[-20,-57],[-91,-125],[-65,-63],[-72,-24],[-209,15],[-145,-27],[-61,-25],[-21,7],[-38,24],[-22,-2],[-29,-43],[-39,-150],[-18,-32],[-39,-16],[-17,-38],[-10,-49],[-19,-48],[-27,-17],[-167,-51],[-204,-250],[-52,-39],[-23,23],[-18,36],[-40,-8],[-73,-41],[-39,16],[-39,39],[-38,27],[-37,-21],[-42,-41],[-30,18],[-53,86],[-29,14],[-44,7],[-41,-13],[-25,-64],[-18,-28],[-21,-19],[-19,4],[-6,32],[7,40],[25,77],[15,80],[3,72],[-4,134],[13,52],[30,64],[61,96],[98,73],[28,37],[15,12],[70,24],[17,12],[159,165],[31,82],[-36,90],[-38,40],[-39,31],[-41,21],[-46,7],[-42,-25],[-63,-89],[-34,15],[-20,58],[-3,74],[15,253],[16,56],[71,81],[40,62],[20,65],[6,75],[1,85],[-7,39],[-28,72],[-5,50],[6,29],[12,17],[13,14],[9,17],[11,77],[3,328],[10,74],[24,66],[44,72],[67,64],[18,25],[53,150],[19,32],[14,9],[16,5],[24,15],[10,16],[19,44],[10,16],[9,3],[23,-6],[8,3],[63,172],[22,27],[11,26],[88,147],[40,98],[23,38],[36,15],[38,7]],[[584034,292679],[63,-203],[53,-86],[59,-19],[24,28],[39,99],[26,41],[31,24],[27,3],[25,-21],[29,-46],[22,-49],[19,-60],[13,-62],[7,-61],[-1,-27],[-7,-64],[0,-21],[6,-47],[2,-7],[8,12],[23,11],[37,7],[33,16],[26,30],[17,50],[2,34],[-9,63],[0,25],[6,27],[19,50],[7,27],[5,108],[12,38],[7,7],[9,0],[18,18],[57,92],[37,90],[7,13],[6,14],[8,32],[9,127],[29,200],[-2,74]],[[571619,341672],[0,0],[-22,-29],[-53,-88],[-18,-19],[-21,4],[-259,168],[2,-16],[6,-40],[4,-18],[15,-25],[18,-10],[15,-15],[6,-38],[-8,-17],[-13,-39],[-1,-39],[28,-18],[40,-5],[37,-16],[31,-30],[17,-45],[-14,-53],[0,-34],[21,-15],[19,13],[20,25],[21,15],[19,-17],[180,-334],[27,-37],[64,-25],[33,-28],[21,-40],[-2,-45],[-30,-41],[-42,-9],[-44,19],[-72,93],[-39,26],[-90,11],[0,-24],[55,-9],[53,-46],[46,-64],[38,-66],[26,-21],[30,8],[30,18],[26,8],[15,-12],[57,-67],[11,-35],[101,-187],[9,-43],[1,-30],[-11,-18],[-32,-7],[-112,23],[25,-55],[37,-41],[83,-52],[78,-28],[22,-31],[-6,-65],[-18,-32],[-56,-59],[-20,-35],[-31,-91],[-24,-25],[-189,-33],[-42,15],[-79,68],[-37,16],[-41,32],[-25,75],[-18,88],[-31,98],[-6,39],[-3,80],[-8,25],[-20,24],[-39,37],[22,-77],[4,-23],[19,-209],[15,-53],[26,-64],[19,-90],[2,-90],[-28,-65],[-30,3],[-83,76],[-39,20],[-247,-5],[-159,51],[-248,-6],[-66,-50],[-49,-136],[45,50],[56,30],[60,15],[106,1],[57,-17],[34,-48],[-22,-95],[-10,-79],[20,-95],[33,-86],[29,-53],[20,-17],[72,-32],[27,-28],[14,-30],[26,-91],[37,38],[57,8],[58,-15],[44,-31],[16,-6],[12,5],[8,-4],[4,-31],[0,-100],[-4,-49],[-9,-44],[-14,-31],[-19,-12],[-19,-18],[-17,-38],[-23,-70],[-13,-24],[-18,-24],[-15,-29],[-7,-34],[-6,-148],[-7,-39],[-28,-50],[-21,6],[-43,71],[-58,42],[-15,6],[-17,-11],[0,-27],[6,-36],[4,-37],[22,-41],[137,-146],[34,-20],[38,-8],[78,1],[45,-15],[7,-39],[-7,-56],[2,-61],[51,-98],[74,-10],[83,12],[81,-30],[22,-28],[7,-23],[5,-25],[13,-35],[8,-38],[-9,-36],[-19,-28],[-20,-10],[-16,-26],[-50,-116],[-19,-34],[29,-17],[41,7],[40,21],[28,27],[36,12],[38,-42],[24,-55],[-5,-27],[-35,-13],[-70,-63],[-34,-21],[-133,-27],[0,-25],[46,2],[139,-51],[185,-25],[-22,-55],[-40,-27],[-44,-21],[-32,-33],[-72,-99],[-14,-41],[26,-49],[77,-47],[79,-18],[73,-35],[61,-96],[15,-57],[8,-70],[9,-239],[13,-70],[17,-60],[19,-51],[78,-126],[28,-77],[187,-308],[17,-37],[23,-80],[14,-32],[33,-158],[25,-65],[14,64],[37,-53],[35,-107],[27,-121],[6,-96],[-15,-57],[-64,-153],[-26,-40],[-132,-61],[-39,-36],[-27,-69],[-4,-72],[-8,-69],[-39,-64],[0,-48],[-62,7],[-108,41],[34,-67],[73,-73],[35,-66],[-78,-42],[-66,-7],[-65,7],[-71,29],[-26,-5],[17,-49],[28,-23],[74,-14],[31,-15],[24,-41],[24,-52],[22,-29],[22,25],[12,0],[3,-155],[-5,-83],[-17,-36],[-13,-20],[1,-42],[11,-43],[15,-19],[20,5],[6,8],[4,-6],[15,-32],[18,-59],[16,-75],[24,-64],[40,-27],[39,-12],[30,-28],[27,-33],[30,-26],[46,2],[22,-7],[9,-31],[-3,-45],[-4,-36],[1,-31],[14,-37],[138,-286],[113,-83],[165,-251],[77,-75],[79,-30],[33,-40],[47,-93],[43,-19],[16,-22],[-13,-47],[-28,-40],[-8,-27],[10,-33],[19,-16],[45,-1],[21,-18],[4,-26],[6,-91],[4,-32],[1Line truncated
{"type":"Topology","bbox":[-180,-89.999,180,83.599609375],"transform":{"scale":[0.00036000036000036,0.00017359878297378296],"translate":[-180,-89.999]},"objects":{"countries":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[1596,1597,1598,1599]]],"id":"716","properties":{"name":"Zimbabwe","a3":"ZWE"}},{"type":"MultiPolygon","arcs":[[[-1599,1600,1601,1602,1603,1604,1605]]],"id":"894","properties":{"name":"Zambia","a3":"ZMB"}},{"type":"MultiPolygon","arcs":[[[102,1606,1607]],[[124]],[[121]],[[123]],[[122]]],"id":"887","properties":{"name":"Yemen","a3":"YEM"}},{"type":"MultiPolygon","arcs":[[[83,1608,1609,1610]],[[125]],[[129]],[[131]],[[130]],[[128]],[[127]],[[126]]],"id":"704","properties":{"name":"Vietnam","a3":"VNM"}},{"type":"MultiPolygon","arcs":[[[1611,1612,1613,168]],[[133]],[[135]],[[134]],[[132]]],"id":"862","properties":{"name":"Venezuela","a3":"VEN"}},{"type":"MultiPolygon","arcs":[[[1614]]],"id":"336","properties":{"name":"Vatican","a3":"VAT"}},{"type":"MultiPolygon","arcs":[[[170]],[[171]],[[183]],[[174]],[[173]],[[182]],[[176]],[[172]],[[175]],[[179]],[[180]],[[177]],[[178]],[[181]]],"id":"548","properties":{"name":"Vanuatu","a3":"VUT"}},{"type":"MultiPolygon","arcs":[[[1615,1616,1617,1618,1619],[1620]],[[1621]],[[1622]]],"id":"860","properties":{"name":"Uzbekistan","a3":"UZB"}},{"type":"MultiPolygon","arcs":[[[139,1623,1624]]],"id":"858","properties":{"name":"Uruguay","a3":"URY"}},{"type":"MultiPolygon","arcs":[[[188]],[[185]],[[186]],[[184]],[[187]]],"id":"583","properties":{"name":"Micronesia","a3":"FSM"}},{"type":"MultiPolygon","arcs":[[[192]],[[189]],[[191]],[[190]],[[193]]],"id":"584","properties":{"name":"Marshall Is.","a3":"MHL"}},{"type":"MultiPolygon","arcs":[[[198]],[[199]],[[196]],[[197]],[[194]],[[195]]],"id":"580","properties":{"name":"N. Mariana Is.","a3":"MNP"}},{"type":"MultiPolygon","arcs":[[[202]],[[200]],[[201]]],"id":"850","properties":{"name":"U.S. Virgin Is.","a3":"VIR"}},{"type":"MultiPolygon","arcs":[[[203]]],"id":"316","properties":{"name":"Guam","a3":"GUM"}},{"type":"MultiPolygon","arcs":[[[204]]],"id":"016","properties":{"name":"American Samoa","a3":"ASM"}},{"type":"MultiPolygon","arcs":[[[205]],[[206]],[[207]]],"id":"630","properties":{"name":"Puerto Rico","a3":"PRI"}},{"type":"MultiPolygon","arcs":[[[1627,159,1628,152,1629]],[[154,1625,156,1626]],[[275]],[[223]],[[263]],[[262]],[[221]],[[260]],[[222]],[[261]],[[224]],[[282]],[[228]],[[267]],[[229]],[[264]],[[265]],[[277]],[[266]],[[278]],[[227]],[[279]],[[269]],[[230]],[[274]],[[232]],[[270]],[[276]],[[271]],[[300]],[[226]],[[209]],[[208]],[[225]],[[248]],[[241]],[[231]],[[309]],[[268]],[[256]],[[258]],[[244]],[[245]],[[281]],[[273]],[[247]],[[235]],[[295]],[[233]],[[283]],[[305]],[[259]],[[304]],[[272]],[[298]],[[320]],[[285]],[[306]],[[280]],[[294]],[[210]],[[252]],[[239]],[[289]],[[297]],[[213]],[[246]],[[250]],[[303]],[[236]],[[251]],[[296]],[[287]],[[255]],[[243]],[[242]],[[284]],[[299]],[[307]],[[253]],[[308]],[[249]],[[312]],[[240]],[[286]],[[238]],[[237]],[[212]],[[234]],[[217]],[[291]],[[318]],[[215]],[[325]],[[254]],[[329]],[[327]],[[331]],[[293]],[[219]],[[324]],[[314]],[[302]],[[257]],[[292]],[[218]],[[330]],[[214]],[[326]],[[317]],[[211]],[[311]],[[319]],[[323]],[[216]],[[288]],[[301]],[[310]],[[220]],[[332]],[[290]],[[328]],[[313]],[[322]],[[321]],[[315]],[[316]]],"id":"840","properties":{"name":"United States of America","a3":"USA"}},{"type":"MultiPolygon","arcs":[[[334]],[[333]]],"id":"239","properties":{"name":"S. Geo. and the Is.","a3":"SGS"}},{"type":"MultiPolygon","arcs":[[[335]]],"id":"086","properties":{"name":"Br. Indian Ocean Ter.","a3":"IOT"}},{"type":"MultiPolygon","arcs":[[[336]],[[337]]],"id":"654","properties":{"name":"Saint Helena","a3":"SHN"}},{"type":"MultiPolygon","arcs":[[[338]]],"id":"612","properties":{"name":"Pitcairn Is.","a3":"PCN"}},{"type":"MultiPolygon","arcs":[[[339]]],"id":"660","properties":{"name":"Anguilla","a3":"AIA"}},{"type":"MultiPolygon","arcs":[[[340]],[[341]],[[342]],[[344]],[[345]],[[343]]],"id":"238","properties":{"name":"Falkland Is.","a3":"FLK"}},{"type":"MultiPolygon","arcs":[[[346]],[[348]],[[347]]],"id":"136","properties":{"name":"Cayman Is.","a3":"CYM"}},{"type":"MultiPolygon","arcs":[[[349]]],"id":"060","properties":{"name":"Bermuda","a3":"BMU"}},{"type":"MultiPolygon","arcs":[[[351]],[[350]],[[352]]],"id":"092","properties":{"name":"British Virgin Is.","a3":"VGB"}},{"type":"MultiPolygon","arcs":[[[353]],[[355]],[[354]]],"id":"796","properties":{"name":"Turks and Caicos Is.","a3":"TCA"}},{"type":"MultiPolygon","arcs":[[[356]]],"id":"500","properties":{"name":"Montserrat","a3":"MSR"}},{"type":"MultiPolygon","arcs":[[[357]]],"id":"832","properties":{"name":"Jersey","a3":"JEY"}},{"type":"MultiPolygon","arcs":[[[358]]],"id":"831","properties":{"name":"Guernsey","a3":"GGY"}},{"type":"MultiPolygon","arcs":[[[359]]],"id":"833","properties":{"name":"Isle of Man","a3":"IMN"}},{"type":"MultiPolygon","arcs":[[[360]],[[1630,381]],[[375]],Line truncated
+68 -62
View File
@@ -1,13 +1,14 @@
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'common'
import { capitalize, chunk, compact, find, findIndex, has, isObject, uniq, uniqBy } from 'lodash'
import { createContext, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { useLatest } from 'react-use'
import { toast } from 'sonner'
import * as tus from 'tus-js-client'
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui'
import { proxy, useSnapshot } from 'valtio'
import { useSelectedBucket } from '@/components/interfaces/Storage/FilesBuckets/useSelectedBucket'
import {
STORAGE_BUCKET_SORT,
STORAGE_ROW_STATUS,
@@ -32,6 +33,7 @@ import {
sanitizeNameForDuplicateInColumn,
validateFolderName,
} from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer.utils'
import { fetchFileUrl } from '@/components/interfaces/Storage/StorageExplorer/useFetchFileUrlQuery'
import { convertFromBytes } from '@/components/interfaces/Storage/StorageSettings/StorageSettings.utils'
import { InlineLink } from '@/components/ui/InlineLink'
import { getOrRefreshTemporaryApiKey } from '@/data/api-keys/temp-api-keys-utils'
@@ -40,6 +42,7 @@ import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import type { ProjectStorageConfigResponse } from '@/data/config/project-storage-config-query'
import { getQueryClient } from '@/data/query-client'
import { deleteBucketObject } from '@/data/storage/bucket-object-delete-mutation'
import { signBucketObjects } from '@/data/storage/bucket-object-sign-mutation'
import { listBucketObjects, StorageObject } from '@/data/storage/bucket-objects-list-mutation'
import { deleteBucketPrefix } from '@/data/storage/bucket-prefix-delete-mutation'
import type { Bucket } from '@/data/storage/buckets-query'
@@ -50,7 +53,6 @@ import { tryParseJson } from '@/lib/helpers'
import { lookupMime } from '@/lib/mime'
import { createProjectSupabaseClient } from '@/lib/project-supabase-client'
import { ResponseError } from '@/types'
import { useSelectedBucket } from '@/components/interfaces/Storage/FilesBuckets/useSelectedBucket'
type UploadProgress = {
percentage: number
@@ -81,15 +83,15 @@ if (typeof window !== 'undefined') {
function createStorageExplorerState({
projectRef,
connectionString,
bucket,
resumableUploadUrl,
clientEndpoint,
bucketId,
}: {
projectRef: string
connectionString: string
bucket?: Bucket
resumableUploadUrl: string
clientEndpoint: string
bucketId?: string
}) {
const localStorageKey = LOCAL_STORAGE_KEYS.STORAGE_PREFERENCE(projectRef)
const { view, sortBy, sortByOrder, sortBucket } =
@@ -98,10 +100,10 @@ function createStorageExplorerState({
const state = proxy({
projectRef,
bucketId,
connectionString,
resumableUploadUrl,
uploadProgresses: [] as UploadProgress[],
selectedBucket: bucket as Bucket,
abortApiCalls: () => {
if (abortController) {
@@ -162,14 +164,6 @@ function createStorageExplorerState({
state.selectedItemsToMove = items
},
selectedBucket: {} as Bucket,
setSelectedBucket: (bucket: Bucket) => {
state.selectedBucket = bucket
state.setSelectedFilePreview(undefined)
state.clearOpenedFolders()
state.clearSelectedItems()
},
setSelectedItemToRename: (file: { name: string; columnIndex: number }) => {
state.updateRowStatus({
name: file.name,
@@ -236,10 +230,6 @@ function createStorageExplorerState({
})
},
openBucket: async (bucket: Bucket) => {
state.setSelectedBucket(bucket)
},
// ======== Folders CRUD ========
addNewFolderPlaceholder: (columnIndex: number) => {
@@ -807,8 +797,34 @@ function createStorageExplorerState({
{ id: toastId, closeButton: false, position: 'top-right' }
)
// Pre-fetch all URLs in a single batch to avoid N management API calls
const filePaths = files.map((file) => `${file.prefix}/${file.name}`)
const urlByPath = new Map<string, string>()
if (state.selectedBucket.public) {
for (const filePath of filePaths) {
urlByPath.set(
filePath,
`${clientEndpoint}/storage/v1/object/public/${state.selectedBucket.id}/${filePath}`
)
}
} else {
const signedUrls = await signBucketObjects({
projectRef: state.projectRef,
bucketId: state.selectedBucket.id,
paths: filePaths,
expiresIn: 60 * 60, // 1 hour — enough for large folder downloads
})
for (const item of signedUrls) {
if (item.path && item.signedUrl) {
urlByPath.set(item.path, item.signedUrl)
}
}
}
const promises = files.map((file) => {
const fileMimeType = (file.metadata?.mimetype as string) ?? null
const filePath = `${file.prefix}/${file.name}`
return () => {
return new Promise<
| {
@@ -819,26 +835,22 @@ function createStorageExplorerState({
| boolean
>(async (resolve) => {
try {
// Get authenticated Supabase client for Storage API access
const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
const url = urlByPath.get(filePath)
if (!url) throw new Error(`Failed to retrieve file ${filePath}`)
// Use Storage API directly instead of Management API to avoid throttling
const { data, error } = await client.storage
.from(state.selectedBucket.id)
.download(`${file.prefix}/${file.name}`)
if (error) throw error
if (!data) throw new Error('No data returned from download')
const response = await fetch(url)
if (!response.ok) throw new Error(`Failed to retrieve file ${filePath}`)
const data = await response.blob()
progress = progress + 1 / files.length
resolve({
name: file.name,
prefix: file.prefix,
blob: new Blob([data], { type: fileMimeType }),
blob: new Blob([data], { type: fileMimeType ?? data.type }),
})
} catch (error) {
console.error('Failed to download file', `${file.prefix}/${file.name}`)
console.error('Failed to download file', filePath)
resolve(false)
}
})
@@ -1542,17 +1554,17 @@ function createStorageExplorerState({
const toastId = showToast ? toast.loading(`Retrieving ${fileName}...`) : undefined
try {
const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
const url = await fetchFileUrl(
file.path,
state.projectRef,
state.selectedBucket.id,
state.selectedBucket.public
)
const response = await fetch(url)
if (!response.ok) throw new Error(`Failed to retrieve file ${file.path}`)
const data = await response.blob()
// Use Storage API directly instead of Management API to avoid throttling
const { data, error } = await client.storage
.from(state.selectedBucket.id)
.download(file.path)
if (error) throw error
if (!data) throw new Error('No data returned from download')
const newBlob = new Blob([data], { type: fileMimeType })
const newBlob = new Blob([data], { type: fileMimeType ?? data.type })
const blobUrl = window.URL.createObjectURL(newBlob)
const link = document.createElement('a')
link.href = blobUrl
@@ -1586,17 +1598,18 @@ function createStorageExplorerState({
if (!file.path) return false
try {
const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
const { data, error } = await client.storage
.from(state.selectedBucket.id)
.download(file.path)
if (error) throw error
if (!data) throw new Error('No data returned from download')
const url = await fetchFileUrl(
file.path,
state.projectRef,
state.selectedBucket.id,
state.selectedBucket.public
)
const response = await fetch(url)
if (!response.ok) throw new Error(`Failed to retrieve file ${file.path}`)
const data = await response.blob()
const fileMimeType = file?.metadata?.mimetype ?? undefined
return { name: file.name, blob: new Blob([data], { type: fileMimeType }) }
return { name: file.name, blob: new Blob([data], { type: fileMimeType ?? data.type }) }
} catch (err) {
console.error('Failed to download file', file.path)
return false
@@ -1860,6 +1873,7 @@ const DEFAULT_STATE_CONFIG = {
connectionString: '',
resumableUploadUrl: '',
clientEndpoint: '',
bucket: {} as Bucket,
}
const StorageExplorerStateContext = createContext<StorageExplorerState>(
@@ -1869,12 +1883,10 @@ const StorageExplorerStateContext = createContext<StorageExplorerState>(
export const StorageExplorerStateContextProvider = ({ children }: PropsWithChildren) => {
const { data: project } = useSelectedProjectQuery()
const { data: bucket } = useSelectedBucket()
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG))
const stateRef = useLatest(state)
const bucketRef = useRef(bucket?.id)
const {
storageEndpoint,
@@ -1888,38 +1900,32 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
// So the useEffect here is to make sure that the project ref is loaded into the state properly
// Although I'd be keen to re-investigate this to see if we can remove this
useEffect(() => {
const hasDataReady = !!project?.ref
const hasDataReady = !!project && !!bucket
const storeAlreadyLoaded = state.projectRef === project?.ref
const hasBucketChanged = bucket?.id !== bucketRef.current
if (
!isPaused &&
hasDataReady &&
isSuccessSettings &&
(!storeAlreadyLoaded || hasBucketChanged)
) {
if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) {
const clientEndpoint = storageEndpoint ?? hostEndpoint ?? ''
const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable`
setState(
createStorageExplorerState({
projectRef: project?.ref ?? '',
bucketId: bucket?.id,
projectRef: project.ref,
connectionString: project.connectionString ?? '',
bucket,
resumableUploadUrl,
clientEndpoint,
})
)
}
}, [
bucket?.id,
state.projectRef,
project?.ref,
project?.connectionString,
project,
stateRef,
isPaused,
hostEndpoint,
storageEndpoint,
isSuccessSettings,
bucket,
])
return (
+2 -1
View File
@@ -2,4 +2,5 @@
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
NEXT_PUBLIC_BASE_PATH=/ui
NEXT_PUBLIC_API_URL="http://localhost:8080/platform"
NEXT_PUBLIC_API_URL="http://localhost:8080/platform"
NEXT_PUBLIC_GOTRUE_URL="http://localhost:8000/auth/v1"
+15 -9
View File
@@ -1,5 +1,6 @@
'use client'
import { QueryClientProvider } from '@tanstack/react-query'
import { AuthProvider } from 'common'
import { Provider as JotaiProvider } from 'jotai'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
@@ -8,19 +9,24 @@ import { TooltipProvider } from 'ui'
import { FrameworkProvider } from '@/context/framework-context'
import { MobileMenuProvider } from '@/context/mobile-menu-context'
import { useRootQueryClient } from '@/lib/fetch/queryClient'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const queryClient = useRootQueryClient()
return (
<AuthProvider>
<JotaiProvider>
<NextThemesProvider {...props}>
<MobileMenuProvider>
<FrameworkProvider>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</FrameworkProvider>
</MobileMenuProvider>
</NextThemesProvider>
</JotaiProvider>
<QueryClientProvider client={queryClient}>
<JotaiProvider>
<NextThemesProvider {...props}>
<MobileMenuProvider>
<FrameworkProvider>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</FrameworkProvider>
</MobileMenuProvider>
</NextThemesProvider>
</JotaiProvider>
</QueryClientProvider>
</AuthProvider>
)
}
@@ -0,0 +1,68 @@
import { OpenAPIDefinition } from '../types'
import { findPrimaryKeys, propAccess, sanitizeIdentifier, toCamelCase } from '../utils'
// Generate db.ts content from OpenAPI definitions
export function generateDbContent(definitions: Record<string, OpenAPIDefinition>): string {
const tableNames = Object.keys(definitions).filter((name) => !name.startsWith('_'))
const lines: string[] = [
"import { supabaseCollectionOptions } from 'supa-tdb-collection'",
"import { createClient } from '@/lib/supabase/client'",
'import {',
...tableNames.map((name) => ` ${toCamelCase(sanitizeIdentifier(name))}Schema,`),
"} from './schemas'",
"import { createCollection } from '@tanstack/db'",
'',
'const supabase = createClient()',
'',
]
for (const [tableName, definition] of Object.entries(definitions)) {
// Skip internal PostgREST tables
if (tableName.startsWith('_')) continue
const safeTableId = sanitizeIdentifier(tableName)
const collectionName = `${toCamelCase(safeTableId)}Collection`
const schemaName = `${toCamelCase(safeTableId)}Schema`
const properties = definition.properties || {}
const primaryKeys = findPrimaryKeys(properties)
if (primaryKeys.length === 0) {
throw new Error(
`Table "${tableName}" has no primary key columns. TanStack DB collections require at least one primary key. Skipping generation for this table.`
)
}
// Generate getKey function using bracket notation for safety
let getKeyFn: string
if (primaryKeys.length === 1) {
getKeyFn = `(item) => ${propAccess('item', primaryKeys[0])}`
} else {
const keyParts = primaryKeys.map((k) => `\${${propAccess('item', k)}}`).join('-')
getKeyFn = `(item) => \`${keyParts}\``
}
// Generate where function
let whereFn: string
if (primaryKeys.length === 1) {
whereFn = `(query, item) => query.eq(${JSON.stringify(primaryKeys[0])}, ${propAccess('item', primaryKeys[0])})`
} else {
const whereParts = primaryKeys
.map((k) => `.eq(${JSON.stringify(k)}, ${propAccess('item', k)})`)
.join('')
whereFn = `(query, item) => query${whereParts}`
}
lines.push(`export const ${collectionName} = createCollection(supabaseCollectionOptions({`)
lines.push(` tableName: ${JSON.stringify(tableName)},`)
lines.push(` getKey: ${getKeyFn},`)
lines.push(` where: ${whereFn},`)
lines.push(` schema: ${schemaName},`)
lines.push(` realtime: true,`)
lines.push(' supabase: supabase,')
lines.push('}))')
lines.push('')
}
return lines.join('\n')
}
@@ -0,0 +1,5 @@
export { generateSchemasContent } from './schemas'
export { generateDbContent } from './db'
export { generatePageContent } from './page'
export { generateSheetContent } from './sheet'
export { generateListContent } from './list'
@@ -0,0 +1,206 @@
import { OpenAPIDefinition } from '../types'
import {
findPrimaryKeys,
propAccess,
sanitizeIdentifier,
toCamelCase,
toPascalCase,
toSingular,
} from '../utils'
// Generate list component content
export function generateListContent(tableName: string, definition: OpenAPIDefinition): string {
const safeTableId = sanitizeIdentifier(tableName)
const pascalName = toPascalCase(safeTableId)
const singularName = toSingular(safeTableId)
const singularPascal = toPascalCase(singularName)
const camelName = toCamelCase(safeTableId)
const collectionName = `${camelName}Collection`
const listComponentName = `${pascalName}List`
const sheetComponentName = `${singularPascal}Sheet`
const properties = definition.properties || {}
const primaryKeys = findPrimaryKeys(properties)
const primaryKey = primaryKeys[0] || 'id'
// Get display fields (non-system fields, first 2 for display)
const editableFields = Object.entries(properties).filter(([name]) => {
const isSystemField =
name === primaryKey || name === 'created_at' || name === 'updated_at' || name.endsWith('_at')
return !isSystemField
})
const displayField = editableFields[0]?.[0] || primaryKey
const secondaryField = editableFields[1]?.[0]
// Get all fields for select
const allFieldNames = Object.keys(properties)
const selectFields = allFieldNames
.map((name) => ` ${JSON.stringify(name)}: ${propAccess('item', name)},`)
.join('\n')
// Determine if we have created_at for display
const hasCreatedAt = properties['created_at'] !== undefined
// Pre-compute safe property access expressions for the template
const displayAccess = propAccess('item', displayField)
const primaryKeyAccess = propAccess('item', primaryKey)
// Generate secondary info display
let secondaryInfo = ''
if (secondaryField) {
const secondaryAccess = propAccess('item', secondaryField)
secondaryInfo = `
{${secondaryAccess} && (
<span className="truncate">{${secondaryAccess}}</span>
)}
${hasCreatedAt ? `<span>•</span>` : ''}`
}
return `'use client'
import { useState } from 'react'
import { useLiveQuery } from '@tanstack/react-db'
import { Pencil, Plus, Trash2, FileText } from 'lucide-react'
import { ${collectionName} } from '@/lib/db'
import { ${pascalName} } from '@/lib/schemas'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { ${sheetComponentName} } from './${singularName}-sheet'
export function ${listComponentName}() {
const [isCreateSheetOpen, setIsCreateSheetOpen] = useState(false)
const [editingItem, setEditingItem] = useState<${pascalName} | null>(null)
const { data: items = [] } = useLiveQuery(
(q) =>
q
.from({ item: ${collectionName} })
.orderBy(({ item }) => ${displayAccess}, 'asc')
.select(({ item }) => ({
${selectFields}
})),
[]
)
const handleDelete = (itemId: string) => {
${collectionName}.delete([itemId])
}
return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="border-border/50 border-b bg-linear-to-r from-primary/5 to-transparent p-6 rounded-t-lg">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="font-bold text-2xl tracking-tight">${pascalName}</h1>
<p className="text-muted-foreground text-sm">
{items.length} ${singularName.replace(/_/g, ' ')}{items.length !== 1 ? 's' : ''} in database
</p>
</div>
</div>
<Button className="gap-2" onClick={() => setIsCreateSheetOpen(true)}>
<Plus className="h-4 w-4" />
Add ${singularPascal}
</Button>
</div>
</div>
{/* Items list */}
<div className="flex-1 overflow-y-auto p-4">
{items.length > 0 ? (
<div className="grid gap-3">
{items.map((item) => (
<div
className={cn(
'flex items-center justify-between rounded-lg p-4',
'border border-border/50 bg-card shadow-sm',
'transition-all duration-150 hover:shadow-md hover:border-border'
)}
key={${primaryKeyAccess}}
>
<div className="flex items-center gap-4 flex-1 min-w-0">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-linear-to-br from-chart-1/20 to-chart-2/20 shrink-0">
<FileText className="h-5 w-5 text-foreground/70" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">
{${displayAccess} || 'Untitled'}
</p>
<div className="flex items-center gap-3 text-muted-foreground text-xs">${secondaryInfo}${
hasCreatedAt
? `
<span>
Created {new Date(item.created_at).toLocaleDateString()}
</span>`
: `
<span className="font-mono truncate max-w-[120px]" title={${primaryKeyAccess}}>
{${primaryKeyAccess}.slice(0, 8)}...
</span>`
}
</div>
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
className="text-muted-foreground hover:text-primary"
onClick={() => setEditingItem(item)}
size="sm"
variant="ghost"
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
<Button
className="text-muted-foreground hover:text-destructive"
onClick={() => handleDelete(${primaryKeyAccess})}
size="sm"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</div>
</div>
))}
</div>
) : (
<div className="flex h-64 flex-col items-center justify-center text-muted-foreground border border-dashed border-border/50 rounded-lg">
<FileText className="mb-3 h-12 w-12 opacity-30" />
<p className="font-medium">No ${tableName.replace(/_/g, ' ')} yet</p>
<p className="text-sm mb-4">Create your first ${singularName.replace(/_/g, ' ')} to get started</p>
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={() => setIsCreateSheetOpen(true)}
>
<Plus className="h-4 w-4" />
Add ${singularPascal}
</Button>
</div>
)}
</div>
{/* Create Sheet */}
<${sheetComponentName}
open={isCreateSheetOpen}
onOpenChange={setIsCreateSheetOpen}
mode="create"
/>
{/* Edit Sheet */}
<${sheetComponentName}
open={!!editingItem}
onOpenChange={(open) => !open && setEditingItem(null)}
mode="edit"
item={editingItem}
/>
</div>
)
}
`
}
@@ -0,0 +1,21 @@
import { safeFileSegment, sanitizeIdentifier, toPascalCase } from '../utils'
// Generate page.tsx content
export function generatePageContent(tableName: string): string {
const safeTableId = sanitizeIdentifier(tableName)
const pascalName = toPascalCase(safeTableId)
const listComponentName = `${pascalName}List`
const safeTable = safeFileSegment(tableName)
return `import { ${listComponentName} } from './${safeTable}-list'
export default async function ${pascalName}Page() {
return (
<div className="container mx-auto max-w-4xl py-8">
<${listComponentName} />
</div>
)
}
`
}
@@ -0,0 +1,34 @@
import { OpenAPIDefinition } from '../types'
import { openApiTypeToZod, sanitizeIdentifier, toCamelCase, toPascalCase } from '../utils'
// Generate schemas.ts content from OpenAPI definitions
export function generateSchemasContent(definitions: Record<string, OpenAPIDefinition>): string {
const lines: string[] = ["import { z } from 'zod'", '']
for (const [tableName, definition] of Object.entries(definitions)) {
// Skip internal PostgREST tables
if (tableName.startsWith('_')) continue
const safeTableId = sanitizeIdentifier(tableName)
const typeName = toPascalCase(safeTableId)
const schemaName = `${toCamelCase(safeTableId)}Schema`
const properties = definition.properties || {}
const required = definition.required || []
lines.push(`// ${typeName} schema`)
lines.push(`export const ${schemaName} = z.object({`)
for (const [propName, prop] of Object.entries(properties)) {
const isRequired = required.includes(propName)
const zodType = openApiTypeToZod(prop, isRequired)
lines.push(` ${JSON.stringify(propName)}: ${zodType},`)
}
lines.push('})')
lines.push('')
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>`)
lines.push('')
}
return lines.join('\n')
}
@@ -0,0 +1,332 @@
import { OpenAPIDefinition } from '../types'
import {
findPrimaryKeys,
propAccess,
sanitizeIdentifier,
toCamelCase,
toLabel,
toPascalCase,
toSingular,
} from '../utils'
// Generate sheet component content
export function generateSheetContent(tableName: string, definition: OpenAPIDefinition): string {
const safeTableId = sanitizeIdentifier(tableName)
const pascalName = toPascalCase(safeTableId)
const singularName = toSingular(safeTableId)
const singularPascal = toPascalCase(singularName)
const camelName = toCamelCase(safeTableId)
const collectionName = `${camelName}Collection`
const sheetComponentName = `${singularPascal}Sheet`
const properties = definition.properties || {}
const required = definition.required || []
const primaryKeys = findPrimaryKeys(properties)
const primaryKey = primaryKeys[0] || 'id'
// Separate fields into editable and read-only
const allFields = Object.entries(properties)
const editableFields = allFields.filter(([name]) => {
const isSystemField =
name === primaryKey || name === 'created_at' || name === 'updated_at' || name.endsWith('_at')
return !isSystemField
})
const readOnlyFields = allFields.filter(([name]) => {
return (
name === primaryKey || name === 'created_at' || name === 'updated_at' || name.endsWith('_at')
)
})
// Generate state declarations
const stateDeclarations = editableFields
.map(([name, prop]) => {
const safeName = sanitizeIdentifier(name)
const stateName = toCamelCase(safeName)
if (prop.type === 'boolean') {
return ` const [${stateName}, set${toPascalCase(safeName)}] = useState(false)`
}
if (prop.type === 'integer' || prop.type === 'number') {
return ` const [${stateName}, set${toPascalCase(safeName)}] = useState<number | ''>(0)`
}
return ` const [${stateName}, set${toPascalCase(safeName)}] = useState('')`
})
.join('\n')
// Generate useEffect reset
const useEffectReset = editableFields
.map(([name, prop]) => {
const safeName = sanitizeIdentifier(name)
const stateName = toCamelCase(safeName)
const setterName = `set${toPascalCase(safeName)}`
const access = propAccess('item?', name)
if (prop.type === 'boolean') {
return ` ${setterName}(${access} ?? false)`
}
if (prop.type === 'integer' || prop.type === 'number') {
return ` ${setterName}(${access} ?? 0)`
}
return ` ${setterName}(${access} ?? '')`
})
.join('\n')
// Generate form fields for editable fields
const editableFormFields = editableFields
.map(([name, prop]) => {
const safeName = sanitizeIdentifier(name)
const stateName = toCamelCase(safeName)
const setterName = `set${toPascalCase(safeName)}`
const label = toLabel(name)
const isRequired = required.includes(name)
if (prop.type === 'boolean') {
return ` {/* ${label} Field */}
<div className="flex items-center gap-3">
<input
id="${singularName}-${name}"
type="checkbox"
checked={${stateName}}
onChange={(e) => ${setterName}(e.target.checked)}
className="h-4 w-4 rounded border"
/>
<Label htmlFor="${singularName}-${name}">${label}</Label>
</div>`
}
if (prop.type === 'integer' || prop.type === 'number') {
return ` {/* ${label} Field */}
<div className="flex flex-col gap-2">
<Label htmlFor="${singularName}-${name}">
${label}${isRequired ? ' <span className="text-destructive">*</span>' : ''}
</Label>
<Input
id="${singularName}-${name}"
type="number"
value={${stateName}}
onChange={(e) => ${setterName}(e.target.value ? Number(e.target.value) : '')}
placeholder="Enter ${label.toLowerCase()}"
/>
</div>`
}
if (prop.enum && prop.enum.length > 0) {
const options = prop.enum
.map((e) => ` <option value="${e}">${toLabel(e)}</option>`)
.join('\n')
return ` {/* ${label} Field */}
<div className="flex flex-col gap-2">
<Label htmlFor="${singularName}-${name}">
${label}${isRequired ? ' <span className="text-destructive">*</span>' : ''}
</Label>
<select
id="${singularName}-${name}"
value={${stateName}}
onChange={(e) => ${setterName}(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
>
<option value="">Select ${label.toLowerCase()}</option>
${options}
</select>
</div>`
}
return ` {/* ${label} Field */}
<div className="flex flex-col gap-2">
<Label htmlFor="${singularName}-${name}">
${label}${isRequired ? ' <span className="text-destructive">*</span>' : ''}
</Label>
<Input
id="${singularName}-${name}"
value={${stateName}}
onChange={(e) => ${setterName}(e.target.value)}
placeholder="Enter ${label.toLowerCase()}"
/>
</div>`
})
.join('\n\n')
// Generate read-only fields for edit mode
const readOnlyFormFields = readOnlyFields
.map(([name]) => {
const label = toLabel(name)
const isTimestamp = name.endsWith('_at')
const itemAccess = propAccess('item', name)
const displayValue = isTimestamp ? `new Date(${itemAccess}).toLocaleString()` : itemAccess
return ` {/* ${label} - Read Only */}
<div className="flex flex-col gap-2">
<Label htmlFor="${singularName}-${name}" className="text-muted-foreground">
${label}
</Label>
<Input
id="${singularName}-${name}"
value={${displayValue}}
disabled
className="${name === primaryKey ? 'font-mono text-sm ' : ''}bg-muted"
/>
</div>`
})
.join('\n\n')
// Generate insert data
const insertData = [
` ${JSON.stringify(primaryKey)}: crypto.randomUUID(),`,
...editableFields.map(([name, prop]) => {
const safeName = sanitizeIdentifier(name)
const stateName = toCamelCase(safeName)
const key = JSON.stringify(name)
if (prop.type === 'boolean') {
return ` ${key}: ${stateName},`
}
if (prop.type === 'integer' || prop.type === 'number') {
return ` ${key}: ${stateName} || 0,`
}
return ` ${key}: ${stateName},`
}),
// Add timestamps if they exist
...readOnlyFields
.filter(([name]) => name === 'created_at')
.map(() => ` "created_at": new Date().toISOString(),`),
].join('\n')
// Generate update data (direct mutations on draft)
const updateData = editableFields
.map(([name, prop]) => {
const safeName = sanitizeIdentifier(name)
const stateName = toCamelCase(safeName)
const draftAccess = propAccess('draft', name)
if (prop.type === 'boolean') {
return ` ${draftAccess} = ${stateName}`
}
if (prop.type === 'integer' || prop.type === 'number') {
return ` ${draftAccess} = ${stateName} || 0`
}
return ` ${draftAccess} = ${stateName}`
})
.join('\n')
// Generate reset states
const resetStates = editableFields
.map(([name, prop]) => {
const safeName = sanitizeIdentifier(name)
const setterName = `set${toPascalCase(safeName)}`
if (prop.type === 'boolean') {
return ` ${setterName}(false)`
}
if (prop.type === 'integer' || prop.type === 'number') {
return ` ${setterName}(0)`
}
return ` ${setterName}('')`
})
.join('\n')
// Generate validation (check required fields)
const requiredEditableFields = editableFields.filter(([name]) => required.includes(name))
const validationCheck =
requiredEditableFields.length > 0
? requiredEditableFields
.map(([name, prop]) => {
const safeName = sanitizeIdentifier(name)
const stateName = toCamelCase(safeName)
if (prop.type === 'boolean') return null
if (prop.type === 'integer' || prop.type === 'number') {
return `${stateName} === ''`
}
return `!${stateName}.trim()`
})
.filter(Boolean)
.join(' || ')
: null
return `'use client'
import { useState, useEffect } from 'react'
import { ${collectionName} } from '@/lib/db'
import { ${pascalName} } from '@/lib/schemas'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
interface ${sheetComponentName}Props {
open: boolean
onOpenChange: (open: boolean) => void
mode: 'create' | 'edit'
item?: ${pascalName} | null
}
export function ${sheetComponentName}({ open, onOpenChange, mode, item }: ${sheetComponentName}Props) {
${stateDeclarations}
const isCreate = mode === 'create'
useEffect(() => {
if (open) {
${useEffectReset}
}
}, [open, item])
const handleSubmit = () => {
${validationCheck ? ` if (${validationCheck}) return\n` : ''}
if (isCreate) {
${collectionName}.insert({
${insertData}
})
} else if (item) {
${collectionName}.update(${propAccess('item', primaryKey)}, (draft) => {
${updateData}
})
}
handleClose()
}
const handleClose = () => {
${resetStates}
onOpenChange(false)
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md">
<SheetHeader>
<SheetTitle>{isCreate ? 'Create New ${singularPascal}' : 'Edit ${singularPascal}'}</SheetTitle>
<SheetDescription>
{isCreate
? 'Add a new ${singularName.replace(/_/g, ' ')} to your database.'
: 'Update the ${singularName.replace(/_/g, ' ')} information.'}
</SheetDescription>
</SheetHeader>
<div className="flex flex-col gap-6 py-6">
{/* Read-only fields (only in edit mode) */}
{!isCreate && item && (
<>
${readOnlyFormFields}
</>
)}
{/* Editable fields */}
${editableFormFields}
</div>
<SheetFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSubmit}>
{isCreate ? 'Create ${singularPascal}' : 'Save Changes'}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
`
}
@@ -0,0 +1,153 @@
import fs from 'fs/promises'
import path from 'path'
import { NextRequest, NextResponse } from 'next/server'
import { RegistryItem } from 'shadcn/schema'
import {
generateDbContent,
generateListContent,
generatePageContent,
generateSchemasContent,
generateSheetContent,
} from './_generators'
import { OpenAPISchema, RegistryFile } from './types'
import { safeFileSegment, toSingular, validateDefinitionNames } from './utils'
// Read the base registry JSON
async function readBaseRegistry(): Promise<RegistryItem> {
const fullPath = path.join(process.cwd(), 'public/r/tanstack-db-nextjs.json')
const content = await fs.readFile(fullPath, 'utf-8')
return JSON.parse(content) as RegistryItem
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const ref = searchParams.get('ref')
const anonKey = searchParams.get('anonKey')
if (!ref || !anonKey) {
return NextResponse.json(
{ error: 'Missing required parameters: ref and anonKey' },
{ status: 400 }
)
}
// Validate Supabase project ref to prevent SSRF via crafted hostnames
// Typical Supabase refs are lowercase alphabetic; adjust if needed.
const refPattern = /^[a-z]{1,64}$/
if (!refPattern.test(ref)) {
return NextResponse.json({ error: 'Invalid project ref format.' }, { status: 400 })
}
try {
// Fetch OpenAPI spec from Supabase
const openApiUrl = `https://${ref}.supabase.co/rest/v1/`
const response = await fetch(openApiUrl, {
headers: {
apikey: anonKey,
},
})
if (!response.ok) {
return NextResponse.json(
{ error: `Failed to fetch OpenAPI spec: ${response.statusText}` },
{ status: response.status }
)
}
const openApiSpec: OpenAPISchema = await response.json()
if (!openApiSpec.definitions) {
return NextResponse.json(
{ error: 'No table definitions found in OpenAPI spec' },
{ status: 400 }
)
}
// Validate all table/column names before generating any code
try {
validateDefinitionNames(openApiSpec.definitions)
} catch (validationError) {
return NextResponse.json(
{
error: `Invalid table or column name: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
},
{ status: 400 }
)
}
// Generate dynamic file contents
const schemasContent = generateSchemasContent(openApiSpec.definitions)
const dbContent = generateDbContent(openApiSpec.definitions)
// Get the first table for the example CRUD page
const tableNames = Object.keys(openApiSpec.definitions).filter((name) => !name.startsWith('_'))
// TODO: remove this once we have a way to select the table
const firstTableName = tableNames.length > 0 ? tableNames[0] : null
const firstTableDefinition = firstTableName ? openApiSpec.definitions[firstTableName] : null
// Read the base registry JSON
const baseRegistry = await readBaseRegistry()
// Create dynamic files
const dynamicFiles: RegistryFile[] = [
{
path: 'lib/schemas.ts',
content: schemasContent,
type: 'registry:lib',
},
{
path: 'lib/db.ts',
content: dbContent,
type: 'registry:lib',
},
]
// Add example CRUD pages for the first table
if (firstTableName && firstTableDefinition) {
const singularTableName = toSingular(firstTableName)
const safeTable = safeFileSegment(firstTableName)
const safeSingular = safeFileSegment(singularTableName)
dynamicFiles.push(
{
path: `app/${safeTable}/page.tsx`,
content: generatePageContent(firstTableName),
type: 'registry:page',
target: `app/${safeTable}/page.tsx`,
},
{
path: `app/${safeTable}/${safeSingular}-sheet.tsx`,
content: generateSheetContent(firstTableName, firstTableDefinition),
type: 'registry:component',
target: `app/${safeTable}/${safeSingular}-sheet.tsx`,
},
{
path: `app/${safeTable}/${safeTable}-list.tsx`,
content: generateListContent(firstTableName, firstTableDefinition),
type: 'registry:component',
target: `app/${safeTable}/${safeTable}-list.tsx`,
}
)
}
// Combine dynamic files with base registry files
const registryResponse: RegistryItem = {
...baseRegistry,
files: [...dynamicFiles, ...(baseRegistry.files || [])],
envVars: {
NEXT_PUBLIC_SUPABASE_URL: `https://${ref}.supabase.co`,
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: anonKey,
},
docs: '',
}
return NextResponse.json(registryResponse)
} catch (error) {
console.error('Error generating tanstack-db block:', error)
return NextResponse.json(
{ error: 'Failed to generate block. Please check your project ref and anon key.' },
{ status: 500 }
)
}
}
Loaded 100 of 236 files, more files were not shown because too many files have changed in this diff. Show more