mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
Merge branch 'master' into dnywh/chore/oauth-app-integrations
This commit is contained in:
236 files changed
+112208
-55779
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
+25476
-17331
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
@@ -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>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
+3
-3
@@ -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_>
|
||||
)
|
||||
}
|
||||
+14
@@ -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>
|
||||
)
|
||||
},
|
||||
|
||||
+6
@@ -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 */
|
||||
|
||||
+2
-1
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-15
@@ -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
|
||||
|
||||
+1
-4
@@ -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'
|
||||
|
||||
-105
@@ -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>
|
||||
)
|
||||
}
|
||||
-242
@@ -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>
|
||||
)
|
||||
}
|
||||
-205
@@ -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>
|
||||
)
|
||||
}
|
||||
-70
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+104
-139
@@ -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,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) =>
|
||||
|
||||
@@ -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>(
|
||||
{
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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`)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
{"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
|
||||
@@ -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,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"
|
||||
@@ -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
Reference in new issue
Block a user