Merge pull request #14067 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-05-02 13:14:48 +08:00
commit d8c07236be
68 files changed
+6506 -4201

No files matched your search

+1 -1
View File
@@ -231,7 +231,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
- [Norwegian (Bokmål) / Norsk (Bokmål)](/i18n/README.nb-no.md)
- [Persian / فارسی](/i18n/README.fa.md)
- [Polish / Polski](/i18n/README.pl.md)
- [Portuguese / Portuguese](/i18n/README.pt.md)
- [Portuguese / Português](/i18n/README.pt.md)
- [Portuguese (Brazilian) / Português Brasileiro](/i18n/README.pt-br.md)
- [Romanian / Română](/i18n/README.ro.md)
- [Russian / Pусский](/i18n/README.ru.md)
+3 -1
View File
@@ -11,7 +11,9 @@ const Footer = () => {
<hr className="border-scale-400 mt-8"></hr>
<div className="flex gap-4 items-center mt-6 justify-between">
<div className="flex flex-col lg:flex-row gap-3 ">
<span className="text-xs text-scale-900">Supabase 2022</span>
<Link href="https://supabase.com/">
<a className="text-xs text-scale-900">&copy; Supabase Inc</a>
</Link>
<span className="text-xs text-scale-900">—</span>
{footerData.map((item) => (
<Link href={item.url} key={item.url}>
@@ -471,6 +471,10 @@ export const database = {
name: 'Managing Indexes',
url: '/guides/database/postgres/indexes',
},
{
name: 'Cascade Deletes',
url: '/guides/database/postgres/cascade-deletes',
},
{
name: 'Drop All Tables in Schema',
url: '/guides/database/postgres/dropping-all-tables-in-schema',
@@ -138,9 +138,9 @@ create table
create table
members (
"id" serial primary key,
"user_id" int references users,
"team_id" int references teams
"team_id" int references teams,
primary key(user_id, team_id)
);
```
@@ -44,7 +44,7 @@ It's good practice to create the extension within a separate schema (like `exten
## API
- [`sign(payload json, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Signs a JWT containng _payload_ with _secret_ using _algorithm_.
- [`sign(payload json, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Signs a JWT containing _payload_ with _secret_ using _algorithm_.
- [`verify(token text, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Decodes a JWT _token_ that was signed with _secret_ using _algorithm_.
Where:
@@ -0,0 +1,195 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Cascade Deletes',
description: 'Understand the types of foreign key constraint deletes',
footerHelpType: 'postgres',
}
There are 5 options for foreign key constraint deletes:
1. **CASCADE:** When a row is deleted from the parent table, all related rows in the child table(s) are deleted as well.
2. **RESTRICT:** When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child table(s).
3. **SET NULL:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to NULL.
4. **SET DEFAULT:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to their default values.
5. **NO ACTION:** This option is similar to RESTRICT, but it also has the option to be “deferred” to the end of a transaction. This means that other cascading deletes can run first, and then this delete constraint will only throw an error if there is referenced data remaining _at the end of the transaction_.
These options can be specified when defining a foreign key constraint using the "ON DELETE" clause. For example, the following SQL statement creates a foreign key constraint with the `CASCADE` option:
```sql
alter table child_table
add constraint fk_parent foreign key (parent_id) references parent_table (id)
on delete cascade;
```
This means that when a row is deleted from the "parent_table", all related rows in the "child_table" will be deleted as well.
## `RESTRICT` vs `NO ACTION`
The difference between `NO ACTION` and `RESTRICT` is subtle and can be a bit confusing.
Both `NO ACTION` and `RESTRICT` are used to prevent deletion of a row in a parent table if there are related rows in a child table. However, there is a subtle difference in how they behave.
When a foreign key constraint is defined with the option `RESTRICT`, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced table(s).
When a foreign key constraint is defined with the option `NO ACTION`, it means that if a row in the parent table is deleted, the database will also raise an error and prevent the deletion of the row in the parent table. However unlike `RESTRICT`, `NO ACTION` has the option defer the check using `INITIALLY DEFERRED`. This will only raise the above error _if_ the referenced rows still exist at the end of the transaction.
The difference from `RESTRICT` is that a constraint marked as `NO ACTION INITIALLY DEFERRED` is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as `CASCADE`, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like `RESTRICT`, the database will not delete, update or set to NULL any rows in the referenced table(s).
In practice, you can use either `NO ACTION` or `RESTRICT` depending on your needs. `NO ACTION` is the default behavior if you do not specify anything. If you prefer to defer the check until the end of the transaction, use `NO ACTION INITIALLY DEFERRED`.
## Example
Let's further illustrate the difference with an example. We'll use the following data:
`grandparent`
| id | name |
| --- | --------- |
| 1 | Elizabeth |
`parent`
| id | name | parent_id |
| --- | ------- | --------- |
| 1 | Charles | 1 |
| 2 | Diana | 1 |
`child`
| id | name | father | mother |
| --- | ------- | ------ | ------ |
| 1 | William | 1 | 2 |
To create these tables and their data, we run:
```sql
create table grandparent (
id serial primary key,
name text
);
create table parent (
id serial primary key,
name text,
parent_id integer references grandparent (id)
on delete cascade
);
create table child (
id serial primary key,
name text,
father integer references parent (id)
on delete restrict
);
insert into grandparent
(id, name)
values
(1, 'Elizabeth');
insert into parent
(id, name, parent_id)
values
(1, 'Charles', 1);
insert into parent
(id, name, parent_id)
values
(2, 'Diana', 1);
-- We'll just link the father for now
insert into child
(id, name, father)
values
(1, 'William', 1);
```
### `RESTRICT`
`RESTRICT` will prevent a delete and raise an error:
```shell
postgres=# delete from grandparent;
ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"
DETAIL: Key (id)=(1) is still referenced from table "child".
```
Even though the foreign key constraint between parent and grandparent is `CASCADE`, the constraint between child and father is `RESTRICT`. Therefore an error is raised and no records are deleted.
### `NO ACTION`
Let's change the child-father relationship to `NO ACTION`:
```sql
alter table child
drop constraint child_father_fkey;
alter table child
add constraint child_father_fkey foreign key (father) references parent (id)
on delete no action;
```
We see that `NO ACTION` will also prevent a delete and raise an error:
```shell
postgres=# delete from grandparent;
ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"
DETAIL: Key (id)=(1) is still referenced from table "child".
```
### `NO ACTION INITIALLY DEFERRED`
We'll change the foreign key constraint between child and father to be `NO ACTION INITIALLY DEFERRED`:
```sql
alter table child
drop constraint child_father_fkey;
alter table child
add constraint child_father_fkey foreign key (father) references parent (id)
on delete no action initially deferred;
```
Here you will see that `INITIALLY DEFFERED` seems to operate like `NO ACTION` or `RESTRICT`. When we run a delete, it seems to make no difference:
```shell
postgres=# delete from grandparent;
ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"
DETAIL: Key (id)=(1) is still referenced from table "child".
```
But, when we combine it with _other_ constraints, then any other constraints take precedence. For example, let's run the same but add a `mother` column that has a `CASCADE` delete:
```sql
alter table child
add column mother integer references parent (id)
on delete cascade;
update child
set mother = 2
where id = 1;
```
Then let's run a delete on the `grandparent` table:
```shell
postgres=# delete from grandparent;
DELETE 1
postgres=# select * from parent;
id | name | parent_id
----+------+-----------
(0 rows)
postgres=# select * from child;
id | name | father | mother
----+------+--------+--------
(0 rows)
```
The `mother` deletion took precedence over the `father`, and so William was deleted. After William was deleted, there was no reference to “Charles” and so he was free to be deleted, even though previously he wasn't (without `INITIALLY DEFERRED`).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -22,7 +22,7 @@ Let's start building the Vue 3 app from scratch.
### Initialize a Nuxt 3 app
We can use [`nuxi init`](https://v3.nuxtjs.org/getting-started/quick-start/) to create an app called `nuxt-user-management`:
We can use [`nuxi init`](https://nuxt.com/docs/getting-started/installation) to create an app called `nuxt-user-management`:
```bash
npx nuxi init nuxt-user-management
@@ -207,7 +207,7 @@ You can configure Postgres by executing the following statement, followed by a s
alter system set max_connections = '<val-here>';
```
Note that [the default configuration used by the Supabase platform](https://github.com/supabase/supabase-admin-api/blob/master/optimizations/postgres.go) optimizes the database to maximize resource utilization, and as a result, you might also need to configure other options (e.g. `work_mem`, `shared_buffers`, `maintenance_work_mem`) in order to tune them towards your use-case, and to avoid causing instability in your database.
Note that the default configuration used by the Supabase platform optimizes the database to maximize resource utilization, and as a result, you might also need to configure other options (e.g. `work_mem`, `shared_buffers`, `maintenance_work_mem`) in order to tune them towards your use-case, and to avoid causing instability in your database.
Once overridden, the Supabase platform will continue to respect your manually configured value (even if the add-on size is changed), unless the override is removed with the following statement, followed by a server restart:
+10 -4
View File
@@ -153,25 +153,31 @@ export const postgres = [
{
title: 'Managing Indexes',
hasLightIcon: true,
href: '/guides/resources/postgres/indexes',
href: '/guides/database/postgres/indexes',
description: 'Improve query performance using various index types in Postgres.',
},
{
title: 'Cascade Deletes',
hasLightIcon: true,
href: '/guides/database/postgres/cascade-deletes',
description: 'Understand the types of foreign key constraint deletes.',
},
{
title: 'Drop all tables in schema',
hasLightIcon: true,
href: '/guides/resources/postgres/dropping-all-tables-in-schema',
href: '/guides/database/postgres/dropping-all-tables-in-schema',
description: 'Delete all tables in a given schema.',
},
{
title: 'Select first row per group',
hasLightIcon: true,
href: '/guides/resources/postgres/first-row-in-group',
href: '/guides/database/postgres/first-row-in-group',
description: 'Retrieve the first row in each distinct group.',
},
{
title: 'Print PostgreSQL version',
hasLightIcon: true,
href: '/guides/resources/postgres/which-version-of-postgres',
href: '/guides/database/postgres/which-version-of-postgres',
description: 'Find out which version of Postgres you are running.',
},
]
@@ -109,6 +109,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
- Subscriptions with Supabase and Stripe Billing [Blog](https://www.sandromaglione.com/2021/04/29/supabase-auth-create-stripe-customer-subscription-supabase-stripe-billing-part-1/)
- Flutter Supabase Authentication [Blog](https://www.sandromaglione.com/2021/04/24/flutter-supabase-authentication/)
- Supabase in 6 minutes [Video](https://www.youtube.com/watch?v=c8DNV9yl0mg)
- Supabase React Auth UI Tutorial [Video](https://youtu.be/6ch1PtIqCUw)
- Let's build SupaAuth - Build an Authentication System using Supabase, Next.js and Typescript [6-part Blog Series](https://aalam.in/blog/supabase-auth-intro-setup-next)
- In-depth self-hosting guide using Nginx [Blog](https://dev.to/chronsyn/self-hosting-with-supabase-1aii)
- Build an Email and Social Auth for Next JS with Supabase, Tailwind CSS 3.0 and TypeScript [Blog](https://creativedesignsguru.com/next-js-supabase-auth/)
@@ -0,0 +1,70 @@
---
title: 'Next steps for Postgres pluggable storage'
description: Exploring history of Postgres pluggable storage and the possibility of landing it in the Postgres core.
author: paul_copplestone
image: pluggable-storage/pluggable-storage-og.jpg
thumb: pluggable-storage/pluggable-storage.jpg
tags:
- postgres
- database
date: '2023-04-28'
toc_depth: 2
---
You might have recently read “[The Part of PostgreSQL We Hate the Most](https://ottertune.com/blog/the-part-of-postgresql-we-hate-the-most/)” which highlights some of the deficiencies with Postgres storage. Fortunately, there is ongoing work in the Postgres ecosystem to solve this using “pluggable storage”.
This article explores the history of Postgres pluggable storage and the possibility of landing it in the Postgres core.
## What is pluggable storage?
Pluggable storage gives developers the ability to use different storage engines for different tables within the same database.
Developers will be able to choose a storage method that is optimized for their specific needs. Some tables could be configured for high transactional loads, others for analytics workloads, and still others for archiving.
Something like this is already [available in MySQL](https://en.wikipedia.org/wiki/Comparison_of_MySQL_database_engines), which uses the [InnoDB](https://en.wikipedia.org/wiki/InnoDB) as the default storage engine since MySQL 5.5. This replaced [MyISAM](https://en.wikipedia.org/wiki/MyISAM) which had it’s own [set of problems](https://www.percona.com/blog/using-myisam-in-production/).
The “pluggable” part refers to the ability for developers to develop their own storage methods, similar to Postgres Extensions. While some databases offer multiple built-in storage methods by default, Postgres follows a core philosophy of flexibility and extensibility, and using a pluggable approach makes a lot of sense.
## Current progress for pluggable storage
In version 12, PostgreSQL introduced basic support for pluggable storage with the goal of [adding ZHeap](https://anarazel.de/talks/2019-05-30-pgcon-pluggable-table-storage/pluggable.pdf). Andres Freund and several contributors enabled that feature by refactoring and expanding a variety of table access method (TAM) APIs.
ZHeap [was released](https://github.com/EnterpriseDB/zheap) with [promising results](https://www.pgcon.org/2018/schedule/attachments/501_zheap-a-new-storage-format-postgresql-5.pdf), providing an undo log and solving some long-term table bloat issues in Postgres.
While it was a great start, unfortunately it appears that the work on ZHeap and the TAM API [has stalled](https://twitter.com/andy_pavlo/status/1590703943176589312).
This leaves a few holes in that need to be fixed before the community can use the TAM API for pluggable storage. In particular:
- The TAM API doesn’t provide much flexibility for working with indexes. For example, when you update a row an index update is an all-or-nothing procedure.
- Row versions need to be identified by the pairing of a block number and offset number. This means you can’t implement some useful functions, like an index-organized table or undo-based versioning.
Even without a full API for pluggable storage, there are a couple of solutions available on the market today:
- **Citus’s** [Columnar Storage](https://docs.citusdata.com/en/v11.1/admin_guide/table_management.html#columnar-storage), which powers Microsoft’s [Hyperscale](https://azure.microsoft.com/en-us/updates/azure-database-for-postgresql-hyperscale-citus-is-now-available/) offering. Citus has a few [limitations](https://docs.citusdata.com/en/v11.1/admin_guide/table_management.html#limitations) due to the restrictions of the current API.
- EDB’s [Advanced Storage Pack](https://www.infoworld.com/article/3681112/3-key-features-in-edb-postgresql-15.html), which is appears to provide some enhancements to heap storage, although the source is not available for us to confirm.
## The future of pluggable storage
Right now, pluggable storage requires a fork of Postgres. But there are several initiatives in the Postgres community that we’re excited about.
- There have been community updates for [custom resource managers](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=5c279a6d350205cc98f91fb8e1d3e4442a6b25d1) that expand [write-ahead log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) support for TAM.
- The creators at [OrioleDB](https://github.com/orioledb/orioledb) started with a fork of Postgres and have been quietly upstreaming code that would make pluggable storage a reality. In Postgres 14 their patchset was ~5,000 lines of code, representing the total number of changes required in their fork. In the upcoming Postgres 16 release, it is ~2,000 lines of code. That means that around 60% of the code is already committed to the Postgres core. (Disclosure: Supabase started [sponsoring OrioleDB](https://supabase.com/blog/supabase-series-b#where-were-going) in 2022).
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
Alexander Korotkov, the maintainer of OrioleDB, will be talking at PGCON 23 about some of the [changes](https://www.pgcon.org/events/pgcon_2023/schedule/session/470-future-of-table-access-methods/) they are making. They are solving many ["wicked Postgres problems"](https://www.slideshare.net/AlexanderKorotkov/solving-postgresql-wicked-problems) with [dual pointers](https://www.orioledata.com/blog/buffer-management/) instead of buffer mapping, row-level WAL instead of block-level WAL, undo logs instead of bloat-prone MVCC, and index-organized tables instead of heap.
</div>
## Timelines for pluggable storage
Ambitiously, many of the changes required for pluggable storage could land in Postgres 17. If that happens, new storage engines like OrioleDB would be available as a simple extension for developers to use in any Postgres instance. This paves the way for other Pluggable Storage Engines, just like MySQL’s [support for multiple storage engines](https://dev.mysql.com/doc/refman/5.7/en/storage-engines.html).
As a community project, Postgres requires support for these initiatives. You can support pluggable storage by getting involved during Postgres [Commitfests](https://supabase.com/blog/postgresql-commitfest). Postgres is already one of the best database engines in the world, and pluggable storage would make it even more attractive and maintainable.
## More Postgres content
- [Storing OpenAI embeddings in Postgres with pgvector](https://supabase.com/blog/openai-embeddings-postgres-vector)
- [Choosing a Postgres Primary Key](https://supabase.com/blog/choosing-a-postgres-primary-key)
- [SQL or NoSQL? Why not use both (with PostgreSQL)?](https://supabase.com/blog/sql-or-nosql-both-with-postgresql)
- [pg_jsonschema: JSON Schema support for Postgres](https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation)
- [Implementing "seen by" functionality with Postgres](https://supabase.com/blog/seen-by-in-postgresql)
@@ -87,6 +87,7 @@ function CodeBlock(props: CodeBlockProps) {
</div>
)}
<div className="relative">
{/* @ts-ignore */}
<SyntaxHighlighter
language={lang}
style={monokaiCustomTheme}
@@ -150,6 +150,7 @@ const CodeExamples = () => {
</div>
<SyntaxHighlighter
language="javascript"
// @ts-ignore
style={monokaiCustomTheme}
className="rounded-b-lg"
customStyle={{
+1 -1
View File
@@ -111,7 +111,7 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
Explore remote possibilities and join our team to help us achieve it.
</p>
<a href="#positions">
<Button className="text-white xl:text-sm">Open positions</Button>
<Button className="text-white xl:text-sm mt-4">Open positions</Button>
</a>
</div>
-1
View File
@@ -69,7 +69,6 @@ export default function IndexPage() {
'8GB database & 100GB file storage',
'50GB bandwidth',
'5GB file uploads',
'Social OAuth providers',
'100,000 monthly active users',
'2M Edge Function invocations',
'Daily backups',
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+20
View File
@@ -39040,6 +39040,7 @@
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^17.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"sql-formatter": "^12.2.0",
"storybook-dark-mode": "^2.1.1",
"typescript": "4.3.4"
@@ -39077,6 +39078,15 @@
"@types/react": "*"
}
},
"packages/ui/node_modules/@types/react-syntax-highlighter": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.6.tgz",
"integrity": "sha512-i7wFuLbIAFlabTeD2I1cLjEOrG/xdMa/rpx2zwzAoGHuXJDhSqp9BSfDlMHSh9JSuNfxHk9eEmMX6D55GiyjGg==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"packages/ui/node_modules/clsx": {
"version": "1.2.1",
"license": "MIT",
@@ -68097,6 +68107,7 @@
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^17.0.11",
"@types/react-syntax-highlighter": "*",
"autoprefixer": "^10.4.2",
"clsx": "^1.2.1",
"cmdk-supabase": "^0.2.2",
@@ -68150,6 +68161,15 @@
"@types/react": "*"
}
},
"@types/react-syntax-highlighter": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.6.tgz",
"integrity": "sha512-i7wFuLbIAFlabTeD2I1cLjEOrG/xdMa/rpx2zwzAoGHuXJDhSqp9BSfDlMHSh9JSuNfxHk9eEmMX6D55GiyjGg==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"clsx": {
"version": "1.2.1"
},
+3 -5
View File
@@ -5,8 +5,6 @@ export interface UseThemeProps {
/**
* Toggle between dark mode and light mode.
*
* ---
*
* If `darkMode` left `undefined`, toggles between modes.
*
* If `darkMode` set, forces
@@ -31,11 +29,11 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
useEffect(() => {
const key = localStorage.getItem('supabaseDarkMode')
const mode = key === 'true'
// Default to dark mode if no preference config
const mode = !key || key === 'true'
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
toggleTheme(mode)
prefersDark ? toggleTheme(true) : toggleTheme(mode)
}, [])
const toggleTheme: UseThemeProps['toggleTheme'] = (darkMode) => {
+3 -2
View File
@@ -35,6 +35,7 @@
"postcss": "^8.4.5",
"prop-types": "^15.7.2",
"react-copy-to-clipboard": "^5.1.0",
"react-countdown": "^2.3.5",
"react-feather": "^2.0.10",
"react-markdown": "^8.0.3",
"react-syntax-highlighter": "^15.5.0",
@@ -42,8 +43,7 @@
"sse.js": "^0.6.1",
"tailwind-merge": "^1.10.0",
"tailwindcss": "^3.0.15",
"tailwindcss-radix": "^1.6.0",
"react-countdown": "^2.3.5"
"tailwindcss-radix": "^1.6.0"
},
"devDependencies": {
"@storybook/react": "^6.5.16",
@@ -51,6 +51,7 @@
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^17.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"sql-formatter": "^12.2.0",
"storybook-dark-mode": "^2.1.1",
"typescript": "4.3.4"
@@ -89,9 +89,11 @@ export const CodeBlock = ({
)}
{className ? (
<div className="relative max-w-[90vw] md:max-w-none overflow-auto">
{/* @ts-ignore */}
<SyntaxHighlighter
language={lang}
wrapLines={true}
// @ts-ignore
style={monokaiTheme}
className={[
'code-block border p-4 w-full !my-0 !bg-scale-300',
@@ -282,7 +282,7 @@ const GenerateSQL = () => {
)}
<Input
inputRef={inputRef}
className="bg-scale-100 rounded mx-3 mb-4"
className="bg-scale-100 rounded mx-3 mb-4 [&_input]:pr-32 md:[&_input]:pr-40"
autoFocus
placeholder={
isLoading || isResponding
@@ -609,7 +609,7 @@ const DomainsForm = () => {
<div className="px-5">
<p className="text-scale-1100 text-sm">
This will add a domain to a list of allowed domains that can interact with
your Authenticaton services for this project.
your Authentication services for this project.
</p>
</div>
<div className="border-overlay-border border-t"></div>
File diff suppressed because it is too large. Load diff
+40 -40
View File
@@ -3672,7 +3672,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 400,
"line": 410,
"character": 8
}
],
@@ -3724,7 +3724,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 613,
"line": 625,
"character": 8
}
],
@@ -3762,7 +3762,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 615,
"line": 627,
"character": 8
}
],
@@ -3784,7 +3784,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 616,
"line": 628,
"character": 10
}
],
@@ -3814,7 +3814,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 618,
"line": 630,
"character": 8
}
],
@@ -3851,7 +3851,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 621,
"line": 633,
"character": 8
}
],
@@ -3873,7 +3873,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 622,
"line": 634,
"character": 10
}
],
@@ -3902,7 +3902,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 624,
"line": 636,
"character": 8
}
],
@@ -3940,7 +3940,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 627,
"line": 639,
"character": 8
}
],
@@ -3962,7 +3962,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 628,
"line": 640,
"character": 10
}
],
@@ -3991,7 +3991,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 630,
"line": 642,
"character": 8
}
],
@@ -4029,7 +4029,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 676,
"line": 688,
"character": 8
}
],
@@ -4125,7 +4125,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 987,
"line": 1000,
"character": 2
}
],
@@ -4227,7 +4227,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 988,
"line": 1001,
"character": 4
}
],
@@ -4249,7 +4249,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 988,
"line": 1001,
"character": 12
}
],
@@ -4292,7 +4292,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 822,
"line": 834,
"character": 8
}
],
@@ -4336,7 +4336,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 822,
"line": 834,
"character": 42
}
],
@@ -4382,7 +4382,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1027,
"line": 1040,
"character": 8
}
],
@@ -4440,7 +4440,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1031,
"line": 1044,
"character": 6
}
],
@@ -4463,7 +4463,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1030,
"line": 1043,
"character": 6
}
],
@@ -4509,7 +4509,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1035,
"line": 1048,
"character": 8
}
],
@@ -4533,7 +4533,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1036,
"line": 1049,
"character": 8
}
],
@@ -4570,7 +4570,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1038,
"line": 1051,
"character": 8
}
],
@@ -4588,7 +4588,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1038,
"line": 1051,
"character": 20
}
],
@@ -4627,7 +4627,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 758,
"line": 770,
"character": 8
}
],
@@ -4669,7 +4669,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 759,
"line": 771,
"character": 4
}
],
@@ -4687,7 +4687,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 760,
"line": 772,
"character": 4
}
],
@@ -4733,7 +4733,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 430,
"line": 440,
"character": 8
}
],
@@ -4792,7 +4792,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 386,
"line": 396,
"character": 8
}
],
@@ -4845,7 +4845,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 478,
"line": 488,
"character": 8
}
],
@@ -4899,7 +4899,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 336,
"line": 346,
"character": 8
}
],
@@ -4953,7 +4953,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 584,
"line": 596,
"character": 8
}
],
@@ -5007,7 +5007,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 962,
"line": 974,
"character": 8
}
],
@@ -5043,7 +5043,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 962,
"line": 974,
"character": 29
}
],
@@ -5144,7 +5144,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1325,
"line": 1346,
"character": 8
}
],
@@ -5157,7 +5157,7 @@
"flags": {},
"comment": {
"shortText": "Starts an auto-refresh process in the background. The session is checked\nevery few seconds. Close to the time of expiration a process is started to\nrefresh the session. If refreshing fails it will be retried for as long as\nnecessary.",
"text": "If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need\nto call this function, it will be called for you.\n\nOn browsers the refresh process works only when the tab/window is in the\nforeground to conserve resources as well as prevent race conditions and\nflooding auth with requests. If you call this method any managed\nvisibility change callback will be removed and you must manage visibility\nchanges on your own.\n\nOn non-browser platforms the refresh process works *continuously* in the\nbackground, which may not be desirable. You should hook into your\nplatform's foreground indication mechanism and call these methods\nappropriately to conserve resources.\n\n{@see #stopAutoRefresh}\n"
"text": "If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need\nto call this function, it will be called for you.\n\nOn browsers the refresh process works only when the tab/window is in the\nforeground to conserve resources as well as prevent race conditions and\nflooding auth with requests. If you call this method any managed\nvisibility change callback will be removed and you must manage visibility\nchanges on your own.\n\nOn non-browser platforms the refresh process works *continuously* in the\nbackground, which may not be desireable. You should hook into your\nplatform's foreground indication mechanism and call these methods\nappropriately to conserve resources.\n\n{@see #stopAutoRefresh}\n"
},
"type": {
"type": "reference",
@@ -5183,7 +5183,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1338,
"line": 1359,
"character": 8
}
],
@@ -5222,7 +5222,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 705,
"line": 717,
"character": 8
}
],
@@ -5275,7 +5275,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 708,
"line": 720,
"character": 6
}
],
@@ -5322,7 +5322,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 531,
"line": 543,
"character": 8
}
],
@@ -8642,7 +8642,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 400,
"line": 410,
"character": 8
}
],
@@ -8694,7 +8694,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 613,
"line": 625,
"character": 8
}
],
@@ -8732,7 +8732,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 615,
"line": 627,
"character": 8
}
],
@@ -8754,7 +8754,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 616,
"line": 628,
"character": 10
}
],
@@ -8784,7 +8784,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 618,
"line": 630,
"character": 8
}
],
@@ -8821,7 +8821,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 621,
"line": 633,
"character": 8
}
],
@@ -8843,7 +8843,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 622,
"line": 634,
"character": 10
}
],
@@ -8872,7 +8872,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 624,
"line": 636,
"character": 8
}
],
@@ -8910,7 +8910,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 627,
"line": 639,
"character": 8
}
],
@@ -8932,7 +8932,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 628,
"line": 640,
"character": 10
}
],
@@ -8961,7 +8961,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 630,
"line": 642,
"character": 8
}
],
@@ -8999,7 +8999,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 676,
"line": 688,
"character": 8
}
],
@@ -9095,7 +9095,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 987,
"line": 1000,
"character": 2
}
],
@@ -9197,7 +9197,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 988,
"line": 1001,
"character": 4
}
],
@@ -9219,7 +9219,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 988,
"line": 1001,
"character": 12
}
],
@@ -9467,7 +9467,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 822,
"line": 834,
"character": 8
}
],
@@ -9511,7 +9511,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 822,
"line": 834,
"character": 42
}
],
@@ -9557,7 +9557,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1027,
"line": 1040,
"character": 8
}
],
@@ -9615,7 +9615,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1031,
"line": 1044,
"character": 6
}
],
@@ -9638,7 +9638,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1030,
"line": 1043,
"character": 6
}
],
@@ -9684,7 +9684,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1035,
"line": 1048,
"character": 8
}
],
@@ -9708,7 +9708,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1036,
"line": 1049,
"character": 8
}
],
@@ -9745,7 +9745,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1038,
"line": 1051,
"character": 8
}
],
@@ -9763,7 +9763,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1038,
"line": 1051,
"character": 20
}
],
@@ -9802,7 +9802,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 758,
"line": 770,
"character": 8
}
],
@@ -9844,7 +9844,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 759,
"line": 771,
"character": 4
}
],
@@ -9862,7 +9862,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 760,
"line": 772,
"character": 4
}
],
@@ -9908,7 +9908,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 430,
"line": 440,
"character": 8
}
],
@@ -10146,7 +10146,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 386,
"line": 396,
"character": 8
}
],
@@ -10517,7 +10517,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 478,
"line": 488,
"character": 8
}
],
@@ -10963,7 +10963,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 336,
"line": 346,
"character": 8
}
],
@@ -11326,7 +11326,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 584,
"line": 596,
"character": 8
}
],
@@ -11669,7 +11669,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 962,
"line": 974,
"character": 8
}
],
@@ -11705,7 +11705,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 962,
"line": 974,
"character": 29
}
],
@@ -12194,7 +12194,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1325,
"line": 1346,
"character": 8
}
],
@@ -12207,7 +12207,7 @@
"flags": {},
"comment": {
"shortText": "Starts an auto-refresh process in the background. The session is checked\nevery few seconds. Close to the time of expiration a process is started to\nrefresh the session. If refreshing fails it will be retried for as long as\nnecessary.",
"text": "If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need\nto call this function, it will be called for you.\n\nOn browsers the refresh process works only when the tab/window is in the\nforeground to conserve resources as well as prevent race conditions and\nflooding auth with requests. If you call this method any managed\nvisibility change callback will be removed and you must manage visibility\nchanges on your own.\n\nOn non-browser platforms the refresh process works *continuously* in the\nbackground, which may not be desirable. You should hook into your\nplatform's foreground indication mechanism and call these methods\nappropriately to conserve resources.\n\n{@see #stopAutoRefresh}\n"
"text": "If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need\nto call this function, it will be called for you.\n\nOn browsers the refresh process works only when the tab/window is in the\nforeground to conserve resources as well as prevent race conditions and\nflooding auth with requests. If you call this method any managed\nvisibility change callback will be removed and you must manage visibility\nchanges on your own.\n\nOn non-browser platforms the refresh process works *continuously* in the\nbackground, which may not be desireable. You should hook into your\nplatform's foreground indication mechanism and call these methods\nappropriately to conserve resources.\n\n{@see #stopAutoRefresh}\n"
},
"type": {
"type": "reference",
@@ -12233,7 +12233,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 1338,
"line": 1359,
"character": 8
}
],
@@ -12272,7 +12272,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 705,
"line": 717,
"character": 8
}
],
@@ -12441,7 +12441,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 708,
"line": 720,
"character": 6
}
],
@@ -12488,7 +12488,7 @@
"sources": [
{
"fileName": "src/GoTrueClient.ts",
"line": 531,
"line": 543,
"character": 8
}
],
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -5020,7 +5020,7 @@
"kindString": "Call signature",
"flags": {},
"comment": {
"shortText": "Creates a signed upload URL.\nSigned upload URLs can be used to upload files to the bucket without further authentication.\nThey are valid for one minute."
"shortText": "Creates a signed upload URL.\nSigned upload URLs can be used to upload files to the bucket without further authentication.\nThey are valid for 2 hours."
},
"parameters": [
{
@@ -5257,7 +5257,7 @@
"kindString": "Call signature",
"flags": {},
"comment": {
"shortText": "Creates a signed upload URL.\nSigned upload URLs can be used to upload files to the bucket without further authentication.\nThey are valid for one minute."
"shortText": "Creates a signed upload URL.\nSigned upload URLs can be used to upload files to the bucket without further authentication.\nThey are valid for 2 hours."
},
"parameters": [
{
+3 -2
View File
@@ -91,8 +91,9 @@
},
"allowed_mime_types": {
"type": "array",
"items": { "type": "string" },
"examples": [["image/png", "image/jpg"]]
"nullable": true,
"examples": [["image/png", "image/jpg"]],
"items": { "type": "string" }
}
},
"required": ["name"]
@@ -43,10 +43,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
},
@@ -179,9 +176,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
},
@@ -200,10 +195,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -257,20 +249,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
},
@@ -283,10 +267,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -335,13 +316,7 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
@@ -401,10 +376,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
},
@@ -416,9 +388,7 @@
}
},
"summary": "List endpoints",
"tags": [
"management"
]
"tags": ["management"]
},
"post": {
"callbacks": {},
@@ -465,10 +435,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
}
@@ -519,10 +486,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
}
@@ -542,9 +506,7 @@
}
},
"summary": "Create endpoint",
"tags": [
"management"
]
"tags": ["management"]
}
},
"/api/endpoints/{token}": {
@@ -585,10 +547,7 @@
}
},
"summary": "Delete endpoint",
"tags": [
"management",
"management"
]
"tags": ["management", "management"]
},
"get": {
"callbacks": {},
@@ -646,10 +605,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
}
@@ -669,9 +625,7 @@
}
},
"summary": "Fetch endpoint",
"tags": [
"management"
]
"tags": ["management"]
},
"patch": {
"callbacks": {},
@@ -728,10 +682,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
}
@@ -782,10 +733,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
}
@@ -805,9 +753,7 @@
}
},
"summary": "Update endpoint",
"tags": [
"management"
]
"tags": ["management"]
},
"put": {
"callbacks": {},
@@ -864,10 +810,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
}
@@ -918,10 +861,7 @@
"type": "string"
}
},
"required": [
"name",
"query"
],
"required": ["name", "query"],
"title": "Endpoint",
"type": "object"
}
@@ -941,9 +881,7 @@
}
},
"summary": "Update endpoint",
"tags": [
"management"
]
"tags": ["management"]
}
},
"/api/sources": {
@@ -1046,9 +984,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
},
@@ -1060,9 +996,7 @@
}
},
"summary": "List sources",
"tags": [
"management"
]
"tags": ["management"]
},
"post": {
"callbacks": {},
@@ -1161,9 +1095,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
}
@@ -1266,9 +1198,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
}
@@ -1288,9 +1218,7 @@
}
},
"summary": "Create source",
"tags": [
"management"
]
"tags": ["management"]
}
},
"/api/sources/{token}": {
@@ -1331,9 +1259,7 @@
}
},
"summary": "Delete source",
"tags": [
"management"
]
"tags": ["management"]
},
"get": {
"callbacks": {},
@@ -1443,9 +1369,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
}
@@ -1465,9 +1389,7 @@
}
},
"summary": "Fetch source",
"tags": [
"management"
]
"tags": ["management"]
},
"patch": {
"callbacks": {},
@@ -1576,9 +1498,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
}
@@ -1681,9 +1601,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
}
@@ -1703,9 +1621,7 @@
}
},
"summary": "Update source",
"tags": [
"management"
]
"tags": ["management"]
},
"put": {
"callbacks": {},
@@ -1814,9 +1730,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
}
@@ -1919,9 +1833,7 @@
"type": "string"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Source",
"type": "object"
}
@@ -1941,9 +1853,7 @@
}
},
"summary": "Update source",
"tags": [
"management"
]
"tags": ["management"]
}
},
"/api/teams": {
@@ -1971,10 +1881,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2028,20 +1935,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
},
@@ -2053,9 +1952,7 @@
}
},
"summary": "List teams",
"tags": [
"management"
]
"tags": ["management"]
},
"post": {
"callbacks": {},
@@ -2079,10 +1976,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2136,20 +2030,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
}
@@ -2177,10 +2063,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2234,20 +2117,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
}
@@ -2267,9 +2142,7 @@
}
},
"summary": "Create Team",
"tags": [
"management"
]
"tags": ["management"]
}
},
"/api/teams/{token}": {
@@ -2310,9 +2183,7 @@
}
},
"summary": "Delete Team",
"tags": [
"management"
]
"tags": ["management"]
},
"get": {
"callbacks": {},
@@ -2347,10 +2218,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2404,20 +2272,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
}
@@ -2437,9 +2297,7 @@
}
},
"summary": "Fetch team",
"tags": [
"management"
]
"tags": ["management"]
},
"patch": {
"callbacks": {},
@@ -2473,10 +2331,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2530,20 +2385,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
}
@@ -2571,10 +2418,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2628,20 +2472,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
}
@@ -2661,9 +2497,7 @@
}
},
"summary": "Update team",
"tags": [
"management"
]
"tags": ["management"]
},
"put": {
"callbacks": {},
@@ -2697,10 +2531,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2754,20 +2585,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
}
@@ -2795,10 +2618,7 @@
"type": "string"
}
},
"required": [
"email",
"name"
],
"required": ["email", "name"],
"title": "TeamUser",
"type": "object"
},
@@ -2852,20 +2672,12 @@
"type": "string"
}
},
"required": [
"email",
"provider",
"token",
"provider_uid",
"api_key"
],
"required": ["email", "provider", "token", "provider_uid", "api_key"],
"title": "User",
"type": "object"
}
},
"required": [
"name"
],
"required": ["name"],
"title": "Team",
"type": "object"
}
@@ -2885,9 +2697,7 @@
}
},
"summary": "Update team",
"tags": [
"management"
]
"tags": ["management"]
}
}
},
@@ -147,10 +147,11 @@
},
"allowed_mime_types": {
"type": "array",
"nullable": true,
"examples": [["image/png", "image/jpg"]],
"items": {
"type": "string"
},
"examples": [["image/png", "image/jpg"]]
}
}
},
"required": ["name"]
@@ -141,7 +141,7 @@ const RedirectUrls = () => {
<div className="px-5">
<p className="text-sm text-scale-1100">
This will add a URL to a list of allowed URLs that can interact with your
Authenticaton services for this project.
Authentication services for this project.
</p>
</div>
<div className="border-overlay-border border-t" />
@@ -73,6 +73,19 @@ export const USAGE_BASED_PRODUCTS = [
</span>
),
},
{
key: 'monthly_active_sso_users',
attribute: 'total_auth_billing_period_sso_mau',
title: 'Monthly Single Sign-On Users',
units: 'absolute',
costPerUnit: 0.015,
tooltip: (
<span>
The amount of distinct Single Sign-On users requesting your API throughout the billing period. Resets
at the beginning of every billing period.
</span>
),
},
],
},
{
@@ -272,42 +272,40 @@ const EnterpriseUpdate: FC<Props> = ({
</div>
</div>
</div>
<div className="min-w-[440px]">
<PaymentSummaryPanel
isSpendCapEnabled={true}
isSubmitting={isSubmitting}
isRefreshingPreview={isRefreshingPreview}
currentSubscription={currentSubscription}
subscriptionPreview={subscriptionPreview}
// Current subscription configuration based on DB
currentPlan={currentSubscription.tier}
currentAddons={currentAddons}
// Selected subscription configuration based on UI
selectedAddons={selectedAddons}
paymentMethods={paymentMethods}
isLoadingPaymentMethods={isLoadingPaymentMethods}
selectedPaymentMethod={selectedPaymentMethodId}
onSelectPaymentMethod={setSelectedPaymentMethodId}
onSelectAddNewPaymentMethod={() => {
setShowAddPaymentMethodModal(true)
}}
beforeConfirmPayment={beforeConfirmPayment}
onConfirmPayment={onConfirmPayment}
captcha={
<HCaptcha
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
}
/>
</div>
<PaymentSummaryPanel
isSpendCapEnabled={true}
isSubmitting={isSubmitting}
isRefreshingPreview={isRefreshingPreview}
currentSubscription={currentSubscription}
subscriptionPreview={subscriptionPreview}
// Current subscription configuration based on DB
currentPlan={currentSubscription.tier}
currentAddons={currentAddons}
// Selected subscription configuration based on UI
selectedAddons={selectedAddons}
paymentMethods={paymentMethods}
isLoadingPaymentMethods={isLoadingPaymentMethods}
selectedPaymentMethod={selectedPaymentMethodId}
onSelectPaymentMethod={setSelectedPaymentMethodId}
onSelectAddNewPaymentMethod={() => {
setShowAddPaymentMethodModal(true)
}}
beforeConfirmPayment={beforeConfirmPayment}
onConfirmPayment={onConfirmPayment}
captcha={
<HCaptcha
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
}
/>
</Transition>
<AddNewPaymentMethodModal
@@ -1,10 +1,11 @@
import { FC, useReducer, useState, useEffect, useRef } from 'react'
import { useReducer, useState, useEffect, useRef } from 'react'
import { useRouter } from 'next/router'
import { includes, without } from 'lodash'
import { Transition } from '@headlessui/react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Form, Input, Modal, IconArrowLeft, Alert } from 'ui'
import { useStore } from 'hooks'
import { useFlag, useStore } from 'hooks'
import { post, patch } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { CANCELLATION_REASONS } from '../Billing.constants'
@@ -13,13 +14,13 @@ import { SubscriptionPreview } from '../Billing.types'
import { StripeSubscription } from 'components/interfaces/Billing'
import HCaptcha from '@hcaptcha/react-hcaptcha'
interface Props {
interface ExitSurveyProps {
freeTier: any
subscription?: StripeSubscription
onSelectBack: () => void
}
const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
const ExitSurvey = ({ freeTier, subscription, onSelectBack }: ExitSurveyProps) => {
const router = useRouter()
const { app, ui } = useStore()
const projectId = ui.selectedProject?.id ?? -1
@@ -27,6 +28,7 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
const captchaRef = useRef<HCaptcha>(null)
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const formRef = useRef<any>()
@@ -252,26 +254,52 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
name="message"
label="Anything else that we can improve on?"
/>
<div className="flex items-center space-x-2">
<Button
htmlType="button"
type="default"
onClick={() => router.push(`/project/${projectRef}/settings/billing/update`)}
>
Cancel
</Button>
<Button
type="danger"
htmlType="submit"
loading={isSubmitting}
disabled={isSubmitting}
>
Confirm downgrade
</Button>
<div className="flex items-center justify-between">
<p className="text-xs text-scale-900">
The unused amount for the remaining of your billing cycle will be refunded as
credits
</p>
<div className="flex items-center space-x-2">
<Button
htmlType="button"
type="default"
onClick={() =>
router.push(`/project/${projectRef}/settings/billing/update`)
}
>
Cancel
</Button>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger className="w-full">
<Button
type="danger"
htmlType="submit"
loading={isSubmitting}
disabled={projectUpdateDisabled || isSubmitting}
>
Confirm downgrade
</Button>
</Tooltip.Trigger>
{projectUpdateDisabled ? (
<Tooltip.Portal>
<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">
Subscription changes are currently disabled. Our engineers are
working on a fix.
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
) : null}
</Tooltip.Root>
</div>
</div>
<div className="self-center">
<HCaptcha
@@ -3,7 +3,7 @@ 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 { checkPermissions, useFlag, useStore } from 'hooks'
import { BASE_PATH, PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { SubscriptionPreview } from '../Billing.types'
import { getProductPrice, validateSubscriptionUpdatePayload } from '../Billing.utils'
@@ -14,6 +14,7 @@ import { getPITRDays } from './PaymentSummaryPanel.utils'
import ConfirmPaymentModal from './ConfirmPaymentModal'
import { StripeSubscription } from '../Subscription/Subscription.types'
import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import clsx from 'clsx'
// [Joshen] PITR stuff can be undefined for now until we officially launch PITR self serve
@@ -92,6 +93,7 @@ const PaymentSummaryPanel = ({
)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const selectedPlanCost =
selectedPlan !== undefined
@@ -178,7 +180,11 @@ const PaymentSummaryPanel = ({
return (
<>
<div
className="w-full px-6 py-10 space-y-8 overflow-y-auto border-l bg-panel-body-light dark:bg-panel-body-dark lg:px-12"
className={clsx(
'bg-panel-body-light dark:bg-panel-body-dark overflow-y-auto border-l',
'px-6 lg:px-12 py-10 space-y-8',
'min-w-[450px] max-w-[450px] 2xl:min-w-[630px] 2xl:max-w-[630px]'
)}
style={{ height: 'calc(100vh - 57px)' }}
>
<p>Payment Summary</p>
@@ -447,6 +453,7 @@ const PaymentSummaryPanel = ({
loading={isSubmitting}
disabled={
!isActive ||
projectUpdateDisabled ||
isSubmitting ||
isLoadingPaymentMethods ||
!hasChangesToPlan ||
@@ -474,6 +481,8 @@ const PaymentSummaryPanel = ({
? 'Please select a payment method'
: !isActive
? 'Unable to update subscription as project is not active'
: projectUpdateDisabled
? 'Subscription changes are currently disabled. Our engineers are working on a fix.'
: ''}
</span>
</div>
@@ -30,7 +30,7 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
enterTo="transform opacity-100 translate-x-0"
>
<div>
<h4 className="text-lg mb-8">Change your project's subscription</h4>
<h4 className="text-lg">Change your project's subscription</h4>
<div className={`grid py-8 grid-cols-2 ${gridCols}`}>
{formattedTiers.map((plan) => {
return (
@@ -3,7 +3,7 @@ import { Button, IconLoader } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useFlag } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { StripeProduct } from 'components/interfaces/Billing'
import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -14,9 +14,11 @@ interface PlanCTAButtonProps {
}
const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps) => {
const isActive = useIsProjectActive()
const isProjectActive = useIsProjectActive()
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const isTeamTier = currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const getButtonType = (plan: any, currentPlan: any) => {
if (['Free tier'].includes(plan.name)) {
// Free is always default
@@ -85,7 +87,8 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
const type = getButtonType(plan, currentPlan)
const ctaText = getButtonText(plan, currentPlan)
const disabled =
(!isActive && plan.name !== 'Enterprise') ||
(!isProjectActive && plan.name !== 'Enterprise') ||
(isTeamTier && plan.name !== 'Enterprise' && plan.name !== 'Team tier') ||
(plan.id === STRIPE_PRODUCT_IDS.FREE && currentPlan.prod_id === STRIPE_PRODUCT_IDS.FREE)
if (plan.name === 'Enterprise') {
@@ -113,7 +116,7 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
{ctaText}
</Button>
</Tooltip.Trigger>
{(projectUpdateDisabled || !isActive) && (
{((disabled && isTeamTier) || projectUpdateDisabled || !isProjectActive) && (
<Tooltip.Portal>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
@@ -127,8 +130,10 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
<span className="text-xs text-scale-1200 text-center">
{projectUpdateDisabled
? 'Subscription changes are currently disabled, our engineers are working on a fix'
: !isActive
: !isProjectActive
? 'Unable to update subscription as project is not active'
: isTeamTier
? "Unable to update subscription from Team tier. Please reach out to us via support if you'd like to change your plan"
: ''}
</span>
</div>
@@ -39,7 +39,6 @@ export const PRICING_META = {
'8GB database & 100GB file storage',
'50GB bandwidth',
'5GB file uploads',
'Social OAuth providers',
'100,000 monthly active users',
'2M Edge Function invocations',
'Daily backups',
@@ -321,43 +321,41 @@ const ProUpgrade: FC<Props> = ({
</div>
</div>
</div>
<div className="min-w-[440px]">
<PaymentSummaryPanel
isRefreshingPreview={isRefreshingPreview}
subscriptionPreview={subscriptionPreview}
isSpendCapEnabled={isSpendCapEnabled}
// Current subscription configuration based on DB
currentPlan={currentSubscription.tier}
currentAddons={currentAddons}
currentSubscription={currentSubscription}
// Selected subscription configuration based on UI
selectedPlan={selectedTier}
selectedAddons={selectedAddons}
paymentMethods={paymentMethods}
isLoadingPaymentMethods={isLoadingPaymentMethods}
selectedPaymentMethod={selectedPaymentMethodId}
onSelectPaymentMethod={setSelectedPaymentMethodId}
onSelectAddNewPaymentMethod={() => {
setShowAddPaymentMethodModal(true)
}}
beforeConfirmPayment={beforeConfirmPayment}
onConfirmPayment={onConfirmPayment}
isSubmitting={isSubmitting}
captcha={
<HCaptcha
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
}
/>
</div>
<PaymentSummaryPanel
isRefreshingPreview={isRefreshingPreview}
subscriptionPreview={subscriptionPreview}
isSpendCapEnabled={isSpendCapEnabled}
// Current subscription configuration based on DB
currentPlan={currentSubscription.tier}
currentAddons={currentAddons}
currentSubscription={currentSubscription}
// Selected subscription configuration based on UI
selectedPlan={selectedTier}
selectedAddons={selectedAddons}
paymentMethods={paymentMethods}
isLoadingPaymentMethods={isLoadingPaymentMethods}
selectedPaymentMethod={selectedPaymentMethodId}
onSelectPaymentMethod={setSelectedPaymentMethodId}
onSelectAddNewPaymentMethod={() => {
setShowAddPaymentMethodModal(true)
}}
beforeConfirmPayment={beforeConfirmPayment}
onConfirmPayment={onConfirmPayment}
isSubmitting={isSubmitting}
captcha={
<HCaptcha
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
}
/>
</Transition>
<AddNewPaymentMethodModal
@@ -79,6 +79,42 @@ const SpendCapModal: FC<Props> = ({ visible, onHide }) => {
<p className="w-[25%] text-sm">$0.00325/user</p>
</div>
</div>
<div className="py-2">
<div className="flex items-center px-4 py-1">
<div className="flex w-[50%] items-center space-x-2">
<p className="text-sm">Single Sign-On (SAML 2.0) MAUs</p>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconHelpCircle
size={16}
strokeWidth={1.5}
className="transition opacity-50 cursor-pointer hover:opacity-100"
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Portal>
<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">
Single Sign-On Monthly Active Users: A user that has made an API
request in the last month
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Portal>
</Tooltip.Root>
</div>
<p className="w-[25%] text-sm">50</p>
<p className="w-[25%] text-sm">$0.015/user</p>
</div>
</div>
<div className="py-2">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Storage size</p>
@@ -1,6 +1,6 @@
import dayjs from 'dayjs'
import Link from 'next/link'
import { FC, useEffect } from 'react'
import { useRouter } from 'next/router'
import { Button, Loading } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
@@ -23,7 +23,6 @@ interface Props {
}
const Subscription: FC<Props> = ({ showProjectName = false }) => {
const router = useRouter()
const { ui } = useStore()
const { ref: projectRef } = useParams()
const isActive = useIsProjectActive()
@@ -41,10 +40,12 @@ const Subscription: FC<Props> = ({ showProjectName = false }) => {
isLoading: loading,
error,
} = useProjectSubscriptionQuery({ projectRef: ui.selectedProject?.ref })
const { data: usage, isLoading: loadingUsage } = useProjectUsageQuery({ projectRef })
const { data: usage } = useProjectUsageQuery({ projectRef })
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
const isTeam = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const isEnterprise = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
const canOnlyUpdateAddons = isTeam || isEnterprise
const addOns = subscription?.addons ?? []
const paid = subscription && subscription.tier.unit_amount > 0
@@ -69,6 +70,12 @@ const Subscription: FC<Props> = ({ showProjectName = false }) => {
return basePlanCost + totalAddOnCost + totalUsageCost
}
const subscriptionUpdateURL = isEnterprise
? `/project/${projectRef}/settings/billing/update/enterprise`
: isTeam
? `/project/${projectRef}/settings/billing/update/team`
: `/project/${projectRef}/settings/billing/update`
useEffect(() => {
if (error) {
ui.setNotification({
@@ -92,18 +99,16 @@ const Subscription: FC<Props> = ({ showProjectName = false }) => {
<div className="flex flex-col items-end space-y-2">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
disabled={!canUpdateSubscription || projectUpdateDisabled || !isActive}
onClick={() => {
const url = isEnterprise
? `/project/${projectRef}/settings/billing/update/enterprise`
: `/project/${projectRef}/settings/billing/update`
router.push(url)
}}
type="primary"
>
{isEnterprise ? 'Change add-ons' : 'Change subscription'}
</Button>
<Link href={subscriptionUpdateURL}>
<a>
<Button
type="primary"
disabled={!canUpdateSubscription || projectUpdateDisabled || !isActive}
>
{canOnlyUpdateAddons ? 'Change add-ons' : 'Change subscription'}
</Button>
</a>
</Link>
</Tooltip.Trigger>
{!canUpdateSubscription || projectUpdateDisabled || !isActive ? (
<Tooltip.Portal>
@@ -1,4 +1,4 @@
import { FC, useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Transition } from '@headlessui/react'
import { useRouter } from 'next/router'
@@ -25,27 +25,24 @@ import {
formatCustomDomainOptions,
formatPITROptions,
} from './AddOns/AddOns.utils'
import BackButton from 'components/ui/BackButton'
import SupportPlan from './AddOns/SupportPlan'
import HCaptcha from '@hcaptcha/react-hcaptcha'
// Do not allow compute size changes for af-south-1
interface Props {
interface TeamUpgradeProps {
products: { tiers: any[]; addons: SubscriptionAddon[] }
paymentMethods?: PaymentMethod[]
currentSubscription: StripeSubscription
isLoadingPaymentMethods: boolean
onSelectBack: () => void
}
const TeamUpgrade: FC<Props> = ({
const TeamUpgrade = ({
products,
paymentMethods,
currentSubscription,
isLoadingPaymentMethods,
onSelectBack,
}) => {
}: TeamUpgradeProps) => {
const { app, ui } = useStore()
const router = useRouter()
@@ -225,8 +222,7 @@ const TeamUpgrade: FC<Props> = ({
>
<div className="flex-grow mt-10">
<div className="relative space-y-4">
<div className="px-32 mx-auto space-y-4 2xl:max-w-5xl">
<BackButton onClick={() => onSelectBack()} />
<div className="relative px-32 mx-auto space-y-4 2xl:max-w-5xl">
<h4 className="text-lg text-scale-900 !mb-8">Change your project's subscription</h4>
</div>
@@ -291,43 +287,41 @@ const TeamUpgrade: FC<Props> = ({
</div>
</div>
</div>
<div className="min-w-[440px]">
<PaymentSummaryPanel
isRefreshingPreview={isRefreshingPreview}
subscriptionPreview={subscriptionPreview}
isSpendCapEnabled={false}
// Current subscription configuration based on DB
currentPlan={currentSubscription.tier}
currentAddons={currentAddons}
currentSubscription={currentSubscription}
// Selected subscription configuration based on UI
selectedPlan={selectedTier}
selectedAddons={selectedAddons}
paymentMethods={paymentMethods}
isLoadingPaymentMethods={isLoadingPaymentMethods}
selectedPaymentMethod={selectedPaymentMethodId}
onSelectPaymentMethod={setSelectedPaymentMethodId}
onSelectAddNewPaymentMethod={() => {
setShowAddPaymentMethodModal(true)
}}
beforeConfirmPayment={beforeConfirmPayment}
onConfirmPayment={onConfirmPayment}
isSubmitting={isSubmitting}
captcha={
<HCaptcha
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
}
/>
</div>
<PaymentSummaryPanel
isRefreshingPreview={isRefreshingPreview}
subscriptionPreview={subscriptionPreview}
isSpendCapEnabled={false}
// Current subscription configuration based on DB
currentPlan={currentSubscription.tier}
currentAddons={currentAddons}
currentSubscription={currentSubscription}
// Selected subscription configuration based on UI
selectedPlan={selectedTier}
selectedAddons={selectedAddons}
paymentMethods={paymentMethods}
isLoadingPaymentMethods={isLoadingPaymentMethods}
selectedPaymentMethod={selectedPaymentMethodId}
onSelectPaymentMethod={setSelectedPaymentMethodId}
onSelectAddNewPaymentMethod={() => {
setShowAddPaymentMethodModal(true)
}}
beforeConfirmPayment={beforeConfirmPayment}
onConfirmPayment={onConfirmPayment}
isSubmitting={isSubmitting}
captcha={
<HCaptcha
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
}
/>
</Transition>
<AddNewPaymentMethodModal
@@ -33,7 +33,7 @@ const ProjectUsage: FC<Props> = ({}) => {
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
const startDate = dayjs()
.subtract(selectedInterval.startValue, selectedInterval.startUnit)
.subtract(selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType)
.format(DATE_FORMAT)
const endDate = dayjs().format(DATE_FORMAT)
const charts = data?.data
@@ -14,7 +14,8 @@ export interface ReportWidgetProps<T = any> {
tooltip?: string
className?: string
renderer: (props: ReportWidgetRendererProps) => React.ReactNode
params: BaseReportParams | LogsEndpointParams
// omitting params will hide the "View in logs explorer" button
params?: BaseReportParams | LogsEndpointParams
isLoading: boolean
}
@@ -64,38 +65,40 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
</div>
<p className="text-sm text-scale-1100">{props.description}</p>
</div>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
type="default"
icon={<IconExternalLink strokeWidth={1.5} />}
className="px-1"
onClick={() => {
router.push({
pathname: `/project/${projectRef}/logs/explorer`,
query: {
q: props.params?.sql,
its: props.params.iso_timestamp_start,
ite: props.params.iso_timestamp_end,
},
})
}}
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 max-w-xs leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">Open in Logs Explorer</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
{props.params && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
type="default"
icon={<IconExternalLink strokeWidth={1.5} />}
className="px-1"
onClick={() => {
router.push({
pathname: `/project/${projectRef}/logs/explorer`,
query: {
q: props.params?.sql,
its: props.params!.iso_timestamp_start,
ite: props.params!.iso_timestamp_end,
},
})
}}
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 max-w-xs leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">Open in Logs Explorer</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
)}
</div>
<LoadingOpacity className="w-full" active={props.isLoading}>
@@ -68,10 +68,8 @@ const ConnectionPooling: FC<Props> = () => {
)
}
const { project } = poolingConfiguration
// for older projects
if (!project.pgbouncer_enabled && project.pool_mode == null)
if (!poolingConfiguration.pgbouncer_enabled && poolingConfiguration.pool_mode == null)
return (
<Panel
title={
@@ -85,7 +83,7 @@ const ConnectionPooling: FC<Props> = () => {
</Panel.Content>
</Panel>
)
const formModel = project
const formModel = poolingConfiguration
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const connectionInfo = pluckObjectFields(formModel, DB_FIELDS)
const BOUNCER_FIELDS = [
@@ -137,15 +135,22 @@ export const PgbouncerConfig: FC<ConfigProps> = observer(
pool_mode: bouncerInfo.pool_mode || 'transaction',
default_pool_size: bouncerInfo.default_pool_size || '',
ignore_startup_parameters: bouncerInfo.ignore_startup_parameters || '',
pgbouncer_enabled: bouncerInfo.pgbouncer_enabled,
})
const updateConfig = async (updatedConfig: any) => {
try {
const response = await patch(`${API_URL}/props/pooling/${projectRef}/config`, updatedConfig)
const response = await patch(`${API_URL}/projects/${projectRef}/config/pgbouncer`, {
pgbouncer_enabled: updatedConfig.pgbouncer_enabled,
default_pool_size: updatedConfig.default_pool_size,
ignore_startup_parameters: updatedConfig.ignore_startup_parameters,
pool_mode: updatedConfig.pool_mode,
max_client_conn: updatedConfig.max_client_conn,
})
if (response.error) {
throw response.error
} else {
setUpdates({ ...response.project })
setUpdates({ ...response })
ui.setNotification({ category: 'success', message: 'Successfully saved settings' })
}
} catch (error: any) {
@@ -1,117 +1,29 @@
import { BarChart } from 'components/to-be-cleaned/Charts/ChartRenderer'
import dayjs from 'dayjs'
import BarChart, { BarChartProps } from 'components/ui/Charts/BarChart'
import { EventChartData, isUnixMicro, LogData, unixMicroToIsoTimestamp } from '.'
import { useMemo } from 'react'
interface Props {
data?: EventChartData[]
export interface LogEventChartProps {
data: EventChartData[]
onBarClick: (isoTimestamp: string) => void
}
const LogEventChart: React.FC<Props> = ({ data, onBarClick }) => {
// TODO: remove once endpoint returns iso timestamp directly
const transformedData = useMemo(() => {
return data?.map((d) => {
const iso = isUnixMicro(d.timestamp)
? unixMicroToIsoTimestamp(d.timestamp)
: dayjs(d.timestamp).toISOString()
return {
...d,
timestamp: iso,
// needed for bar chart
// TODO: remove once bar chart is refactored
period_start: iso,
}
})
}, [JSON.stringify(data)])
if (!data) return null
return (
<BarChart
minimalHeader
chartSize="tiny"
data={transformedData}
attribute="count"
label="Logs / Time"
onBarClick={(v?: { activePayload?: { payload: any }[] }) => {
if (!v || !v?.activePayload?.[0]?.payload) return
const unixOrIsoTimestamp = v.activePayload[0].payload.timestamp
const isoTimestamp = isUnixMicro(unixOrIsoTimestamp)
? unixMicroToIsoTimestamp(unixOrIsoTimestamp)
: unixOrIsoTimestamp
// 60s before
onBarClick(isoTimestamp)
}}
customDateFormat="MMM D, HH:mm:s"
noDataMessage={''}
/>
)
}
// const useAggregated = (data: LogData[]) => {
// const truncateToMinute = (micro: number) => Math.floor(micro / 1000 / 1000 / 60) * 60
// const truncateToHour = (micro: number) => Math.floor(micro / 1000 / 1000 / 60 / 60) * 60 * 60
// const getDiffMinute = (currentTimestamp: number, olderTimestamp: number) =>
// Math.round((currentTimestamp - olderTimestamp) / 1000 / 60)
// const getDiffHour = (currentTimestamp: number, olderTimestamp: number) =>
// Math.round((currentTimestamp - olderTimestamp) / 1000 / 60 / 60)
// const diffMultiplierMinute = (v: number) => v * 60
// const diffMultiplierHour = (v: number) => v * 60 * 60
// return useMemo(() => {
// const oldest = data[data.length - 1]
// if (!oldest) return
// const latest = data[0]
// const oldestDayjs = dayjs(oldest.timestamp / 1000)
// const latestDayjs = dayjs(latest.timestamp / 1000)
// let truncFunc = truncateToMinute
// let getDiff = getDiffMinute
// let diffMultiplier = diffMultiplierMinute
// if (Math.abs(oldestDayjs.diff(latestDayjs, 'day', true)) > 0.25) {
// truncFunc = truncateToHour
// getDiff = getDiffHour
// diffMultiplier = diffMultiplierHour
// }
// const countMap = data
// .map((d) => {
// // truncate to per-minute
// return { ...d, timestamp: truncateToMinute(d.timestamp) }
// })
// .reduce((acc, d) => {
// if (acc[d.timestamp]) {
// acc[d.timestamp] = acc[d.timestamp] + 1
// } else {
// acc[d.timestamp] = 1
// }
// return acc
// }, {} as TimestampMap)
// // Add in additional data points for empty minutes
// const oldestEvent = data[data.length - 1]
// if (!oldestEvent) return []
// const currentTimestamp = new Date().getTime()
// const oldestTimestampMicro = oldestEvent.timestamp
// const latestTimestamp = truncFunc(data[0]['timestamp'])
// const diff = getDiff(currentTimestamp, oldestTimestampMicro / 1000)
// for (const toAdd of Array.from(Array(diff).keys())) {
// const tsToCheck = truncFunc(oldestTimestampMicro) + diffMultiplier(toAdd)
// if (!(tsToCheck in countMap) && tsToCheck <= latestTimestamp) {
// countMap[tsToCheck] = 0
// }
// }
// let aggregated = []
// for (const [key, value] of Object.entries(countMap)) {
// const v: number = Number(key)
// aggregated.push({
// timestamp: key,
// timestampMicro: v * 1000 * 1000,
// count: value,
// })
// }
// return aggregated.sort((a, b) => a.timestampMicro - b.timestampMicro)
// }, [JSON.stringify(data)])
// }
const LogEventChart = ({ data, onBarClick }: LogEventChartProps) => (
<BarChart
minimalHeader
size="tiny"
yAxisKey="count"
xAxisKey="timestamp"
data={data as unknown as BarChartProps['data']}
title="Logs / Time"
onBarClick={(v?: { activePayload?: { payload: any }[] }) => {
if (!v || !v?.activePayload?.[0]?.payload) return
const unixOrIsoTimestamp = v.activePayload[0].payload.timestamp
const isoTimestamp = isUnixMicro(unixOrIsoTimestamp)
? unixMicroToIsoTimestamp(unixOrIsoTimestamp)
: unixOrIsoTimestamp
// 60s before
onBarClick(isoTimestamp)
}}
customDateFormat="MMM D, HH:mm:s"
/>
)
export default LogEventChart
@@ -1,11 +1,11 @@
import { Filters, LogData, LogsEndpointParams, LogsTableName, SQL_FILTER_TEMPLATES } from '.'
import dayjs, { Dayjs } from 'dayjs'
import { get } from 'lodash'
import { get, isEqual } from 'lodash'
import { StripeSubscription } from 'components/interfaces/Billing'
import { useMonaco } from '@monaco-editor/react'
import logConstants from 'shared-data/logConstants'
import BackwardIterator from 'components/ui/CodeEditor/Providers/BackwardIterator'
import { uniqBy } from 'lodash'
import uniqBy from 'lodash/uniqBy'
import { useEffect } from 'react'
/**
@@ -228,13 +228,22 @@ export const genChartQuery = (
) => {
const [startOffset, trunc] = calcChartStart(params)
const where = _genWhereStatement(table, filters)
let joins = 'cross join unnest(t.metadata) as metadata'
if (table === LogsTableName.EDGE) {
joins += ' \n cross join unnest(metadata.request) as request'
joins += ' \n cross join unnest(metadata.response) as response'
} else if (table === LogsTableName.POSTGRES) {
joins += ' \n cross join unnest(metadata.parsed) as parsed'
}
return `
SELECT
timestamp_trunc(t.timestamp, ${trunc}) as timestamp,
count(t.timestamp) as count
FROM
${table} t
cross join unnest(t.metadata) as metadata
${joins}
${
where
? where + ` and t.timestamp > '${startOffset.toISOString()}'`
@@ -349,3 +358,64 @@ export const useEditorHints = () => {
}
}, [monaco])
}
/**
* Assumes that all timestamps are in ISO-8601 UTC timezone.
*
* min/max are the datetime strings that extend beyond the given timeseries data.
*/
export const fillTimeseries = (
timeseriesData: any[],
timestampKey: string,
valueKey: string,
defaultValue: number,
min?: string,
max?: string
) => {
if (timeseriesData.length <= 1 && !(min || max)) return timeseriesData
const dates: unknown[] = timeseriesData.map((datum) => dayjs.utc(datum[timestampKey]))
const maxDate = max ? dayjs.utc(max) : dayjs.utc(Math.max.apply(null, dates as number[]))
const minDate = min ? dayjs.utc(min) : dayjs.utc(Math.min.apply(null, dates as number[]))
const truncationSample = timeseriesData.length > 0 ? timeseriesData[0][timestampKey] : min || max
const truncation = getTimestampTruncation(truncationSample)
const newData = timeseriesData.map((datum) => {
const iso = dayjs.utc(datum[timestampKey]).toISOString()
datum[timestampKey] = iso
return datum
})
const diff = maxDate.diff(minDate, truncation as dayjs.UnitType)
for (let i = 0; i <= diff; i++) {
const dateToMaybeAdd = minDate.add(i, truncation as dayjs.ManipulateType)
if (!dates.find((d) => isEqual(d, dateToMaybeAdd))) {
newData.push({
[timestampKey]: dateToMaybeAdd.toISOString(),
[valueKey]: defaultValue,
})
}
}
return newData
}
export const getTimestampTruncation = (datetime: string): 'second' | 'minute' | 'hour' | 'day' => {
const values = ['second', 'minute', 'hour', 'day'].map((key) =>
dayjs(datetime).get(key as dayjs.UnitType)
)
const zeroCount = values.reduce((acc, value) => {
if (value === 0) {
acc += 1
}
return acc
}, 0)
const truncation = {
0: 'second' as const,
1: 'minute' as const,
2: 'hour' as const,
3: 'day' as const,
}[zeroCount]!
return truncation
}
@@ -191,9 +191,9 @@ export const LogsPreviewer: React.FC<Props> = ({
}
>
<div className={condensedLayout ? 'px-4' : ''}>
{showChart && (
{!isLoading && showChart && (
<LogEventChart
data={!isLoading && eventChartData ? eventChartData : undefined}
data={eventChartData}
onBarClick={(isoTimestamp) => {
handleSearch('event-chart-bar-click', {
query: filters.search_query as string,
@@ -317,11 +317,26 @@ const ForeignKeySelector: FC<Props> = ({ column, visible = false, closePanel, sa
id="deletionAction"
value={selectedForeignKey.deletionAction}
label="Action if referenced row is removed"
// @ts-ignore
descriptionText={generateDeletionActionDescription(
selectedForeignKey.deletionAction,
`${selectedForeignKey.schema}.${selectedForeignKey.table}`
)}
descriptionText={
<>
<p>
{generateDeletionActionDescription(
selectedForeignKey.deletionAction,
`${selectedForeignKey.schema}.${selectedForeignKey.table}`
)}
</p>
<p className="mt-2">
<a
href="https://supabase.com/docs/guides/database/postgres/cascade-deletes"
target="_blank"
rel="noreferrer"
className="text-brand-900 opacity-75"
>
Learn more about cascade deletes
</a>
</p>
</>
}
error={errors.column}
onChange={(value: string) => updateDeletionAction(value)}
>
@@ -35,6 +35,7 @@ import { ForeignRowSelectorProps } from './SidePanelEditor/RowEditor/ForeignRowS
import TwoOptionToggle from 'components/ui/TwoOptionToggle'
import TableDefinition from './TableDefinition'
import APIDocumentationPanel from './APIDocumentationPanel'
import { ERROR_PRIMARY_KEY_NOTFOUND } from 'components/grid/constants'
export interface TableGridEditorProps {
/** Theme for the editor */
@@ -320,6 +321,12 @@ const TableGridEditor = ({
)
const configuration = { identifiers }
if (Object.keys(identifiers).length === 0) {
return ui.setNotification({
category: 'error',
message: ERROR_PRIMARY_KEY_NOTFOUND,
})
}
mutateUpdateTableRow({
projectRef: project.ref,
@@ -16,12 +16,9 @@ export function getTableRowDeleteSql({
rows,
}: Pick<TableRowDeleteVariables, 'table' | 'rows'>) {
const { primaryKeys, error } = getPrimaryKeys({ table })
if (error) {
throw error
}
if (error) throw error
let queryChains = new Query().from(table.name, table.schema ?? undefined).delete()
primaryKeys!.forEach((key) => {
const primaryKeyValues = rows.map((x) => x[key])
queryChains = queryChains.filter(key, 'in', primaryKeyValues)
@@ -0,0 +1,18 @@
import { fillTimeseries } from 'components/interfaces/Settings/Logs'
import { useMemo } from 'react'
/**
* Convenience hook for memoized filling of timeseries data.
*/
const useFillTimeseriesSorted = (...args: Parameters<typeof fillTimeseries>) => {
return useMemo(() => {
const [data, timestampKey] = args
if (!data[0]?.[timestampKey]) return data;
const filled = fillTimeseries(...args)
return filled.sort((a, b) => {
return (new Date(a[args[1]]) as any) - (new Date(b[args[1]]) as any)
})
}, [JSON.stringify(args[0]), ...args])
}
export default useFillTimeseriesSorted
+21 -3
View File
@@ -15,10 +15,12 @@ import {
LogsTableName,
PREVIEWER_DATEPICKER_HELPERS,
} from 'components/interfaces/Settings/Logs'
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import dayjs from 'dayjs'
import useFillTimeseriesSorted from './useFillTimeseriesSorted'
import useTimeseriesUnixToIso from './useTimeseriesUnixToIso'
interface Data {
logData: LogData[]
@@ -29,7 +31,7 @@ interface Data {
filters: Filters
params: LogsEndpointParams
oldestTimestamp?: string
eventChartData: EventChartData[] | null
eventChartData: EventChartData[]
}
interface Handlers {
loadOlder: () => void
@@ -176,6 +178,22 @@ function useLogsPreview(
setFilters({ ...newFilters, ...filterOverride })
}
}
const normalizedEventChartData = useTimeseriesUnixToIso(
eventChartResponse?.result || [],
'timestamp'
)
const eventChartData = useFillTimeseriesSorted(
normalizedEventChartData,
'timestamp',
'count',
0,
params.iso_timestamp_start ,
// default to current time if not set
params.iso_timestamp_end || new Date().toISOString()
)
return [
{
newCount,
@@ -186,7 +204,7 @@ function useLogsPreview(
filters,
params,
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
eventChartData: eventChartResponse?.result || null,
eventChartData,
},
{
setFilters: handleSetFilters,
@@ -0,0 +1,21 @@
import { isUnixMicro, unixMicroToIsoTimestamp } from 'components/interfaces/Settings/Logs'
import { useMemo } from 'react'
/**
* Convenience hook for converting timeseries timestamp from unix microsecond to iso
*
* memoized
*/
const useTimeseriesUnixToIso = (data: any[], timestampKey: string) => {
return useMemo(() => {
// check if need to convert or not
if (data.length === 0) return data
if (!isUnixMicro(data[0][timestampKey])) return data
return data?.map((d) => {
d[timestampKey] = unixMicroToIsoTimestamp(d[timestampKey])
return d
})
}, [JSON.stringify(data)])
}
export default useTimeseriesUnixToIso
+6
View File
@@ -151,6 +151,12 @@ export const METRICS = [
provider: 'daily-stats',
category: METRIC_CATEGORIES.API_AUTH,
},
{
key: 'total_auth_billing_period_sso_mau',
label: 'Auth Monthly Active SSO User',
provider: 'daily-stats',
category: METRIC_CATEGORIES.API_AUTH,
},
{
key: 'total_auth_ingress',
label: 'Auth Ingress',
@@ -1,20 +1,22 @@
import dayjs from 'dayjs'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { useRouter } from 'next/router'
import { Button } from 'ui'
import { observer } from 'mobx-react-lite'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions } from 'hooks'
import { useParams } from 'common/hooks'
import { ChartIntervals, NextPageWithLayout } from 'types'
import { DATE_FORMAT } from 'lib/constants'
import Panel from 'components/ui/Panel'
import NoPermission from 'components/ui/NoPermission'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import FunctionsLayout from 'components/layouts/FunctionsLayout'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useFunctionsInvStatsQuery } from 'data/analytics/functions-inv-stats-query'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import BarChart from 'components/ui/Charts/BarChart'
import AreaChart from 'components/ui/Charts/AreaChart'
import { isUnixMicro, unixMicroToIsoTimestamp } from 'components/interfaces/Settings/Logs'
import meanBy from 'lodash/meanBy'
import sumBy from 'lodash/sumBy'
const CHART_INTERVALS: ChartIntervals[] = [
{
@@ -22,40 +24,18 @@ const CHART_INTERVALS: ChartIntervals[] = [
label: '5 min',
startValue: 1,
startUnit: 'hour',
format: 'MMM D, h:mma',
},
{ key: '15min', label: '15 min', startValue: 24, startUnit: 'hour', format: 'MMM D, ha' },
{ key: '1hr', label: '1 hour', startValue: 7, startUnit: 'day', format: 'MMM D' },
{ key: '1day', label: '1 day', startValue: 7, startUnit: 'day', format: 'MMM D' },
{ key: '7day', label: '7 days', startValue: 7, startUnit: 'day', format: 'MMM D' },
{ key: '15min', label: '15 min', startValue: 15, startUnit: 'min' },
{ key: '1hr', label: '1 hour', startValue: 1, startUnit: 'hour' },
{ key: '1day', label: '1 day', startValue: 1, startUnit: 'day' },
{ key: '7day', label: '7 days', startValue: 7, startUnit: 'day' },
]
function calculateHighlightedValue(array: any, attribute: string, options?: { sum: boolean }) {
if (!array) return ''
let total = 0
let count = 0
array.forEach(function (item: any, index: number) {
const _item = item[attribute]
total += _item
count++
})
if (options?.sum) {
return total
} else {
return total / count
}
}
const PageLayout: NextPageWithLayout = () => {
const router = useRouter()
const { ref: projectRef, functionSlug } = useParams()
const [interval, setInterval] = useState<string>('15min')
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
const id = selectedFunction?.id
@@ -64,37 +44,22 @@ const PageLayout: NextPageWithLayout = () => {
functionId: id,
interval: selectedInterval.key,
})
const isChartLoading = !data?.result && !error ? true : false
const chartData = useMemo(() => {
return (data?.result || []).map((d: any) => ({
...d,
timestamp: isUnixMicro(d.timestamp) ? unixMicroToIsoTimestamp(d.timestamp) : d.timestamp,
}))
}, [data?.result])
const startDate = dayjs()
.subtract(selectedInterval.startValue, selectedInterval.startUnit)
.format(DATE_FORMAT)
const endDate = dayjs().format(DATE_FORMAT)
const charts: any = {}
charts.data = data?.result
const datetimeFormat = selectedInterval.format || 'MMM D, ha'
const handleBarClick = (v: any, search: string) => {
if (!v || !v.activePayload?.[0]?.payload) return
// returns rechart internal tooltip data type
const payload = v.activePayload[0].payload
const timestamp = payload.timestamp
const timestampDigits = String(timestamp).length
if (timestampDigits < 16) {
// pad unix timestamp with additional 0 and then forward
const paddedTimestamp = String(timestamp) + '0'.repeat(16 - timestampDigits)
router.push(`/project/${projectRef}/functions/${id}/logs?te=${paddedTimestamp}`)
} else {
router.push(`/project/${projectRef}/functions/${id}/logs?te=${timestamp}`)
}
}
const startDate = dayjs().subtract(
selectedInterval.startValue,
selectedInterval.startUnit as dayjs.ManipulateType
)
const canReadFunction = checkPermissions(PermissionAction.FUNCTIONS_READ, functionSlug as string)
if (!canReadFunction) {
return <NoPermission isFullPage resourceText="access this edge function" />
}
return (
<div className="space-y-6 py-2">
<div className="flex flex-row items-center gap-2">
@@ -128,79 +93,45 @@ const PageLayout: NextPageWithLayout = () => {
</span>
</div>
<div className="">
{startDate && endDate && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-2 lg:gap-8">
<Panel key="database-chart">
<Panel.Content className="space-y-4">
{/*
// @ts-ignore */}
<ChartHandler
label="Execution time"
defaultChartStyle="line"
startDate={startDate}
endDate={endDate}
attribute={'avg_execution_time'}
provider="log-stats"
// interval="1d"
hideChartType
customDateFormat={datetimeFormat}
data={charts}
format="ms"
isLoading={!charts.data && !error ? true : false}
highlightedValue={calculateHighlightedValue(data?.result, 'avg_execution_time')}
// onBarClick={(v) => handleBarClick(v, '/rest')}
/>
</Panel.Content>
</Panel>
<Panel key="invocation-chart">
<Panel.Content className="space-y-4">
{/*
{/*
{/*
// @ts-ignore */}
<ChartHandler
label="Invocations"
startDate={startDate}
endDate={endDate}
attribute={'count'}
provider="log-stats"
// interval="1d"
hideChartType
customDateFormat={datetimeFormat}
data={charts}
isLoading={!charts.data && !error ? true : false}
highlightedValue={calculateHighlightedValue(data?.result, 'count', {
sum: true,
})}
onBarClick={(v) => handleBarClick(v, '/auth')}
/>
</Panel.Content>
</Panel>
</div>
{/* <Panel key="error-chart">
<Panel.Content className="space-y-4">
<ChartHandler
defaultChartStyle="line"
label="Error count"
startDate={startDate}
endDate={endDate}
attribute={'min_execution_time'}
provider="log-stats"
// interval="1d"
hideChartType
customDateFormat={datetimeFormat}
data={charts}
isLoading={!charts.data && !error ? true : false}
highlightedValue={calculateHighlightedValue(data?.result, 'min_execution_time', {
sum: true,
})}
onBarClick={(v) => handleBarClick(v, '/auth')}
/>
</Panel.Content>
</Panel> */}
</>
)}
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-2 lg:gap-8">
<ReportWidget
title="Execution time"
tooltip="Average execution time of function invocations"
data={chartData}
isLoading={isChartLoading}
renderer={(props) => (
<AreaChart
className="w-full"
xAxisKey="timestamp"
yAxisKey="avg_execution_time"
data={props.data}
format="ms"
highlightedValue={meanBy(props.data, 'avg_execution_time')}
/>
)}
/>
<ReportWidget
title="Invocations"
data={chartData}
isLoading={isChartLoading}
renderer={(props) => (
<BarChart
className="w-full"
xAxisKey="timestamp"
yAxisKey="count"
data={props.data}
highlightedValue={sumBy(props.data, 'count')}
onBarClick={(v) => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}&ite=${
v.timestamp
}`
)
}}
/>
)}
/>
</div>
</div>
</div>
)
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore, useFlag } from 'hooks'
import { useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { get } from 'lib/common/fetch'
@@ -18,7 +18,6 @@ const BillingUpdateEnterprise: NextPageWithLayout = () => {
const projectRef = ui.selectedProject?.ref
const orgSlug = ui.selectedOrganization?.slug
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const [isLoadingPaymentMethods, setIsLoadingPaymentMethods] = useState(false)
const [subscription, setSubscription] = useState<StripeSubscription>()
@@ -29,9 +28,7 @@ const BillingUpdateEnterprise: NextPageWithLayout = () => {
subscription && subscription.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
useEffect(() => {
if (projectUpdateDisabled) {
router.push(`/project/${projectRef}/settings/billing/update`)
} else if (projectRef) {
if (projectRef) {
getStripeProducts()
getSubscription()
}
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { NextPageWithLayout } from 'types'
import { useStore, useFlag } from 'hooks'
import { useStore } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL, PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
@@ -16,8 +16,6 @@ const BillingUpdateFree: NextPageWithLayout = () => {
const router = useRouter()
const projectRef = ui.selectedProject?.ref
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const [products, setProducts] = useState<{ tiers: any[]; addons: any[] }>()
const [subscription, setSubscription] = useState<StripeSubscription>()
@@ -25,9 +23,7 @@ const BillingUpdateFree: NextPageWithLayout = () => {
subscription && subscription.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
useEffect(() => {
if (projectUpdateDisabled) {
router.push(`/project/${projectRef}/settings/billing/update`)
} else if (projectRef) {
if (projectRef) {
getStripeProducts()
getSubscription()
}
@@ -31,8 +31,7 @@ const BillingUpdate: NextPageWithLayout = () => {
const [subscription, setSubscription] = useState<StripeSubscription>()
const [selectedPlan, setSelectedPlan] = useState<any>()
const isEnterprise =
subscription && subscription.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
const isEnterprise = subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
useEffect(() => {
if (projectRef) {
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore, useFlag } from 'hooks'
import { useStore } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
@@ -21,8 +21,6 @@ const BillingUpdatePro: NextPageWithLayout = () => {
const projectRef = ui.selectedProject?.ref
const orgSlug = ui.selectedOrganization?.slug
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const [subscription, setSubscription] = useState<StripeSubscription>()
const [products, setProducts] = useState<{ tiers: any[]; addons: SubscriptionAddon[] }>()
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>()
@@ -39,9 +37,7 @@ const BillingUpdatePro: NextPageWithLayout = () => {
}, [])
useEffect(() => {
if (projectUpdateDisabled) {
router.push(`/project/${projectRef}/settings/billing/update`)
} else if (projectRef) {
if (projectRef) {
getStripeProducts()
getSubscription()
}
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore, useFlag } from 'hooks'
import { useStore } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
@@ -21,8 +21,6 @@ const BillingUpdateTeam: NextPageWithLayout = () => {
const projectRef = ui.selectedProject?.ref
const orgSlug = ui.selectedOrganization?.slug
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const [subscription, setSubscription] = useState<StripeSubscription>()
const [products, setProducts] = useState<{ tiers: any[]; addons: SubscriptionAddon[] }>()
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>()
@@ -39,9 +37,7 @@ const BillingUpdateTeam: NextPageWithLayout = () => {
}, [])
useEffect(() => {
if (projectUpdateDisabled) {
router.push(`/project/${projectRef}/settings/billing/update`)
} else if (projectRef) {
if (projectRef) {
getStripeProducts()
getSubscription()
}
@@ -122,7 +118,6 @@ const BillingUpdateTeam: NextPageWithLayout = () => {
currentSubscription={subscription}
isLoadingPaymentMethods={isLoadingPaymentMethods}
paymentMethods={paymentMethods || []}
onSelectBack={() => router.push(`/project/${projectRef}/settings/billing/update`)}
/>
)
}
+1 -1
View File
@@ -24,7 +24,7 @@ export default class DatabaseStore extends PostgresMetaInterface<any> {
async getPoolingConfiguration(projectRef: string) {
try {
// Need to use project ref -> shouldn;t need to pass it in via argument
const url = `${API_URL}/props/pooling/${projectRef}/config`
const url = `${API_URL}/projects/${projectRef}/config/pgbouncer`
const response = await get(url, { headers: this.headers })
if (response.error) throw response.error
return response
@@ -1,11 +1,13 @@
import {
ensureNoTimestampConflict,
fillTimeseries,
genChartQuery,
genDefaultQuery,
LogsTableName,
SQL_FILTER_TEMPLATES,
} from 'components/interfaces/Settings/Logs'
import dayjs from 'dayjs'
import { isEqual } from 'lodash'
describe.each(Object.values(LogsTableName))('%s', (table) => {
const templates = SQL_FILTER_TEMPLATES[table]
@@ -98,3 +100,57 @@ test.each([
expect(result[0]).toEqual(expected[0])
expect(result[1]).toEqual(expected[1])
})
// test for log trunc filling
test.each([
{
// truncate timestamp string from bigquery
case: 'bq utc timestamp, truncated minutely',
data: [
{ timestamp: '2023-04-26T17:18:00', count: 123 },
{ timestamp: '2023-04-26T17:20:00', count: 123 },
],
// expected
len: 3,
includes: [{ timestamp: '2023-04-26T17:19:00.000Z', count: 0 }],
},
// hourly truncation
{
case: 'bq timestamp truncated hourly, different options',
data: [
{ period: '2023-04-26T17:00:00.000Z', value: 123 },
{ period: '2023-04-26T20:00:00.000Z', value: 123 },
],
// expected
len: 4,
defaultVal: 1,
valKey: 'value',
tsKey: 'period',
includes: [
{ period: '2023-04-26T18:00:00.000Z', value: 1 },
{ period: '2023-04-26T19:00:00.000Z', value: 1 },
],
},
// fill beyong data min/max
{
case: 'fill beyond min/max',
data: [],
len: 3,
min: '2023-04-26T18:00:00.000Z',
max: '2023-04-26T20:00:00.000Z',
includes: [
{ timestamp: '2023-04-26T18:00:00.000Z', count: 0 },
{ timestamp: '2023-04-26T19:00:00.000Z', count: 0 },
{ timestamp: '2023-04-26T20:00:00.000Z', count: 0 },
],
},
])(
'fillTimeseries : $case',
({ data, len, includes, min, max, tsKey = 'timestamp', valKey = 'count', defaultVal = 0 }) => {
const result = fillTimeseries(data, tsKey, valKey, defaultVal, min, max)
expect(result.length).toEqual(len)
for (const inc of includes) {
expect(result.find((d) => isEqual(d, inc))).toBeTruthy()
}
}
)
@@ -347,7 +347,7 @@ test('log event chart hide', async () => {
return { result: [] }
})
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await screen.findByText(/Logs \/ Time/)
await screen.findByText(/No data/)
const toggle = await screen.findByText(/Chart/)
userEvent.click(toggle)
await expect(screen.findByText('Events')).rejects.toThrow()
+3 -3
View File
@@ -18,9 +18,9 @@ interface NotificationMetadata {
export interface ChartIntervals {
key: 'minutely' | 'hourly' | 'daily' | '5min' | '15min' | '1hr' | '1day' | '7day'
label: string
startValue: 1 | 24 | 7
startUnit: 'hour' | 'day'
format: 'MMM D, h:mma' | 'MMM D, ha' | 'MMM D'
startValue: number
startUnit: 'min' | 'hour' | 'day'
format?: 'MMM D, h:mma' | 'MMM D, ha' | 'MMM D'
}
export interface VaultSecret {