mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 23:59:08 +03:00
Merge branch 'master' into feat/homepage-integrations-section
This commit is contained in:
7571 files changed
+542142
-358518
No files matched your search
+3
-4
@@ -3,18 +3,17 @@
|
||||
.git
|
||||
.github
|
||||
about
|
||||
apps
|
||||
docker
|
||||
examples
|
||||
i18n
|
||||
**/node_modules
|
||||
spec
|
||||
apps/docs/spec
|
||||
supabase
|
||||
tests
|
||||
|
||||
**/.next
|
||||
.vercel
|
||||
.env.*
|
||||
**/.env.*
|
||||
|
||||
.dockerignore
|
||||
studio/Dockerfile
|
||||
apps/studio/Dockerfile
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
/packages/shared-data/pricing.ts @roryw10 @kevcodez
|
||||
/packages/shared-data/plans.ts @roryw10 @kevcodez
|
||||
|
||||
/studio/ @supabase/Dashboard
|
||||
/apps/studio/ @supabase/Dashboard
|
||||
/apps/www/ @supabase/Website
|
||||
/apps/www/_blog
|
||||
/docker/ @supabase/cli
|
||||
@@ -0,0 +1,19 @@
|
||||
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file.
|
||||
|
||||
YES/NO
|
||||
|
||||
## What kind of change does this PR introduce?
|
||||
|
||||
Bug fix, feature, docs update, ...
|
||||
|
||||
## What is the current behavior?
|
||||
|
||||
Please link any relevant issues here.
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
Feel free to include screenshots if it includes visual changes.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context or screenshots.
|
||||
@@ -0,0 +1,46 @@
|
||||
name: AI Unit Tests & Type Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ai-commands/**'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ai-commands/**'
|
||||
schedule:
|
||||
- cron: '15 0 * * 1' # Every Monday @ 12:15am UTC (off the hour to avoid heavy load times)
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/ai-commands
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Auto Label Issues
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
jobs:
|
||||
check-external:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
result: ${{ steps.is-member.outputs.result }}
|
||||
steps:
|
||||
- name: Get deps
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y jq
|
||||
- name: Get org members
|
||||
id: is-member
|
||||
env:
|
||||
GH_ORG: supabase
|
||||
GH_USER: ${{ github.event.issue.user.login }}
|
||||
GH_TOKEN: ${{ secrets.SB_ORG_MEMBERSHIP_PAT }}
|
||||
run: |
|
||||
gh api /orgs/supabase/members/$GH_USER
|
||||
- if: failure()
|
||||
name: is-member
|
||||
run: |
|
||||
echo "User is not a member of Supabase org"
|
||||
echo "result=false" >> "$GITHUB_OUTPUT"
|
||||
- if: success()
|
||||
run: |
|
||||
echo "User is a member of Supabase org"
|
||||
echo "result=true" >> "$GITHUB_OUTPUT"
|
||||
- name: Label contributor
|
||||
id: label-contributor
|
||||
if: ${{ steps.is-member.outputs.result == 'false' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
USERNAME: ${{ github.event.issue.user.login }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.issue.number }}
|
||||
LABELS: external-issue
|
||||
run: |
|
||||
echo "applying external label"
|
||||
gh issue edit $NUMBER --add-label $LABELS
|
||||
- name: Label internal
|
||||
id: label-internal
|
||||
if: ${{ steps.is-member.outputs.result == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
USERNAME: ${{ github.event.issue.user.login }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.issue.number }}
|
||||
LABELS: internal-issue
|
||||
run: |
|
||||
echo "applying internal label"
|
||||
gh issue edit $NUMBER --add-label $LABELS
|
||||
triage-new:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Label triage
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NUMBER: ${{ github.event.issue.number }}
|
||||
LABELS: to-triage
|
||||
run: |
|
||||
echo "applying triage label"
|
||||
gh issue edit $NUMBER --add-label $LABELS
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Autofix Linting Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'autofix') }}
|
||||
steps:
|
||||
- name: Calculate number of commits
|
||||
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
sparse-checkout: apps
|
||||
fetch-depth: ${{ env.PR_FETCH_DEPTH }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
rm package.json
|
||||
rm package-lock.json
|
||||
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
|
||||
|
||||
- name: Run Prettier in fix mode
|
||||
run: |
|
||||
npx prettier --write 'apps/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
|
||||
|
||||
- name: Commit changes and push to existing branch
|
||||
run: |
|
||||
git checkout -- package.json
|
||||
git checkout -- package-lock.json
|
||||
git config --global user.name 'github-tidy-bot'
|
||||
git config --global user.email 'github-tidy-bot@supabase.com'
|
||||
if [[ `git status --porcelain` ]]; then
|
||||
echo "[bot] Changes detected, committing."
|
||||
|
||||
echo "[bot] Running in non-squash mode."
|
||||
git commit -am "ci: Autofix updates from GitHub workflow"
|
||||
git push
|
||||
else
|
||||
echo "[bot] No changes detected, nothing to commit."
|
||||
fi
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Config (Preview)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "supabase/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
wait:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
status: ${{ steps.check.outputs.conclusion }}
|
||||
steps:
|
||||
- uses: fountainhead/action-wait-for-check@v1.2.0
|
||||
id: check
|
||||
with:
|
||||
checkName: Supabase
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
apply:
|
||||
needs:
|
||||
- wait
|
||||
if: ${{ needs.wait.outputs.status == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
TF_VAR_linked_project: xguihxuzqibwxjnimxev
|
||||
TF_VAR_git_branch: ${{ github.head_ref }}
|
||||
TF_CLI_ARGS_apply: -target=supabase_settings.preview
|
||||
defaults:
|
||||
run:
|
||||
working-directory: supabase/remotes
|
||||
outputs:
|
||||
db_user: ${{ steps.branch.outputs.user }}
|
||||
db_pass: ${{ steps.branch.outputs.password }}
|
||||
db_host: ${{ steps.branch.outputs.host }}
|
||||
db_port: ${{ steps.branch.outputs.port }}
|
||||
jwt_secret: ${{ steps.branch.outputs.jwt_secret }}
|
||||
ref: ${{ steps.branch.outputs.id }}
|
||||
status: ${{ steps.branch.outputs.status }}
|
||||
version: ${{ steps.branch.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_wrapper: false
|
||||
|
||||
- run: terraform init
|
||||
- run: terraform apply -auto-approve -no-color
|
||||
- id: branch
|
||||
run: |
|
||||
terraform output -json branch_database \
|
||||
| jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" \
|
||||
>> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Config (Production)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "supabase/remotes/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
apply:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
TF_VAR_linked_project: xguihxuzqibwxjnimxev
|
||||
defaults:
|
||||
run:
|
||||
working-directory: supabase/remotes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_wrapper: false
|
||||
|
||||
- run: terraform init
|
||||
- run: terraform apply -auto-approve -no-color
|
||||
@@ -0,0 +1,45 @@
|
||||
name: '[Docs] Update last-changed dates'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reset:
|
||||
description: 'Reset last-updated dates using Git commit dates'
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Download dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Update last-changed dates
|
||||
working-directory: ./apps/docs
|
||||
if: ${{ !inputs.reset }}
|
||||
run: npm run last-changed
|
||||
|
||||
- name: Reset last-changed dates
|
||||
working-directory: ./apps/docs
|
||||
if: ${{ inputs.reset }}
|
||||
run: npm run last-changed:reset
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Update Mgmt Api Docs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 00:00 UTC every Monday
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Change to apps/docs/spec directory and run make command
|
||||
working-directory: apps/docs/spec
|
||||
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'feat: update mgmt api docs'
|
||||
title: 'feat: update mgmt api docs'
|
||||
body: 'This PR updates mgmt api docs automatically.'
|
||||
branch: 'gha/auto-update-mgmt-api-docs'
|
||||
base: 'master'
|
||||
@@ -15,10 +15,6 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -26,10 +22,10 @@ jobs:
|
||||
apps/docs
|
||||
packages
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: reviewdog
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.vale.ini'
|
||||
- 'vale/**'
|
||||
- 'apps/docs/content/**'
|
||||
|
||||
jobs:
|
||||
vale:
|
||||
name: runner / vale
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.vale.ini
|
||||
vale
|
||||
apps/docs/content
|
||||
- uses: errata-ai/vale-action@reviewdog
|
||||
with:
|
||||
files: apps/docs/content
|
||||
# [Charis] Temporary because we still have to go through giant backlog of existing errors
|
||||
# Don't want people to worry about this (just want them looking at the specific PR comments)
|
||||
# fail_on_error: true
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'apps/studio/**'
|
||||
- 'package-lock.json'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'apps/studio/**'
|
||||
- 'package-lock.json'
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- uses: supabase/setup-cli@v1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:playwright
|
||||
# mark the action as succeeded even if the tests failed. This is temporarily until we make the tests more stable.
|
||||
# continue-on-error: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-tests/playwright-report/
|
||||
retention-days: 7
|
||||
@@ -17,23 +17,18 @@ jobs:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps
|
||||
studio
|
||||
sparse-checkout: apps
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: '.nvmrc'
|
||||
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
rm package.json
|
||||
rm package-lock.json
|
||||
npm i prettier@2 prettier-plugin-sql-cst
|
||||
npm ci
|
||||
- name: Run prettier
|
||||
run: |-
|
||||
npx prettier -c '{apps,studio}/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
|
||||
|
||||
npm run test:prettier
|
||||
|
||||
# i18n is not a node package, so we handle that one separately
|
||||
format-i18n:
|
||||
@@ -45,15 +40,15 @@ jobs:
|
||||
sparse-checkout: |
|
||||
i18n
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: '.nvmrc'
|
||||
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
rm package.json
|
||||
rm package-lock.json
|
||||
npm i prettier@2 prettier-plugin-sql-cst
|
||||
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
|
||||
- name: Run prettier
|
||||
run: |-
|
||||
npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
|
||||
@@ -66,13 +61,14 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps/docs/pages
|
||||
apps/docs/content
|
||||
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
rm package.json
|
||||
rm package-lock.json
|
||||
npm i prettier@2 prettier-plugin-sql-cst
|
||||
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
|
||||
- name: Run prettier
|
||||
run: |-
|
||||
# Check mdx files which contain sql code blocks
|
||||
grep -lr '```sql' apps/docs/pages/**/*.mdx | xargs npx prettier -c
|
||||
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs npx prettier -c
|
||||
@@ -1,17 +1,10 @@
|
||||
name: Publish to Image Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'studio'
|
||||
tags:
|
||||
- '*'
|
||||
# run this action every Monday at 04:00 UTC (Singapore noon)
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Image tag'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
settings:
|
||||
@@ -25,11 +18,10 @@ jobs:
|
||||
images: |
|
||||
supabase/studio
|
||||
flavor: |
|
||||
latest=false
|
||||
latest=true
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'push' && github.ref_type == 'branch' }}
|
||||
type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'schedule' }}
|
||||
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
release_x86:
|
||||
needs: settings
|
||||
@@ -61,7 +53,7 @@ jobs:
|
||||
with:
|
||||
push: true
|
||||
context: '{{defaultContext}}'
|
||||
file: studio/Dockerfile
|
||||
file: apps/studio/Dockerfile
|
||||
target: production
|
||||
platforms: linux/${{ env.arch }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -104,7 +96,7 @@ jobs:
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: studio/Dockerfile
|
||||
file: apps/studio/Dockerfile
|
||||
target: production
|
||||
platforms: linux/${{ env.arch }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
- '.github/workflows/search.yml'
|
||||
- 'supabase/migrations/**'
|
||||
- 'apps/docs/**'
|
||||
- 'spec/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
refresh:
|
||||
@@ -23,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
PROJECT_ID: ${{ secrets.SEARCH_SUPABASE_PROJECT_ID }}
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
|
||||
@@ -39,12 +38,13 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
apps/www/.env.local.example
|
||||
supabase
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Download dependencies
|
||||
run: npm ci
|
||||
@@ -55,6 +55,10 @@ jobs:
|
||||
- name: Run migrations
|
||||
run: npx supabase db push
|
||||
|
||||
# Need the miscellaneous use API, which is available publicly (by design) in www
|
||||
- name: Copy environment variables
|
||||
run: cp apps/www/.env.local.example apps/docs/.env
|
||||
|
||||
- name: Update embeddings
|
||||
working-directory: ./apps/docs
|
||||
if: ${{ !inputs.refresh }}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Automatically label stale issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Label Stale Issues
|
||||
uses: actions/stale@v9.0.0
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
stale-issue-message: 'After 30 days of inactivity, this issue has been marked as stale. Commenting (or other activity) will remove the stale label.'
|
||||
stale-issue-label: stale
|
||||
any-of-labels: contributor
|
||||
exempt-issue-labels: dnc, needs-analysis
|
||||
exempt-all-assignees: true
|
||||
@@ -7,12 +7,12 @@ on:
|
||||
push:
|
||||
branches: [master, studio]
|
||||
paths:
|
||||
- 'studio/**'
|
||||
- 'apps/studio/**'
|
||||
- 'package-lock.json'
|
||||
pull_request:
|
||||
branches: [master, studio]
|
||||
paths:
|
||||
- 'studio/**'
|
||||
- 'apps/studio/**'
|
||||
- 'package-lock.json'
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
@@ -25,30 +25,23 @@ jobs:
|
||||
# Uses larger hosted runner as it significantly decreases build times
|
||||
runs-on: [larger-runner-4cpu]
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
cmd:
|
||||
- npm run test:studio
|
||||
- npm run build:studio
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
studio
|
||||
apps/studio
|
||||
packages
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
- name: Run ${{ matrix.cmd }}
|
||||
- name: Run Tests
|
||||
env:
|
||||
# Default is 2 GB, increase to have less frequent OOM errors
|
||||
NODE_OPTIONS: '--max_old_space_size=3072'
|
||||
run: ${{ matrix.cmd }}
|
||||
run: npm run test:studio
|
||||
working-directory: ./
|
||||
@@ -12,20 +12,17 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
# Uses larger hosted runner as it significantly decreases build times
|
||||
runs-on: [larger-runner-4cpu]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: UI Patterns Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui-patterns/**'
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:ui-patterns
|
||||
@@ -15,20 +15,16 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
|
||||
+3
-4
@@ -4,7 +4,7 @@ node_modules
|
||||
out
|
||||
.docz
|
||||
tmp
|
||||
.swp
|
||||
*.swp
|
||||
|
||||
coverage
|
||||
allure-results
|
||||
@@ -82,8 +82,10 @@ typings/
|
||||
.env
|
||||
# excempt the apps/www .env
|
||||
!apps/www/.env
|
||||
.env.tmp
|
||||
.env.test
|
||||
.env.local
|
||||
.env.staging
|
||||
.env.production
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
@@ -125,9 +127,6 @@ apps/new-docs/*
|
||||
# UI tokens
|
||||
packages/ui/tokens/**/*.json
|
||||
|
||||
# UI Storybook build
|
||||
packages/ui/storybook-static
|
||||
|
||||
# For self-hosted logs: https://github.com/supabase/supabase/blob/86e3ab20abfdb9c3e666334d3d2f8efeef9ccf2c/docker/docker-compose-logging.yml#L101
|
||||
gcloud.json
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
^./i18n
|
||||
^./i18n
|
||||
^./packages/api-types
|
||||
+5
-8
@@ -4,6 +4,7 @@ node_modules
|
||||
package-lock.json
|
||||
docker*
|
||||
apps/**/out
|
||||
apps/docs/spec/**
|
||||
# prettier-plugin-sql-cst only supports sqlite syntax
|
||||
**/supabase/migrations/*.sql
|
||||
apps/www/schema.sql
|
||||
@@ -12,11 +13,7 @@ examples/slack-clone/nextjs-slack-clone/full-schema.sql
|
||||
# ignore files with custom js formatting
|
||||
apps/docs/pages/guides/auth/*.mdx
|
||||
apps/docs/pages/guides/integrations/*.mdx
|
||||
studio/public
|
||||
studio/__mocks__
|
||||
studio/.next
|
||||
studio/.storybook
|
||||
studio/.swc
|
||||
studio/.turbo
|
||||
studio/node
|
||||
studio/data
|
||||
apps/studio/public
|
||||
apps/**/.turbo
|
||||
apps/docs/CONTRIBUTING.md
|
||||
apps/design-system/__registry__
|
||||
+7
-1
@@ -7,5 +7,11 @@
|
||||
"endOfLine": "lf",
|
||||
"sqlKeywordCase": "lower",
|
||||
"pluginSearchDirs": false,
|
||||
"plugins": ["prettier-plugin-sql-cst"]
|
||||
"plugins": ["prettier-plugin-sql-cst"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.json",
|
||||
"options": { "parser": "json" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
StylesPath = './vale/styles'
|
||||
MinAlertLevel = 'suggestion'
|
||||
|
||||
Vocab = Supabase
|
||||
|
||||
[formats]
|
||||
mdx = md
|
||||
|
||||
[*]
|
||||
BasedOnStyles = Vale, Custom
|
||||
BlockIgnores = (?s) *(<StepHikeCompact>.*?</StepHikeCompact>), \
|
||||
(?s) *(export\s+const\s+\S+\s*=\s*\[.*?\]), \
|
||||
(?s) *(<Image.*?/>)
|
||||
TokenIgnores = (href\s*([=:])\s*['"].*?\1), \
|
||||
@@ -0,0 +1,35 @@
|
||||
# CONTRIBUTING.md
|
||||
|
||||
Thank you for contributing to Supabase! We’re a big, exciting open source project and we’d love to have you contribute! Here’s some resources and guidance to help you get started:
|
||||
|
||||
[1. Getting Started](#getting-started)
|
||||
[2. Issues](#issues)
|
||||
[3. Pull Requests](#pull-requests)
|
||||
|
||||
## Getting Started
|
||||
|
||||
To ensure a positive and inclusive environment, please read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. For help setting up the code in this repo, please follow our [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) file. For the [docs](https://supabase.com/docs) site, follow this [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide.
|
||||
|
||||
## Issues
|
||||
|
||||
If you find a bug, please create an Issue and we’ll triage it.
|
||||
|
||||
- Please search [existing Issues](https://github.com/supabase/supabase/issues) before creating a new one.
|
||||
- Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
We actively welcome your Pull Requests! A couple of things to keep in mind before you submit:
|
||||
|
||||
- If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s).
|
||||
- We will always try to accept the first viable PR that resolves the Issue.
|
||||
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/labels/good%20first%20issue).
|
||||
- If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed.
|
||||
- Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed.
|
||||
|
||||
Prior to submitting your PR, please conduct the following pre-flight checks:
|
||||
|
||||
- Run `npm run build` locally to ensure that your code builds successfully without having to wait on us to approve Vercel Preview deploys.
|
||||
- Ensure that the Prettier tests run successfully on your PR.
|
||||
|
||||
Running these before you create the PR will help reduce back and forth with the team.
|
||||
+69
-49
@@ -1,15 +1,26 @@
|
||||
# Developing Supabase
|
||||
|
||||
1. [Getting started](#getting-started)
|
||||
- [Install dependencies](#install-dependencies)
|
||||
2. [Local development](#local-development)
|
||||
- [Fork the repository](#fork-the-repository)
|
||||
- [Clone the repo](#clone-the-repo)
|
||||
- [Running turborepo](#running-turborepo)
|
||||
- [Shared components](#shared-components)
|
||||
- [Installing packages](#installing-packages)
|
||||
- [New Supabase docs](#new-supabase-docs)
|
||||
3. [Create a pull request](#create-a-pull-request)
|
||||
- [Developing Supabase](#developing-supabase)
|
||||
|
||||
- [Getting started](#getting-started)
|
||||
- [Install dependencies](#install-dependencies)
|
||||
- [Local development](#local-development)
|
||||
- [Fork the repo](#fork-the-repo)
|
||||
- [Clone the repo](#clone-the-repo)
|
||||
- [Install dependencies](#install-dependencies-1)
|
||||
- [Running sites individually](#running-sites-individually)
|
||||
- [Shared components](#shared-components)
|
||||
- [Installing packages](#installing-packages)
|
||||
- [Running Docker for Supabase Studio](#running-docker-for-supabase-studio)
|
||||
- [Prerequsites](#prerequsites)
|
||||
- [Get Started](#get-started)
|
||||
- [Create a pull request](#create-a-pull-request)
|
||||
- [Issue assignment](#issue-assignment)
|
||||
- [Common tasks](#common-tasks)
|
||||
- [Add a redirect](#add-a-redirect)
|
||||
- [Federated docs](#federated-docs)
|
||||
- [Community channels](#community-channels)
|
||||
- [Contributors](#contributors)
|
||||
|
||||
- [Common tasks](#common-tasks)
|
||||
- [Add a redirect](#add-a-redirect)
|
||||
@@ -17,31 +28,32 @@
|
||||
|
||||
## Getting started
|
||||
|
||||
Thank you for expressing your interest in [Supabase](https://supabase.com) and your willingness to contribute!
|
||||
Thank you for your interest in [Supabase](https://supabase.com) and your willingness to contribute!
|
||||
|
||||
To ensure a positive and inclusive environment, we kindly request you to read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md). Additionally, we encourage you to explore the existing [issues](https://github.com/supabase/supabase/issues) to see how you can make a meaningful impact. This document will guide you through the process of setting up your development environment, enabling you to successfully build and test [Supabase](https://supabase.com).
|
||||
To ensure a positive and inclusive environment, please read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md). We encourage you to explore the existing [issues](https://github.com/supabase/supabase/issues) to see how you can make a meaningful impact. This document will help you setup your development environment.
|
||||
|
||||
### Install dependencies
|
||||
|
||||
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
|
||||
|
||||
- [Git](http://git-scm.com/)
|
||||
- [Node.js v18.x (LTS)](http://nodejs.org)
|
||||
- [npm](https://www.npmjs.com/) version 9.x.x
|
||||
- [Node.js v20.x (LTS)](http://nodejs.org)
|
||||
- [npm](https://www.npmjs.com/) version 9.x.x or higher
|
||||
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
|
||||
|
||||
## Local development
|
||||
|
||||
We are in the process of migrating this repository to monorepo, using [Turborepo](https://turborepo.org/docs).
|
||||
This repo uses [Turborepo](https://turborepo.org/docs).
|
||||
|
||||
Eventually, all the apps will be run using [Turborepo](https://turborepo.org/docs), which will significantly improve the developer workflow.
|
||||
All of our apps are in this [Turborepo](https://turborepo.org/docs), which make it easy to share packages and config between projects.
|
||||
|
||||
### Fork the repository
|
||||
### Fork the repo
|
||||
|
||||
To contribute code to [Supabase](https://supabase.com), you must fork the [Supabase Repository](https://github.com/supabase/supabase).
|
||||
To contribute code to [Supabase](https://supabase.com), you must fork the [Supabase repo](https://github.com/supabase/supabase).
|
||||
|
||||
### Clone the repo
|
||||
|
||||
1. Clone your GitHub forked repository:
|
||||
1. Clone your GitHub forked repo:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/<github_username>/supabase.git
|
||||
@@ -52,9 +64,7 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
|
||||
cd supabase
|
||||
```
|
||||
|
||||
### Running turborepo
|
||||
|
||||
[Supabase](https://supabase.com) uses [Turborepo](https://turborepo.org/docs) to manage and run this monorepo.
|
||||
### Install dependencies
|
||||
|
||||
1. Install the dependencies in the root of the repo.
|
||||
|
||||
@@ -62,18 +72,24 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
|
||||
npm install # install dependencies
|
||||
```
|
||||
|
||||
2. After that you can run the apps simultaneously with the following.
|
||||
2. Copy the example `.env.local.example` to `.env.local`
|
||||
|
||||
```sh
|
||||
cp apps/www/.env.local.example apps/www/.env.local
|
||||
```
|
||||
|
||||
3. After that you can run the apps simultaneously with the following.
|
||||
```sh
|
||||
npm run dev # start all the applications
|
||||
```
|
||||
|
||||
Then visit, and edit, any of the following sites:
|
||||
|
||||
| Site | Directory | Scope name | Description | Local development server |
|
||||
| -------------------------------------------------------- | ------------ | ---------- | ------------------------------------ | -------------------------- |
|
||||
| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 |
|
||||
| [supabase.com/dashboard](https://supabase.com/dashboard) | `/studio` | studio | Studio dashboard | http://localhost:8082 |
|
||||
| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001/docs |
|
||||
| Site | Directory | Scope name | Description | Local development server |
|
||||
| -------------------------------------------------------- | -------------- | ---------- | --------------------------------------------- | -------------------------- |
|
||||
| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 |
|
||||
| [supabase.com/dashboard](https://supabase.com/dashboard) | `/apps/studio` | studio | Studio dashboard (requires Docker, see below) | http://localhost:8082 |
|
||||
| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001/docs |
|
||||
|
||||
#### Running sites individually
|
||||
|
||||
@@ -83,14 +99,18 @@ You can run any of the sites individually by using the scope name. For example:
|
||||
npm run dev:www
|
||||
```
|
||||
|
||||
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`
|
||||
|
||||
#### Shared components
|
||||
|
||||
The monorepo has a set of shared components under `/packages`:
|
||||
|
||||
- `/packages/common`: Common React code, shared between all sites.
|
||||
- `/packages/ai-commands`: Helpers/Commands for AI related functions
|
||||
- `/packages/common`: Common React components, shared between all sites
|
||||
- `/packages/config`: All shared config
|
||||
- `/packages/spec`: Generates documentation using spec files.
|
||||
- `/packages/shared-data`: Shared data that can be used across all apps
|
||||
- `/packages/tsconfig`: Shared Typescript settings
|
||||
- `/packages/ui`: Common UI components
|
||||
|
||||
#### Installing packages
|
||||
|
||||
@@ -102,29 +122,21 @@ For example:
|
||||
|
||||
- `npm install react -w common`: installs into `./packages/common`
|
||||
- `npm install react -w www`: installs into `./apps/www`
|
||||
- `npm install react -w studio`: installs into `./studio`
|
||||
- `npm install react -w studio`: installs into `./apps/studio`
|
||||
|
||||
You do not need to install `devDependencies` in each workspace. These can all be installed in the root package.
|
||||
|
||||
#### New Supabase docs
|
||||
|
||||
Following the changes to the [Supabase docs](https://supabase.com/blog/new-supabase-docs-built-with-nextjs) the following is needed to run the new docs locally:
|
||||
|
||||
- Inside of `apps/docs` create a `.env.local` file with the following: `NEXT_PUBLIC_NEW_DOCS=true`
|
||||
|
||||
Now when you run a local development docs server you will see the new docs site.
|
||||
|
||||
---
|
||||
|
||||
## Running Docker
|
||||
## Running Docker for Supabase Studio
|
||||
|
||||
To test your changes, you need to run Supabase using Docker. Docker lets you use features of your locally modified Supabase version.
|
||||
To run Studio locally, you'll need to setup Docker in addition to your NextJS frontend.
|
||||
|
||||
#### Prerequsites
|
||||
|
||||
First, make sure you have the Docker application installed on your device. You can download and install it from [here](https://docs.docker.com/get-docker/).
|
||||
First, make sure you have the Docker installed on your device. You can download and install it from [here](https://docs.docker.com/get-docker/).
|
||||
|
||||
#### Getting Started
|
||||
#### Get Started
|
||||
|
||||
1. Navigate to the `docker` directory in your forked repo
|
||||
|
||||
@@ -141,20 +153,28 @@ First, make sure you have the Docker application installed on your device. You c
|
||||
3. Run docker
|
||||
|
||||
```sh
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
This command initializes the containers specified in the docker-compose.yml file. It might take a few moments to complete, depending on your computer and internet connection.
|
||||
This command initializes the containers specified in the `docker-compose.yml` file. It might take a few moments to complete, depending on your computer and internet connection.
|
||||
|
||||
Once the `docker-compose up` process completes, you should have your local version of Supabase up and running within Docker containers. You can access and test the features you've modified or added.
|
||||
Once the `docker compose up` process completes, you should have your local version of Supabase up and running within Docker containers. You can access it at `http://localhost:8082`.
|
||||
|
||||
Remember to keep the Docker application open as long as you're working with your local Supabase instance.
|
||||
|
||||
## Create a pull request
|
||||
|
||||
After making your changes, open a pull request. Once you submit your pull request, the Supabase team will review it with you.
|
||||
After making any changes, open a pull request. Once you submit your pull request, the Supabase team will review it with you.
|
||||
|
||||
Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors).
|
||||
Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors)!
|
||||
|
||||
## Issue assignment
|
||||
|
||||
We don't have a process for assigning issues to contributors. Please feel free to jump into any issues in this repo that you are able to help with. Our intention is to encourage anyone to help without feeling burdened by an assigned task. Life can sometimes get in the way, and we don't want to leave contributors feeling obligated to complete issues when they may have limited time or unexpected commitments.
|
||||
|
||||
We also recognize that not having a process can sometimes lead to competing or duplicate PRs. There's no perfect solution here. We encourage you to communicate early and often on an Issue to indicate that you're actively working on it. If you see that an Issue already has a PR, try working with that author instead of drafting your own.
|
||||
|
||||
We review PRs in the order of their submission. We try to accept the earliest one that is closest to being ready to merge.
|
||||
|
||||
---
|
||||
|
||||
@@ -188,7 +208,7 @@ Some things to consider:
|
||||
|
||||
## Community channels
|
||||
|
||||
If you are stuck somewhere or have any questions, join our [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
|
||||
If you get stuck somewhere or have any questions, join our [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023 Supabase
|
||||
Copyright 2024 Supabase
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
<img src="https://user-images.githubusercontent.com/8291514/213727225-56186826-bee8-43b5-9b15-86e839d89393.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
# Supabase
|
||||
|
||||
[Supabase](https://supabase.com) is an open source Firebase alternative. We're building the features of Firebase using enterprise-grade open source tools.
|
||||
@@ -12,9 +10,9 @@
|
||||
- [x] Hosted Postgres Database. [Docs](https://supabase.com/docs/guides/database)
|
||||
- [x] Authentication and Authorization. [Docs](https://supabase.com/docs/guides/auth)
|
||||
- [x] Auto-generated APIs.
|
||||
- [x] REST. [Docs](https://supabase.com/docs/guides/api#rest-api-overview)
|
||||
- [x] GraphQL. [Docs](https://supabase.com/docs/guides/api#graphql-api-overview)
|
||||
- [x] Realtime subscriptions. [Docs](https://supabase.com/docs/guides/api#realtime-api-overview)
|
||||
- [x] REST. [Docs](https://supabase.com/docs/guides/api)
|
||||
- [x] GraphQL. [Docs](https://supabase.com/docs/guides/graphql)
|
||||
- [x] Realtime subscriptions. [Docs](https://supabase.com/docs/guides/realtime)
|
||||
- [x] Functions.
|
||||
- [x] Database Functions. [Docs](https://supabase.com/docs/guides/database/functions)
|
||||
- [x] Edge Functions [Docs](https://supabase.com/docs/guides/functions)
|
||||
@@ -24,6 +22,10 @@
|
||||
|
||||

|
||||
|
||||
Watch "releases" of this repo to get notified of major updates.
|
||||
|
||||
<kbd><img src="https://raw.githubusercontent.com/supabase/supabase/d5f7f413ab356dc1a92075cb3cee4e40a957d5b1/web/static/watch-repo.gif" alt="Watch this repo"/></kbd>
|
||||
|
||||
## Documentation
|
||||
|
||||
For full documentation, visit [supabase.com/docs](https://supabase.com/docs)
|
||||
@@ -37,19 +39,6 @@ To see how to Contribute, visit [Getting Started](./DEVELOPERS.md)
|
||||
- [Email Support](https://supabase.com/docs/support#business-support). Best for: problems with your database or infrastructure.
|
||||
- [Discord](https://discord.supabase.com). Best for: sharing your applications and hanging out with the community.
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Alpha: We are testing Supabase with a closed set of customers
|
||||
- [x] Public Alpha: Anyone can sign up over at [supabase.com/dashboard](https://supabase.com/dashboard). But go easy on us, there are a few kinks
|
||||
- [x] Public Beta: Stable enough for most non-enterprise use-cases
|
||||
- [ ] Public: General Availability [[status](https://supabase.com/docs/guides/getting-started/features#feature-status)]
|
||||
|
||||
We are currently in Public Beta. Watch "releases" of this repo to get notified of major updates.
|
||||
|
||||
<kbd><img src="https://raw.githubusercontent.com/supabase/supabase/d5f7f413ab356dc1a92075cb3cee4e40a957d5b1/web/static/watch-repo.gif" alt="Watch this repo"/></kbd>
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
Supabase is a combination of open source tools. We’re building the features of Firebase using enterprise-grade, open source products. If the tools and communities exist, with an MIT, Apache 2, or equivalent open license, we will use and support that tool. If the tool doesn't exist, we build and open source it ourselves. Supabase is not a 1-to-1 mapping of Firebase. Our aim is to give developers a Firebase-like developer experience using open source tools.
|
||||
@@ -59,15 +48,15 @@ Supabase is a combination of open source tools. We’re building the features of
|
||||
Supabase is a [hosted platform](https://supabase.com/dashboard). You can sign up and start using Supabase without installing anything.
|
||||
You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and [develop locally](https://supabase.com/docs/guides/local-development).
|
||||
|
||||

|
||||

|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
|
||||
- [Postgres](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
|
||||
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
|
||||
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API
|
||||
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API
|
||||
- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions.
|
||||
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
|
||||
- [GoTrue](https://github.com/supabase/gotrue) is a JWT based API for managing users and issuing JWT tokens.
|
||||
- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions.
|
||||
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API
|
||||
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
|
||||
- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway.
|
||||
|
||||
#### Client libraries
|
||||
@@ -122,6 +111,15 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/PostgREST" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Auth" target="_blank" rel="noopener noreferrer">auth-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Realtime" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Storage" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Functions" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<!-- /notranslate -->
|
||||
<th colspan="7">💚 Community 💚</th>
|
||||
<!-- notranslate -->
|
||||
@@ -188,15 +186,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
@@ -291,7 +280,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
- [Simplified Chinese / 简体中文](/i18n/README.zh-cn.md)
|
||||
- [Swedish / Svenska](/i18n/README.sv.md)
|
||||
- [Thai / ไทย](/i18n/README.th.md)
|
||||
- [Traditional Chinese / 繁体中文](/i18n/README.zh-tw.md)
|
||||
- [Traditional Chinese / 繁體中文](/i18n/README.zh-tw.md)
|
||||
- [Turkish / Türkçe](/i18n/README.tr.md)
|
||||
- [Ukrainian / Українська](/i18n/README.uk.md)
|
||||
- [Vietnamese / Tiếng Việt](/i18n/README.vi-vn.md)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Update these with your Supabase details from your project settings > API
|
||||
# https://app.supabase.com/project/_/settings/api
|
||||
NEXT_PUBLIC_SUPABASE_URL=your-project-url
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['eslint-config-supabase/next'],
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,5 @@
|
||||
# database.design
|
||||
|
||||
## Overview
|
||||
|
||||
This site will host a tool for designing SQL schemas.
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { usePathname, useParams } from 'next/navigation'
|
||||
import { LoadingLine as LoadingLineComponent } from 'ui'
|
||||
|
||||
function LoadingLine() {
|
||||
const snap = useAppStateSnapshot()
|
||||
const params = useParams()
|
||||
const pathName = usePathname()
|
||||
|
||||
const runId = params.runId as string
|
||||
|
||||
const loading =
|
||||
runId && snap.runsLoading.includes(runId) && pathName.includes(runId) ? true : false
|
||||
|
||||
return <LoadingLineComponent loading={loading} />
|
||||
}
|
||||
|
||||
export { LoadingLine }
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { DialogContent, Dialog } from 'ui'
|
||||
|
||||
const LoginDialog = ({ children }: { children: React.ReactNode }) => {
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={snap.loginDialogOpen}
|
||||
onOpenChange={() => snap.setLoginDialogOpen(!snap.loginDialogOpen)}
|
||||
>
|
||||
<DialogContent className="max-w-md">{children}</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export { LoginDialog }
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { AssistantChatForm } from '@/components/AssistantChatForm'
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
|
||||
const NewThreadInput = () => {
|
||||
const supabase = createClient()
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-10/12 xl:w-11/12 max-w-xl">
|
||||
<AssistantChatForm
|
||||
key={'new-thread-form'}
|
||||
chatContext={'new'}
|
||||
canSubmit={async () => {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) {
|
||||
snap.setLoginDialogOpen(true)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}}
|
||||
placeholder="e.g Create a Telegram-like chat application"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewThreadInput
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { createRef, useEffect } from 'react'
|
||||
|
||||
const BottomMarker = () => {
|
||||
const ref = createRef<HTMLDivElement>()
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
setTimeout(() => {
|
||||
if (ref.current) ref.current.scrollIntoView({ behavior: 'smooth' })
|
||||
}, 700)
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
|
||||
export { BottomMarker }
|
||||
@@ -0,0 +1,23 @@
|
||||
import { format } from 'sql-formatter'
|
||||
|
||||
import { MonacoEditor } from '@/components/MonacoEditor'
|
||||
import { CodeEditorContainer } from './CodeEditorContainer'
|
||||
|
||||
export async function CodeEditor({ promisedMessage }: { promisedMessage: Promise<string> }) {
|
||||
const code = await promisedMessage
|
||||
|
||||
// strip the ```sql ``` from the code
|
||||
// not sure why we need this & can't get openai to stop returning it
|
||||
const strippedCode = code.replace('```sql\n', '').replace('\n```', '')
|
||||
const formattedCode = format(strippedCode, { language: 'postgresql' })
|
||||
|
||||
/**
|
||||
* - CodeEditorContainer is a client component, which uses valtio state
|
||||
* - MonacoEditor is a server component injected into it
|
||||
*/
|
||||
return (
|
||||
<CodeEditorContainer>
|
||||
<MonacoEditor id="sql-editor" language="pgsql" value={formattedCode} />
|
||||
</CodeEditorContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
function CodeEditorContainer({ children }: { children: ReactNode }) {
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
snap.hideCode ? 'max-w-0' : 'max-w-lg 2xl:max-w-xl',
|
||||
'w-full xl:border-l',
|
||||
'grow flex flex-col h-full'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { CodeEditorContainer }
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import type { Message as MessageItem } from 'ai/react'
|
||||
import Link from 'next/link'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface MessageItemProps {
|
||||
message: MessageItem
|
||||
isLatest: boolean
|
||||
times: {
|
||||
hoursFromNow: number
|
||||
formattedTimeFromNow: string
|
||||
formattedCreatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
const MessageItem = ({ message, isLatest, times }: MessageItemProps) => {
|
||||
const { thread_id } = useParams()
|
||||
// using the local state for run loading
|
||||
const isLoading = false
|
||||
|
||||
const { hoursFromNow, formattedTimeFromNow, formattedCreatedAt } = times
|
||||
|
||||
// chat shown as selected when url matches
|
||||
const isSelected = usePathname().includes(message.id)
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
'group',
|
||||
'transition flex w-full gap-x-5 px-4 xl:px-8 hover:bg-surface-200/50 cursor-pointer border-r',
|
||||
isSelected && 'bg-surface-200',
|
||||
isSelected ? 'border-r-foreground' : 'border-r border-r-transparent'
|
||||
)}
|
||||
href={`/${thread_id}/${message.id}`}
|
||||
>
|
||||
<div className="flex flex-col justify-between items-center relative top-3">
|
||||
<div
|
||||
className={cn(
|
||||
'transition w-2.5 h-2.5 mt-[1px] ml-[1px] rounded-full border',
|
||||
isSelected
|
||||
? 'bg-dbnew border-dbnew'
|
||||
: 'bg-transparent border-foreground-muted group-hover:border-foreground'
|
||||
)}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute w-4 h-4 -top-0.5',
|
||||
'after:content-spinner after:t-0 after:block after:absolute after:h-4 after:w-4 after:border-r after:border-r-dbnew after:rounded-[50%] after:rotate-45 z-10',
|
||||
'animate-spin'
|
||||
)}
|
||||
>
|
||||
<div className="absolute border w-4 h-4 rounded-full z-0" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Node line*/}
|
||||
{!isLatest && <div className="border-l border-strong flex-grow" />}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-y-2 py-4">
|
||||
<div className="group relative">
|
||||
<span className="z-10 absolute top-0 -left-[8px]">
|
||||
<svg viewBox="0 0 8 13" height="13" width="8">
|
||||
<path
|
||||
className={
|
||||
// 'transition fill-background-surface-100 border'
|
||||
isSelected
|
||||
? 'transition stroke-border fill-background-surface-100 stroke-border-default'
|
||||
: 'transition stroke-border fill-background-surface-100 stroke-border-default'
|
||||
}
|
||||
d="M1.533,2.568L8,11.193V0L2.812,0C1.042,0,0.474,1.156,1.533,2.568z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
title={message.content}
|
||||
className={cn(
|
||||
'cursor-pointer transition relative overflow-hidden',
|
||||
'w-full rounded-lg rounded-tl-none',
|
||||
'bg-alternative',
|
||||
'border',
|
||||
isSelected ? 'bg-surface-100' : 'bg-surface-100 group-hover:bg-surface-200'
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
'transition p-4 text-xs',
|
||||
isSelected ? 'text-foreground' : 'text-light group-hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
</p>
|
||||
{isLoading && <div className="chat-shimmering-loader w-full h-0.5 absolute bottom-0" />}
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<p
|
||||
className={cn(
|
||||
'transition-all',
|
||||
isSelected ? 'h-inherit opacity-100' : 'h-0 opacity-0',
|
||||
'font-mono text-xs text-foreground-lighter'
|
||||
)}
|
||||
>
|
||||
Sent {hoursFromNow > 6 ? `on ${formattedCreatedAt}` : formattedTimeFromNow}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageItem
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ScrollArea } from 'ui'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { BottomMarker } from './BottomMarker'
|
||||
import { getMessages } from './getMessages'
|
||||
|
||||
import MessageItem from './MessageItem'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const Messages = async ({ threadId }: { threadId: string }) => {
|
||||
const { data: messages, error } = await getMessages(threadId)
|
||||
|
||||
if (error) {
|
||||
return <>Error happened</>
|
||||
}
|
||||
|
||||
const initialMessages = messages.flatMap((m) => [
|
||||
{
|
||||
id: m.message_id,
|
||||
content: m.message_input,
|
||||
role: 'user' as const,
|
||||
createdAt: new Date(m.created_at),
|
||||
},
|
||||
{
|
||||
id: m.message_id,
|
||||
content: m.message_content,
|
||||
role: 'assistant' as const,
|
||||
createdAt: new Date(m.created_at),
|
||||
},
|
||||
])
|
||||
|
||||
const userMessages = initialMessages.filter((m) => m.role === 'user')
|
||||
|
||||
return (
|
||||
<ScrollArea className="grow h-px">
|
||||
<div className="flex flex-col mb-6 xl:mb-12">
|
||||
{userMessages.map((message, idx) => {
|
||||
const createdAtTimestamp = dayjs(message.createdAt)
|
||||
const isLatest = Array.isArray(messages) && idx === messages.length - 1
|
||||
|
||||
const hoursFromNow = dayjs().diff(createdAtTimestamp, 'hour')
|
||||
const formattedTimeFromNow = dayjs(createdAtTimestamp).fromNow()
|
||||
|
||||
const formattedCreatedAt = dayjs(createdAtTimestamp).format('DD MMM YYYY, HH:mm')
|
||||
|
||||
const times = {
|
||||
hoursFromNow,
|
||||
formattedTimeFromNow,
|
||||
formattedCreatedAt,
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageItem key={message.id} message={message} isLatest={isLatest} times={times} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<BottomMarker />
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import SchemaFlowHandler from '@/components/SchemaFlowHandler'
|
||||
|
||||
export async function SchemaFlow({ promisedMessage }: { promisedMessage: Promise<string> }) {
|
||||
const code = await promisedMessage
|
||||
const strippedCode = code.replace('```sql\n', '').replace('\n```', '')
|
||||
|
||||
return <SchemaFlowHandler content={strippedCode} />
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { stripIndent } from 'common-tags'
|
||||
import { cookies } from 'next/headers'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
// import is weird, what's up with this?
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { ContextLengthError } from '../../../../../packages/ai-commands/src/errors'
|
||||
import { getMessages } from './getMessages'
|
||||
|
||||
// Create an OpenAI API client (that's edge friendly!)
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
})
|
||||
|
||||
// Set the runtime to edge for best performance
|
||||
export const runtime = 'edge'
|
||||
|
||||
export type AiAssistantMessage = {
|
||||
content: string
|
||||
role: 'user' | 'assistant'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest AI generated message. In the DB, there's a pair of user prompt and AI response, so if the
|
||||
* user sent a message but that message lacks the corresponding AI response, a call will be made to OpenAI to generate
|
||||
* it.
|
||||
*/
|
||||
export const getAssistantResponse = async (threadId: string, messageId: string) => {
|
||||
const supabase = createClient()
|
||||
|
||||
const { data: message, error } = await supabase
|
||||
.from('messages')
|
||||
.select()
|
||||
.eq('message_id', messageId)
|
||||
.single()
|
||||
|
||||
if (!message || error) {
|
||||
throw new Error('A message with that id does not exist.')
|
||||
}
|
||||
|
||||
// if the message lacks the AI response, fetch it from OpenAI
|
||||
if (!message.message_content) {
|
||||
const { data: messages, error } = await getMessages(threadId)
|
||||
|
||||
if (error) {
|
||||
throw new Error('Error while trying to fetch the existing messages.')
|
||||
}
|
||||
|
||||
// transform the DB messages into a format that OpenAI understands
|
||||
const existingMessages = messages.flatMap((m) => [
|
||||
{
|
||||
content: m.message_input,
|
||||
role: 'user' as const,
|
||||
},
|
||||
{
|
||||
content: m.message_content,
|
||||
role: 'assistant' as const,
|
||||
},
|
||||
])
|
||||
|
||||
const initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: stripIndent`
|
||||
Your purpose is to generate a SQL schema where the user will give commands to you via a chat.
|
||||
The output should use the following instructions:
|
||||
- The generated SQL must be valid SQL.
|
||||
- For primary keys, always use "id bigint primary key generated always as identity" (not serial)
|
||||
- Always create foreign key references in the create statement
|
||||
- Prefer 'text' over 'varchar'
|
||||
- Prefer 'timestamp with time zone' over 'date'
|
||||
- Use vector(384) data type for any embedding/vector related query
|
||||
- Always use double apostrophe in SQL strings (eg. 'Night''s watch')
|
||||
- Always omit \`\`\`sql from your reply
|
||||
- You can use only CREATE TABLE queries, no other queries are allowed under no circumstances (ALTER TABLE etc).
|
||||
- On each subsequent message from the user, rewrite the original response to include the new requirement.
|
||||
- Don't add any SQL comments in the code
|
||||
- Never put a comma before a round bracket
|
||||
|
||||
The output should look like this: "CREATE TABLE users (id bigint primary key generated always as identity)"
|
||||
|
||||
DO NOT RESPOND WITH ANYTHING ELSE.
|
||||
YOU MUST NOT ANSWER WITH ANY PLAIN TEXT
|
||||
ONLY RESPOND WITH 1 CODE BLOCK
|
||||
YOU MUST NOT FOLLOW UP ANY CODE BLOCKS WITH ANY EXPLANATION
|
||||
`,
|
||||
},
|
||||
]
|
||||
|
||||
initMessages.push(...existingMessages)
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-3.5-turbo-16k-0613',
|
||||
messages: initMessages,
|
||||
max_tokens: 1024,
|
||||
temperature: 0,
|
||||
stream: false,
|
||||
})
|
||||
|
||||
const newMessage = response.choices[0].message.content || ''
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (userError || !user) {
|
||||
throw new Error('Failed to get user')
|
||||
}
|
||||
|
||||
// Insert the message from the completion
|
||||
const { data, error } = await supabase
|
||||
.from('messages')
|
||||
.update({ message_content: newMessage })
|
||||
.eq('id', message.id)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
throw new Error('Error while updating the existing message with the completion data.')
|
||||
}
|
||||
|
||||
return data.message_content
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error && error.code === 'context_length_exceeded') {
|
||||
throw new ContextLengthError()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return message.message_content
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getMessages = cache(async (thread_id: string) => {
|
||||
const supabase = createClient()
|
||||
|
||||
return await supabase.from('messages').select().eq('thread_id', thread_id).order('created_at')
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { AssistantChatForm } from '@/components/AssistantChatForm'
|
||||
import SaveSchemaDropdown from '@/components/Header/SaveSchemaDropdown'
|
||||
import ToggleCodeEditorButton from '@/components/Header/ToggleCodeEditorButton'
|
||||
import GraphLoader from '@/components/Loaders/GraphLoader'
|
||||
import SchemaLoader from '@/components/Loaders/SchemaLoader'
|
||||
import { cn } from '@ui/lib/utils/cn'
|
||||
import { Suspense } from 'react'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import { Messages } from './Messages'
|
||||
import { SchemaFlow } from './SchemaFlow'
|
||||
import { getAssistantResponse } from './getAssistantMessage'
|
||||
|
||||
export interface ThreadPageProps {
|
||||
params: {
|
||||
thread_id: string
|
||||
message_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ThreadPage({ params }: ThreadPageProps) {
|
||||
const promise = getAssistantResponse(params.thread_id, params.message_id)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col-reverse items-between xl:flex-row xl:items-center xl:justify-between bg-alternative h-full">
|
||||
<div
|
||||
className={cn(
|
||||
'bg',
|
||||
'h-full',
|
||||
'border-t xl:border-t-0 xl:border-r relative',
|
||||
'flex flex-col h-full border-r',
|
||||
'w-full xl:w-[400px] 2xl:w-[500px]'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col grow items-between">
|
||||
<Messages threadId={params.thread_id} />
|
||||
<div className="px-4 pb-4">
|
||||
<AssistantChatForm chatContext={'edit'} placeholder={'Any changes to make?'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="xl:hidden flex items-center gap-x-2 justify-end border-t py-2 px-2 bg-background">
|
||||
<ToggleCodeEditorButton />
|
||||
<SaveSchemaDropdown />
|
||||
</div>
|
||||
|
||||
<div className="grow max-h-screen flex flex-row items-center justify-between bg-alternative h-full">
|
||||
<Suspense fallback={<SchemaLoader />}>
|
||||
<SchemaFlow promisedMessage={promise} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<GraphLoader />}>
|
||||
<CodeEditor promisedMessage={promise} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { Message } from 'ai'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
export async function logout() {
|
||||
const supabase = createClient()
|
||||
|
||||
const { error } = await supabase.auth.signOut()
|
||||
|
||||
if (error) {
|
||||
console.log('Error logging out:', error.message)
|
||||
return
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
//export async function createThread(prevState: any, formData: FormData) {
|
||||
export async function createThread(input: string, message: Message, currentThread?: string) {
|
||||
const supabase = createClient()
|
||||
|
||||
let thread_id = currentThread ?? ''
|
||||
let message_id = ''
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (userError || !user) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to get user',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// create a new thread
|
||||
if (!thread_id) {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('threads')
|
||||
.insert({ user_id: user.id, thread_title: input })
|
||||
.select()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
if (data) {
|
||||
console.log({ data })
|
||||
thread_id = data[0].id
|
||||
// message_id = data[0].message_id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
//insert the message from the completion
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('messages')
|
||||
.insert({
|
||||
thread_id,
|
||||
message_content: message.content,
|
||||
message_input: input,
|
||||
message_role: 'assistant',
|
||||
user_id: user.id,
|
||||
})
|
||||
.select()
|
||||
|
||||
if (error) throw error
|
||||
if (data) {
|
||||
message_id = data[0].message_id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to update title to update title',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
redirect(`/${thread_id}/${message_id}`)
|
||||
}
|
||||
|
||||
export async function upsertThreadMessage(input: string, currentThread?: string) {
|
||||
const supabase = createClient()
|
||||
|
||||
let thread_id = currentThread ?? ''
|
||||
let message_id = ''
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (userError || !user) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to get user',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// create a new thread
|
||||
if (!thread_id) {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('threads')
|
||||
.insert({ user_id: user.id, thread_title: input })
|
||||
.select()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
if (data) {
|
||||
console.log({ data })
|
||||
thread_id = data[0].id
|
||||
// message_id = data[0].message_id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
//insert the message from the completion
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('messages')
|
||||
.insert({
|
||||
thread_id,
|
||||
message_content: '',
|
||||
message_input: input,
|
||||
message_role: 'assistant',
|
||||
user_id: user.id,
|
||||
})
|
||||
.select()
|
||||
|
||||
if (error) throw error
|
||||
if (data) {
|
||||
message_id = data[0].message_id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to update title to update title',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
redirect(`/${thread_id}/${message_id}`)
|
||||
}
|
||||
|
||||
export async function deleteThread(prevState: any, formData: FormData) {
|
||||
const supabase = createClient()
|
||||
|
||||
try {
|
||||
const schema = z.object({
|
||||
thread_id: z.string(),
|
||||
})
|
||||
|
||||
const data = schema.parse({
|
||||
thread_id: formData.get('thread_id'),
|
||||
})
|
||||
|
||||
await supabase.from('threads').delete().eq('id', data.thread_id)
|
||||
|
||||
revalidatePath('/profile')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to delete the thread',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateThreadName(prevState: any, formData: FormData) {
|
||||
const supabase = createClient()
|
||||
|
||||
try {
|
||||
const schema = z.object({
|
||||
thread_title: z.string(),
|
||||
thread_id: z.string(),
|
||||
})
|
||||
|
||||
const data = schema.parse({
|
||||
thread_title: formData.get('thread_title'),
|
||||
thread_id: formData.get('thread_id'),
|
||||
})
|
||||
|
||||
const { error } = await supabase
|
||||
.from('threads')
|
||||
.update({ thread_title: data.thread_title })
|
||||
.eq('id', data.thread_id)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
revalidatePath('/profile')
|
||||
|
||||
return {
|
||||
message: 'Thread name updated to ' + data.thread_title,
|
||||
success: true,
|
||||
data,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to update title to update title',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// The `/auth/callback` route is required for the server-side auth flow implemented
|
||||
// by the Auth Helpers package. It exchanges an auth code for the user's session.
|
||||
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
|
||||
const requestUrl = new URL(request.url)
|
||||
const code = requestUrl.searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
const supabase = createClient()
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
// URL to redirect to after sign in process completes
|
||||
return NextResponse.redirect(requestUrl.origin)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { FAQS, faq } from '@/data/faqs'
|
||||
import { slugify } from '@/lib/utils'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'database.design | Faq',
|
||||
}
|
||||
|
||||
const FAQ = async () => {
|
||||
return (
|
||||
<div className="bg-background">
|
||||
<div className="py-16 bg-white ">
|
||||
<div className="max-w-screen-md px-4 mx-auto sm:px-6 lg:px-8">
|
||||
<h1 className="text-center font-display text-3xl font-bold text-black sm:text-5xl">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<p className="mt-5 text-lg text-center text-foreground-light">
|
||||
Everything you have ever wondered about database.design
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center max-w-screen-md px-4 py-10 mx-auto sm:pt-20 sm:px-6 lg:px-8">
|
||||
{FAQS.map((faq: faq, index) => (
|
||||
<article
|
||||
key={index}
|
||||
className="prose prose-headings:scroll-mt-20 prose-headings:font-display prose-headings:font-semibold w-full"
|
||||
>
|
||||
<a href={`#${slugify(faq.question)}`} className="no-underline hover:underline">
|
||||
<h2 id={slugify(faq.question)}>{faq.question}</h2>
|
||||
</a>
|
||||
<p>{faq.answer}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FAQ
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,199 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import '~ui/build/css/source/global.css';
|
||||
@import '~ui/build/css/themes/dark.css';
|
||||
@import '~ui/build/css/themes/light.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Book.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Book.woff) format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-BookItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-BookItalic.woff) format('woff');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Medium.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Medium.woff) format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-MediumItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-MediumItalic.woff) format('woff');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Bold.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Bold.woff) format('woff');
|
||||
font-weight: 700;
|
||||
font-style: 600;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-BoldItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-BoldItalic.woff) format('woff');
|
||||
font-style: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Black.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Black.woff) format('woff');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-BlackItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-BlackItalic.woff) format('woff');
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'source code pro';
|
||||
src: url('/fonts/source-code-pro/SourceCodePro-Regular.eot');
|
||||
src:
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.woff2') format('woff2'),
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.woff') format('woff'),
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.ttf') format('truetype'),
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.svg#SourceCodePro-Regular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
main {
|
||||
/* @apply bg-background;
|
||||
@apply text-foreground;
|
||||
height: 100%;
|
||||
width: 100%; */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.chat-shimmering-loader {
|
||||
animation: shimmer 1.5s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--background-default)) 0%,
|
||||
hsl(var(--brand-default)) 25%,
|
||||
hsl(var(--brand-300)) 35%,
|
||||
hsl(var(--background-default)) 45%,
|
||||
hsl(var(--background-surface-100)) 75%
|
||||
);
|
||||
background-size: 3000px 100%;
|
||||
}
|
||||
|
||||
.shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--border-default)) 4%,
|
||||
hsl(var(--background-surface-200)) 25%,
|
||||
hsl(var(--border-default)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
.dark .shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--border-default)) 4%,
|
||||
hsl(var(--border-control)) 25%,
|
||||
hsl(var(--border-default)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.monaco-editor,
|
||||
.monaco-diff-editor {
|
||||
--vscode-editor-background: hsl(var(--background-alternative)) !important;
|
||||
--vscode-editorGutter-background: hsl(var(--background-alternative)) !important;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* .monaco-editor .overflow-guard */
|
||||
|
||||
.shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--border-default)) 4%,
|
||||
hsl(var(--background-surface-100)) 25%,
|
||||
hsl(var(--border-default)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
.dark .shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--background-surface-100)) 4%,
|
||||
hsl(var(--border-default)) 25%,
|
||||
hsl(var(--background-surface-100)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 500ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css'
|
||||
import './globals.css'
|
||||
|
||||
import Footer from '@/components/Footer'
|
||||
import Header from '@/components/Header/Header'
|
||||
import { ThemeProvider } from '@/components/providers'
|
||||
import type { Metadata } from 'next'
|
||||
import { LoadingLine } from './LoadingLine'
|
||||
|
||||
const defaultUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: 'http://localhost:3000'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(defaultUrl),
|
||||
title: 'database.design',
|
||||
description: 'Generate schemas from your ideas',
|
||||
}
|
||||
|
||||
// suppressHydrationWarning:
|
||||
// https://github.com/pacocoursey/next-themes#with-app
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="dark h-full bg-background" suppressHydrationWarning>
|
||||
<body className="flex flex-col h-full">
|
||||
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<Header />
|
||||
<LoadingLine />
|
||||
<main role="main" className="h-full w-full flex flex-col grow">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import LoginForm from '@/components/Auth/LoginForm'
|
||||
|
||||
export default function Login({ searchParams }: { searchParams: { message: string } }) {
|
||||
return <LoginForm searchParams={searchParams} />
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// import LoginForm from '@/components/Auth/LoginForm'
|
||||
import type { Metadata } from 'next'
|
||||
import { LoginDialog } from './LoginDialog'
|
||||
import NewThreadInput from './NewThreadInput'
|
||||
import LoginForm from '@/components/Auth/LoginForm'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'database.design | Create',
|
||||
}
|
||||
|
||||
const NewThread = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { prompt: string; message: string }
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="h-full flex items-center justify-center w-full flex-col gap-y-4">
|
||||
<NewThreadInput />
|
||||
</div>
|
||||
<LoginDialog>
|
||||
<LoginForm searchParams={searchParams} />
|
||||
</LoginDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewThread
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import { deleteThread } from '@/app/actions'
|
||||
import { createRef, useEffect } from 'react'
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
import { Button, Input_Shadcn_, Modal } from 'ui'
|
||||
import { ThreadType } from './Threads'
|
||||
|
||||
const ConfirmDeleteThreadModal = ({
|
||||
thread,
|
||||
onClose,
|
||||
visible,
|
||||
}: {
|
||||
thread: ThreadType
|
||||
onClose: () => void
|
||||
visible: boolean
|
||||
}) => {
|
||||
const formRef = createRef<HTMLFormElement>()
|
||||
|
||||
const initialState = {
|
||||
message: '',
|
||||
success: false,
|
||||
data: {
|
||||
thread_id: thread.thread_id,
|
||||
},
|
||||
}
|
||||
|
||||
const [state, formAction] = useFormState(deleteThread, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = false
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = false
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button type="warning" htmlType="submit" aria-disabled={pending} loading={pending}>
|
||||
Delete thread
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="small"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
hideFooter
|
||||
header="Confirm to delete thread?"
|
||||
className="pb-2"
|
||||
>
|
||||
<form action={formAction} key={`${thread.thread_id}-delete-thread-form`}>
|
||||
<Modal.Content className="py-4">
|
||||
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Input_Shadcn_ name="thread_id" value={state.data.thread_id} type="hidden" />
|
||||
<Modal.Content className="flex flex-row gap-3 justify-end">
|
||||
<Button type="default">Cancel</Button>
|
||||
<SubmitButton />
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmDeleteThreadModal
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { updateThreadName } from '@/app/actions'
|
||||
import { createRef, useEffect } from 'react'
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
import { Button, Input_Shadcn_, Label_Shadcn_, Modal } from 'ui'
|
||||
import { ThreadType } from './Threads'
|
||||
|
||||
const EditThreadModal = ({
|
||||
thread,
|
||||
onClose,
|
||||
visible,
|
||||
}: {
|
||||
thread: ThreadType
|
||||
onClose: () => void
|
||||
visible: boolean
|
||||
}) => {
|
||||
const formRef = createRef<HTMLFormElement>()
|
||||
|
||||
const initialState = {
|
||||
message: undefined,
|
||||
success: undefined,
|
||||
data: {
|
||||
thread_id: thread.thread_id,
|
||||
thread_title: thread.thread_title,
|
||||
},
|
||||
}
|
||||
|
||||
const [state, formAction] = useFormState(updateThreadName, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = undefined
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button type="secondary" htmlType="submit" aria-disabled={pending} loading={pending}>
|
||||
Update thread name
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
alignFooter="right"
|
||||
size="medium"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
hideFooter
|
||||
header="Edit thread name"
|
||||
className="pb-2"
|
||||
>
|
||||
<form ref={formRef} action={formAction} key={`${thread.thread_id}-update-thread-title-form`}>
|
||||
<Modal.Content className="py-4">
|
||||
<Label_Shadcn_ htmlFor="thread_title">Provide a name for your thread</Label_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
placeholder="Type in a name for the thread..."
|
||||
type="text"
|
||||
name="thread_title"
|
||||
defaultValue={state.data.thread_title}
|
||||
/>
|
||||
<Input_Shadcn_ name="thread_id" value={state.data.thread_id} type="hidden" />
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content className="flex flex-row gap-3 justify-end">
|
||||
<Button type="default">Cancel</Button>
|
||||
<SubmitButton />
|
||||
<p aria-live="polite" className="sr-only" role="status">
|
||||
{state?.message}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditThreadModal
|
||||
@@ -0,0 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
|
||||
const EmptyState = () => {
|
||||
return (
|
||||
<div className="border rounded py-6 flex flex-col items-center justify-center gap-y-2">
|
||||
<p className="text-sm text-foreground-light">No conversations created yet</p>
|
||||
<Button type="default">
|
||||
<Link href="/">Start a conversation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmptyState
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
|
||||
import { timeAgo } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconEdit2,
|
||||
IconMoreVertical,
|
||||
IconTrash2,
|
||||
} from 'ui'
|
||||
import ConfirmDeleteThreadModal from './ConfirmDeleteThreadModal'
|
||||
import EditThreadModal from './EditThreadModal'
|
||||
import { ThreadType } from './Threads'
|
||||
|
||||
const Thread = ({ thread }: { thread: ThreadType }) => {
|
||||
const { created_at, thread_id, thread_title } = thread
|
||||
const formattedTimeAgo = timeAgo(created_at!)
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={thread_id}
|
||||
className="group w-full flex flex-row items-center gap-6 border rounded pl-5 pr-2 transition bg-surface-100 hover:bg-surface-200 h-14"
|
||||
>
|
||||
<div className="flex flex-col grow overflow-hidden">
|
||||
<Link
|
||||
className="flex text-sm group-hover:underline truncate"
|
||||
href={`/${thread.thread_id}/${thread.message_id}`}
|
||||
>
|
||||
<span className="truncate">{thread_title}</span>
|
||||
</Link>
|
||||
<span className="text-xs text-foreground-lighter font-mono">
|
||||
Last updated {formattedTimeAgo}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="px-1 group">
|
||||
<IconMoreVertical size={14} className="text-light group-hover:text" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-32" align="end">
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => {
|
||||
console.log('trying to edit thread')
|
||||
setEditOpen(true)
|
||||
}}
|
||||
>
|
||||
<IconEdit2 size={14} />
|
||||
<p>Edit name</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => setDeleteOpen(true)}>
|
||||
<IconTrash2 size={14} />
|
||||
<p>Delete thread</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDeleteThreadModal
|
||||
key={`${thread_id}-delete-dialog`}
|
||||
thread={thread}
|
||||
visible={deleteOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
/>
|
||||
<EditThreadModal
|
||||
key={`${thread_id}-edit-dialog`}
|
||||
thread={thread}
|
||||
visible={editOpen}
|
||||
onClose={() => setEditOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Thread
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { Database } from '@/types/supabase'
|
||||
import { cookies } from 'next/headers'
|
||||
import EmptyState from './EmptyState'
|
||||
import Thread from './Thread'
|
||||
|
||||
export type ThreadType = Database['public']['Views']['profile_threads']['Row']
|
||||
|
||||
async function Threads() {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) return <p>Error fetching user details</p>
|
||||
|
||||
const { data } = await supabase
|
||||
.from('profile_threads')
|
||||
.select()
|
||||
.eq('user_id', user.id)
|
||||
.order('created_at', { ascending: false })
|
||||
const threads = data ?? []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{threads.length > 0 ? (
|
||||
threads
|
||||
.sort()
|
||||
.map((thread) => <Thread key={`thread-item-${thread.thread_id}`} thread={thread} />)
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Threads
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="px-4 xl:px-0 xl:max-w-4xl mx-auto h-full">{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { default as dayjs, default as relativeTime } from 'dayjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Threads from './Threads'
|
||||
import { Suspense } from 'react'
|
||||
import ShimmeringLoader from '@/components/Shimmer'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
export const metadata: Metadata = {
|
||||
title: 'database.design | Profile',
|
||||
}
|
||||
|
||||
// revalidatePath not working in the create route handler
|
||||
// force-dynamic to refetch every time if needed
|
||||
// just a hack for now
|
||||
// export const dynamic = 'force-dynamic'
|
||||
|
||||
const Profile = async () => {
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-x-8 py-6 xl:py-12 gap-y-6 xl:gap-y-0">
|
||||
<div className="col-span-4 xl:col-span-1 flex flex-col gap-y-6">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Suspense fallback={<p>Loading user...</p>}>
|
||||
<ProfileDetails />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-4 xl:col-span-3 flex flex-col gap-y-4">
|
||||
<h2>Past conversations</h2>
|
||||
|
||||
<div className="w-full h-px border-t" />
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex flex-col gap-4">
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-50" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-25" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-10" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Threads />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function ProfileDetails() {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) redirect('/')
|
||||
|
||||
return !user ? (
|
||||
// make this better!
|
||||
<p>No user found</p>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="border border-foreground-lighter rounded-full w-12 h-12 bg-no-repeat bg-center bg-cover"
|
||||
style={{ backgroundImage: `url('${user.user_metadata.avatar_url}')` }}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg">{user.user_metadata.full_name}</p>
|
||||
<p className="text-foreground-light">@{user.user_metadata.user_name}</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Profile
|
||||
@@ -0,0 +1,2 @@
|
||||
export const NODE_WIDTH = 700
|
||||
export const NODE_HEIGHT = 200
|
||||
@@ -0,0 +1,127 @@
|
||||
// 'use client'
|
||||
|
||||
// import { useQuery } from '@tanstack/react-query'
|
||||
// import { Modal } from 'ui'
|
||||
// import { useParams } from 'next/navigation'
|
||||
// import { AssistantMessage, ReadThreadAPIResult, UserMessage } from '@/lib/types'
|
||||
// import { useEffect, useMemo } from 'react'
|
||||
// import { sortBy } from 'lodash'
|
||||
// import ReactFlow, {
|
||||
// Background,
|
||||
// BackgroundVariant,
|
||||
// ReactFlowProvider,
|
||||
// useReactFlow,
|
||||
// MiniMap,
|
||||
// } from 'reactflow'
|
||||
// import { getGraphDataFromMessages } from './AllThreadsModal.utils'
|
||||
// import MessageNode from './MessageNode'
|
||||
|
||||
// interface AllThreadsProps {
|
||||
// visible: boolean
|
||||
// onClose: () => void
|
||||
// onSelectMessage: (messageId: string, replyId: string) => void
|
||||
// }
|
||||
|
||||
// // [Joshen] POC idea for forking/branching - not for phase 1
|
||||
|
||||
// const AllThreads = ({ visible, onClose, onSelectMessage }: AllThreadsProps) => {
|
||||
// const params = useParams()
|
||||
// const reactFlowInstance = useReactFlow()
|
||||
// const nodeTypes = useMemo(() => ({ message: MessageNode }), [])
|
||||
|
||||
// // [Joshen] Scaffolding a query cause im presuming we need a different endpoint to retrieve _all_ threads
|
||||
// const { data, isSuccess } = useQuery<ReadThreadAPIResult>({
|
||||
// queryFn: async () => {
|
||||
// const response = await fetch(`/api/ai/sql/threads/${params.threadId}/read/${params.runId}`, {
|
||||
// method: 'GET',
|
||||
// })
|
||||
|
||||
// const result = await response.json()
|
||||
// return result
|
||||
// },
|
||||
// queryKey: [params.threadId, params.runId],
|
||||
// refetchInterval: (options) => {
|
||||
// const data = options.state.data
|
||||
// if (data && data.status === 'completed') {
|
||||
// return Infinity
|
||||
// }
|
||||
// return 5000
|
||||
// },
|
||||
// enabled: !!(params.threadId && params.runId),
|
||||
// })
|
||||
|
||||
// const messages = useMemo(() => {
|
||||
// if (isSuccess) return sortBy(data.messages, (m) => m.created_at)
|
||||
// return []
|
||||
// }, [data?.messages, isSuccess])
|
||||
|
||||
// const userMessages = messages.filter((m) => m.role === 'user') as UserMessage[]
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reactFlowInstance !== undefined && userMessages.length > 0) {
|
||||
// const { nodes, edges } = getGraphDataFromMessages({
|
||||
// messages: userMessages,
|
||||
// onSelectMessage: (message: UserMessage) => {
|
||||
// const index = messages.indexOf(message)
|
||||
// const reply = messages[index + 1] as AssistantMessage
|
||||
// onSelectMessage(message.id, reply.id)
|
||||
// },
|
||||
// })
|
||||
// reactFlowInstance.setNodes(nodes)
|
||||
// reactFlowInstance.setEdges(edges)
|
||||
// setTimeout(() => {
|
||||
// reactFlowInstance.fitView({})
|
||||
// const viewport = reactFlowInstance.getViewport()
|
||||
// reactFlowInstance.setViewport({ x: viewport.x - 70, y: 150, zoom: 1 })
|
||||
// })
|
||||
// }
|
||||
// }, [reactFlowInstance, userMessages, messages, onSelectMessage])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reactFlowInstance !== undefined && visible) {
|
||||
// reactFlowInstance.fitView({})
|
||||
// }
|
||||
// }, [reactFlowInstance, visible])
|
||||
|
||||
// return (
|
||||
// <Modal
|
||||
// showCloseButton
|
||||
// hideFooter
|
||||
// size="xxlarge"
|
||||
// visible={visible}
|
||||
// onCancel={onClose}
|
||||
// header="All threads in current conversation"
|
||||
// >
|
||||
// <div className="h-[700px] border-t bg-background">
|
||||
// <ReactFlow
|
||||
// nodesDraggable={false}
|
||||
// edgesUpdatable={false}
|
||||
// defaultNodes={[]}
|
||||
// defaultEdges={[]}
|
||||
// maxZoom={1.1}
|
||||
// nodeTypes={nodeTypes}
|
||||
// proOptions={{ hideAttribution: true }}
|
||||
// >
|
||||
// <Background gap={16} color="#000" variant={BackgroundVariant.Lines} />
|
||||
// <MiniMap
|
||||
// pannable
|
||||
// zoomable
|
||||
// nodeColor={'rgba(0,0,0,0.7)'}
|
||||
// maskColor={'rgba(0,0,0,0.85)'}
|
||||
// className="border border-control rounded-md shadow-sm"
|
||||
// />
|
||||
// </ReactFlow>
|
||||
// </div>
|
||||
// </Modal>
|
||||
// )
|
||||
// }
|
||||
|
||||
// const AllThreadsModal = ({ visible, onClose, onSelectMessage }: AllThreadsProps) => {
|
||||
// return (
|
||||
// <ReactFlowProvider>
|
||||
// <AllThreads visible={visible} onClose={onClose} onSelectMessage={onSelectMessage} />
|
||||
// </ReactFlowProvider>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export default AllThreadsModal
|
||||
@@ -0,0 +1,65 @@
|
||||
// import { UserMessage } from '@/lib/types'
|
||||
// import dagre from '@dagrejs/dagre'
|
||||
// import { Edge, Node, Position } from 'reactflow'
|
||||
// import { NODE_HEIGHT, NODE_WIDTH } from './AllThreadsModal.constants'
|
||||
// import { MessageNodeData } from './MessageNode'
|
||||
|
||||
// export const getGraphDataFromMessages = ({
|
||||
// messages,
|
||||
// onSelectMessage,
|
||||
// }: {
|
||||
// messages: UserMessage[]
|
||||
// onSelectMessage: (message: UserMessage) => void
|
||||
// }): { nodes: Node<MessageNodeData>[]; edges: Edge[] } => {
|
||||
// const nodes: Node[] = messages.map((message, idx) => {
|
||||
// return {
|
||||
// id: message.id,
|
||||
// type: 'message',
|
||||
// data: {
|
||||
// id: message.id,
|
||||
// text: message.text,
|
||||
// isStart: idx === 0,
|
||||
// isEnd: idx === messages.length - 1,
|
||||
// onSelectMessage: () => onSelectMessage(message),
|
||||
// },
|
||||
// position: { x: 0, y: 0 },
|
||||
// }
|
||||
// })
|
||||
|
||||
// const edges: Edge[] = []
|
||||
// messages.forEach((message, idx) => {
|
||||
// if (idx > 0) {
|
||||
// edges.push({
|
||||
// id: `edge-${idx}`,
|
||||
// source: messages[idx - 1].id,
|
||||
// target: message.id,
|
||||
// type: 'smoothstep',
|
||||
// animated: true,
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// const dagreGraph = new dagre.graphlib.Graph()
|
||||
// dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
// dagreGraph.setGraph({ rankdir: 'TB', ranksep: 50, nodesep: 20 })
|
||||
|
||||
// nodes.forEach((node) =>
|
||||
// dagreGraph.setNode(node.id, { width: NODE_WIDTH / 2, height: NODE_HEIGHT / 2 })
|
||||
// )
|
||||
// edges.forEach((edge) => dagreGraph.setEdge(edge.source, edge.target))
|
||||
// dagre.layout(dagreGraph)
|
||||
|
||||
// nodes.forEach((node) => {
|
||||
// const nodeWithPosition = dagreGraph.node(node.id)
|
||||
// node.targetPosition = Position.Top
|
||||
// node.sourcePosition = Position.Bottom
|
||||
// node.position = {
|
||||
// x: nodeWithPosition.x - nodeWithPosition.width / 2,
|
||||
// y: nodeWithPosition.y - nodeWithPosition.height / 2,
|
||||
// }
|
||||
|
||||
// return node
|
||||
// })
|
||||
|
||||
// return { nodes, edges }
|
||||
// }
|
||||
@@ -0,0 +1,44 @@
|
||||
// import { Handle, NodeProps, Position } from 'reactflow'
|
||||
// import { NODE_HEIGHT, NODE_WIDTH } from './AllThreadsModal.constants'
|
||||
|
||||
// export interface MessageNodeData {
|
||||
// id: string
|
||||
// text: string
|
||||
// isStart: boolean
|
||||
// isEnd: boolean
|
||||
// onSelectMessage: () => void
|
||||
// }
|
||||
|
||||
// const MessageNode = ({ data }: NodeProps<MessageNodeData>) => {
|
||||
// const { id, text, isStart, isEnd, onSelectMessage } = data
|
||||
// return (
|
||||
// <>
|
||||
// {!isStart && (
|
||||
// <Handle
|
||||
// type="target"
|
||||
// id="handle-t"
|
||||
// position={Position.Top}
|
||||
// style={{ background: 'transparent' }}
|
||||
// />
|
||||
// )}
|
||||
// <div
|
||||
// className="flex flex-col gap-y-1 rounded bg-surface-100 border border-default p-3 hover:bg-surface-200 transition cursor-pointer"
|
||||
// style={{ width: NODE_WIDTH / 2 - 10, height: NODE_HEIGHT / 2 }}
|
||||
// onClick={() => onSelectMessage()}
|
||||
// >
|
||||
// <p className="text-xs text-foreground-light font-mono">{id}</p>
|
||||
// <p className="text-sm">{text}</p>
|
||||
// </div>
|
||||
// {!isEnd && (
|
||||
// <Handle
|
||||
// type="source"
|
||||
// id="handle-s"
|
||||
// position={Position.Bottom}
|
||||
// style={{ background: 'transparent' }}
|
||||
// />
|
||||
// )}
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export default MessageNode
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
// something a little funny here maybe?
|
||||
const textArray = [
|
||||
"We're working hard to compute your results",
|
||||
'Installing a new flux capacitor',
|
||||
'Reconfiguring the warp drive',
|
||||
'Analyzing the space modulator',
|
||||
'etc....',
|
||||
]
|
||||
|
||||
// this is just an idea.
|
||||
// maybe a bad one
|
||||
// needs better animation if we're keeping it
|
||||
const ChatLoadingAnimation = () => {
|
||||
const [currentTextIndex, setCurrentTextIndex] = useState(0)
|
||||
const [currentText, setCurrentText] = useState(textArray[0])
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTextIndex((prevIndex) => (prevIndex + 1) % textArray.length)
|
||||
setCurrentText(textArray[currentTextIndex])
|
||||
}, 2000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [currentTextIndex, textArray])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{currentText}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatLoadingAnimation
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CHAT_EXAMPLES } from '@/data/chat-examples'
|
||||
import { cn } from '@ui/lib/utils/cn'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
|
||||
export const ChatSuggestions = ({ setInput }: { setInput: (s: string) => void }) => {
|
||||
const suggestions = CHAT_EXAMPLES
|
||||
return (
|
||||
<div className="flex gap-3 mt-4">
|
||||
{suggestions.map((suggestion, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
'flex items-center gap-3 !pr-3',
|
||||
'transition border rounded-full px-3 py-1.5',
|
||||
'text-light',
|
||||
'hover:border-stronger hover:text'
|
||||
)}
|
||||
onClick={(event) => {
|
||||
setInput(suggestion.prompt)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{suggestion.label}
|
||||
<ExternalLink size={12} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
'use server'
|
||||
|
||||
import { upsertThreadMessage } from '@/app/actions'
|
||||
|
||||
export async function upsertMessageFormAction(formData: FormData) {
|
||||
const threadId = formData.get('threadId') as string | undefined
|
||||
const prompt = formData.get('prompt') as string
|
||||
|
||||
return upsertThreadMessage(prompt, threadId)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { ChangeEvent, FormHTMLAttributes, ReactNode, forwardRef, useRef } from 'react'
|
||||
import { useFormStatus } from 'react-dom'
|
||||
import { TextArea_Shadcn_, cn } from 'ui'
|
||||
|
||||
import { ChatSuggestions } from './ChatSuggestions'
|
||||
import { upsertMessageFormAction } from './action'
|
||||
|
||||
export interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
message?: string
|
||||
children?: ReactNode
|
||||
chatContext: 'new' | 'edit'
|
||||
}
|
||||
|
||||
const SubmitButton = forwardRef<HTMLButtonElement, { isLoading: boolean; canSubmit: boolean }>(
|
||||
({ canSubmit }, ref) => {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<div className="absolute right-1.5 top-1.5 flex gap-3 items-center">
|
||||
{pending && (
|
||||
<Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} />
|
||||
)}
|
||||
|
||||
<button
|
||||
title="Send AI prompt"
|
||||
ref={ref}
|
||||
type="submit"
|
||||
disabled={pending || canSubmit}
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative',
|
||||
canSubmit ? 'text-default opacity-100' : 'text-muted opacity-50',
|
||||
pending ? 'hidden' : ''
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SubmitButton.displayName = 'SubmitButton'
|
||||
|
||||
interface AssistantChatFormProps {
|
||||
chatContext: string
|
||||
placeholder: string
|
||||
canSubmit?: () => Promise<boolean>
|
||||
}
|
||||
|
||||
const AssistantChatForm = ({
|
||||
chatContext,
|
||||
placeholder,
|
||||
canSubmit = () => Promise.resolve(true),
|
||||
}: AssistantChatFormProps) => {
|
||||
const { thread_id } = useParams()
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const disabled = false
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!e.shiftKey && (e.key === 'Enter' || e.key === 'NumpadEnter')) {
|
||||
e.preventDefault()
|
||||
e.currentTarget.form?.requestSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target
|
||||
let newHeight = target.scrollHeight + 'px'
|
||||
const input = target.value
|
||||
if (!input) {
|
||||
newHeight = '40px'
|
||||
}
|
||||
if (target.style.height !== newHeight) {
|
||||
target.style.height = newHeight
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="relative"
|
||||
action={async (formData) => {
|
||||
const flag = await canSubmit()
|
||||
if (flag) {
|
||||
upsertMessageFormAction(formData)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}></div>
|
||||
<input hidden name="threadId" defaultValue={thread_id} />
|
||||
<TextArea_Shadcn_
|
||||
name="prompt"
|
||||
ref={textAreaRef}
|
||||
autoFocus
|
||||
rows={1}
|
||||
defaultValue=""
|
||||
disabled={disabled}
|
||||
contentEditable
|
||||
required
|
||||
className={
|
||||
'transition-all text-sm pl-12 pr-10 rounded-[18px] resize-none box-border leading-6'
|
||||
}
|
||||
style={{ height: '40px' }}
|
||||
placeholder={placeholder}
|
||||
spellCheck={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<SubmitButton
|
||||
canSubmit={(textAreaRef?.current?.value || '').length > 0}
|
||||
isLoading={disabled}
|
||||
/>
|
||||
</form>
|
||||
{chatContext === 'new' && (
|
||||
<ChatSuggestions
|
||||
setInput={(v) => {
|
||||
if (textAreaRef?.current) textAreaRef.current.value = v
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AssistantChatForm.displayName = 'AssistantChatForm'
|
||||
|
||||
export { AssistantChatForm }
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Button } from '@ui/components/Button'
|
||||
import { Github } from 'lucide-react'
|
||||
import { cookies, headers } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
|
||||
export default function LoginForm({ searchParams }: { searchParams?: { message?: string } }) {
|
||||
const signUp = async () => {
|
||||
'use server'
|
||||
|
||||
const origin = headers().get('origin')
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: { redirectTo: `${origin}/auth/callback` },
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.log(error.message)
|
||||
return redirect(`/login?message=${error.message}`)
|
||||
} else {
|
||||
return redirect(data.url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center h-full w-full px-8 sm:max-w-sm gap-2 mx-auto">
|
||||
<form
|
||||
className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
|
||||
action={signUp}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 justify-center mb-4">
|
||||
<span className="text-foreground-light">Sign in to</span>{' '}
|
||||
<div className="flex items-center gap-x-1.5 font-mono font-bold">
|
||||
<span>database</span>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-purple-900"></div>
|
||||
<span>design</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 mb-4">
|
||||
<p className="text-center text-sm ">You need to sign in to generate a schema</p>
|
||||
<p className="text-center text-xs">Takes just a few seconds</p>
|
||||
</div>
|
||||
<Button
|
||||
className="border text-sm bg-surface-100 rounded-md px-4 py-2 text-foreground mb-2 flex items-center justify-center gap-x-2"
|
||||
type="default"
|
||||
htmlType="submit"
|
||||
icon={<Github size={18} />}
|
||||
>
|
||||
Sign in with Github
|
||||
</Button>
|
||||
|
||||
{searchParams?.message && (
|
||||
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
|
||||
{searchParams.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import Link from 'next/link'
|
||||
import ThemeSwitcher from './Header/ThemeSwitcher'
|
||||
|
||||
export const links = [
|
||||
{ title: ` © Supabase`, url: 'https://supabase.com/' },
|
||||
{ title: 'FAQs', url: '/faq' },
|
||||
{ title: 'Open Source', url: 'https://supabase.com/open-source' },
|
||||
{ title: 'Privacy Settings', url: 'https://supabase.com/privacy' },
|
||||
]
|
||||
|
||||
const Footer = () => (
|
||||
<footer className="border-t py-4 w-full px-4 flex justify-between" role="contentinfo">
|
||||
<ul className="flex items-center gap-4 text-xs" role="menu">
|
||||
{links.map((link, index) => (
|
||||
<li key={index} role="menuitem">
|
||||
<Link href={link.url}>{link.title}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ThemeSwitcher />
|
||||
</footer>
|
||||
)
|
||||
|
||||
export default Footer
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { logout } from '@/app/actions'
|
||||
import { User } from '@supabase/supabase-js'
|
||||
import { HelpCircle, LogOut } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, IconUser } from 'ui'
|
||||
|
||||
interface AvatarDropdownProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
export default function AvatarDropdown({ user }: AvatarDropdownProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-sm text-foreground">{user.user_metadata.full_name}</p>
|
||||
<p className="text-sm text-foreground-light">{user.user_metadata.email}</p>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<Link href="/profile">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
|
||||
<IconUser size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Profile</p>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href="/faq">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
|
||||
<HelpCircle size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>FAQs</p>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<a href="https://supabase.com" target="_blank" rel="noreferrer">
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
|
||||
<p>Supabase</p>
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
</DropdownMenuGroup>
|
||||
<form action={logout}>
|
||||
<DropdownMenuItem className="space-x-2 w-full" asChild>
|
||||
<button type="submit">
|
||||
<LogOut size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Sign out</p>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import HeaderActions from './HeaderActions'
|
||||
import UserDropdown from './UserDropdown'
|
||||
|
||||
const Header = async () => {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
className="bg-background border flex items-center justify-between px-4 min-h-[50px]"
|
||||
>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Link href="/">
|
||||
<div className="flex items-center gap-x-1.5 font-mono">
|
||||
<span>database</span>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-dbnew"></div>
|
||||
<span>design</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<HeaderActions />
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { useSelectedLayoutSegment } from 'next/navigation'
|
||||
import { Button } from 'ui'
|
||||
|
||||
import SaveSchemaDropdown from './SaveSchemaDropdown'
|
||||
import ToggleCodeEditorButton from './ToggleCodeEditorButton'
|
||||
|
||||
const HeaderActions = () => {
|
||||
const segment = useSelectedLayoutSegment()
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
{segment && segment.includes('thread') && (
|
||||
<div className="hidden xl:flex items-center gap-x-2">
|
||||
<ToggleCodeEditorButton />
|
||||
<SaveSchemaDropdown />
|
||||
<div className="border-r py-3" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="default" className="hidden xl:block">
|
||||
<Link href="/">New conversation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderActions
|
||||
@@ -0,0 +1,32 @@
|
||||
import { LogIn } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { DropdownMenuItem, DropdownMenuSeparator } from 'ui'
|
||||
|
||||
const NoUserDropdown = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-2 p-2">
|
||||
<div className="text-sm">
|
||||
Sign in to <span className="text-foreground">database.design</span> to save your database
|
||||
designs!
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/login">
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<LogIn size={14} className="text-lighter" />
|
||||
<p>Sign in</p>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<a href="https://supabase.com" target="_blank" rel="noreferrer">
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
|
||||
<p>Supabase</p>
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoUserDropdown
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import { getAppStateSnapshot } from '@/lib/state'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconChevronDown,
|
||||
IconClipboard,
|
||||
IconDownload,
|
||||
} from 'ui'
|
||||
|
||||
const SaveSchemaDropdown = () => {
|
||||
const copyToClipboard = () => {
|
||||
const snap = getAppStateSnapshot()
|
||||
const focused = window.document.hasFocus()
|
||||
if (focused) {
|
||||
window.navigator?.clipboard?.writeText(snap.selectedCode)
|
||||
} else {
|
||||
console.warn('Unable to copy to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadSQL = () => {
|
||||
const snap = getAppStateSnapshot()
|
||||
const blob = new Blob([snap.selectedCode], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.download = 'migration.sql'
|
||||
link.href = url
|
||||
link.click()
|
||||
}
|
||||
|
||||
const loadSQLInSupabase = () => {
|
||||
const snap = getAppStateSnapshot()
|
||||
window.open(
|
||||
`https://supabase.com/dashboard/project/_/sql?content=${encodeURIComponent(
|
||||
snap.selectedCode
|
||||
)}`,
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<IconChevronDown />}>
|
||||
Save schema
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-44" align="end">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => copyToClipboard()}>
|
||||
<IconClipboard size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Copy SQL</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => downloadSQL()}>
|
||||
<IconDownload size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Download SQL</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => loadSQLInSupabase()}>
|
||||
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
|
||||
<p>Load SQL in Supabase</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default SaveSchemaDropdown
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client'
|
||||
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
return <ThemeToggle />
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { Button } from 'ui'
|
||||
|
||||
const ToggleCodeEditorButton = () => {
|
||||
const snap = useAppStateSnapshot()
|
||||
return (
|
||||
<Button type="default" onClick={() => snap.setHideCode(!snap.hideCode)}>
|
||||
{snap.hideCode ? 'Show code' : 'Hide code'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToggleCodeEditorButton
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { User2 } from 'lucide-react'
|
||||
import { cookies } from 'next/headers'
|
||||
import { Suspense } from 'react'
|
||||
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui'
|
||||
import AvatarDropdown from './AvatarDropdown'
|
||||
import NoUserDropdown from './NoUserDropdown'
|
||||
|
||||
const EmptyUserButton = () => (
|
||||
<Button type="outline" className="p-1.5 rounded-full" icon={<User2 size={16} />} />
|
||||
)
|
||||
|
||||
export function UserDropdown() {
|
||||
return (
|
||||
<Suspense fallback={<EmptyUserButton />}>
|
||||
<Dropdowns />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
async function Dropdowns() {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild className="flex">
|
||||
{user ? (
|
||||
<button
|
||||
title="User dropdown menu"
|
||||
className="border border-foreground-lighter rounded-full w-[30px] h-[30px] bg-no-repeat bg-center bg-cover"
|
||||
style={{ backgroundImage: `url('${user.user_metadata.avatar_url}')` }}
|
||||
/>
|
||||
) : (
|
||||
<EmptyUserButton />
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-64">
|
||||
{user ? <AvatarDropdown user={user} /> : <NoUserDropdown />}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserDropdown
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
const GraphLoader = () => {
|
||||
return (
|
||||
<div className="h-full w-full text-muted justify-start border-l">
|
||||
<div className="flex gap-4">
|
||||
<LineNumbers />
|
||||
<div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Loader2 className="animate-spin opacity-100" size={16} strokeWidth={1} />
|
||||
<span className="text-sm">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GraphLoader
|
||||
|
||||
const LineNumbers = () => {
|
||||
const lines = [1, 2, 3, 4, 5, 6]
|
||||
return (
|
||||
<div className="grid gap-2 border-r py-4 px-4 text-sm font-mono text-muted">
|
||||
{lines.map((line) => (
|
||||
<span key={line}>{line}</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
const SchemaLoader = () => {
|
||||
return (
|
||||
<div className="h-full w-full text-muted justify-start">
|
||||
<div className="flex items-center m-4 gap-2">
|
||||
<Loader2 className="animate-spin opacity-100" size={16} strokeWidth={1} />
|
||||
<span className="text-sm">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SchemaLoader
|
||||
@@ -0,0 +1,20 @@
|
||||
export const getTheme = (theme: string) => {
|
||||
const isDarkMode = theme.includes('dark')
|
||||
|
||||
return {
|
||||
base: isDarkMode ? 'vs-dark' : 'vs', // can also be vs-dark or hc-black
|
||||
inherit: true, // can also be false to completely replace the builtin rules
|
||||
rules: [
|
||||
{ token: '', background: isDarkMode ? '1f1f1f' : 'f0f0f0' },
|
||||
{
|
||||
token: '',
|
||||
background: isDarkMode ? '1f1f1f' : 'f0f0f0',
|
||||
foreground: isDarkMode ? 'd4d4d4' : '444444',
|
||||
},
|
||||
{ token: 'string.sql', foreground: '24b47e' },
|
||||
{ token: 'comment', foreground: '666666' },
|
||||
{ token: 'predefined.sql', foreground: isDarkMode ? 'D4D4D4' : '444444' },
|
||||
],
|
||||
colors: { 'editor.background': isDarkMode ? '#1f1f1f' : '#f0f0f0' },
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client'
|
||||
|
||||
import Editor, { BeforeMount, EditorProps } from '@monaco-editor/react'
|
||||
import { merge } from 'lodash'
|
||||
|
||||
interface MonacoEditorProps {
|
||||
id: string
|
||||
language: 'pgsql' | 'json' | 'html'
|
||||
autofocus?: boolean
|
||||
defaultValue?: string
|
||||
isReadOnly?: boolean
|
||||
onInputChange?: (value?: string) => void
|
||||
onInputRun?: (value: string) => void
|
||||
hideLineNumbers?: boolean
|
||||
loading?: boolean
|
||||
options?: EditorProps['options']
|
||||
value?: string
|
||||
}
|
||||
|
||||
const MonacoEditor = ({
|
||||
id,
|
||||
language,
|
||||
defaultValue,
|
||||
hideLineNumbers = false,
|
||||
options,
|
||||
value,
|
||||
}: MonacoEditorProps) => {
|
||||
// const monacoRef = useRef<any>()
|
||||
// const { resolvedTheme } = useTheme()
|
||||
|
||||
const beforeMount: BeforeMount = (monaco) => {
|
||||
// monacoRef.current = monaco
|
||||
monaco.editor.defineTheme('supabase', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: '', background: '1f1f1f' },
|
||||
{ token: '', background: '1f1f1f', foreground: 'd4d4d4' },
|
||||
{ token: 'string.sql', foreground: '24b47e' },
|
||||
{ token: 'comment', foreground: '666666' },
|
||||
{ token: 'predefined.sql', foreground: 'D4D4D4' },
|
||||
],
|
||||
colors: { 'editor.background': '#1f1f1f' },
|
||||
})
|
||||
}
|
||||
|
||||
// const onMount: OnMount = async (editor) => {
|
||||
// // Add margin above first line
|
||||
// editor.changeViewZones((accessor) => {
|
||||
// accessor.addZone({
|
||||
// afterLineNumber: 0,
|
||||
// heightInPx: 4,
|
||||
// domNode: document.createElement('div'),
|
||||
// })
|
||||
// })
|
||||
|
||||
// if (resolvedTheme) {
|
||||
// const mode: any = getTheme(resolvedTheme)
|
||||
// monacoRef.current.editor.defineTheme('supabase', mode)
|
||||
// }
|
||||
// }
|
||||
|
||||
const optionsMerged = merge(
|
||||
{
|
||||
tabSize: 2,
|
||||
fontSize: 13,
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on',
|
||||
fixedOverflowWidgets: true,
|
||||
contextmenu: true,
|
||||
lineNumbers: hideLineNumbers ? 'off' : undefined,
|
||||
glyphMargin: hideLineNumbers ? false : undefined,
|
||||
lineNumbersMinChars: hideLineNumbers ? 0 : undefined,
|
||||
occurrencesHighlight: false,
|
||||
folding: hideLineNumbers ? false : undefined,
|
||||
renderLineHighlight: 'none',
|
||||
selectionHighlight: false,
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
merge({ cpp: '12' }, { java: '23' }, { python: '35' })
|
||||
|
||||
return (
|
||||
<Editor
|
||||
path={id}
|
||||
theme="supabase"
|
||||
className={'bg-alternative [&>div]:p-0'}
|
||||
value={value ?? undefined}
|
||||
defaultLanguage={language}
|
||||
defaultValue={defaultValue ?? undefined}
|
||||
loading={false}
|
||||
options={optionsMerged}
|
||||
beforeMount={beforeMount}
|
||||
// onMount={onMount}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { MonacoEditor }
|
||||
@@ -0,0 +1,3 @@
|
||||
// ReactFlow is scaling everything by the factor of 2
|
||||
export const NODE_WIDTH = 320
|
||||
export const NODE_ROW_HEIGHT = 40
|
||||
@@ -0,0 +1,169 @@
|
||||
import { PostgresTable } from '@/lib/types'
|
||||
import dagre from '@dagrejs/dagre'
|
||||
import { uniqBy } from 'lodash'
|
||||
import { Edge, Node, Position } from 'reactflow'
|
||||
import { TableNodeData } from 'ui-patterns/SchemaTableNode'
|
||||
|
||||
import { NODE_ROW_HEIGHT, NODE_WIDTH } from './SchemaFlow.constants'
|
||||
|
||||
export async function getGraphDataFromTables(tables: PostgresTable[]): Promise<{
|
||||
nodes: Node<TableNodeData>[]
|
||||
edges: Edge[]
|
||||
}> {
|
||||
if (!tables.length) {
|
||||
return { nodes: [], edges: [] }
|
||||
}
|
||||
|
||||
const nodes = tables.map((table) => {
|
||||
const columns = (table.columns || []).map((column) => {
|
||||
return {
|
||||
id: column.id,
|
||||
isPrimary: table.primary_keys.some((pk) => pk.name === column.name),
|
||||
name: column.name,
|
||||
format: column.format,
|
||||
isNullable: column.is_nullable,
|
||||
isUnique: column.is_unique,
|
||||
isIdentity: column.is_identity,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: `${table.id}`,
|
||||
type: 'table',
|
||||
data: {
|
||||
name: table.name,
|
||||
isForeign: false,
|
||||
columns,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
}
|
||||
})
|
||||
|
||||
const edges: Edge[] = []
|
||||
// const currentSchema = tables[0].schema
|
||||
const uniqueRelationships = uniqBy(
|
||||
tables.flatMap((t) => t.relationships),
|
||||
'id'
|
||||
)
|
||||
|
||||
for (const rel of uniqueRelationships) {
|
||||
// TODO: Support [external->this] relationship?
|
||||
// if (rel.source_schema !== currentSchema) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// Create additional [this->foreign] node that we can point to on the graph.
|
||||
// if (rel.target_table_schema !== currentSchema) {
|
||||
// nodes.push({
|
||||
// id: rel.constraint_name,
|
||||
// type: 'table',
|
||||
// data: {
|
||||
// name: `${rel.target_table_schema}.${rel.target_table_name}.${rel.target_column_name}`,
|
||||
// isForeign: true,
|
||||
// columns: [],
|
||||
// },
|
||||
// position: { x: 0, y: 0 },
|
||||
// })
|
||||
|
||||
// const [source, sourceHandle] = findTablesHandleIds(
|
||||
// tables,
|
||||
// rel.source_table_name,
|
||||
// rel.source_column_name
|
||||
// )
|
||||
|
||||
// if (source) {
|
||||
// edges.push({
|
||||
// id: String(rel.id),
|
||||
// source,
|
||||
// sourceHandle,
|
||||
// target: rel.constraint_name,
|
||||
// targetHandle: rel.constraint_name,
|
||||
// })
|
||||
// }
|
||||
|
||||
// continue
|
||||
// }
|
||||
|
||||
const [source, sourceHandle] = findTablesHandleIds(
|
||||
tables,
|
||||
rel.source_table_name,
|
||||
rel.source_column_name
|
||||
)
|
||||
const [target, targetHandle] = findTablesHandleIds(
|
||||
tables,
|
||||
rel.target_table_name,
|
||||
rel.target_column_name
|
||||
)
|
||||
|
||||
// We do not support [external->this] flow currently.
|
||||
if (source && target) {
|
||||
edges.push({
|
||||
id: String(rel.id),
|
||||
source,
|
||||
sourceHandle,
|
||||
target,
|
||||
targetHandle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return getLayoutedElements(nodes, edges)
|
||||
}
|
||||
|
||||
function findTablesHandleIds(
|
||||
tables: PostgresTable[],
|
||||
table_name: string,
|
||||
column_name: string
|
||||
): [string?, string?] {
|
||||
for (const table of tables) {
|
||||
if (table_name !== table.name) continue
|
||||
|
||||
for (const column of table.columns || []) {
|
||||
if (column_name !== column.name) continue
|
||||
|
||||
return [String(table.id), column.id]
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
dagreGraph.setGraph({
|
||||
rankdir: 'TB',
|
||||
align: 'UR',
|
||||
nodesep: 25,
|
||||
ranksep: 50,
|
||||
})
|
||||
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, {
|
||||
width: NODE_WIDTH / 2,
|
||||
height: (NODE_ROW_HEIGHT / 2) * (node.data.columns.length + 1), // columns + header
|
||||
})
|
||||
})
|
||||
|
||||
edges.forEach((edge) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target)
|
||||
})
|
||||
|
||||
dagre.layout(dagreGraph)
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
node.targetPosition = Position.Left
|
||||
node.sourcePosition = Position.Right
|
||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
||||
// so it matches the React Flow node anchor point (top left).
|
||||
node.position = {
|
||||
x: nodeWithPosition.x - nodeWithPosition.width / 2,
|
||||
y: nodeWithPosition.y - nodeWithPosition.height / 2,
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
ReactFlowProvider,
|
||||
useReactFlow,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
|
||||
import { PostgresTable } from '@/lib/types'
|
||||
import { parseTables } from '@/lib/utils'
|
||||
import { getGraphDataFromTables } from './SchemaFlow.utils'
|
||||
import { TableNode } from 'ui-patterns'
|
||||
|
||||
interface SchemaGraphProps {
|
||||
tables?: PostgresTable[]
|
||||
content: string
|
||||
}
|
||||
|
||||
const SchemaFlowHandler = ({ content }: SchemaGraphProps) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [tables, setTables] = useState<PostgresTable[]>([])
|
||||
const reactFlowInstance = useReactFlow()
|
||||
const nodeTypes = useMemo(() => ({ table: TableNode }), [])
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
||||
const parseTableData = async () => {
|
||||
try {
|
||||
const tables = await parseTables(content)
|
||||
setTables(tables)
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
parseTableData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getGraphDataFromTables(tables).then(({ nodes, edges }) => {
|
||||
reactFlowInstance.setNodes(nodes)
|
||||
reactFlowInstance.setEdges(edges)
|
||||
setTimeout(() => reactFlowInstance.fitView({ minZoom: 1 }), 50)
|
||||
})
|
||||
}, [tables, resolvedTheme, reactFlowInstance])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full grow fade-in">
|
||||
<ReactFlow
|
||||
defaultNodes={[]}
|
||||
defaultEdges={[]}
|
||||
defaultEdgeOptions={{
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
deletable: false,
|
||||
style: {
|
||||
stroke: 'hsl(var(--border-stronger))',
|
||||
strokeWidth: 0.5,
|
||||
},
|
||||
}}
|
||||
nodeTypes={nodeTypes}
|
||||
minZoom={0.8}
|
||||
maxZoom={1.8}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
onInit={(instance) => {
|
||||
instance.fitView()
|
||||
}}
|
||||
>
|
||||
{mounted && (
|
||||
<Background
|
||||
gap={16}
|
||||
className="[&>*]:stroke-foreground-muted opacity-[25%]"
|
||||
variant={BackgroundVariant.Dots}
|
||||
color={'inherit'}
|
||||
/>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ExportedSchemaGraph = ({ content }: SchemaGraphProps) => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<SchemaFlowHandler content={content} />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExportedSchemaGraph
|
||||
@@ -0,0 +1,24 @@
|
||||
const ANIMATION_DELAY = 150
|
||||
|
||||
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = ANIMATION_DELAY }) => {
|
||||
return (
|
||||
<div
|
||||
className={`shimmering-loader rounded py-3 ${className}`}
|
||||
style={{
|
||||
animationFillMode: 'backwards',
|
||||
animationDelay: `${delayIndex * animationDelay}ms`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const GenericSkeletonLoader = () => (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export { GenericSkeletonLoader }
|
||||
export default ShimmeringLoader
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export function ReactQueryProvider({ children }: PropsWithChildren) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
}
|
||||
|
||||
export { ThemeProvider } from 'common'
|
||||
@@ -0,0 +1,15 @@
|
||||
const CHAT_EXAMPLES = [
|
||||
{ label: 'Twitter clone', prompt: 'Create a twitter clone' },
|
||||
{
|
||||
label: 'Chat application',
|
||||
prompt:
|
||||
'Create a chat application that supports sending messages either through channels or directly between users',
|
||||
},
|
||||
{
|
||||
label: 'User management',
|
||||
prompt: 'Create a simple user management schema that supports role based access control',
|
||||
},
|
||||
{ label: 'To do list', prompt: 'Create a simple schema for me to track a to do list' },
|
||||
]
|
||||
|
||||
export { CHAT_EXAMPLES }
|
||||
@@ -0,0 +1,32 @@
|
||||
export type faq = {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export const FAQS = [
|
||||
{
|
||||
question: 'What is database.design?',
|
||||
answer:
|
||||
'database.design is a new way to get ideas out of your head and into code as fast as possible. Supabase provides you tools to making building incredibly fast. This tool focuses on the very first step: turning your idea into a database schema.',
|
||||
},
|
||||
{
|
||||
question: 'How does it work?',
|
||||
answer: `We use OpenAI under the hood to provide responses to your prompts. Just like ChatGPT, you can fine-tune your responses until you're happy with the schema. We're using React Flow to visualize the schemas.`,
|
||||
},
|
||||
{
|
||||
question: 'Can I see the code?',
|
||||
answer: 'You sure can. All of the source code is open source and available on Github. ',
|
||||
},
|
||||
{
|
||||
question: 'Can I contribute to this project?',
|
||||
answer: 'You sure can. All of the source code is open source and available on Github. ',
|
||||
},
|
||||
{
|
||||
question: `What's the license? Can we fork this project and build our own?`,
|
||||
answer: 'Yes. We release software under the Apache License 2.0.',
|
||||
},
|
||||
{
|
||||
question: 'Can I use this for non-Postgres databases?',
|
||||
answer: `Not at the moment. We've tailored the responses to try and do a great job returning Postgres-flavored SQL`,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
import { proxy, snapshot, useSnapshot } from 'valtio'
|
||||
|
||||
export const appState = proxy({
|
||||
hideCode: false,
|
||||
setHideCode: (value: boolean) => {
|
||||
appState.hideCode = value
|
||||
},
|
||||
|
||||
selectedCode: '',
|
||||
setSelectedCode: (value: string) => {
|
||||
appState.selectedCode = value
|
||||
},
|
||||
|
||||
loginDialogOpen: false,
|
||||
setLoginDialogOpen: (value: boolean) => {
|
||||
appState.loginDialogOpen = value
|
||||
},
|
||||
|
||||
runsLoading: [] as string[],
|
||||
setRunsLoading: (value: string[]) => {
|
||||
appState.runsLoading = value
|
||||
},
|
||||
})
|
||||
|
||||
export const getAppStateSnapshot = () => snapshot(appState)
|
||||
export const useAppStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
|
||||
useSnapshot(appState, options)
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Database } from '@/types/supabase'
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export const createClient = () =>
|
||||
createBrowserClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Database } from '@/types/supabase'
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export const createClient = () => {
|
||||
const cookieStore = cookies()
|
||||
|
||||
return createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value, ...options })
|
||||
} catch (error) {
|
||||
// The `set` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value: '', ...options })
|
||||
} catch (error) {
|
||||
// The `delete` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export type GeneratedTable = Partial<PostgresTable>
|
||||
|
||||
export type AssistantMessage = {
|
||||
id: string
|
||||
role: 'assistant'
|
||||
created_at: number
|
||||
sql: string
|
||||
json: GeneratedTable[]
|
||||
}
|
||||
|
||||
export type UserMessage = {
|
||||
id: string
|
||||
role: 'user'
|
||||
created_at: number
|
||||
text: string
|
||||
}
|
||||
export type Message = AssistantMessage | UserMessage
|
||||
|
||||
export type ReadThreadAPIResult = {
|
||||
id: string
|
||||
status: 'loading' | 'completed'
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
export type PostgresColumn = {
|
||||
id: string
|
||||
name: string
|
||||
format: string
|
||||
is_nullable: boolean
|
||||
is_unique: boolean
|
||||
is_identity: boolean
|
||||
}
|
||||
export type PostgresTable = {
|
||||
id: string
|
||||
name: string
|
||||
primary_keys: { name: string }[]
|
||||
relationships: {
|
||||
id: string
|
||||
target_table_name: string
|
||||
target_column_name: string
|
||||
source_table_name: string
|
||||
source_column_name: string
|
||||
}[]
|
||||
columns: PostgresColumn[]
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { parseQuery } from 'libpg-query'
|
||||
import { compact } from 'lodash'
|
||||
import { z } from 'zod'
|
||||
import { PostgresColumn, PostgresTable } from './types'
|
||||
|
||||
const NameDefinition = z.union([
|
||||
z.object({
|
||||
sval: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
str: z.string(),
|
||||
}),
|
||||
])
|
||||
|
||||
const constraintDefinitionSchema = z.object({
|
||||
Constraint: z.discriminatedUnion('contype', [
|
||||
z
|
||||
.object({
|
||||
contype: z.literal('CONSTR_PRIMARY'),
|
||||
keys: z
|
||||
.array(
|
||||
z.object({
|
||||
String: z.object({
|
||||
sval: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_IDENTITY'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_NOTNULL'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_UNIQUE'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_DEFAULT'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_FOREIGN'),
|
||||
pktable: z.object({
|
||||
relname: z.string(),
|
||||
}),
|
||||
fk_attrs: z
|
||||
.array(
|
||||
z.object({
|
||||
String: NameDefinition,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
pk_attrs: z.array(
|
||||
z.object({
|
||||
String: NameDefinition,
|
||||
})
|
||||
),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
const columnDefinitionSchema = z.object({
|
||||
ColumnDef: z.object({
|
||||
colname: z.string(),
|
||||
typeName: z.object({
|
||||
names: z.array(z.object({ String: NameDefinition })),
|
||||
}),
|
||||
constraints: z.array(constraintDefinitionSchema).optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
const tableDefinitionSchema = z.object({
|
||||
CreateStmt: z
|
||||
.object({
|
||||
relation: z.object({
|
||||
relname: z.string(),
|
||||
}),
|
||||
tableElts: z.array(z.union([columnDefinitionSchema, constraintDefinitionSchema])),
|
||||
})
|
||||
// the optional() allows other types of statements, but only CreateStmt will be type-safe, we
|
||||
// don't care for the other ones.
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const parseQueryResultSchema = z.object({
|
||||
stmts: z.array(
|
||||
z.object({
|
||||
stmt: tableDefinitionSchema,
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const extractNameDefinition = (obj: z.infer<typeof NameDefinition>) => {
|
||||
if ('sval' in obj) {
|
||||
return obj?.sval
|
||||
}
|
||||
if ('str' in obj) {
|
||||
return obj?.str
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses SQL into tables compatible with the existing schema visualizer.
|
||||
*
|
||||
* TODO: consider running in WebWorker
|
||||
*/
|
||||
export async function parseTables(sql: string) {
|
||||
// Parse SQL using the real Postgres parser (compiled to WASM)
|
||||
|
||||
const result = await parseQuery(sql)
|
||||
const parsedSql = parseQueryResultSchema.safeParse(result)
|
||||
|
||||
if (!parsedSql.success) {
|
||||
console.log(parsedSql.error)
|
||||
return []
|
||||
}
|
||||
|
||||
// This code generates all columns with their constraints
|
||||
const pgTables: PostgresTable[] = compact(
|
||||
parsedSql.data.stmts.map(({ stmt }) => {
|
||||
if (!stmt.CreateStmt) {
|
||||
return
|
||||
}
|
||||
|
||||
const statement = stmt.CreateStmt
|
||||
|
||||
const columns = compact(
|
||||
statement.tableElts.map((column) => {
|
||||
if ('ColumnDef' in column) {
|
||||
const format = compact(
|
||||
column.ColumnDef.typeName.names
|
||||
.map((name) => extractNameDefinition(name.String))
|
||||
.filter((str) => str !== 'pg_catalog' || !!str)
|
||||
)[0]
|
||||
|
||||
if (!format) {
|
||||
return undefined
|
||||
}
|
||||
const constraints = (column.ColumnDef.constraints || []).map(
|
||||
(c) => c.Constraint.contype
|
||||
)
|
||||
|
||||
const result: PostgresColumn = {
|
||||
name: column.ColumnDef.colname,
|
||||
format: format,
|
||||
id: column.ColumnDef.colname,
|
||||
is_nullable: !constraints.includes('CONSTR_NOTNULL'),
|
||||
is_unique: constraints.includes('CONSTR_UNIQUE'),
|
||||
is_identity: constraints.includes('CONSTR_IDENTITY'),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// This code processes user_id bigint references users (id) SQL.
|
||||
const columnRelationships = compact(
|
||||
statement.tableElts.map((column) => {
|
||||
if ('ColumnDef' in column) {
|
||||
const found = (column.ColumnDef.constraints || []).find(
|
||||
(c) => c.Constraint.contype === 'CONSTR_FOREIGN'
|
||||
)
|
||||
|
||||
if (found && found.Constraint.contype === 'CONSTR_FOREIGN') {
|
||||
const targetColumn = compact(
|
||||
found.Constraint.pk_attrs.map((name) => extractNameDefinition(name.String))
|
||||
)[0]
|
||||
|
||||
if (!targetColumn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${statement.relation.relname}_${column.ColumnDef.colname}_${found.Constraint.pktable.relname}_${targetColumn}`,
|
||||
source_table_name: statement.relation.relname,
|
||||
source_column_name: column.ColumnDef.colname,
|
||||
target_table_name: found.Constraint.pktable.relname,
|
||||
target_column_name: targetColumn,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// This code processes foreign key (tweet_user_username, tweet_content) references tweets (user_username, content) SQL. It supports composite keys between two tables
|
||||
const tableRelationships = compact(
|
||||
statement.tableElts.flatMap((constraint) => {
|
||||
if ('Constraint' in constraint) {
|
||||
if (
|
||||
constraint.Constraint.contype === 'CONSTR_FOREIGN' &&
|
||||
constraint.Constraint.fk_attrs &&
|
||||
constraint.Constraint.pk_attrs.length === constraint.Constraint.fk_attrs.length
|
||||
) {
|
||||
const pkTable = constraint.Constraint.pktable
|
||||
const pgAttrs = constraint.Constraint.pk_attrs
|
||||
return compact(
|
||||
constraint.Constraint.fk_attrs.map((attr, index) => {
|
||||
const attrString = extractNameDefinition(attr.String)
|
||||
|
||||
const pgAttrString = extractNameDefinition(pgAttrs[index].String)
|
||||
|
||||
if (!attrString || !pgAttrString) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${statement.relation.relname}_${attrString}_${pkTable.relname}_${pgAttrString}`,
|
||||
source_table_name: statement.relation.relname,
|
||||
source_column_name: attrString,
|
||||
target_table_name: pkTable.relname,
|
||||
target_column_name: pgAttrString,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const columnPrimaryKeys = compact(
|
||||
statement.tableElts.map((column) => {
|
||||
if ('ColumnDef' in column) {
|
||||
const constraint = (column.ColumnDef.constraints || []).find(
|
||||
(c) => c.Constraint.contype === 'CONSTR_PRIMARY'
|
||||
)
|
||||
if (constraint) {
|
||||
return { name: column.ColumnDef.colname }
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// This code processes primary key (user_username, tweet_user_username, tweet_content) SQL.
|
||||
const tablePrimaryKeys = compact(
|
||||
statement.tableElts.flatMap((constraint) => {
|
||||
if ('Constraint' in constraint) {
|
||||
if (constraint.Constraint.contype === 'CONSTR_PRIMARY' && constraint.Constraint.keys) {
|
||||
return constraint.Constraint.keys.map((key) => ({ name: key.String.sval }))
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
const table: PostgresTable = {
|
||||
name: statement.relation.relname,
|
||||
columns,
|
||||
id: statement.relation.relname,
|
||||
primary_keys: [...columnPrimaryKeys, ...tablePrimaryKeys],
|
||||
relationships: [...columnRelationships, ...tableRelationships],
|
||||
}
|
||||
|
||||
return table
|
||||
})
|
||||
)
|
||||
|
||||
return pgTables
|
||||
}
|
||||
|
||||
export function timeAgo(date: string) {
|
||||
const createdAt = dayjs(date)
|
||||
const currentTime = dayjs()
|
||||
const timeDifference = currentTime.diff(createdAt, 'seconds') // Time difference in seconds
|
||||
|
||||
if (timeDifference < 60) {
|
||||
// Less than 1 minute
|
||||
return 'just now'
|
||||
} else if (timeDifference < 3600) {
|
||||
// Less than 1 hour
|
||||
return `${Math.floor(timeDifference / 60)} minutes ago`
|
||||
} else if (timeDifference < 86400) {
|
||||
// Less than 1 day (24 hours)
|
||||
return `${Math.floor(timeDifference / 3600)} hours ago`
|
||||
} else {
|
||||
// More than 1 day
|
||||
return createdAt.format('MMM DD, YYYY')
|
||||
}
|
||||
}
|
||||
|
||||
export function slugify(str: string) {
|
||||
return str
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/[^\w-]+/g, '') // Remove non-word characters except hyphens
|
||||
.replace(/--+/g, '-') // Replace multiple consecutive hyphens with a single hyphen
|
||||
.replace(/^-+|-+$/g, '') // Remove leading and trailing hyphens
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
// Redirect to supabase.com until this site is live
|
||||
// when ready, remove the VERCEL env var at
|
||||
// https://vercel.com/supabase/database-new/settings/environment-variables
|
||||
export function middleware(request: NextRequest) {
|
||||
if (process.env.VERCEL === '1') {
|
||||
return NextResponse.redirect(new URL('https://supabase.com', request.url))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ['ui', 'ui-patterns'],
|
||||
webpack: (config, { dev, isServer, webpack, nextRuntime }) => {
|
||||
config.module.rules.push({
|
||||
test: /\.node$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'nextjs-node-loader',
|
||||
options: {
|
||||
includeWebpackPublicPath: false,
|
||||
outputPath: config.output.path,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "database-design",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3002",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.0.4",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"ai": "^2.2.31",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint-config-supabase": "*",
|
||||
"libpg-query": "^15.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"next": "^14.2.3",
|
||||
"openai": "^4.20.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flow": "^1.0.3",
|
||||
"sql-formatter": "^13.1.0",
|
||||
"ui": "*",
|
||||
"ui-patterns": "*",
|
||||
"valtio": "^1.12.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "^18.2.24",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"nextjs-node-loader": "^1.1.5",
|
||||
"postcss": "^8.4.31",
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
}
|
||||
File renamed without changes.
Loaded 100 of 7571 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user