Merge pull request #13159 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-03-23 15:13:51 +08:00
commit 8108ba493f
173 files changed
+7008 -28076

No files matched your search

-32
View File
@@ -1,32 +0,0 @@
name: Studio Build
on:
pull_request:
branches:
- master
- studio
paths:
- 'studio/**'
- 'package-lock.json'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm ci
working-directory: ./
- name: Run build
run: npx turbo run build --filter=studio
working-directory: ./
@@ -36,8 +36,9 @@ Go to the [GitHub Developer Settings](https://github.com/settings/developers) pa
<SocialProviderSetup provider="GitHub" />
## Register a new OAuth application
## Register a new OAuth application on Github.
- Navigate to `Settings`/`Developer settings`/`OAuth Apps`
- Click `Register a new application`. If you've created an app before, click `New OAuth App` here.
- In `Application name`, type the name of your app.
- In `Homepage URL`, type the full URL to your app's website.
@@ -217,7 +217,7 @@ employees (
id bigint primary key generated always as identity,
name text,
email text,
created_at timestampz default now()
created_at timestamptz default now()
);
````
+10 -6
View File
@@ -40,15 +40,19 @@ drop
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension call `drop extension`.
<Admonition type="caution">
Enabling some extensions with `create extension <extension-name> with schema extensions` may lead to permission issues (e.g., `dblink`, `http`, `pg_cron`).
</Admonition>
</TabPanel>
</Tabs>
<Admonition type="note">
Most extensions are installed under the `extensions` schema, which is accessible to public by default. To avoid namespace pollution, we do not recommend creating other entities in the `extensions` schema.
If you need to restrict user access to tables managed by extensions, we recommend creating a separate schema for installing that specific extension.
Some extensions can only be created under a specific schema, for eg. `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
</Admonition>
### Full list of extensions
Supabase is pre-configured with over 50 extensions. You can also install your own SQL extensions directly in the database through our SQL editor.
@@ -162,7 +162,7 @@ Update your Prisma schema by setting the `directUrl` in the datasource block:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directURL = env("DIRECT_URL")
directUrl = env("DIRECT_URL")
}
```
+74
View File
@@ -0,0 +1,74 @@
/* NOTE: this file is a duplicate of /studio/public/css/fonts.css and should be updated there and then copied here - once we migrate fully to supabase.com/dashboard, this can be deleted */
/* header and body font */
@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;
}
/* mono font */
@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;
}
File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 461 KiB

@@ -3326,9 +3326,9 @@
}
},
"node_modules/adjust-sourcemap-loader/node_modules/loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -3589,9 +3589,9 @@
}
},
"node_modules/babel-loader/node_modules/loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -6303,9 +6303,9 @@
"dev": true
},
"node_modules/http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"node_modules/http-deceiver": {
@@ -10011,9 +10011,9 @@
}
},
"node_modules/resolve-url-loader/node_modules/loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -11332,9 +11332,9 @@
}
},
"node_modules/ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz",
"integrity": "sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==",
"dev": true,
"funding": [
{
@@ -14460,9 +14460,9 @@
},
"dependencies": {
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -14644,9 +14644,9 @@
},
"dependencies": {
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -16627,9 +16627,9 @@
"dev": true
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"http-deceiver": {
@@ -19281,9 +19281,9 @@
},
"dependencies": {
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -20273,9 +20273,9 @@
"dev": true
},
"ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz",
"integrity": "sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==",
"dev": true
},
"unicode-canonical-property-names-ecmascript": {
@@ -1,13 +0,0 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
@@ -1,90 +0,0 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
@@ -1,92 +0,0 @@
# Supabase Nuxt.js User Management
This is a WIP!
### 1. Create new project
Sign up to Supabase - [https://app.supabase.com](https://app.supabase.com) and create a new project. Wait for your database to start.
### 2. Run "User Management" Quickstart
Once your database has started, head over to your project's `SQL Editor` and run the "User Management Starter" quickstart. On the `SQL editor` page, scroll down until you see `User Management Starter: Sets up a public Profiles table which you can access with your API`. Click that, then click `RUN` to execute that query and create a new `profiles` table. When that's finished, head over to the `Table Editor` and see your new `profiles` table.
### 3. Get the URL and Key
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
![image](https://user-images.githubusercontent.com/10214025/88916245-528c2680-d298-11ea-8a71-708f93e1ce4f.png)
**_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
### 4. Env vars
Create a file in this folder `.env`
```
NUXT_PUBLIC_SUPABASE_URL=
NUXT_PUBLIC_SUPABASE_ANON_KEY=
```
Populate this file with your URL and Key.
### 5. Run the application
Run the application: `npm run dev`. Open your browser to `https://localhost:3000/` and you are ready to go 🚀.
## Supabase details
### Postgres Row level security
This project uses very high-level Authorization using Postgres' Role Level Security.
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
When a user logs in, they are issued a JWT with the role `authenticated` and their UUID.
We can use these details to provide fine-grained control over what each user can and cannot do.
This is a trimmed-down schema, with the policies:
```sql
-- Create a table for Public Profiles
create table profiles (
id uuid references auth.users not null,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
primary key (id),
unique(username),
constraint username_length check (char_length(username) >= 3)
);
alter table profiles enable row level security;
create policy "Public profiles are viewable by everyone."
on profiles for select
using ( true );
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
-- Set up Realtime!
begin;
drop publication if exists supabase_realtime;
create publication supabase_realtime;
commit;
alter publication supabase_realtime add table profiles;
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
create policy "Avatar images are publicly accessible."
on storage.objects for select
using ( bucket_id = 'avatars' );
create policy "Anyone can upload an avatar."
on storage.objects for insert
with check ( bucket_id = 'avatars' );
```
## Authors
- [Supabase](https://supabase.com)
Supabase is open source. We'd love for you to follow along and get involved at https://github.com/supabase/supabase
@@ -1,7 +0,0 @@
# ASSETS
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
@@ -1,432 +0,0 @@
html,
body {
--custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
--custom-bg-color: #101010;
--custom-panel-color: #222;
--custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8);
--custom-color: #fff;
--custom-color-brand: #24b47e;
--custom-color-secondary: #666;
--custom-border: 1px solid #333;
--custom-border-radius: 5px;
--custom-spacing: 5px;
padding: 0;
margin: 0;
font-family: var(--custom-font-family);
background-color: var(--custom-bg-color);
}
* {
color: var(--custom-color);
font-family: var(--custom-font-family);
box-sizing: border-box;
}
html,
body,
#__next {
height: 100vh;
width: 100vw;
overflow-x: hidden;
}
/* Grid */
.container {
width: 90%;
margin-left: auto;
margin-right: auto;
}
.row {
position: relative;
width: 100%;
}
.row [class^="col"] {
float: left;
margin: 0.5rem 2%;
min-height: 0.125rem;
}
.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-10,
.col-11,
.col-12 {
width: 96%;
}
.col-1-sm {
width: 4.33%;
}
.col-2-sm {
width: 12.66%;
}
.col-3-sm {
width: 21%;
}
.col-4-sm {
width: 29.33%;
}
.col-5-sm {
width: 37.66%;
}
.col-6-sm {
width: 46%;
}
.col-7-sm {
width: 54.33%;
}
.col-8-sm {
width: 62.66%;
}
.col-9-sm {
width: 71%;
}
.col-10-sm {
width: 79.33%;
}
.col-11-sm {
width: 87.66%;
}
.col-12-sm {
width: 96%;
}
.row::after {
content: "";
display: table;
clear: both;
}
.hidden-sm {
display: none;
}
@media only screen and (min-width: 33.75em) {
/* 540px */
.container {
width: 80%;
}
}
@media only screen and (min-width: 45em) {
/* 720px */
.col-1 {
width: 4.33%;
}
.col-2 {
width: 12.66%;
}
.col-3 {
width: 21%;
}
.col-4 {
width: 29.33%;
}
.col-5 {
width: 37.66%;
}
.col-6 {
width: 46%;
}
.col-7 {
width: 54.33%;
}
.col-8 {
width: 62.66%;
}
.col-9 {
width: 71%;
}
.col-10 {
width: 79.33%;
}
.col-11 {
width: 87.66%;
}
.col-12 {
width: 96%;
}
.hidden-sm {
display: block;
}
}
@media only screen and (min-width: 60em) {
/* 960px */
.container {
width: 75%;
max-width: 60rem;
}
}
/* Forms */
label {
display: block;
margin: 5px 0;
color: var(--custom-color-secondary);
font-size: 0.8rem;
text-transform: uppercase;
}
input {
width: 100%;
border-radius: 5px;
border: var(--custom-border);
padding: 8px;
font-size: 0.9rem;
background-color: var(--custom-bg-color);
color: var(--custom-color);
}
input[disabled] {
color: var(--custom-color-secondary);
}
/* Utils */
.block {
display: block;
width: 100%;
}
.inline-block {
display: inline-block;
width: 100%;
}
.flex {
display: flex;
}
.flex.column {
flex-direction: column;
}
.flex.row {
flex-direction: row;
}
.flex.flex-1 {
flex: 1 1 0;
}
.flex-end {
justify-content: flex-end;
}
.flex-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.text-sm {
font-size: 0.8rem;
font-weight: 300;
}
.text-right {
text-align: right;
}
.font-light {
font-weight: 300;
}
.opacity-half {
opacity: 50%;
}
/* Button */
button,
.button {
color: var(--custom-color);
border: var(--custom-border);
background-color: var(--custom-bg-color);
display: inline-block;
text-align: center;
border-radius: var(--custom-border-radius);
padding: 0.5rem 1rem;
cursor: pointer;
text-align: center;
font-size: 0.9rem;
text-transform: uppercase;
}
button.primary,
.button.primary {
background-color: var(--custom-color-brand);
border: 1px solid var(--custom-color-brand);
}
/* Widgets */
.card {
width: 100%;
display: block;
border: var(--custom-border);
border-radius: var(--custom-border-radius);
padding: var(--custom-spacing);
}
.avatar {
border-radius: var(--custom-border-radius);
overflow: hidden;
max-width: 100%;
}
.avatar.image {
object-fit: cover;
}
.avatar.no-image {
background-color: #333;
border: 1px solid rgb(200, 200, 200);
border-radius: 5px;
}
.footer {
position: absolute;
max-width: 100%;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-flow: row;
border-top: var(--custom-border);
background-color: var(--custom-bg-color);
}
.footer div {
padding: var(--custom-spacing);
display: flex;
align-items: center;
width: 100%;
}
.footer div > img {
height: 20px;
margin-left: 10px;
}
.footer > div:first-child {
display: none;
}
.footer > div:nth-child(2) {
justify-content: left;
}
@media only screen and (min-width: 60em) {
/* 960px */
.footer > div:first-child {
display: flex;
}
.footer > div:nth-child(2) {
justify-content: center;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.mainHeader {
width: 100%;
font-size: 1.3rem;
margin-bottom: 20px;
}
.avatarPlaceholder {
border: var(--custom-border);
border-radius: var(--custom-border-radius);
width: 35px;
height: 35px;
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
/* Auth */
.auth-widget {
display: flex;
flex-direction: column;
gap: 20px;
}
.auth-widget > .button {
display: flex;
align-items: center;
justify-content: center;
border: none;
background-color: #444444;
text-transform: none !important;
transition: all 0.2s ease;
}
.auth-widget .button:hover {
background-color: #2a2a2a;
}
.auth-widget .button > .loader {
width: 17px;
animation: spin 1s linear infinite;
filter: invert(1);
}
/* Account */
.account {
display: flex;
flex-direction: column;
gap: 20px;
}
.account > * > .avatarField {
display: flex;
align-items: center;
margin-bottom: 30px;
}
.account > * > .avatarField > .avatarContainer {
margin-right: 20px;
}
/* Profile Card */
.profileCard {
border-radius: 5px;
display: flex;
border: var(--custom-border);
background-color: var(--custom-panel-color);
padding: 20px 20px;
margin-bottom: 20px;
}
.profileCard:last-child {
margin-bottom: 0px;
}
.profileCard > .userInfo {
margin-left: 20px;
font-weight: 300;
display: flex;
flex-direction: column;
justify-content: center;
}
.profileCard > .userInfo > p {
margin: 0;
}
.profileCard > .userInfo > .username {
font-size: 1.3rem;
font-weight: 500;
margin-bottom: 5px;
}
.profileCard > .userInfo > .website {
font-size: 0.9rem;
color: var(--custom-color-brand);
margin-bottom: 10px;
text-decoration: none;
}
@@ -1,102 +0,0 @@
<template>
<div class="account">
<div>
<label for="email">Email</label>
<input id="email" type="text" v-model="state.propSession.user.email" disabled />
</div>
<div>
<label for="username">Username</label>
<input id="username" type="text" v-model="state.username" />
</div>
<div>
<label for="website">Website</label>
<input id="website" type="website" v-model="state.website" />
</div>
<div>
<button class="button block primary" @click="updateProfile" :disabled="state.loading">
{{ state.loading ? 'Loading ...' : 'Update' }}
</button>
</div>
<div>
<button @click="signOut" class="button block">Sign Out</button>
</div>
</div>
</template>
<script>
import { supabase } from '../lib/supabaseClient'
export default {
props: ['session'],
data() {
return {
state: {
loading: true,
username: '',
website: '',
propSession: this.session,
},
}
},
mounted() {
this.getProfile()
},
methods: {
async signOut() {
const { error } = await supabase.auth.signOut()
if (error) console.log('Error logging out:', error.message)
},
setProfile(profile) {
this.state.username = profile.username
this.state.website = profile.website
},
async getProfile() {
try {
this.state.loading = true
const { data, error } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', this.state.propSession.user.id)
.single()
if (error || !data) {
throw error || new Error('Profile not found')
}
this.setProfile(data)
} catch (error) {
console.log('error', error.message)
} finally {
this.state.loading = false
}
},
async updateProfile() {
try {
this.state.loading = true
const updates = {
id: this.state.propSession.user.id,
username: this.state.username,
website: this.state.website,
updated_at: new Date(),
}
const { error } = await supabase.from('profiles').upsert(updates)
if (error) {
throw error
}
} catch (error) {
alert(error.message)
} finally {
this.state.loading = false
}
},
},
}
</script>
@@ -1,65 +0,0 @@
<template>
<div class="row">
<div class="col-6">
<h1 class="header">Supabase Auth + Storage</h1>
<p className="">
Experience our Auth and Storage through a simple profile management example. Create a user
profile and upload an avatar image. Fast, simple, secure.
</p>
</div>
<form class="col-6 auth-widget" @submit.prevent="handleSubmit">
<p class="description">Sign in via magic link with your email below</p>
<div>
<label for="email-register">Email</label>
<input
class="inputField"
v-model="formValues.email"
id="email-register"
type="email"
placeholder="Your email"
/>
</div>
<div>
<button class="button block" :disabled="formValues.loader">
{{ formValues.loader ? "Loading ..." : "Sign up with magic link" }}
</button>
</div>
</form>
</div>
</template>
<script>
import { supabase } from "../lib/supabaseClient";
export default {
data() {
return {
formValues: {
email: "",
loader: false,
},
};
},
methods: {
async handleSubmit() {
try {
this.formValues.loader = true;
const { error, data: { user } } = await supabase.auth.signInWithOtp({
email: this.formValues.email,
});
if (error) {
throw error;
}
alert("Check your email for the login link!");
} catch (error) {
alert(error.message);
} finally {
this.formValues.loader = false;
}
},
},
};
</script>
@@ -1,35 +0,0 @@
<template>
<div class="profileCard">
<div class="userInfo">
<p class="username">{{ state.propProfile.username }}</p>
<p class="website">{{ state.propProfile.website }}</p>
<p>
<small>
Last updated
{{
state.lastUpdated
? `${state.lastUpdated.toLocaleDateString()}
${state.lastUpdated.toLocaleTimeString()}`
: "Never"
}}
</small>
</p>
</div>
</div>
</template>
<script>
export default {
props: ["profile"],
data() {
return {
state: {
propProfile: this.profile,
lastUpdated: this.profile.updated_at
? new Date(this.profile.updated_at)
: null,
},
};
},
};
</script>
@@ -1,22 +0,0 @@
<template>
<div class="profileList">
<template v-for="profile in state.propProfiles">
<profile-card :key="profile.id" :profile="profile" />
</template>
</div>
</template>
<script>
import ProfileCard from "./ProfileCard.vue";
export default {
components: { ProfileCard },
props: ["profiles"],
data() {
return {
state: {
propProfiles: this.profiles,
},
};
},
};
</script>
@@ -1,3 +0,0 @@
<template>
<Nuxt />
</template>
@@ -1,10 +0,0 @@
export const NUXT_PUBLIC_SUPABASE_URL = process.env.NUXT_PUBLIC_SUPABASE_URL
export const NUXT_PUBLIC_SUPABASE_ANON_KEY = process.env.NUXT_PUBLIC_SUPABASE_ANON_KEY
// export type Profile = {
// id: string
// avatar_url: string
// username: string
// website: string
// updated_at: string
// }
@@ -1,15 +0,0 @@
import { createClient } from "@supabase/supabase-js";
import {
NUXT_PUBLIC_SUPABASE_URL,
NUXT_PUBLIC_SUPABASE_ANON_KEY,
} from "./constants";
if (!NUXT_PUBLIC_SUPABASE_URL)
throw new Error("Missing env.NUXT_PUBLIC_SUPABASE_URL");
if (!NUXT_PUBLIC_SUPABASE_ANON_KEY)
throw new Error("Missing env.NUXT_PUBLIC_SUPABASE_KEY");
export const supabase = createClient(
NUXT_PUBLIC_SUPABASE_URL,
NUXT_PUBLIC_SUPABASE_ANON_KEY
);
@@ -1,8 +0,0 @@
# MIDDLEWARE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your application middleware.
Middleware lets you define custom functions that can be run before rendering either a page or a group of pages.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
@@ -1,41 +0,0 @@
export default {
// publicRuntimeConfig: {
// URL: process.env.SUPABASE_URL,
// KEY: process.env.SUPABASE_KEY
// },
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false,
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'nuxtjs-user-management',
htmlAttrs: {
lang: 'en',
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: ['@/assets/global.css'],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: ['@nuxtjs/dotenv'],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
}
File diff suppressed because it is too large. Load diff
@@ -1,17 +0,0 @@
{
"name": "nuxtjs-user-management",
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
},
"dependencies": {
"@nuxtjs/dotenv": "^1.4.1",
"@supabase/supabase-js": "^2.0.4",
"core-js": "^3.9.1",
"nuxt": "^2.15.8"
}
}
@@ -1,60 +0,0 @@
<template>
<div class="container" style="padding: 50px 0 100px">
<Auth v-if="!state.session" />
<div class="row" v-else>
<div class="col-6">
<h3>Account</h3>
<Account :session="state.session" />
</div>
<div class="col-6">
<h3>Public Profiles</h3>
<template v-if="state.profiles.length > 0">
<ProfileList :profiles="state.profiles" />
</template>
</div>
</div>
</div>
</template>
<script>
import { supabase } from "../lib/supabaseClient";
export default {
data() {
return {
state: {
session: null,
profiles: [],
},
};
},
mounted() {
supabase.auth.getSession().then(({ data: { session } }) => {
this.state.session = session;
});
supabase.auth.onAuthStateChange((_event, session) => {
this.state.session = session;
});
this.getPublicProfiles();
},
methods: {
async getPublicProfiles() {
try {
const { data, error } = await supabase
.from("profiles")
.select("id, username, avatar_url, website, updated_at")
.order("updated_at", { ascending: false });
if (error || !data) {
throw error || new Error("No data");
}
console.log("data", data);
this.state.profiles = data;
} catch (error) {
console.log("error", error.message);
}
},
},
};
</script>
@@ -1,7 +0,0 @@
# PLUGINS
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains JavaScript plugins that you want to run before mounting the root Vue.js application.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

+5 -7
View File
@@ -1,5 +1,4 @@
import React from 'react'
import { Space } from '../Space'
import Typography from '../Typography'
import { MenuContextProvider, useMenuContext } from './MenuContext'
@@ -8,11 +7,12 @@ import styleHandler from '../../lib/theme/styleHandler'
interface MenuProps {
children: React.ReactNode
className?: string
ulClassName?: string
style?: React.CSSProperties
type?: 'text' | 'pills' | 'border'
}
function Menu({ children, className, style, type = 'text' }: MenuProps) {
function Menu({ children, className, ulClassName, style, type = 'text' }: MenuProps) {
return (
<nav
role="menu"
@@ -23,7 +23,7 @@ function Menu({ children, className, style, type = 'text' }: MenuProps) {
style={style}
>
<MenuContextProvider type={type}>
<ul>{children}</ul>
<ul className={ulClassName}>{children}</ul>
</MenuContextProvider>
</nav>
)
@@ -86,7 +86,7 @@ export function Item({
onClick={onClick}
aria-current={active ? 'page' : undefined}
>
{icon && <span className={iconClasses.join(' ')}>{icon}</span>}
{icon && <div className={`${iconClasses.join(' ')} min-w-fit`}>{icon}</div>}
<span className={contentClasses.join(' ')}>{children}</span>
</a>
</li>
@@ -103,9 +103,7 @@ export function Group({ children, icon, title }: GroupProps) {
const __styles = styleHandler('menu')
const { type } = useMenuContext()
return (
<div
className={[__styles.group.base, __styles.group.variants[type]].join(' ')}
>
<div className={[__styles.group.base, __styles.group.variants[type]].join(' ')}>
{icon && <span className={__styles.group.icon}>{icon}</span>}
<span className={__styles.group.content}>{title}</span>
{children}
+1 -1
View File
@@ -137,7 +137,7 @@ function RadioGroup({
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
label: string
afterLabel?: string
beforeLabel?: string
beforeLabel?: string | React.ReactNode
description?: string
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
hidden?: boolean
+18 -11
View File
@@ -7,6 +7,8 @@ import { useDispatch, useTrackedState } from '../../store'
import RowRenderer from './RowRenderer'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
function rowKeyGetter(row: SupaRow) {
return row?.idx ?? -1
@@ -77,14 +79,23 @@ export const Grid = memo(
const table = state.table
function getColumnForeignKey(columnName: string) {
const foreignKey = table?.columns.find((x) => x.name == columnName)?.foreignKey ?? {}
const { project } = useProjectContext()
const { data } = useForeignKeyConstraintsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: table?.schema ?? undefined,
})
return Boolean(
foreignKey.targetTableSchema && foreignKey.targetTableName && foreignKey.targetColumnName
function getColumnForeignKey(columnName: string) {
const { targetTableSchema, targetTableName, targetColumnName } =
table?.columns.find((x) => x.name == columnName)?.foreignKey ?? {}
return data?.find(
(key) =>
key.target_schema == targetTableSchema &&
key.target_table == targetTableName &&
key.target_columns == targetColumnName
)
? foreignKey
: undefined
}
function onRowDoubleClick(row: any, column: any) {
@@ -92,11 +103,7 @@ export const Grid = memo(
if (foreignKey) {
onEditForeignKeyColumnValue({
foreignKey: {
target_column_name: foreignKey.targetColumnName!,
target_table_name: foreignKey.targetTableName!,
target_table_schema: foreignKey.targetTableSchema!,
},
foreignKey,
row,
column,
})
@@ -17,6 +17,7 @@ import { useTableRowDeleteAllMutation } from 'data/table-rows/table-row-delete-a
import { useTableRowTruncateMutation } from 'data/table-rows/table-row-truncate-mutation'
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import RLSBannerWarning from './RLSBannerWarning'
// [Joshen] CSV exports require this guard as a fail-safe if the table is
// just too large for a browser to keep all the rows in memory before
@@ -37,13 +38,16 @@ const Header = ({ table, sorts, filters, onAddColumn, onAddRow, headerActions }:
const { selectedRows } = state
return (
<div className="flex h-10 items-center justify-between bg-scale-100 px-5 py-1.5 dark:bg-scale-300">
{selectedRows.size > 0 ? (
<RowHeader table={table} sorts={sorts} filters={filters} />
) : (
<DefaultHeader table={table} onAddColumn={onAddColumn} onAddRow={onAddRow} />
)}
<div className="sb-grid-header__inner">{headerActions}</div>
<div>
<div className="flex h-10 items-center justify-between bg-scale-100 px-5 py-1.5 dark:bg-scale-300">
{selectedRows.size > 0 ? (
<RowHeader table={table} sorts={sorts} filters={filters} />
) : (
<DefaultHeader table={table} onAddColumn={onAddColumn} onAddRow={onAddRow} />
)}
<div className="sb-grid-header__inner">{headerActions}</div>
</div>
<RLSBannerWarning />
</div>
)
}
@@ -0,0 +1,86 @@
import { useState } from 'react'
import { Button, IconAlertCircle, IconExternalLink, Modal } from 'ui'
import Link from 'next/link'
import { useParams, useStore } from 'hooks'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import type { PostgresTable } from '@supabase/postgres-meta'
const RLS_ACKNOWLEDGED_KEY = 'supabase-acknowledge-rls-warning'
export default function RLSBannerWarning() {
const { meta } = useStore()
const { ref: projectRef, id: tableID } = useParams()
const isAcknowledged = localStorage.getItem(`${RLS_ACKNOWLEDGED_KEY}-${tableID}`) === 'true'
const [isOpen, setIsOpen] = useState(false)
const tables: PostgresTable[] = meta.tables.list()
const currentTable = tables.find((table) => table.id.toString() === tableID)
const rlsEnabled = currentTable?.rls_enabled
const isPublicTable = currentTable?.schema === 'public'
function handleDismissWarning() {
localStorage.setItem(`${RLS_ACKNOWLEDGED_KEY}-${tableID}`, 'true')
setIsOpen(false)
}
return (
<>
{!isAcknowledged && !rlsEnabled && isPublicTable ? (
<div>
<div className="text-center bg-amber-500 text-amber-900 text-xs py-2.5 flex items-center justify-center relative">
<IconAlertCircle size={16} strokeWidth={2} />
<span className="uppercase font-bold ml-2">Warning</span>: This table is publicly
readable and writable.{' '}
<Link href={`/project/${projectRef}/auth/policies?search=${tableID}`}>
<a className="underline ml-2 opacity-80 hover:opacity-100 transition">
Enable Row Level Security
</a>
</Link>
<div className="ml-20 absolute right-2">
<Button
type="outline"
className="text-white hover:text-scale-1200"
onClick={() => setIsOpen(true)}
>
Dismiss
</Button>
</div>
</div>
<ConfirmationModal
danger
visible={isOpen}
header="Warning: Your data is public"
buttonLabel="I understand, dismiss this warning"
onSelectCancel={() => setIsOpen(false)}
onSelectConfirm={handleDismissWarning}
children={
<Modal.Content>
<div className="mt-4 mb-6 space-y-4">
<p className="text-sm text-scale-1100">
Data in this table is publicly <u>readable and writable</u> by anyone with the
anon key. We strongly recommend creating RLS (Row-Level Security) policies to
allow access to this table.
</p>
<div>
<Link href="https://supabase.com/docs/guides/auth/row-level-security">
<a target="_blank">
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
About RLS Policies
</Button>
</a>
</Link>
</div>
</div>
</Modal.Content>
}
/>
</div>
) : (
<></>
)}
</>
)
}
@@ -62,8 +62,9 @@ const AccessTokenList = observer(() => {
<Table.td>
<Button
as="span"
type="danger"
title="delete token"
type="default"
title="Delete token"
className="px-1"
onClick={() => {
setToken(x)
setIsOpen(true)
@@ -1,22 +1,22 @@
import { FC, useState } from 'react'
import { Input, Button, Modal, Form, Alert } from 'ui'
import { useState } from 'react'
import { Input, Button, Modal, Form, Alert, IconChevronDown, Dropdown, IconExternalLink } from 'ui'
import { useStore } from 'hooks'
import {
NewAccessToken,
useAccessTokenCreateMutation,
} from 'data/access-tokens/access-tokens-create-mutation'
import { useAccessTokenCreateMutation } from 'data/access-tokens/access-tokens-create-mutation'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
const NewAccessTokenButton = observer(() => {
export interface NewAccessTokenButtonProps {
onCreateToken: (token: any) => void
}
const NewAccessTokenButton = observer(({ onCreateToken }: NewAccessTokenButtonProps) => {
const { ui } = useStore()
const [isOpen, setIsOpen] = useState(false)
const [newToken, setNewToken] = useState<NewAccessToken | undefined>(undefined)
const [tokenScope, setTokenScope] = useState<'V0' | undefined>(undefined)
const validate = (values: any) => {
const errors: any = {}
if (!values.tokenName) {
errors.tokenName = 'Please enter a name for the token'
}
if (!values.tokenName) errors.tokenName = 'Please enter a name for the token'
return errors
}
@@ -26,9 +26,8 @@ const NewAccessTokenButton = observer(() => {
setSubmitting(true)
try {
const response = await createAccessToken({ name: values.tokenName })
setNewToken(response)
const response = await createAccessToken({ name: values.tokenName, scope: tokenScope })
onCreateToken(response)
setSubmitting(false)
setIsOpen(false)
} catch (error: any) {
@@ -36,31 +35,62 @@ const NewAccessTokenButton = observer(() => {
category: 'error',
message: `Failed to create token: ${error.message}`,
})
setSubmitting(false)
}
}
return (
<>
<Button
onClick={() => {
setNewToken(undefined)
setIsOpen(!isOpen)
}}
>
Generate new token
</Button>
{newToken && <NewTokenItem data={newToken} />}
<div className="container max-w-7xl">
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="rounded-r-none px-3"
onClick={() => {
setTokenScope(undefined)
setIsOpen(true)
}}
>
Generate new token
</Button>
<Dropdown
align="end"
side="bottom"
overlay={[
<Dropdown.Item
key="experimental-token"
onClick={() => {
setTokenScope('V0')
setIsOpen(true)
}}
>
<div className="space-y-1">
<p className="block text-scale-1200">Generate token for experimental API</p>
</div>
</Dropdown.Item>,
]}
>
<Button
type="primary"
className="rounded-l-none px-[4px] py-[5px]"
icon={<IconChevronDown />}
/>
</Dropdown>
</div>
</div>
</div>
<Modal
closable
hideFooter
size="small"
size="medium"
visible={isOpen}
onCancel={() => setIsOpen(!isOpen)}
header={
<div className="flex items-baseline gap-2">
<h5 className="text-sm text-scale-1200">Generate New Token</h5>
<h5 className="text-sm text-scale-1200">
{tokenScope === 'V0' ? 'Generate token for experimental API' : 'Generate New Token'}
</h5>
</div>
}
>
@@ -72,19 +102,47 @@ const NewAccessTokenButton = observer(() => {
>
{({ isSubmitting }: { isSubmitting: boolean }) => (
<div className="py-3 space-y-4">
{tokenScope === 'V0' && (
<Modal.Content>
<Alert
withIcon
variant="warning"
title="The experimental API provides additional endpoints which allows you to manage your organizations and projects."
>
<p>
These include deleting organizations and projects which cannot be undone. As
such, be very careful when using this API.
</p>
<div className="mt-4">
<Link href="https://api.supabase.com/api/v0">
<a target="_blank">
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
Experimental API documentation
</Button>
</a>
</Link>
</div>
</Alert>
</Modal.Content>
)}
<Modal.Content>
<Input
id="tokenName"
label="Name"
placeholder="Type in the token name"
placeholder="Provide a name for your token"
className="w-full"
/>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<Button htmlType="submit" loading={isSubmitting} size="small" block danger>
Generate Token
</Button>
<div className="flex items-center space-x-2 justify-end">
<Button type="default" disabled={isSubmitting} onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
Generate token
</Button>
</div>
</Modal.Content>
</div>
)}
@@ -95,28 +153,3 @@ const NewAccessTokenButton = observer(() => {
})
export default NewAccessTokenButton
interface NewTokenItemProps {
data: NewAccessToken
}
const NewTokenItem: FC<NewTokenItemProps> = observer(({ data }) => {
return (
<Alert withIcon variant="success" title="Successfully generated a new token!">
<div className="w-full space-y-2">
<p className="text-sm">
Do copy this access token and store it in a secure place - you will not be able to see it
again.
</p>
<Input
copy
readOnly
size="small"
className="max-w-xl input-mono"
value={data.token}
onChange={() => {}}
/>
</div>
</Alert>
)
})
@@ -0,0 +1,30 @@
import { Alert, Input } from 'ui'
import { observer } from 'mobx-react-lite'
import { NewAccessToken } from 'data/access-tokens/access-tokens-create-mutation'
interface NewTokenBannerProps {
token: NewAccessToken
}
const NewTokenBanner = observer(({ token }: NewTokenBannerProps) => {
return (
<Alert withIcon variant="success" title="Successfully generated a new token!">
<div className="w-full space-y-2">
<p className="text-sm">
Do copy this access token and store it in a secure place - you will not be able to see it
again.
</p>
<Input
copy
readOnly
size="small"
className="max-w-xl input-mono"
value={token.token}
onChange={() => {}}
/>
</div>
</Alert>
)
})
export default NewTokenBanner
@@ -1,4 +1,5 @@
import AccessTokenList from './AccessTokenList'
import NewTokenBanner from './NewTokenBanner'
import NewAccessTokenButton from './NewAccessTokenButton'
export { AccessTokenList, NewAccessTokenButton }
export { AccessTokenList, NewTokenBanner, NewAccessTokenButton }
@@ -1,3 +1,4 @@
import { BASE_PATH } from 'lib/constants'
import { FC, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { Button, Input, InputNumber, Toggle, Listbox, IconEye, IconEyeOff } from 'ui'
@@ -123,7 +124,9 @@ const FormField: FC<Props> = ({ name, properties, formValues, disabled = false }
key={option.value}
label={option.label}
value={option.value}
addOnBefore={() => <img className="h-6 w-6" src={`/img/icons/${option.icon}`} />}
addOnBefore={() => (
<img className="h-6 w-6" src={`${BASE_PATH}/img/icons/${option.icon}`} />
)}
>
{option.label}
</Listbox.Option>
@@ -5,6 +5,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Alert, Button, Collapsible, Form, IconCheck, IconChevronUp, Input } from 'ui'
import { useStore, checkPermissions } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import { Provider } from './AuthProvidersForm.types'
import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants'
import FormField from './FormField'
@@ -78,7 +79,7 @@ const ProviderForm: FC<Props> = ({ provider }) => {
width={14}
/>
<img
src={`/img/icons/${provider.misc.iconKey}.svg`}
src={`${BASE_PATH}/img/icons/${provider.misc.iconKey}.svg`}
width={18}
alt={`${provider.title} auth icon`}
/>
@@ -53,9 +53,10 @@ const PolicyTableRow: FC<Props> = ({
withIcon
variant="warning"
className="!px-4 !py-3 !mt-3"
title="Warning: RLS is disabled"
title="Warning: RLS is disabled. Your table is publicly readable and writable."
>
Anonymous access is allowed to this table
Anyone with the anon. key can modify or delete your data. You should turn on RLS and
create access policies to keep your data secure.
</Alert>
)}
</div>
@@ -1,10 +1,11 @@
import { FC, useState } from 'react'
import Router from 'next/router'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle, IconCreditCard } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useStore } from 'hooks'
import { PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { BASE_PATH, PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { SubscriptionPreview } from '../Billing.types'
import { getProductPrice, validateSubscriptionUpdatePayload } from '../Billing.utils'
import PaymentTotal from './PaymentTotal'
@@ -400,7 +401,7 @@ const PaymentSummaryPanel: FC<Props> = ({
addOnBefore={() => {
return (
<img
src={`/img/payment-methods/${method.card.brand
src={`${BASE_PATH}/img/payment-methods/${method.card.brand
.replace(' ', '-')
.toLowerCase()}.png`}
width="32"
@@ -7,7 +7,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useStore, checkPermissions, useParams } from 'hooks'
import ExtensionCard from './ExtensionCard'
import { HIDDEN_EXTENSIONS } from './Extensions.constants'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import NoSearchResults from 'components/ui/NoSearchResults'
import InformationBox from 'components/ui/InformationBox'
import Link from 'next/link'
@@ -69,7 +69,9 @@ const Extensions: FC<Props> = ({}) => {
</div>
</div>
{extensions.length === 0 && <NoSearchResults />}
{extensions.length === 0 && (
<NoSearchResults searchString={filterString} onResetFilter={() => setFilterString('')} />
)}
<div className="my-8 w-full space-y-12">
{enabledExtensions.length > 0 && (
@@ -1,56 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Checkbox } from 'ui'
import { union, without } from 'lodash'
import { CreateHookContext } from './'
const CheckboxEvents: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
// @ts-ignore
<Checkbox.Group
name="events"
label="Events"
id="events"
layout="horizontal"
size="medium"
onChange={(e) => {
const temp = _localState.formState.events.value
const value = e.target.checked
? union(temp, [e.target.value])
: without(temp, e.target.value)
_localState.onFormChange({
key: 'events',
value: value,
})
}}
error={_localState.formState.events.error}
labelOptional="The type of events that will trigger your function hook"
descriptionText="These are the events that are watched by the function hook, only the events selected above will fire the function hook on the table you've selected."
>
<Checkbox
value="INSERT"
id="INSERT"
label="Insert"
description={'Any insert operation on the table'}
checked={_localState.formState.events.value.includes('INSERT')}
/>
<Checkbox
value="UPDATE"
id="UPDATE"
label="Update"
description="Any update operation, of any column in the table"
checked={_localState.formState.events.value.includes('UPDATE')}
/>
<Checkbox
value="DELETE"
id="DELETE"
label="Delete"
description="Any deletion of a record"
checked={_localState.formState.events.value.includes('DELETE')}
/>
</Checkbox.Group>
)
})
export default CheckboxEvents
@@ -1,27 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Input } from 'ui'
import { CreateHookContext } from './'
const InputName: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<Input
id="name"
label="Name"
layout="horizontal"
placeholder="Name of your function hook"
value={_localState.formState.name.value}
onChange={(e) =>
_localState.onFormChange({
key: 'name',
value: e.target.value,
})
}
size="medium"
error={_localState.formState.name.error}
/>
)
})
export default InputName
@@ -1,41 +0,0 @@
export const hookServiceOptions: {
id: string
label: string
badge: string
badgeType: 'brand' | 'amber'
description: string
img_url: string
}[] = [
{
id: 'http_request',
label: 'HTTP Request',
badge: 'Alpha',
badgeType: 'brand',
description: 'Send an HTTP request to any URL.',
img_url: 'http-request.png',
},
{
id: 'supabase_function',
label: 'Supabase Function',
badge: 'Coming soon',
badgeType: 'amber',
description: 'Choose a Supabase Function to run.',
img_url: 'supabase-severless-function.png',
},
{
id: 'google_cloud_function',
label: 'Google cloud run',
badge: 'Coming soon',
badgeType: 'amber',
description: 'Choose a google cloud function to run',
img_url: 'google-cloud-run.png',
},
{
id: 'aws_lambda_function',
label: 'AWS Lambda',
badge: 'Coming soon',
badgeType: 'amber',
description: 'Choose an AWS Lambda function to run.',
img_url: 'aws-lambda-severless-function.png',
},
]
@@ -1,51 +0,0 @@
import Image from 'next/image'
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Radio, Badge } from 'ui'
import { CreateHookContext } from '../'
interface Props {
id: string
description: string
label: string
img_url: string
badgeType: 'brand' | 'amber'
badge: string
}
const RadioHookService: FC<Props> = observer(
({ id, description, label, img_url, badgeType, badge }) => {
const _localState: any = useContext(CreateHookContext)
return (
<Radio
id={id}
value={id}
checked={_localState.formState.hookService.value == id}
// @ts-ignore
beforeLabel={
<>
<div className="flex items-center space-x-5">
{/* <div className="h-3 w-3"> */}
<Image
src={`/img/function-providers/${img_url}`}
layout="fixed"
width="32"
height="32"
/>
{/* </div> */}
<div className="flex-col space-y-0">
<div className="flex space-x-1">
<span className="text-scale-1200">{label}</span>
<Badge color={badgeType}>{badge}</Badge>
</div>
<span className="text-scale-900">{description}</span>
</div>
</div>
</>
}
/>
)
}
)
export default RadioHookService
@@ -1,35 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Radio } from 'ui'
import { CreateHookContext } from '../'
import RadioHookService from './RadioHookService'
import { hookServiceOptions } from './RadioGroupHookService.constants'
const RadioGroupHookService: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<>
<div className="provider-radio-group">
<Radio.Group
type="cards"
layout="vertical"
onChange={(event) => {
_localState.onFormChange({
key: 'hookService',
value: event.target.value,
})
}}
value={_localState.formState.hookService.value}
error={_localState.formState.hookService.error}
>
{hookServiceOptions.map((x) => (
<RadioHookService key={x.id} {...x} />
))}
</Radio.Group>
</div>
</>
)
})
export default RadioGroupHookService
@@ -1,30 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Select } from 'ui'
import { CreateHookContext } from './'
const SelectEnabledMode: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<Select
id="enabled-mode"
label="Enabled mode"
layout="horizontal"
value={_localState.formState.enabledMode.value}
onChange={(e) =>
_localState.onFormChange({
key: 'enabledMode',
value: e.target.value,
})
}
size="small"
>
<Select.Option value="ORIGIN">Origin</Select.Option>
<Select.Option value="REPLICA">Replica</Select.Option>
<Select.Option value="ALWAYS">Always</Select.Option>
<Select.Option value="DISABLED">Disabled</Select.Option>
</Select>
)
})
export default SelectEnabledMode
@@ -1,49 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, IconPlus } from 'ui'
import { CreateHookContext } from '../'
import InputServiceHeader from './InputServiceHeader'
const InputMultiServiceHeaders: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
function onAddArgument() {
_localState.onFormArrayChange({
key: 'serviceHeaders',
value: { name: '', value: '' },
operation: 'add',
})
}
return (
<div>
<div className="flex items-center justify-between">
<h5>HTTP Headers</h5>
</div>
<div className="space-y-2 pt-4">
{_localState.formState.serviceHeaders.value.map(
(
x: { name: string; value: string; error?: { name?: string; value?: string } },
idx: number
) => (
<InputServiceHeader
key={`serviceHeader-${idx}`}
idx={idx}
name={x.name}
value={x.value}
error={x.error}
/>
)
)}
<div className="">
<Button type="dashed" icon={<IconPlus />} onClick={onAddArgument}>
Add a new header
</Button>
</div>
</div>
</div>
)
})
export default InputMultiServiceHeaders
@@ -1,47 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, IconPlus } from 'ui'
import { CreateHookContext } from '../'
import InputServiceParam from './InputServiceParam'
const InputMultiServiceParams: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
function onAddArgument() {
_localState.onFormArrayChange({
key: 'serviceParams',
value: { name: '', value: '' },
operation: 'add',
})
}
return (
<div>
<div className="flex items-center justify-between">
<h5>HTTP Params</h5>
</div>
<div className="space-y-2 pt-4">
{_localState.formState.serviceParams.value.map(
(
x: { name: string; value: string; error?: { name?: string; value?: string } },
idx: number
) => (
<InputServiceParam
key={`serviceParam-${idx}`}
idx={idx}
name={x.name}
value={x.value}
error={x.error}
/>
)
)}
<Button type="dashed" icon={<IconPlus />} onClick={onAddArgument}>
Add a new param
</Button>
</div>
</div>
)
})
export default InputMultiServiceParams
@@ -1,77 +0,0 @@
import { FC, useContext, FormEvent } from 'react'
import { observer } from 'mobx-react-lite'
import { Input, IconTrash, Button } from 'ui'
import { CreateHookContext } from '../'
interface Props {
idx: number
name: string
value: string
error?: { name?: string; value?: string }
}
const InputServiceHeader: FC<Props> = observer(({ idx, name, value, error }) => {
const _localState: any = useContext(CreateHookContext)
function onNameChange(e: FormEvent<HTMLInputElement>) {
const _value = e.currentTarget.value
_localState.onFormArrayChange({
key: 'serviceHeaders',
value: { name: _value, value },
idx,
operation: 'update',
})
}
function onValueChange(e: FormEvent<HTMLInputElement>) {
const _value = e.currentTarget.value
_localState.onFormArrayChange({
key: 'serviceHeaders',
value: { name, value: _value },
idx,
operation: 'update',
})
}
function onDelete() {
_localState.onFormArrayChange({
key: 'serviceHeaders',
idx,
operation: 'delete',
})
}
return (
<div className="flex space-x-1">
<Input
id={`name-${idx}`}
className="flex-1"
placeholder="Header name"
value={name}
onChange={onNameChange}
size="small"
error={error?.name}
/>
<Input
id={`value-${idx}`}
className="flex-1"
placeholder="Header value"
value={value}
onChange={onValueChange}
size="small"
error={error?.value}
/>
<div>
<Button
danger
type="primary"
icon={<IconTrash size="tiny" />}
onClick={onDelete}
size="small"
/>
</div>
</div>
)
})
export default InputServiceHeader
@@ -1,77 +0,0 @@
import { FC, useContext, FormEvent } from 'react'
import { observer } from 'mobx-react-lite'
import { Input, IconTrash, Button } from 'ui'
import { CreateHookContext } from '../'
interface Props {
idx: number
name: string
value: string
error?: { name?: string; value?: string }
}
const InputServiceParam: FC<Props> = observer(({ idx, name, value, error }) => {
const _localState: any = useContext(CreateHookContext)
function onNameChange(e: FormEvent<HTMLInputElement>) {
const _value = e.currentTarget.value
_localState.onFormArrayChange({
key: 'serviceParams',
value: { name: _value, value },
idx,
operation: 'update',
})
}
function onValueChange(e: FormEvent<HTMLInputElement>) {
const _value = e.currentTarget.value
_localState.onFormArrayChange({
key: 'serviceParams',
value: { name, value: _value },
idx,
operation: 'update',
})
}
function onDelete() {
_localState.onFormArrayChange({
key: 'serviceParams',
idx,
operation: 'delete',
})
}
return (
<div className="flex space-x-1">
<Input
id={`name-${idx}`}
className="flex-1"
placeholder="Param name"
value={name}
onChange={onNameChange}
size="small"
error={error?.name}
/>
<Input
id={`value-${idx}`}
className="flex-1"
placeholder="Param value"
value={value}
onChange={onValueChange}
size="small"
error={error?.value}
/>
<div>
<Button
danger
type="primary"
icon={<IconTrash size="tiny" />}
onClick={onDelete}
size="small"
/>
</div>
</div>
)
})
export default InputServiceParam
@@ -1,30 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Input } from 'ui'
import { CreateHookContext } from '../'
// Not used currently
const InputServiceTimeout: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<Input
id="timeout-ms"
type="number"
label="Request timeout"
layout="horizontal"
descriptionText="Request timeout value in millisecond"
value={_localState.formState.serviceTimeoutMs.value}
onChange={(e) =>
_localState.onFormChange({
key: 'serviceTimeoutMs',
value: e.target.value,
})
}
size="medium"
error={_localState.formState.serviceTimeoutMs.error}
/>
)
})
export default InputServiceTimeout
@@ -1,29 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Input } from 'ui'
import { CreateHookContext } from '../'
const InputServiceUrl: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<Input
id="service-url"
type="url"
label="URL"
layout="horizontal"
placeholder="http://api.com/path/resource"
descriptionText="URL of the HTTP request. Must include HTTP/HTTPS"
value={_localState.formState.serviceUrl.value}
onChange={(e) =>
_localState.onFormChange({
key: 'serviceUrl',
value: e.target.value,
})
}
size="medium"
error={_localState.formState.serviceUrl.error}
/>
)
})
export default InputServiceUrl
@@ -1,28 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Select } from 'ui'
import { CreateHookContext } from '../'
const SelectServiceMethod: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<Select
id="method"
label="Method"
layout="horizontal"
value={_localState.formState.serviceMethod.value}
onChange={(e) =>
_localState.onFormChange({
key: 'serviceMethod',
value: e.target.value,
})
}
size="small"
>
<Select.Option value="GET">GET</Select.Option>
<Select.Option value="POST">POST</Select.Option>
</Select>
)
})
export default SelectServiceMethod
@@ -1,29 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Alert, Button } from 'ui'
import { CreateHookContext } from '../'
const ServiceUnavailableBox: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<Alert variant="warning" title="Service under development" withIcon>
<div className="space-y-4">
<div>We currently do not support this service.</div>
<Button
type="default"
className="flex-grow"
onClick={() =>
_localState.onFormChange({
key: 'hookService',
value: 'http_request',
})
}
>
Switch to HTTP
</Button>
</div>
</Alert>
)
})
export default ServiceUnavailableBox
@@ -1,44 +0,0 @@
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { SidePanel } from 'ui'
import { CreateHookContext } from '../'
import SelectServiceMethod from './SelectServiceMethod'
import InputServiceUrl from './InputServiceUrl'
import InputMultiServiceHeaders from './InputMultiServiceHeaders'
import InputMultiServiceParams from './InputMultiServiceParams'
import ServiceUnavailableBox from './ServiceUnavailableBox'
const ServiceConfigForm: FC = observer(({}) => {
const _localState: any = useContext(CreateHookContext)
return (
<>
{_localState.formState.hookService.value === 'http_request' ? (
<>
<SidePanel.Separator />
<div className="space-y-10">
<div className="space-y-6 px-6">
<h5>HTTP Request</h5>
<SelectServiceMethod />
<InputServiceUrl />
</div>
<SidePanel.Separator />
<div className="px-6">
<InputMultiServiceHeaders />
</div>
<SidePanel.Separator />
<div className="px-6">
<InputMultiServiceParams />
</div>
</div>
</>
) : (
<div className="px-6">
<ServiceUnavailableBox />
</div>
)}
</>
)
})
export default ServiceConfigForm
@@ -1,96 +0,0 @@
import Link from 'next/link'
import SVG from 'react-inlinesvg'
import { FC, useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { Input, Listbox } from 'ui'
import { CreateHookContext } from './'
import { useStore } from 'hooks'
const TableSelection: FC = observer(({}) => {
const { ui } = useStore()
const projectRef = ui.selectedProject?.ref
const _localState: any = useContext(CreateHookContext)
if (_localState.tables.length === 0) {
return (
<Input
label="Table"
layout="horizontal"
disabled
placeholder="No tables created, please create one first"
// @ts-ignore
descriptionText={
<p className="text-scale-1000">
<span className="text-scale-1000">
This is the table the trigger will watch for changes. There's currently no tables
created - please create one{' '}
</span>
<Link href={`/project/${projectRef}/editor`}>
<a className="text-brand-900">here</a>
</Link>
<span className="text-scale-1000"> first.</span>
</p>
}
/>
)
}
return (
<Listbox
id="table"
label="Table"
layout="horizontal"
value={_localState.formState.tableId.value}
onChange={(id) => {
const _table = _localState.tables.find((x: any) => x.id === id)
if (_table) {
_localState.onFormChange({
key: 'schema',
value: _table.schema,
})
_localState.onFormChange({
key: 'table',
value: _table.name,
})
_localState.onFormChange({
key: 'tableId',
value: id,
})
}
}}
size="medium"
error={_localState.formState.table.error}
descriptionText="This is the table the trigger will watch for changes. You can only select 1 table for a trigger."
>
{_localState.tables.map((x: any) => {
return (
<Listbox.Option
id={x.id}
key={x.id}
value={x.id}
label={x.name}
addOnBefore={() => (
<div className="flex items-center justify-center rounded bg-scale-1200 p-1 text-scale-100 ">
<SVG
src={'/img/table-editor.svg'}
style={{ width: `16px`, height: `16px`, strokeWidth: '1px' }}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
}
/>
</div>
)}
>
<div className="flex flex-row items-center space-x-2">
<p>{x.name}</p>
<p className="text-scale-1100">{x.schema}</p>
</div>
</Listbox.Option>
)
})}
</Listbox>
)
})
export default TableSelection
@@ -1,512 +0,0 @@
import { makeAutoObservable } from 'mobx'
import { FC, useEffect, createContext } from 'react'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { isEmpty, mapValues, has, filter, keyBy, isUndefined } from 'lodash'
import { Dictionary } from 'components/grid'
import { Badge, Input, SidePanel } from 'ui'
import { useStore } from 'hooks'
import InputName from './InputName'
import SelectEnabledMode from './SelectEnabledMode'
import CheckboxEvents from './CheckboxEvents'
import TableSelection from './TableSelection'
import RadioGroupHookService from './RadioGroupHookService'
import ServiceConfigForm from './ServiceConfigForm'
import { formatArguments, convertKeyValue, hasWhitespace, isValidHttpUrl } from '../Hooks.utils'
// [Joshen] TODO: Refactor to use Form component
class CreateHookFormState {
id: number | undefined
originalName: string | undefined
// @ts-ignore
enabledMode: { value: string }
// @ts-ignore
events: { value: string[]; error?: string }
// @ts-ignore
name: { value: string; error?: string }
// @ts-ignore
schema: { value: string }
// @ts-ignore
table: { value: string; error?: string }
// @ts-ignore
tableId: { value: number; error?: string }
// @ts-ignore
hookService: { value: string; error?: string }
// @ts-ignore
serviceUrl: { value: string; error?: string }
// @ts-ignore
serviceMethod: { value: string; error?: string }
// @ts-ignore
serviceHeaders: {
value: { name: string; value: string; error?: { name?: string; value?: string } }[]
}
// @ts-ignore
serviceParams: {
value: { name: string; value: string; error?: { name?: string; value?: string } }[]
}
// @ts-ignore
serviceTimeoutMs: { value: string; error?: string }
constructor() {
makeAutoObservable(this)
this.reset()
}
get requestBody() {
return {
id: this.id,
activation: 'AFTER',
enabled_mode: this.enabledMode.value,
events: this.events.value,
function_args: [
this.serviceUrl.value.replaceAll(' ', ''),
this.serviceMethod.value,
JSON.stringify(mapValues(keyBy(this.serviceHeaders.value, 'name'), 'value')),
formatArguments(this.serviceParams.value),
this.serviceTimeoutMs.value,
],
function_name: 'http_request',
function_schema: 'supabase_functions',
orientation: 'ROW',
name: this.name.value,
schema: this.schema.value,
table: this.table.value,
table_id: this.tableId.value,
}
}
reset(hook?: Dictionary<any>) {
this.id = hook?.id
this.originalName = hook?.name
this.enabledMode = { value: hook?.enabled_mode ?? 'ORIGIN' }
this.events = { value: hook?.events ?? [] }
this.name = { value: hook?.name ?? '' }
this.schema = { value: hook?.schema ?? '' }
this.table = { value: hook?.table ?? '' }
this.tableId = { value: hook?.table_id ?? '' }
// TODO: hook obj doesn't have service field
this.hookService = { value: 'http_request' }
/**
* hook.function_args contains 5 params in order
* [url, method, headers, params, timeout]
*/
this.serviceUrl = { value: hook?.function_args?.length == 5 ? hook?.function_args[0] : '' }
this.serviceMethod = {
value: hook?.function_args?.length == 5 ? hook?.function_args[1] : 'GET',
}
this.serviceHeaders =
hook?.function_args?.length == 5
? convertKeyValue(hook?.function_args[2])
: { value: [{ name: 'Content-type', value: 'application/json' }] }
this.serviceParams =
hook?.function_args?.length == 5 ? convertKeyValue(hook?.function_args[3]) : { value: [] }
this.serviceTimeoutMs = {
value: hook?.function_args?.length == 5 ? hook?.function_args[4] : '1000',
}
}
update(state: Dictionary<any>) {
this.enabledMode = state.enabledMode
this.events = state.events
this.name = state.name
this.schema = state.schema
this.table = state.table
this.tableId = state.tableId
this.hookService = state.hookService
this.serviceUrl = state.serviceUrl
this.serviceMethod = state.serviceMethod
this.serviceHeaders = state.serviceHeaders
this.serviceParams = state.serviceParams
this.serviceTimeoutMs = state.serviceTimeoutMs
}
}
interface ICreateHookStore {
loading: boolean
formState: CreateHookFormState
meta: any
project: any
tables: Dictionary<any>[]
onFormChange: (value: { key: string; value: any }) => void
onFormArrayChange: (value: {
operation: 'add' | 'delete' | 'update'
key: string
idx?: number
value?: any
}) => void
setDefaultSelectedTable: () => void
setLoading: (value: boolean) => void
setTables: (value: Dictionary<any>[]) => void
validateForm: () => boolean
}
class CreateHookStore implements ICreateHookStore {
loading = false
formState = new CreateHookFormState()
meta = null
project = null
tables = []
constructor() {
makeAutoObservable(this)
}
get title() {
return this.formState.id
? `Edit '${this.formState.originalName}' database webhook`
: 'Add a new database webhook'
}
get isEditing() {
return this.formState.id != undefined
}
// set first table as default selection
setDefaultSelectedTable = () => {
if (this.tables?.length != 0) {
this.formState.schema.value = (this.tables[0] as any).schema
this.formState.table.value = (this.tables[0] as any).name
this.formState.tableId.value = (this.tables[0] as any).id
}
}
setLoading = (value: boolean) => {
this.loading = value
}
setTables = (value: Dictionary<any>[]) => {
this.tables = value as any
this.setDefaultSelectedTable()
}
onFormChange = ({ key, value }: { key: string; value: any }) => {
if (has(this.formState, key)) {
const temp = (this.formState as any)[key] as any
;(this.formState as any)[key] = { ...temp, value, error: undefined }
} else {
;(this.formState as any)[key] = { value }
}
}
onFormArrayChange = ({
operation,
key,
idx,
value,
}: {
operation: 'add' | 'delete' | 'update'
key: string
idx?: number
value?: any
}) => {
switch (operation) {
case 'add': {
if (has(this.formState, key)) {
;(this.formState as any)[key].value.push(value)
} else {
;(this.formState as any)[key] = { value: [value] }
}
break
}
case 'delete': {
if (has(this.formState, key)) {
const temp = filter(
(this.formState as any)[key].value,
(_: any, index: number) => index != idx
) as any
;(this.formState as any)[key].value = temp
}
break
}
default: {
if (has(this.formState, key) && !isUndefined(idx)) {
;(this.formState as any)[key].value[idx] = value
} else {
;(this.formState as any)[key] = { value: [{ value }] }
}
}
}
}
validateForm = () => {
let isValidated = true
const _state = mapValues(this.formState, (x: { value: any }, key: string) => {
switch (key) {
case 'name': {
if (isEmpty(x.value) || hasWhitespace(x.value)) {
isValidated = false
return { ...x, error: 'Invalid hook name' }
} else {
return x
}
}
case 'events': {
if (isEmpty(x.value)) {
isValidated = false
return { ...x, error: 'Select at least 1 event' }
} else {
return x
}
}
case 'table': {
if (isEmpty(x.value)) {
isValidated = false
return { ...x, error: 'You must choose a table' }
} else {
return x
}
}
case 'hookService': {
if (x.value != 'http_request') {
isValidated = false
return { ...x, error: 'Not supported service' }
} else {
return x
}
}
case 'serviceTimeoutMs': {
if (isEmpty(x.value) || isNaN(x.value)) {
isValidated = false
return { ...x, error: 'Invalid input' }
} else {
return x
}
}
case 'serviceUrl': {
if (isEmpty(x.value) || !isValidHttpUrl(x.value)) {
isValidated = false
return { ...x, error: 'Invalid input' }
} else {
return x
}
}
case 'serviceMethod': {
if (isEmpty(x.value)) {
isValidated = false
return { ...x, error: 'Invalid input' }
} else {
return x
}
}
case 'serviceHeaders':
case 'serviceParams': {
const temp = x.value?.map((i: Dictionary<any>) => {
const error: any = { name: undefined, value: undefined }
if (isEmpty(i.name) || hasWhitespace(i.name)) {
isValidated = false
error.name = 'Invalid name'
}
if (isEmpty(i.value)) {
isValidated = false
error.value = 'Missing value'
}
return { ...i, error }
})
x.value = temp
return x
}
default:
return x
}
})
if (!isValidated) {
this.formState.update(_state)
}
return isValidated
}
}
export const CreateHookContext = createContext<ICreateHookStore | null>(null)
interface CreateHookProps {
hook?: any
visible: boolean
setVisible: (value: boolean) => void
}
const CreateHook: FC<CreateHookProps> = ({ hook, visible = true, setVisible }) => {
const { meta, ui } = useStore()
const project = ui.selectedProject
const _localState: any = useLocalObservable(() => new CreateHookStore())
_localState.meta = meta
_localState.project = project
useEffect(() => {
const fetchSchemas = async () => {
await _localState.meta.tables.load()
const tables = _localState!.meta!.tables.list(
(table: any) => !meta.excludedSchemas.includes(table.schema)
)
_localState.setTables(tables)
}
fetchSchemas()
}, [])
useEffect(() => {
if (hook) {
_localState.formState.reset(hook)
} else {
_localState.formState.reset()
_localState.setDefaultSelectedTable()
}
}, [visible, hook])
async function handleSubmit() {
try {
if (_localState.validateForm()) {
_localState.setLoading(true)
const body = _localState.formState.requestBody
const response: any = _localState.isEditing
? await _localState.meta.hooks.update(body.id, body)
: await _localState.meta.hooks.create(body)
if (response.error) {
ui.setNotification({
category: 'error',
message: `Failed to create hook: ${response.error.message ?? 'Submit request failed'}`,
})
_localState.setLoading(false)
} else {
ui.setNotification({
category: 'success',
message: `${_localState.isEditing ? 'Updated' : 'Created new'} hook called ${
response.name
}`,
})
_localState.setLoading(false)
setVisible(!visible)
}
}
} catch (error: any) {
ui.setNotification({ category: 'error', message: `Failed to create hook: ${error.message}` })
_localState.setLoading(false)
}
}
return (
<SidePanel
size="large"
visible={visible}
loading={_localState.loading}
header={_localState.title}
className="hooks-sidepanel mr-0 transform transition-all duration-300 ease-in-out"
onConfirm={handleSubmit}
onCancel={() => setVisible(!visible)}
>
<CreateHookContext.Provider value={_localState}>
<div className="space-y-10 py-6">
{_localState.isEditing ? (
<>
<div className="space-y-10 px-6">
<InputName />
<SelectEnabledMode />
</div>
{_localState.formState.serviceHeaders.value.length > 0 && (
<>
<SidePanel.Separator />
<div className="space-y-2 px-6">
<div className="flex items-center space-x-2">
<h5 className="text-base text-scale-1200">HTTP Headers</h5>
<Badge color="gray">Read only</Badge>
</div>
<div className="space-y-2">
{_localState.formState.serviceHeaders.value.map(
(serviceHeader: { name: string; value: string }) => (
<div
key={serviceHeader.name}
className="flex items-center justify-center space-x-2"
>
<Input
disabled
className="w-full flex-1"
readOnly
value={serviceHeader.name}
/>
<Input
disabled
className="w-full flex-1"
readOnly
value={serviceHeader.value}
/>
</div>
)
)}
</div>
</div>
<p className="mx-6 mt-12 text-sm text-scale-1100">
<strong>Note:</strong> You can only edit a webhook's name and mode. To change
other settings, you'll need to delete and recreate the webhook.
</p>
</>
)}
{_localState.formState.serviceParams.value.length > 0 && (
<>
<SidePanel.Separator />
<div className="space-y-2 px-6">
<div className="flex items-center space-x-2">
<h5 className="text-base text-scale-1200">HTTP Parameters</h5>
<Badge color="gray">Read only</Badge>
</div>
<div className="space-y-2">
{_localState.formState.serviceParams.value.map(
(serviceParam: { name: string; value: string }) => (
<div
key={serviceParam.name}
className="flex items-center justify-center space-x-2"
>
<Input
disabled
className="w-full flex-1"
readOnly
value={serviceParam.name}
/>
<Input
disabled
className="w-full flex-1"
readOnly
value={serviceParam.value}
/>
</div>
)
)}
</div>
</div>
</>
)}
</>
) : (
<>
<div className="px-6">
<InputName />
</div>
<SidePanel.Separator />
<div className="space-y-6 px-6">
<h5 className="text-base text-scale-1200">Conditions to fire hook</h5>
<TableSelection />
<CheckboxEvents />
</div>
<SidePanel.Separator />
<div className="space-y-6 px-6">
<h5 className="text-base text-scale-1200">Type of hook</h5>
<RadioGroupHookService />
</div>
<ServiceConfigForm />
</>
)}
</div>
</CreateHookContext.Provider>
</SidePanel>
)
}
export default observer(CreateHook)
@@ -1,56 +0,0 @@
import * as React from 'react'
import { observer } from 'mobx-react-lite'
import { useStore } from 'hooks'
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
type DeleteHookProps = {
hook?: any
visible: boolean
setVisible: (value: boolean) => void
} & any
const DeleteHook: React.FC<DeleteHookProps> = ({ hook, visible, setVisible }) => {
const { ui, meta } = useStore()
const [loading, setLoading] = React.useState(false)
const { id, name, schema } = hook ?? {}
async function handleDelete() {
try {
setLoading(true)
if (!id) {
throw Error('Invalid hook info')
}
const response: any = await meta.hooks.del(id)
if (response.error) {
throw response.error
} else {
ui.setNotification({ category: 'success', message: `Successfully removed ${name}` })
setVisible(false)
}
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${name}: ${error.message}`,
})
} finally {
setLoading(false)
}
}
return (
<TextConfirmModal
visible={visible}
onCancel={() => setVisible(!visible)}
onConfirm={handleDelete}
title="Delete this hook"
loading={loading}
confirmLabel={`Delete hook ${name}`}
confirmPlaceholder="Type in name of hook"
confirmString={name}
text={`This will delete your hook called ${name} of schema ${schema}.`}
alert="You cannot recover this hook once it is deleted!"
/>
)
}
export default observer(DeleteHook)
@@ -0,0 +1,67 @@
import { useState } from 'react'
import { observer } from 'mobx-react-lite'
import { useStore } from 'hooks'
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseTriggerDeleteMutation } from 'data/database-triggers/database-trigger-delete-mutation'
import { PostgresTrigger } from '@supabase/postgres-meta'
interface DeleteHookModalProps {
visible: boolean
selectedHook?: PostgresTrigger
onClose: () => void
}
const DeleteHookModal = ({ selectedHook, visible, onClose }: DeleteHookModalProps) => {
const { ui } = useStore()
const [loading, setLoading] = useState(false)
const { id, name, schema } = selectedHook ?? {}
const { project } = useProjectContext()
const { mutateAsync: deleteDatabaseTrigger } = useDatabaseTriggerDeleteMutation()
async function handleDelete() {
if (!project) {
return console.error('Project ref is required')
}
if (!id) {
return ui.setNotification({ category: 'error', message: 'Unable find selected hook' })
}
try {
setLoading(true)
await deleteDatabaseTrigger({
id,
projectRef: project.ref,
connectionString: project.connectionString,
})
ui.setNotification({ category: 'success', message: `Successfully deleted ${name}` })
onClose()
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${name}: ${error.message}`,
})
} finally {
setLoading(false)
}
}
return (
<TextConfirmModal
visible={visible}
size="medium"
onCancel={() => onClose()}
onConfirm={handleDelete}
title="Delete database webhook"
loading={loading}
confirmLabel={`Delete ${name}`}
confirmPlaceholder="Type in name of webhook"
confirmString={name || ''}
text={`This will delete the webhook "${name}" from the schema "${schema}".`}
alert="You cannot recover this webhook once it is deleted!"
/>
)
}
export default observer(DeleteHookModal)
@@ -0,0 +1,454 @@
import Image from 'next/image'
import { useState, useRef, useEffect } from 'react'
import { PostgresTrigger } from '@supabase/postgres-meta'
import { Button, SidePanel, Form, Input, Listbox, Checkbox, Radio, Badge, Modal } from 'ui'
import { useStore } from 'hooks'
import { tryParseJson, uuidv4 } from 'lib/helpers'
import HTTPRequestFields from './HTTPRequestFields'
import { isValidHttpUrl } from './Hooks.utils'
import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants'
import { FormSection, FormSectionLabel, FormSectionContent } from 'components/ui/Forms'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/database-trigger-create-mutation'
import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-transaction-mutation'
import ConfirmationModal from 'components/ui/ConfirmationModal'
export interface EditHookPanelProps {
visible: boolean
selectedHook?: PostgresTrigger
onClose: () => void
}
export type HTTPArgument = { id: string; name: string; value: string }
const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) => {
// [Joshen] Need to change to use RQ once Alaister's PR goes in
const { meta, ui } = useStore()
const submitRef: any = useRef()
const [isEdited, setIsEdited] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isClosingPanel, setIsClosingPanel] = useState(false)
// [Joshen] There seems to be some bug between Checkbox.Group within the Form component
// hence why this external state as a temporary workaround
const [events, setEvents] = useState<string[]>([])
const [eventsError, setEventsError] = useState<string>()
// For HTTP request
const [httpHeaders, setHttpHeaders] = useState<HTTPArgument[]>([])
const [httpParameters, setHttpParameters] = useState<HTTPArgument[]>([])
const { project } = useProjectContext()
const { mutateAsync: createDatabaseTrigger } = useDatabaseTriggerCreateMutation()
const { mutateAsync: updateDatabaseTrigger } = useDatabaseTriggerUpdateMutation()
const tables = meta.tables.list()
const initialValues = {
name: selectedHook?.name ?? '',
table_id: selectedHook?.table_id ?? '',
function_name: selectedHook?.function_name ?? 'http_request',
http_url: selectedHook?.function_args?.[0] ?? '',
http_method: selectedHook?.function_args?.[1] ?? 'GET',
}
useEffect(() => {
if (visible) {
setIsEdited(false)
setIsSubmitting(false)
setIsClosingPanel(false)
// Reset form fields outside of the Form context
if (selectedHook !== undefined) {
setEvents(selectedHook.events)
const [url, method, headers, parameters] = selectedHook.function_args
const formattedHeaders = tryParseJson(headers) || {}
setHttpHeaders(
Object.keys(formattedHeaders).map((key) => {
return { id: uuidv4(), name: key, value: formattedHeaders[key] }
})
)
const formattedParameters = tryParseJson(parameters) || {}
setHttpParameters(
Object.keys(formattedParameters).map((key) => {
return { id: uuidv4(), name: key, value: formattedParameters[key] }
})
)
} else {
setEvents([])
setHttpHeaders([{ id: uuidv4(), name: 'Content-type', value: 'application/json' }])
setHttpParameters([{ id: uuidv4(), name: '', value: '' }])
}
}
}, [visible, selectedHook])
const onClosePanel = () => {
if (isEdited) setIsClosingPanel(true)
else onClose()
}
const onUpdateSelectedEvents = (event: string) => {
if (events.includes(event)) {
setEvents(events.filter((e) => e !== event))
} else {
setEvents(events.concat(event))
}
setEventsError(undefined)
}
const validate = (values: any) => {
const errors: any = {}
if (!values.name) {
errors['name'] = 'Please provide a name for your webhook'
}
if (!values.table_id) {
errors['table_id'] = 'Please select a table for which your webhook will trigger from'
}
if (values.function_name === 'http_request') {
// For HTTP requests
if (!values.http_url) {
errors['http_url'] = 'Please provide a URL'
} else if (!values.http_url.startsWith('http')) {
errors['http_url'] = 'Please include HTTP/HTTPs to your URL'
} else if (!isValidHttpUrl(values.http_url)) {
errors['http_url'] = 'Please provide a valid URL'
}
} else if (values.function_name === 'supabase_function') {
// For Supabase Edge Functions
}
if (JSON.stringify(values) !== JSON.stringify(initialValues)) setIsEdited(true)
return errors
}
const onSubmit = async (values: any) => {
if (!project?.ref) {
return console.error('Project ref is required')
}
if (events.length === 0) {
return setEventsError('Please select at least one event')
}
const selectedTable = meta.tables.byId(values.table_id)
if (!selectedTable) {
return ui.setNotification({ category: 'error', message: 'Unable to find selected table' })
}
const payload: any = {
events,
activation: 'AFTER',
orientation: 'ROW',
enabled_mode: 'ORIGIN',
name: values.name,
table: selectedTable.name,
schema: selectedTable.schema,
table_id: values.table_id,
function_name: values.function_name,
function_schema: 'supabase_functions',
function_args: [],
}
if (values.function_name === 'http_request') {
const serviceTimeoutMs = '1000'
const headers = httpHeaders
.filter((header) => header.name && header.value)
.reduce((a: any, b: any) => {
a[b.name] = b.value
return a
}, {})
const parameters = httpParameters
.filter((param) => param.name && param.value)
.reduce((a: any, b: any) => {
a[b.name] = b.value
return a
}, {})
payload.function_args = [
values.http_url,
values.http_method,
JSON.stringify(headers),
JSON.stringify(parameters),
serviceTimeoutMs,
]
} else if (values.function_name === 'supabase_function') {
payload.function_args = []
}
if (selectedHook === undefined) {
try {
setIsSubmitting(true)
await createDatabaseTrigger({
projectRef: project?.ref,
connectionString: project?.connectionString,
payload,
})
ui.setNotification({
category: 'success',
message: `Successfully created new webhook "${values.name}"`,
})
onClose()
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to create webhook: ${error.message}`,
})
} finally {
setIsSubmitting(false)
}
} else {
try {
setIsSubmitting(true)
await updateDatabaseTrigger({
projectRef: project?.ref,
connectionString: project?.connectionString,
originalTrigger: selectedHook,
updatedTrigger: payload,
})
ui.setNotification({
category: 'success',
message: `Successfully updated webhook "${values.name}"`,
})
onClose()
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to update webhook: ${error.message}`,
})
} finally {
setIsSubmitting(false)
}
}
}
return (
<>
<SidePanel
size="xlarge"
visible={visible}
header={
selectedHook === undefined ? (
'Create a new database webhook'
) : (
<>
Update webhook <code className="text-sm">{selectedHook.name}</code>
</>
)
}
className="hooks-sidepanel mr-0 transform transition-all duration-300 ease-in-out"
onConfirm={() => {}}
onCancel={() => onClosePanel()}
customFooter={
<div className="flex w-full justify-end space-x-3 border-t border-scale-500 px-3 py-4">
<Button
size="tiny"
type="default"
htmlType="button"
onClick={onClosePanel}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
size="tiny"
type="primary"
htmlType="button"
disabled={isSubmitting}
loading={isSubmitting}
onClick={() => submitRef?.current?.click()}
>
{selectedHook === undefined ? 'Create webhook' : 'Update webhook'}
</Button>
</div>
}
>
<Form validateOnBlur initialValues={initialValues} onSubmit={onSubmit} validate={validate}>
{() => {
return (
<div>
<FormSection
header={<FormSectionLabel className="lg:!col-span-4">General</FormSectionLabel>}
>
<FormSectionContent loading={false} className="lg:!col-span-8">
<Input
id="name"
name="name"
label="Name"
descriptionText="Do not use spaces/whitespaces"
/>
</FormSectionContent>
</FormSection>
<SidePanel.Separator />
<FormSection
header={
<FormSectionLabel
className="lg:!col-span-4"
description={
<p className="text-sm text-scale-1000">
Select which table and events will trigger your function hook
</p>
}
>
Conditions to fire hook
</FormSectionLabel>
}
>
<FormSectionContent loading={false} className="lg:!col-span-8">
<Listbox
size="medium"
id="table_id"
name="table_id"
label="Table"
descriptionText="This is the table the trigger will watch for changes. You can only select 1 table for a trigger."
>
<Listbox.Option
key={'table-no-selection'}
id={'table-no-selection'}
label={'---'}
value={'no-selection'}
>
---
</Listbox.Option>
{tables.map((table) => (
<Listbox.Option
key={table.id}
id={table.id.toString()}
value={table.id}
label={table.name}
>
<div className="flex items-center space-x-2">
<p>{table.name}</p>
<p className="text-scale-1100">{table.schema}</p>
</div>
</Listbox.Option>
))}
</Listbox>
<Checkbox.Group
id="events"
name="events"
label="Events"
error={eventsError}
descriptionText="These are the events that are watched by the webhook, only the events selected above will fire the webhook on the table you've selected."
>
{HOOK_EVENTS.map((event) => (
<Checkbox
key={event.value}
value={event.value}
label={event.label}
description={event.description}
checked={events.includes(event.value)}
onChange={() => onUpdateSelectedEvents(event.value)}
/>
))}
</Checkbox.Group>
</FormSectionContent>
</FormSection>
<SidePanel.Separator />
<FormSection
header={
<FormSectionLabel className="lg:!col-span-4">
Hook configuration
</FormSectionLabel>
}
>
<FormSectionContent loading={false} className="lg:!col-span-8">
<Radio.Group
id="function_name"
name="function_name"
label="Type of hook"
type="cards"
>
{AVAILABLE_WEBHOOK_TYPES.map((webhook) => (
<Radio
disabled={webhook.disabled}
key={webhook.value}
id={webhook.value}
value={webhook.value}
label=""
beforeLabel={
<div className="flex items-center space-x-5">
<Image src={webhook.icon} layout="fixed" width="32" height="32" />
<div className="flex-col space-y-0">
<div className="flex space-x-2">
<p className="text-scale-1200">{webhook.label}</p>
{webhook.disabled ? (
<Badge color="amber">Coming soon</Badge>
) : (
<Badge color="green">Alpha</Badge>
)}
</div>
<p className="text-scale-1000">{webhook.description}</p>
</div>
</div>
}
/>
))}
</Radio.Group>
</FormSectionContent>
</FormSection>
<SidePanel.Separator />
<HTTPRequestFields
httpHeaders={httpHeaders}
httpParameters={httpParameters}
onAddHeader={() =>
setHttpHeaders(httpHeaders.concat({ id: uuidv4(), name: '', value: '' }))
}
onUpdateHeader={(idx, property, value) =>
setHttpHeaders(
httpHeaders.map((header, i) => {
if (idx === i) return { ...header, [property]: value }
else return header
})
)
}
onRemoveHeader={(idx) => setHttpHeaders(httpHeaders.filter((_, i) => idx !== i))}
onAddParameter={() =>
setHttpParameters(httpParameters.concat({ id: uuidv4(), name: '', value: '' }))
}
onUpdateParameter={(idx, property, value) =>
setHttpParameters(
httpParameters.map((param, i) => {
if (idx === i) return { ...param, [property]: value }
else return param
})
)
}
onRemoveParameter={(idx) =>
setHttpParameters(httpParameters.filter((_, i) => idx !== i))
}
/>
<button ref={submitRef} type="submit" className="hidden" />
</div>
)
}}
</Form>
</SidePanel>
<ConfirmationModal
visible={isClosingPanel}
header="Confirm to close"
buttonLabel="Confirm"
onSelectCancel={() => setIsClosingPanel(false)}
onSelectConfirm={() => {
setIsClosingPanel(false)
setIsEdited(false)
onClose()
}}
children={
<Modal.Content>
<p className="py-4 text-sm text-scale-1100">
There are unsaved changes. Are you sure you want to close the panel? Your changes will
be lost.
</p>
</Modal.Content>
}
/>
</>
)
}
export default EditHookPanel
@@ -0,0 +1,131 @@
import { Button, Listbox, Input, SidePanel, IconTrash, IconPlus } from 'ui'
import { FormSection, FormSectionLabel, FormSectionContent } from 'components/ui/Forms'
import { HTTPArgument } from './EditHookPanel'
interface HTTPRequestFieldsProps {
httpHeaders: HTTPArgument[]
httpParameters: HTTPArgument[]
onAddHeader: () => void
onUpdateHeader: (idx: number, property: string, value: string) => void
onRemoveHeader: (idx: number) => void
onAddParameter: () => void
onUpdateParameter: (idx: number, property: string, value: string) => void
onRemoveParameter: (idx: number) => void
}
const HTTPRequestFields = ({
httpHeaders = [],
httpParameters = [],
onAddHeader,
onUpdateHeader,
onRemoveHeader,
onAddParameter,
onUpdateParameter,
onRemoveParameter,
}: HTTPRequestFieldsProps) => {
return (
<>
<FormSection
header={<FormSectionLabel className="lg:!col-span-4">HTTP Request</FormSectionLabel>}
>
<FormSectionContent loading={false} className="lg:!col-span-8">
<Listbox id="http_method" name="http_method" size="medium" label="Method">
<Listbox.Option id="GET" value="GET" label="GET">
GET
</Listbox.Option>
<Listbox.Option id="POST" value="POST" label="POST">
POST
</Listbox.Option>
</Listbox>
<Input
id="http_url"
name="http_url"
label="URL"
placeholder="http://api.com/path/resource"
descriptionText="URL of the HTTP request. Must include HTTP/HTTPS"
/>
</FormSectionContent>
</FormSection>
<SidePanel.Separator />
<FormSection
header={<FormSectionLabel className="lg:!col-span-4">HTTP Headers</FormSectionLabel>}
>
<FormSectionContent loading={false} className="lg:!col-span-8">
<div className="space-y-2">
{httpHeaders.map((header, idx: number) => (
<div key={header.id} className="flex items-center space-x-2">
<Input
value={header.name}
size="small"
className="w-full"
placeholder="Header name"
onChange={(event: any) => onUpdateHeader(idx, 'name', event.target.value)}
/>
<Input
value={header.value}
size="small"
className="w-full"
placeholder="Header value"
onChange={(event: any) => onUpdateHeader(idx, 'value', event.target.value)}
/>
<Button
type="default"
size="medium"
icon={<IconTrash size="tiny" />}
className="px-[10px] py-[9px]"
onClick={() => onRemoveHeader(idx)}
/>
</div>
))}
<div>
<Button type="dashed" size="tiny" icon={<IconPlus />} onClick={onAddHeader}>
Add a new header
</Button>
</div>
</div>
</FormSectionContent>
</FormSection>
<SidePanel.Separator />
<FormSection
header={<FormSectionLabel className="lg:!col-span-4">HTTP Parameters</FormSectionLabel>}
>
<FormSectionContent loading={false} className="lg:!col-span-8">
<div className="space-y-2">
{httpParameters.map((parameter, idx: number) => (
<div key={parameter.id} className="flex items-center space-x-2">
<Input
size="small"
value={parameter.name}
className="w-full"
placeholder="Parameter name"
onChange={(event: any) => onUpdateParameter(idx, 'name', event.target.value)}
/>
<Input
size="small"
value={parameter.value}
className="w-full"
placeholder="Parameter value"
onChange={(event: any) => onUpdateParameter(idx, 'value', event.target.value)}
/>
<Button
type="default"
size="medium"
icon={<IconTrash size="tiny" />}
className="px-[10px] py-[9px]"
onClick={() => onRemoveParameter(idx)}
/>
</div>
))}
<div>
<Button type="dashed" size="tiny" icon={<IconPlus />} onClick={onAddParameter}>
Add a new parameter
</Button>
</div>
</div>
</FormSectionContent>
</FormSection>
</>
)
}
export default HTTPRequestFields
@@ -0,0 +1,51 @@
import { BASE_PATH } from 'lib/constants'
export const ENABLED_MODE_OPTIONS = [
{ label: 'Origin', value: 'ORIGIN', description: 'This is the default behaviour' },
{
label: 'Replica',
value: 'REPLICA',
description: 'Will only fire if the session is in “replica” mode',
},
{
label: 'Always',
value: 'ALWAYS',
description: 'Will fire regardless of the current replication role',
},
{ label: 'Disabled', value: 'DISABLED', description: 'Will not fire' },
]
export const HOOK_EVENTS = [
{
label: 'Insert',
value: 'INSERT',
description: 'Any insert operation on the table',
},
{
label: 'Update',
value: 'UPDATE',
description: 'Any update operation, of any column in the table',
},
{
label: 'Delete',
value: 'DELETE',
description: 'Any deletion of a record',
},
]
export const AVAILABLE_WEBHOOK_TYPES = [
{
disabled: false,
value: 'http_request',
icon: `${BASE_PATH}/img/function-providers/http-request.png`,
label: 'HTTP Request',
description: 'Send an HTTP request to any URL.',
},
{
disabled: true,
value: 'supabase_function',
icon: `${BASE_PATH}/img/function-providers/supabase-severless-function.png`,
label: 'Supabase Edge Functions',
description: 'Choose a Supabase edge function to run.',
},
]
@@ -5,8 +5,10 @@ import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Badge, Button, Dropdown, IconMoreVertical, IconTrash, IconEdit3 } from 'ui'
import { checkPermissions, useStore } from 'hooks'
import { checkPermissions } from 'hooks'
import Table from 'components/to-be-cleaned/Table'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseHooks } from 'data/database-triggers/database-triggers-query'
interface Props {
schema: string
@@ -21,12 +23,15 @@ const HookList: FC<Props> = ({
editHook = () => {},
deleteHook = () => {},
}) => {
const { meta } = useStore()
const hooks = meta.hooks.list()
const filteredHooks = hooks.filter((x: any) =>
includes(x.name.toLowerCase(), filterString.toLowerCase())
const { project } = useProjectContext()
const { data: hooks } = useDatabaseHooks({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const filteredHooks = (hooks ?? []).filter(
(x: any) => includes(x.name.toLowerCase(), filterString.toLowerCase()) && x.schema === schema
)
const _hooks = filteredHooks.filter((x: any) => x.schema == schema)
const canUpdateWebhook = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
function onEdit(trigger: any) {
@@ -39,7 +44,7 @@ const HookList: FC<Props> = ({
return (
<>
{_hooks.map((x: any) => (
{filteredHooks.map((x: any) => (
<Table.tr key={x.id}>
<Table.td className="space-x-2">
<p>{x.name}</p>
@@ -1,81 +1,65 @@
import { FC } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { includes, uniqBy, map as lodashMap } from 'lodash'
import { observer } from 'mobx-react-lite'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Input, IconSearch, IconLoader } from 'ui'
import { Button, Input, IconSearch, IconExternalLink } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useStore, checkPermissions } from 'hooks'
import { checkPermissions } from 'hooks'
import SchemaTable from './SchemaTable'
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
function isHooksEnabled(schemas: any): boolean {
return schemas.some((schema: any) => schema.name === 'supabase_functions')
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseHooks } from 'data/database-triggers/database-triggers-query'
import { FormHeader } from 'components/ui/Forms'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import ClientLoadingError from 'components/ui/ClientLoadingError'
import NoSearchResults from 'components/ui/NoSearchResults'
export interface HooksListProps {
createHook: () => void
editHook: (hook: any) => void
deleteHook: (hook: any) => void
}
const HooksList: FC<any> = ({
hooksEnabled,
filterString,
setFilterString = () => {},
const HooksList = ({
createHook = () => {},
editHook = () => {},
deleteHook = () => {},
enableHooks = () => {},
}) => {
const { meta } = useStore()
const hooks = meta.hooks.list()
const schemas = meta.schemas.list()
const filteredHooks = hooks.filter((x: any) =>
}: HooksListProps) => {
const { project } = useProjectContext()
const {
data: hooks,
isLoading,
isError,
} = useDatabaseHooks({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [filterString, setFilterString] = useState<string>('')
const filteredHooks = (hooks || []).filter((x: any) =>
includes(x.name.toLowerCase(), filterString.toLowerCase())
)
const filteredHookSchemas = lodashMap(uniqBy(filteredHooks, 'schema'), 'schema')
const canCreateWebhooks = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
const isEnabled = hooksEnabled || isHooksEnabled(schemas)
if (!isEnabled) {
return (
<>
{schemas.length === 0 ? (
<span className="px-6 py-4 text-scale-1100">Checking if hooks are enabled...</span>
) : (
<ProductEmptyState
title="Hooks"
ctaButtonLabel="Enable Hooks"
onClickCta={() => enableHooks()}
disabled={!canCreateWebhooks}
disabledMessage="You need additional permissions to enable hooks"
>
<AlphaPreview />
<p className="text-sm text-scale-1100">Enable hooks on your project.</p>
</ProductEmptyState>
)}
</>
)
}
if (meta.hooks.isLoading) {
return (
<div className="flex h-full w-full items-center justify-center space-x-2">
<IconLoader className="animate-spin" size={14} />
<p>Loading hooks...</p>
</div>
)
}
if (meta.hooks.hasError) {
return (
<p className="px-6 py-4">
<p>Error connecting to API</p>
<p>{`${meta.hooks.error?.message ?? 'Unknown error'}`}</p>
</p>
)
}
return (
<>
{hooks.length == 0 ? (
{isLoading ? (
<div className="py-4 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : isError ? (
<ClientLoadingError
projectRef={project?.ref ?? ''}
description="Failed to retrieve database webhooks"
/>
) : (hooks || []).length == 0 ? (
<div className="flex h-full w-full items-center justify-center">
<ProductEmptyState
title="Database Webhooks"
@@ -92,56 +76,71 @@ const HooksList: FC<any> = ({
</ProductEmptyState>
</div>
) : (
<div className="w-full space-y-4 py-4">
<div className="flex items-center justify-between px-6">
<Input
placeholder="Filter by name"
size="small"
icon={<IconSearch size="tiny" />}
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
<>
<div className="flex items-center justify-between">
<FormHeader
title="Database Webhooks"
description="Send real-time data from your database to another system whenever a table event occurs"
/>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button disabled={!canCreateWebhooks} onClick={() => createHook()}>
Create a new hook
</Button>
</Tooltip.Trigger>
{!canCreateWebhooks && (
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to create webhooks
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
{filteredHooks.length <= 0 && (
<div className="mx-auto flex max-w-lg items-center justify-center space-x-3 rounded border p-6 shadow-md dark:border-dark">
<p>No results match your filter query</p>
<Button type="outline" onClick={() => setFilterString('')}>
Reset filter
</Button>
<div className="w-full space-y-4">
<div className="flex items-center justify-between">
<Input
placeholder="Search for a webhook"
size="small"
icon={<IconSearch size="tiny" />}
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
/>
<div className="flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/database/webhooks">
<a target="_blank">
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
Documentation
</Button>
</a>
</Link>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button disabled={!canCreateWebhooks} onClick={() => createHook()}>
Create a new hook
</Button>
</Tooltip.Trigger>
{!canCreateWebhooks && (
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to create webhooks
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
</div>
)}
{filteredHookSchemas.map((schema: any) => (
<SchemaTable
key={schema}
filterString={filterString}
schema={schema}
editHook={editHook}
deleteHook={deleteHook}
/>
))}
</div>
{filteredHooks.length <= 0 && (
<NoSearchResults
searchString={filterString}
onResetFilter={() => setFilterString('')}
/>
)}
{filteredHookSchemas.map((schema: any) => (
<SchemaTable
key={schema}
filterString={filterString}
schema={schema}
editHook={editHook}
deleteHook={deleteHook}
/>
))}
</div>
</>
)}
</>
)
@@ -20,13 +20,13 @@ const SchemaTable: FC<Props> = ({
return (
<div key={schema} className="">
<div className="sticky top-0 backdrop-blur backdrop-filter">
<div className="flex items-baseline space-x-1 py-2 px-6">
<div className="flex items-baseline space-x-1 py-2">
<h5 className="text-scale-1000">schema</h5>
<h4>{schema}</h4>
</div>
</div>
<Table
className="table-fixed px-6"
className="table-fixed"
head={
<>
<Table.th key="name" className="space-x-4">
@@ -6,7 +6,7 @@ import { Button, Input, Toggle, IconSearch, IconAlertCircle } from 'ui'
import { checkPermissions, useStore } from 'hooks'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
import Table from 'components/to-be-cleaned/Table'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import NoSearchResults from 'components/ui/NoSearchResults'
import InformationBox from 'components/ui/InformationBox'
interface Props {
@@ -83,7 +83,7 @@ const PublicationsList: FC<Props> = ({ onSelectPublication = () => {} }) => {
</div>
</div>
{publications.length === 0 ? (
<NoSearchResults />
<NoSearchResults searchString={filterString} onResetFilter={() => setFilterString('')} />
) : (
<div className="overflow-hidden rounded">
<Table
@@ -1,5 +1,5 @@
import { partition } from 'lodash'
import { FC, useState, useEffect } from 'react'
import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PostgresRole } from '@supabase/postgres-meta'
@@ -13,12 +13,9 @@ import RoleRow from './RoleRow'
import { SUPABASE_ROLES } from './Roles.constants'
import CreateRolePanel from './CreateRolePanel'
import DeleteRoleModal from './DeleteRoleModal'
import NoSearchResults from 'components/ui/NoSearchResults'
interface Props {
onSelectRole: (role: any) => void
}
const RolesList: FC<Props> = ({}) => {
const RolesList = ({}) => {
const { meta } = useStore()
const [maxConnectionLimit, setMaxConnectionLimit] = useState(0)
@@ -66,7 +63,7 @@ const RolesList: FC<Props> = ({}) => {
/>
</div>
<div className="mb-4 -mt-2 flex items-center justify-between">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center space-x-4">
<Input
size="small"
@@ -216,12 +213,7 @@ const RolesList: FC<Props> = ({}) => {
</div>
{filterString.length > 0 && filteredRoles.length === 0 && (
<div className="bg-scale-100 dark:bg-scale-300 border border-scale-300 dark:border-scale-500 px-6 py-4 rounded space-y-1">
<p className="text-sm text-scale-1100">No results found</p>
<p className="text-sm text-scale-1000">
Your search for "{filterString}" did not return any results
</p>
</div>
<NoSearchResults searchString={filterString} onResetFilter={() => setFilterString('')} />
)}
</div>
@@ -1,72 +0,0 @@
import { FC } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, IconChevronLeft } from 'ui'
import Divider from 'components/ui/Divider'
import Panel from 'components/ui/Panel'
interface Props {
selectedRole: any
onSelectBack: () => void
}
const RolesSettings: FC<Props> = ({ selectedRole, onSelectBack = () => {} }) => {
return (
<>
<div className="mb-8">
<div className="">
<nav className="-mb-px flex">
<div className="group mr-4 inline-flex items-center p-1 ">
<Button
type="outline"
onClick={() => onSelectBack()}
icon={<IconChevronLeft size="small" />}
style={{ padding: '5px' }}
/>
</div>
<a
href="#"
className="
group mx-4
inline-flex items-center border-b-2 border-green-500 p-1 text-sm font-medium text-typography-body-strong-light focus:outline-none dark:text-typography-body-strong-dark "
aria-current="page"
>
<span>Permissions</span>
</a>
</nav>
</div>
</div>
<Panel>
<Panel.Content className="flex w-full items-center justify-between">
<p className="text-sm">Super user</p>
<code className="text-sm">{selectedRole.is_superuser ? 'true' : 'false'}</code>
</Panel.Content>
<Divider light />
<Panel.Content className="flex w-full items-center justify-between">
<p className="text-sm">User can login</p>
<code className="text-sm">{selectedRole.can_login ? 'true' : 'false'}</code>
</Panel.Content>
<Divider light />
<Panel.Content className="flex w-full items-center justify-between">
<p className="text-sm">User can create databases</p>
<code className="text-sm">{selectedRole.can_create_db ? 'true' : 'false'}</code>
</Panel.Content>
<Divider light />
<Panel.Content className="flex w-full items-center justify-between">
<p className="text-sm">
User can initiate streaming replication and put the system in and out of backup mode
</p>
<code className="text-sm">{selectedRole.is_replication_role ? 'true' : 'false'}</code>
</Panel.Content>
<Divider light />
<Panel.Content className="flex w-full items-center justify-between">
<p className="text-sm">User bypasses every row level security policy</p>
<code className="text-sm">{selectedRole.can_bypass_rls ? 'true' : 'false'}</code>
</Panel.Content>
</Panel>
</>
)
}
export default observer(RolesSettings)
@@ -20,6 +20,7 @@ import { useStore, checkPermissions } from 'hooks'
import Table from 'components/to-be-cleaned/Table'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import type { PostgresTable, PostgresSchema } from '@supabase/postgres-meta'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
interface Props {
selectedSchema: string
@@ -39,6 +40,7 @@ const TableList: FC<Props> = ({
onOpenTable = () => {},
}) => {
const { meta } = useStore()
const { isLoading } = meta.tables
const [filterString, setFilterString] = useState<string>('')
const canUpdateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
@@ -150,7 +152,14 @@ const TableList: FC<Props> = ({
</div>
)}
</div>
{tables.length === 0 ? (
{isLoading ? (
<div className="py-4 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : tables.length === 0 ? (
<NoSearchResults />
) : (
<div className="my-4 w-full">
@@ -21,6 +21,7 @@ import ChooseFunctionForm from './ChooseFunctionForm'
import FormEmptyBox from 'components/ui/FormBoxEmpty'
import NoTableState from 'components/ui/States/NoTableState'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
class CreateTriggerFormState {
id: number | undefined
@@ -545,7 +546,7 @@ const ListboxTable: FC = observer(({}) => {
addOnBefore={() => (
<div className="flex items-center justify-center rounded bg-scale-1200 p-1 text-scale-100 ">
<SVG
src={'/img/table-editor.svg'}
src={`${BASE_PATH}/img/table-editor.svg`}
style={{ width: `16px`, height: `16px`, strokeWidth: '1px' }}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
@@ -1,3 +1,4 @@
import { BASE_PATH } from 'lib/constants'
import { WrapperMeta } from './Wrappers.types'
export const WRAPPERS: WrapperMeta[] = [
@@ -5,7 +6,7 @@ export const WRAPPERS: WrapperMeta[] = [
name: 'stripe_wrapper',
handlerName: 'stripe_fdw_handler',
validatorName: 'stripe_fdw_validator',
icon: '/img/icons/stripe-icon.svg',
icon: `${BASE_PATH}/img/icons/stripe-icon.svg`,
extensionName: 'StripeFdw',
label: 'Stripe',
docsUrl: 'https://supabase.com/docs/guides/database/wrappers/stripe',
@@ -409,7 +410,7 @@ export const WRAPPERS: WrapperMeta[] = [
name: 'firebase_wrapper',
handlerName: 'firebase_fdw_handler',
validatorName: 'firebase_fdw_validator',
icon: '/img/icons/firebase-icon.svg',
icon: `${BASE_PATH}/img/icons/firebase-icon.svg`,
extensionName: 'FirebaseFdw',
label: 'Firebase',
docsUrl: 'https://supabase.com/docs/guides/database/wrappers/firebase',
@@ -6,6 +6,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useParams, useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
const WrappersDisabledState = () => {
const { ui, meta } = useStore()
@@ -65,8 +66,8 @@ const WrappersDisabledState = () => {
backgroundSize: '45%',
backgroundPosition: '105% 40%',
backgroundImage: ui.isDarkTheme
? 'url("/img/wrappers-dark.png")'
: 'url("/img/wrappers-light.png")',
? `url("${BASE_PATH}/img/wrappers-dark.png")`
: `url("${BASE_PATH}/img/wrappers-light.png")`,
}}
>
<div className="w-3/5 space-y-8">
@@ -2,7 +2,6 @@ import TableList from './Tables/TableList'
import ColumnList from './Tables/ColumnList'
import RolesList from './Roles/RolesList'
import RolesSettings from './Roles/RolesSettings'
import Extensions from './Extensions/Extensions'
@@ -27,7 +26,6 @@ export {
TableList,
ColumnList,
RolesList,
RolesSettings,
Extensions,
Wrappers,
CreateWrapper,
@@ -1,5 +1,6 @@
import { FC } from 'react'
import { IconBookOpen, IconGitHub, Button, Badge } from 'ui'
import { BASE_PATH } from 'lib/constants'
interface Props {
language: string
@@ -13,7 +14,7 @@ const ClientLibrary: FC<Props> = ({ language, releaseState, officialSupport, doc
return (
<div className="flex items-start space-x-6">
<img
src={`/img/libraries/${language.toLowerCase()}-icon.svg`}
src={`${BASE_PATH}/img/libraries/${language.toLowerCase()}-icon.svg`}
alt={`${language} logo`}
width="21"
/>
@@ -2,6 +2,7 @@ import { FC } from 'react'
import { IconChevronRight } from 'ui'
import { useStore } from 'hooks'
import Link from 'next/link'
import { BASE_PATH } from 'lib/constants'
interface Props {
framework: string
@@ -29,8 +30,12 @@ const ExampleProject: FC<Props> = ({ framework, title, description, url }) => {
<div className="mr-4 flex flex-col">
<img
className="transition-all group-hover:scale-110"
src={`/img/libraries/${framework.toLowerCase()}${
['expo', 'nextjs'].includes(framework.toLowerCase()) ? (isDarkTheme ? '-dark' : '') : ''
src={`${BASE_PATH}/img/libraries/${framework.toLowerCase()}${
['expo', 'nextjs'].includes(framework.toLowerCase())
? isDarkTheme
? '-dark'
: ''
: ''
}-icon.svg`}
alt={`${framework} logo`}
width={26}
@@ -47,7 +47,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
type="default"
icon={
<SVG
src="/img/table-editor.svg"
src={`${router.basePath}/img/table-editor.svg`}
style={{ width: `${14}px`, height: `${14}px` }}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
@@ -65,7 +65,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
type="default"
icon={
<SVG
src="/img/sql-editor.svg"
src={`${router.basePath}/img/sql-editor.svg`}
style={{ width: `${14}px`, height: `${14}px` }}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
@@ -17,7 +17,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useStore } from 'hooks'
import { delete_, patch } from 'lib/common/fetch'
import { getURL } from 'lib/helpers'
import { API_URL } from 'lib/constants'
import { API_URL, BASE_PATH } from 'lib/constants'
import Panel from 'components/ui/Panel'
import { AddNewPaymentMethodModal } from 'components/interfaces/Billing'
import NoPermission from 'components/ui/NoPermission'
@@ -158,7 +158,7 @@ const PaymentMethods: FC<Props> = ({
<div key={paymentMethod.id} className="flex items-center justify-between">
<div className="flex items-center space-x-8">
<img
src={`/img/payment-methods/${paymentMethod.card.brand
src={`${BASE_PATH}/img/payment-methods/${paymentMethod.card.brand
.replace(' ', '-')
.toLowerCase()}.png`}
width="32"
@@ -1,19 +1,22 @@
import Image from 'next/image'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useState, FC, Fragment } from 'react'
import { useState, Fragment } from 'react'
import { observer } from 'mobx-react-lite'
import { Badge, Button, Loading, Listbox, IconUser, Modal, IconAlertCircle } from 'ui'
import { Badge, Button, Loading, Listbox, IconUser, Modal, IconAlertCircle, IconLoader } from 'ui'
import { Member, Role } from 'types'
import { Member } from 'types'
import { useStore, useParams } from 'hooks'
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
import { useProfileQuery } from 'data/profile/profile-query'
import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import Table from 'components/to-be-cleaned/Table'
import MemberActions from './MemberActions'
import RolesHelperModal from './RolesHelperModal/RolesHelperModal'
import { getRolesManagementPermissions } from './TeamSettings.utils'
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
import { useProfileQuery } from 'data/profile/profile-query'
import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query'
import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query'
import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
interface SelectedMember extends Member {
oldRoleId: number
@@ -21,24 +24,38 @@ interface SelectedMember extends Member {
}
export interface MembersViewProps {
roles: Role[]
members: Member[]
searchString: string
}
const MembersView = ({ searchString, roles, members }: MembersViewProps) => {
const MembersView = ({ searchString }: MembersViewProps) => {
const { ui } = useStore()
const { slug } = useParams()
const { data: profile } = useProfileQuery()
const { data: detailData, isLoading: isLoadingOrgDetails } = useOrganizationDetailQuery({ slug })
const { data: rolesData, isLoading: isLoadingRoles } = useOrganizationRolesQuery({ slug })
const { mutate: updateOrganizationMember, isLoading } = useOrganizationMemberUpdateMutation({
onSuccess() {
ui.setNotification({
category: 'success',
message: `Successfully updated role for ${getUserDisplayName(selectedMember)}`,
})
},
onError() {
ui.setNotification({
category: 'error',
message: `Failed to update role for ${getUserDisplayName(selectedMember)}`,
})
},
})
const roles = rolesData?.roles ?? []
const members = detailData?.members ?? []
const { rolesAddable, rolesRemovable } = getRolesManagementPermissions(roles)
const [selectedMember, setSelectedMember] = useState<SelectedMember>()
const [userRoleChangeModalVisible, setUserRoleChangeModalVisible] = useState(false)
if (!members) return <div />
const filteredMembers = (
!searchString
? members
@@ -59,21 +76,6 @@ const MembersView = ({ searchString, roles, members }: MembersViewProps) => {
return roles.find((x: any) => x.id === id)?.name
}
const { isLoading, mutate } = useOrganizationMemberUpdateMutation({
onSuccess() {
ui.setNotification({
category: 'success',
message: `Successfully updated role for ${getUserDisplayName(selectedMember)}`,
})
},
onError() {
ui.setNotification({
category: 'error',
message: `Failed to update role for ${getUserDisplayName(selectedMember)}`,
})
},
})
const handleRoleChange = async () => {
if (!selectedMember) return
@@ -86,11 +88,20 @@ const MembersView = ({ searchString, roles, members }: MembersViewProps) => {
throw new Error('gotrue_id is required')
}
mutate({ slug, gotrueId: gotrue_id, roleId: newRoleId })
updateOrganizationMember({ slug, gotrueId: gotrue_id, roleId: newRoleId })
setUserRoleChangeModalVisible(false)
}
if (isLoadingOrgDetails) {
return (
<div className="py-4 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)
}
return (
<>
<div className="rounded">
@@ -176,10 +187,13 @@ const MembersView = ({ searchString, roles, members }: MembersViewProps) => {
</Table.td>
<Table.td>
{!role && <p>{x.is_owner ? 'Owner' : 'Developer'}</p>}
{role && (
{isLoadingRoles ? (
<div className="w-[140px]">
<IconLoader className="animate-spin" size={16} strokeWidth={1.5} />
</div>
) : role !== undefined ? (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Tooltip.Trigger className="w-[140px]">
<Listbox
className={disableRoleEdit ? 'pointer-events-none' : ''}
disabled={disableRoleEdit}
@@ -231,6 +245,29 @@ const MembersView = ({ searchString, roles, members }: MembersViewProps) => {
<></>
)}
</Tooltip.Root>
) : (
<div className="flex items-center space-x-2">
<p className="text-sm text-scale-1100">Invalid role</p>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconAlertCircle size={16} strokeWidth={1.5} />
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
This user has an invalid role, please reach out to us via
support
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
</div>
)}
</Table.td>
<Table.td>
@@ -117,7 +117,7 @@ const TeamSettings = () => {
</div>
</div>
<div className="container my-4 max-w-4xl space-y-8">
<MembersView searchString={searchString} roles={roles} members={members} />
<MembersView searchString={searchString} />
</div>
</>
)
@@ -6,6 +6,7 @@ import { Button, IconExternalLink } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useParams, useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
interface Props {}
@@ -63,8 +64,8 @@ const VaultToggle: FC<Props> = () => {
backgroundSize: isNotAvailable ? '50%' : '40%',
backgroundPosition: '100% 24%',
backgroundImage: ui.isDarkTheme
? 'url("/img/vault-dark.png")'
: 'url("/img/vault-light.png")',
? `url("${BASE_PATH}/img/vault-dark.png")`
: `url("${BASE_PATH}/img/vault-light.png")`,
}}
>
<div className="w-3/5 space-y-8">
@@ -1,7 +1,7 @@
import HCaptcha from '@hcaptcha/react-hcaptcha'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { API_URL, BASE_PATH } from 'lib/constants'
import { useRouter } from 'next/router'
import { useRef, useState } from 'react'
import { Button, Form, Input } from 'ui'
@@ -35,9 +35,9 @@ const ForgotPasswordForm = () => {
hcaptchaToken: token ?? undefined,
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: process.env.NEXT_PUBLIC_SITE_URL
}/reset-password`,
}${BASE_PATH}/reset-password`,
})
const error = response.error
@@ -71,7 +71,7 @@ const ForgotPasswordForm = () => {
>
{({ isSubmitting }: { isSubmitting: boolean }) => {
return (
<div className="flex flex-col space-y-4 pt-4">
<div className="flex flex-col pt-4 space-y-4">
<Input
id="email"
name="email"
@@ -96,7 +96,7 @@ const ForgotPasswordForm = () => {
/>
</div>
<div className="border-overlay-border border-t" />
<div className="border-t border-overlay-border" />
<Button
block
@@ -1,6 +1,7 @@
import HCaptcha from '@hcaptcha/react-hcaptcha'
import { useQueryClient } from '@tanstack/react-query'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import { auth, getReturnToPath } from 'lib/gotrue'
import { useRef, useState } from 'react'
import { Button, Form, Input } from 'ui'
@@ -35,9 +36,9 @@ const SignInSSOForm = () => {
captchaToken: token ?? undefined,
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: process.env.NEXT_PUBLIC_SITE_URL
}${getReturnToPath()}`,
}${BASE_PATH}${getReturnToPath()}`,
},
})
@@ -1,3 +1,4 @@
import { BASE_PATH } from 'lib/constants'
import { auth, getReturnToPath } from 'lib/gotrue'
import { Button, IconGitHub } from 'ui'
@@ -9,9 +10,9 @@ const SignInWithGitHub = () => {
options: {
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: process.env.NEXT_PUBLIC_SITE_URL
}${getReturnToPath()}`,
}${BASE_PATH}${getReturnToPath()}`,
},
})
if (error) throw error
@@ -5,7 +5,7 @@ import { Alert, Button, Form, IconEye, IconEyeOff, Input } from 'ui'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { API_URL, BASE_PATH } from 'lib/constants'
import { passwordSchema } from 'lib/schemas'
import PasswordConditionsHelper from './PasswordConditionsHelper'
@@ -39,9 +39,9 @@ const SignUpForm = () => {
hcaptchaToken: token ?? null,
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: process.env.NEXT_PUBLIC_SITE_URL
}/sign-in`,
}${BASE_PATH}/sign-in`,
})
const error = response.error
@@ -73,7 +73,7 @@ const GridHeaderActions: FC<Props> = ({
<div className="mt-[1px]">
<RenderAPIPreviewToggle />
</div>
<Link href={`/project/${projectRef}/auth/policies#${table.id}`}>
<Link href={`/project/${projectRef}/auth/policies?search=${table.id}`}>
<a>
<Button
type={table.rls_enabled ? 'link' : 'warning'}
@@ -85,7 +85,6 @@ const GridHeaderActions: FC<Props> = ({
)
}
>
{/* RLS {table.rls_enabled ? 'is' : 'not'} enabled */}
{!table.rls_enabled
? 'RLS is not enabled'
: `${policies.length == 0 ? 'No' : policies.length} active RLS polic${
@@ -8,22 +8,19 @@ import SortPopover from 'components/grid/components/header/sort'
import { formatFilterURLParams, formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query'
import { useStore } from 'hooks'
import { IconLoader, SidePanel } from 'ui'
import ActionBar from '../../ActionBar'
import { useEncryptedColumns } from './ForeignRowSelector.utils'
import SelectorGrid from './SelectorGrid'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import Pagination from './Pagination'
export interface ForeignRowSelectorProps {
visible: boolean
foreignKey?: {
target_table_schema: string
target_table_name: string
target_column_name: string
}
foreignKey?: ForeignKeyConstraint
onSelect: (value: any) => void
closePanel: () => void
}
@@ -38,13 +35,20 @@ const ForeignRowSelector = ({
const { project } = useProjectContext()
const {
target_table_schema: schemaName,
target_table_name: tableName,
target_column_name: columnName,
target_id: tableId,
target_schema: schemaName,
target_table: tableName,
target_columns: columnName,
} = foreignKey ?? {}
const tables = meta.tables.list()
const table = tables.find((table) => table.schema === schemaName && table.name === tableName)
const table = tableId ? meta.tables.byId(tableId) : undefined
useEffect(() => {
if (!table && tableId) {
meta.tables.loadById(tableId)
}
}, [table, tableId])
const encryptedColumns = useEncryptedColumns({ schemaName, tableName })
const supaTable =
@@ -85,7 +89,6 @@ const ForeignRowSelector = ({
return (
<SidePanel
key="ForeignRowSelector"
visible={visible}
size="large"
header={
@@ -3,7 +3,7 @@ import { SidePanel } from 'ui'
import { useStore } from 'hooks'
import JsonEditor from './JsonCodeEditor'
import TwoOptionToggle from './TwoOptionToggle'
import TwoOptionToggle from 'components/ui/TwoOptionToggle'
import DrilldownViewer from './DrilldownViewer'
import ActionBar from '../../ActionBar'
import { minifyJSON, prettifyJSON, tryParseJson } from 'lib/helpers'
@@ -1,9 +1,11 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { isUndefined, partition, isEmpty, noop } from 'lodash'
import { SidePanel } from 'ui'
import { Dictionary } from 'components/grid'
import type { PostgresTable } from '@supabase/postgres-meta'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ActionBar from '../ActionBar'
import HeaderTitle from './HeaderTitle'
import InputField from './InputField'
@@ -51,6 +53,21 @@ const RowEditor = ({
(rowField: any) => !rowField.isNullable
)
const { project } = useProjectContext()
const { data } = useForeignKeyConstraintsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedTable.schema,
})
const foreignKey = useMemo(
() =>
data && referenceRow?.foreignKey?.id
? data.find((key) => key.id === referenceRow.foreignKey?.id)
: undefined,
[data, referenceRow?.foreignKey?.id]
)
useEffect(() => {
if (visible) {
setErrors({})
@@ -201,8 +218,9 @@ const RowEditor = ({
</form>
<ForeignRowSelector
key={`foreign-row-selector-${foreignKey?.id ?? 'null'}`}
visible={isSelectingForeignKey}
foreignKey={referenceRow?.foreignKey}
foreignKey={foreignKey}
onSelect={onSelectForeignRowValue}
closePanel={() => {
setIsSelectingForeignKey(false)
@@ -6,6 +6,7 @@ import { Modal } from 'ui'
import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
import { useStore } from 'hooks'
import { entityTypeKeys } from 'data/entity-types/keys'
import { useTableRowCreateMutation } from 'data/table-rows/table-row-create-mutation'
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
import { RowEditor, ColumnEditor, TableEditor } from '.'
@@ -263,7 +264,7 @@ const SidePanelEditor = ({
ui.setNotification({ category: 'error', message: response.error.message })
} else {
await meta.tables.loadById(selectedTable!.id)
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreignKeyConstraints']))
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
onColumnSaved(configuration.isEncrypted)
setIsEdited(false)
closePanel()
@@ -297,21 +298,27 @@ const SidePanelEditor = ({
try {
if (isDuplicating) {
const duplicateTable = find(tables, { id: tableId }) as PostgresTable
toastId = ui.setNotification({
category: 'loading',
message: `Duplicating table: ${duplicateTable.name}...`,
})
const table: any = await meta.duplicateTable(payload, {
isRLSEnabled,
isRealtimeEnabled,
isDuplicateRows,
duplicateTable,
})
await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref))
ui.setNotification({
id: toastId,
category: 'success',
message: `Table ${duplicateTable.name} has been successfully duplicated into ${table.name}!`,
})
onTableCreated(table)
} else if (isNewRecord) {
toastId = ui.setNotification({
@@ -327,17 +334,22 @@ const SidePanelEditor = ({
isRealtimeEnabled,
importContent
)
await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref))
ui.setNotification({
id: toastId,
category: 'success',
message: `Table ${table.name} is good to go!`,
})
onTableCreated(table)
} else if (selectedTableToEdit) {
toastId = ui.setNotification({
category: 'loading',
message: `Updating table: ${selectedTableToEdit?.name}...`,
})
const { table, hasError }: any = await meta.updateTable(
toastId,
selectedTableToEdit,
@@ -345,6 +357,7 @@ const SidePanelEditor = ({
columns,
isRealtimeEnabled
)
if (hasError) {
ui.setNotification({
id: toastId,
@@ -352,6 +365,8 @@ const SidePanelEditor = ({
message: `Table ${table.name} has been updated, but there were some errors`,
})
} else {
await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref))
ui.setNotification({
id: toastId,
category: 'success',
@@ -359,7 +374,8 @@ const SidePanelEditor = ({
})
}
}
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreignKeyConstraints']))
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
} catch (error: any) {
saveTableError = true
ui.setNotification({ id: toastId, category: 'error', message: error.message })
@@ -422,6 +438,7 @@ const SidePanelEditor = ({
onSaveJSON={onSaveJSON}
/>
<ForeignRowSelector
key={`foreign-row-selector-${selectedForeignKeyToEdit?.foreignKey?.id ?? 'null'}`}
visible={sidePanelKey === 'foreign-row-selector'}
foreignKey={selectedForeignKeyToEdit?.foreignKey}
closePanel={onClosePanel}
@@ -1,6 +1,6 @@
import { FC, useEffect, useState } from 'react'
import { isUndefined, isEmpty } from 'lodash'
import { Badge, Checkbox, SidePanel, Input, Alert } from 'ui'
import { Badge, Checkbox, SidePanel, Input, Alert, IconBookOpen, Button } from 'ui'
import type { PostgresTable, PostgresType } from '@supabase/postgres-meta'
import { useStore } from 'hooks'
@@ -19,6 +19,7 @@ import {
} from './TableEditor.utils'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Link from 'next/link'
interface Props {
table?: PostgresTable
@@ -55,7 +56,6 @@ const TableEditor: FC<Props> = ({
const { project } = useProjectContext()
const isNewRecord = isUndefined(table)
const tables = meta.tables.list()
const enumTypes = meta.types.list(
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
)
@@ -226,18 +226,46 @@ const TableEditor: FC<Props> = ({
<p>
Restrict access to your table by enabling RLS and writing Postgres policies.
</p>
<p>
RLS is secure by default - all normal access to this table must be allowed by a
policy.
</p>
{!tableFields.isRLSEnabled && (
{tableFields.isRLSEnabled ? (
<Alert
withIcon
variant="warning"
className="!px-4 !py-3 mt-3"
title="RLS policies are required to query data"
>
<p>
You need to write a policy before you can query data from this table.
Without a policy, querying this table will result in an <u>empty array</u>{' '}
of results.
</p>
<p className="mt-4">
<Link href="https://supabase.com/docs/guides/auth/row-level-security">
<a target="_blank">
<Button type="default" icon={<IconBookOpen strokeWidth={1.5} />}>
RLS Documentation
</Button>
</a>
</Link>
</p>
</Alert>
) : (
<Alert
withIcon
variant="danger"
className="!px-4 !py-3 mt-3"
title="Turning off RLS means that you are allowing anonymous access to your table"
>
As such, anyone with the anonymous key can modify or delete your data.
<p>As such, anyone with the anonymous key can modify or delete your data.</p>
<p className="mt-4">
<Link href="https://supabase.com/docs/guides/auth/row-level-security">
<a target="_blank">
<Button type="default" icon={<IconBookOpen strokeWidth={1.5} />}>
RLS Documentation
</Button>
</a>
</Link>
</p>
</Alert>
)}
</>
@@ -1,4 +1,4 @@
import { FC, useRef, useEffect, useState } from 'react'
import { useRef, useEffect, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { find, isUndefined, noop } from 'lodash'
@@ -75,14 +75,14 @@ const AccountLayout: FC<Props> = ({ children, title, breadcrumbs }) => {
links: [
{
isActive: router.pathname === `/account/me`,
icon: '/img/user.svg',
icon: `${router.basePath}/img/user.svg`,
label: 'Preferences',
href: `/account/me`,
key: `/account/me`,
},
{
isActive: router.pathname === `/account/tokens`,
icon: '/img/user.svg',
icon: `${router.basePath}/img/user.svg`,
label: 'Access Tokens',
href: `/account/tokens`,
key: `/account/tokens`,
@@ -97,14 +97,14 @@ const AccountLayout: FC<Props> = ({ children, title, breadcrumbs }) => {
links: [
{
key: 'ext-guides',
icon: '/img/book.svg',
icon: `${router.basePath}/img/book.svg`,
label: 'Guides',
href: 'https://supabase.com/docs',
isExternal: true,
},
{
key: 'ext-guides',
icon: '/img/book-open.svg',
icon: `${router.basePath}/img/book-open.svg`,
label: 'API Reference',
href: 'https://supabase.com/docs/guides/database/api',
isExternal: true,
@@ -16,7 +16,8 @@ interface Props {
const DatabaseLayout: FC<Props> = ({ title, children }) => {
const { meta, ui, vault, backups } = useStore()
const { isInitialized, isLoading, error } = meta.tables
const { isLoading } = meta.schemas
const { isInitialized, error } = meta.tables
const project = ui.selectedProject
const router = useRouter()
@@ -1,6 +1,7 @@
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import SVG from 'react-inlinesvg'
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import { IconBook, IconBookOpen } from 'ui'
import { BASE_PATH } from 'lib/constants'
export const generateDocsMenu = (
ref: string,
@@ -71,7 +72,7 @@ export const generateDocsMenu = (
url: `/project/${ref}/api/graphiql`,
icon: (
<SVG
src="/img/graphql.svg"
src={`${BASE_PATH}/img/graphql.svg`}
style={{ width: `${16}px`, height: `${16}px` }}
className="text-scale-1200"
preProcessor={(code) =>
@@ -83,14 +83,16 @@ const HelpPopover: FC<Props> = () => {
>
<Image
className="absolute left-0 top-0 opacity-50"
src={'/img/support/discord-bg-small.jpg'}
src={`${router.basePath}/img/support/discord-bg-small.jpg`}
layout="fill"
objectFit="cover"
alt="discord illustration header"
/>
<Button
type="secondary"
icon={<SVG src={`/img/discord-icon.svg`} className="h-4 w-4" />}
icon={
<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />
}
>
<span style={{ color: '#404EED' }}>Join Discord server</span>
</Button>
@@ -106,7 +108,7 @@ const HelpPopover: FC<Props> = () => {
>
<Image
className="absolute left-0 top-0 opacity-50"
src={'/img/support/github-bg.jpg?v-1'}
src={`${router.basePath}/img/support/github-bg.jpg?v-1`}
layout="fill"
objectFit="cover"
alt="discord illustration header"
Loaded 100 of 173 files, more files were not shown because too many files have changed in this diff. Show more