From d369c11f66e35b00d19a6c34c0c52fc106eb9625 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Wed, 2 Aug 2023 03:29:18 -0700 Subject: [PATCH 001/163] chore: update cli reference doc --- spec/cli_v1_commands.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/cli_v1_commands.yaml b/spec/cli_v1_commands.yaml index d7db35da785..eed0f49f7d1 100644 --- a/spec/cli_v1_commands.yaml +++ b/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.82.2 + version: 1.82.5 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -2221,7 +2221,7 @@ commands: Requires your local project to be linked to a remote database by running `supabase link`. For self-hosted databases, you can pass in the connection parameters using `--db-url` flag. - Runs `pg_dump` in a container with additional flags to exclude Supabase managed schemas. The ignored schemas include auth, storage, and those created by extensions. + Runs `pg_dump` in a container with additional flags to exclude Supabase managed schemas. The ignored schemas include auth, stroage, and those created by extensions. The default dump does not contain any data or custom roles. To dump those contents explicitly, specify either the `--data-only` and `--role-only` flag. examples: From c92c64013599715b9aeb289fff84d5c0e0b78161 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Thu, 3 Aug 2023 03:30:53 +0200 Subject: [PATCH 002/163] chore: update cli reference doc --- spec/cli_v1_commands.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/cli_v1_commands.yaml b/spec/cli_v1_commands.yaml index eed0f49f7d1..eb1d12e44bb 100644 --- a/spec/cli_v1_commands.yaml +++ b/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.82.5 + version: 1.82.6 title: Supabase CLI language: sh source: https://github.com/supabase/cli From c40bf30dbb9cfdd5f1ab960547fe2692394d3d5d Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Fri, 4 Aug 2023 16:36:44 -0500 Subject: [PATCH 003/163] add indexing guide and index advisor extension --- .../NavigationMenu.constants.ts | 5 + .../docs/pages/guides/database/extensions.mdx | 7 +- .../database/extensions/index_advisor.mdx | 147 ++++++++++++++++++ .../guides/database/query-optimization.mdx | 134 ++++++++++++++++ 4 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 apps/docs/pages/guides/database/extensions/index_advisor.mdx create mode 100644 apps/docs/pages/guides/database/query-optimization.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 6dbec8a0a36..94d6249babd 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -539,6 +539,7 @@ export const database: NavMenuConstant = { { name: 'Implementing Cascade Deletes', url: '/guides/database/postgres/cascade-deletes' }, { name: 'Implementing column encryption', url: '/guides/database/column-encryption' }, { name: 'Partitioning your tables', url: '/guides/database/partitions' }, + { name: 'Query Optimization', url: '/guides/database/query-optimization' }, { name: 'Testing your database', url: '/guides/database/testing' }, { name: 'Managing Timeouts', url: '/guides/database/timeouts' }, { name: 'Managing Passwords', url: '/guides/database/managing-passwords' }, @@ -559,6 +560,10 @@ export const database: NavMenuConstant = { url: '/guides/database/extensions/plv8', }, { name: 'http: RESTful Client', url: '/guides/database/extensions/http' }, + { + name: 'index_advisor: Query optimization', + url: '/guides/database/extensions/index_advisor', + }, { name: 'PGAudit: Postgres Auditing', url: '/guides/database/extensions/pgaudit', diff --git a/apps/docs/pages/guides/database/extensions.mdx b/apps/docs/pages/guides/database/extensions.mdx index 8b729b6ba73..45b743ab41d 100644 --- a/apps/docs/pages/guides/database/extensions.mdx +++ b/apps/docs/pages/guides/database/extensions.mdx @@ -1,9 +1,10 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { - id: 'extensions', - title: 'Postgres Extensions Overview', - description: 'Using Postgres extensions.', + +id: 'extensions', +title: 'Postgres Extensions Overview', +description: 'Using Postgres extensions.', } Extensions are exactly as they sound - they "extend" the database with functionality which isn't part of the Postgres core. diff --git a/apps/docs/pages/guides/database/extensions/index_advisor.mdx b/apps/docs/pages/guides/database/extensions/index_advisor.mdx new file mode 100644 index 00000000000..8ba0e81140b --- /dev/null +++ b/apps/docs/pages/guides/database/extensions/index_advisor.mdx @@ -0,0 +1,147 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'index-advisor', + title: 'index_advisor: query optimization', + description: 'Automatically optimize SQL queries', +} + +[Index advisor](https://github.com/supabase/index_advisor) is a Postgres extension for recommending indexes to improve query performance. + +Features: + +- Supports generic parameters e.g. `$1`, `$2` +- Supports materialized views +- Identifies tables/columns obfuscaed by views +- Skips duplicate indexes + +## Installation + +index_advisor is a trusted language extension, which means it is directly installable by users from the [database.dev](database.dev) SQL package repository. + +To get started, enable the dbdev client by executing the [setup SQL script](https://database.dev/installer). + +Then, install index_advisor by running + +```sql +select dbdev.install('olirice-index_advisor'); +create extension if not exists hypopg; +create extension "olirice-index_advisor" + version '0.1.0'; +``` + +## API + +Index advisor exposes a single function `index_advisor(query text)` that accepts a query and searches for a set of SQL DDL `create index` statements that improve the query's execution time. + +The function's signature is: + +```sql +index_advisor(query text) +returns + table ( + startup_cost_before jsonb, + startup_cost_after jsonb, + total_cost_before jsonb, + total_cost_after jsonb, + index_statements text[], + errors text[] + ) +``` + +## Usage + +As a minimal example, the `index_advisor` function can be given a single table query with a filter on an unindexed column. + +```sql +create extension if not exists index_advisor cascade; + +create table book( + id int primary key, + title text not null +); + +select + * +from + index_advisor('select book.id from book where title = $1'); + + startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements | errors +---------------------+--------------------+-------------------+------------------+-----------------------------------------------------+-------- + 0.00 | 1.17 | 25.88 | 6.40 | {"CREATE INDEX ON public.book USING btree (title)"},| {} +(1 row) +``` + +and will return a row recommending an index on the unindexed column. + +More complex queries may generate additional suggested indexes + +```sql +create extension if not exists index_advisor cascade; + +create table author( + id serial primary key, + name text not null +); + +create table publisher( + id serial primary key, + name text not null, + corporate_address text +); + +create table book( + id serial primary key, + author_id int not null references author(id), + publisher_id int not null references publisher(id), + title text +); + +create table review( + id serial primary key, + book_id int references book(id), + body text not null +); + +select + * +from + index_advisor(' + select + book.id, + book.title, + publisher.name as publisher_name, + author.name as author_name, + review.body review_body + from + book + join publisher + on book.publisher_id = publisher.id + join author + on book.author_id = author.id + join review + on book.id = review.book_id + where + author.id = $1 + and publisher.id = $2 + '); + + startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements | errors +---------------------+--------------------+-------------------+------------------+-----------------------------------------------------------+-------- + 27.26 | 12.77 | 68.48 | 42.37 | {"CREATE INDEX ON public.book USING btree (author_id)", | {} + "CREATE INDEX ON public.book USING btree (publisher_id)", + "CREATE INDEX ON public.review USING btree (book_id)"} +(3 rows) +``` + +## Limitations + +Currently, index_advisor will only recommend single column, B-tree indexes. This restriction will be relaxed in future releases. + +## Resources + +- Official [`index_advisor` GitHub Repository](https://github.com/supabase/index_advisor) + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/database/query-optimization.mdx b/apps/docs/pages/guides/database/query-optimization.mdx new file mode 100644 index 00000000000..ecae3fc4559 --- /dev/null +++ b/apps/docs/pages/guides/database/query-optimization.mdx @@ -0,0 +1,134 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + title: 'Query Optimization', + description: 'Choosing indexes', + footerHelpType: 'postgres', +} + +When working with PostgreSQL, or any relational database, indexing is key to improving query performance. +Aligning indexes with common query patterns can speed up data retrieval by order of magnitude. + +This guide is intended to: + +- help identify parts of a query that have the potential to be improved by indexes +- introduce the most frequently applicable indexing tool + +It is in no way comprehensive and should be thought of as a starting point on your optimization journey. + +If you're new to query optimization, you may be interested in [index_advisor](/docs/guides/database/extensions/index_advisor), our tool for automatically detecting indexes that improve performance on a given query. + +## Example Query + +Consider the following example query that retrieves customer names and purchase dates from two tables: + +```sql +select + a.name, + b.date_of_purchase +from + customers a + join orders b + on a.id = b.customer_id +where + a.sign_up_date > '2023-01-01' + and b.status = 'shipped' +order by + b.date_of_purchase; +limit 10 +``` + +In this query, there are several parts indexes could likely help in optimizing the performance: + +### `where` Clause: + +The `where` clause filters rows based on certain conditions, and indexing the columns involved can improve this process: + +- `a.sign_up_date`: If filtering by `sign_up_date` is common, indexing this column can speed up the query. +- `b.status`: Indexing the status may be beneficial if the column has diverse values. + +```sql +create index idx_customers_sign_up_date on customers (sign_up_date); + +create index idx_orders_status on orders (status); +``` + +### `join` Columns + +Indexes on the columns used for joining tables can help Postgres avoid scanning tables in their entirety when connecting tables. + +- Indexing and `b.customer_id` would likely improve the performance of the join in this query. +- Note that if `a.id` is the primary key of the `customers` table it is already indexed + +```sql +create index idx_orders_customer_id on orders (customer_id); +``` + +### `order by` Clause + +Sorting can also be optimized by indexing: + +- An index on `b.date_of_purchase` wcan improve the sorting process, and is particularly beneficial when a subset of rows is being returned with a `limit` clause. + +```sql +create index idx_orders_date_of_purchase on orders (date_of_purchase); +``` + +## Key Concepts + +Here are some concepts and tools to keep in mind to help you identify the best index for the job, and measure the impact that your index had: + +### Analyze the Query Plan + +Use the `explain` command to understand the query's execution. Look for slow parts, such as Sequential Scans or high cost numbers. If creating an index does not reduce the cost of the query plan, remove it. + +For example: + +```sql +explain select * from customers where sign_up_date > 25; +``` + +### Use Appropriate Index Types + +PostgreSQL offers various index types like [B-tree, Hash, GIN, etc](https://www.postgresql.org/docs/current/indexes-types.html). Select the type that best suits your data and query pattern. Using the right index type can make a significant difference. For example, using a BRIN index on field that is always increasing on a table with infrequent updates, like `created_at` on an `orders` table, routinely results in indexes that are +10x smaller than the equivalent default B-tree index. That translates into better scalability. + +```sql +create index idx_orders_created_at ON customers using brin(created_at); +``` + +### Partial Indexes + +For queries that frequently target a subset of data, a partial index could be faster and smaller than indexing the entire column. A partial index contains a `where` clause to filter the values included in the index. Note that a query's `where` clause must match the index for it to be used. + +```sql +create index idx_orders_status on orders (status) +where status = 'shipped'; +``` + +### Composite Indexes + +If filtering or joining on multiple columns, a composite index can help postgres referring to multiple indexes to identify the relevant rows. + +```sql +create index idx_customers_sign_up_date_priority on customers (sign_up_date, priority); +``` + +### Over-Indexing + +Avoid the urge to index columns you operate on infrequently. While indexes can speed up reads, they also slow down writes, so its important to balance those factors when making indexing decisions. + +### Statistics + +Postgres maintains a set of statistics about the contents of your tables. Those statistics are used by the query planner to decide e.g. when its is more efficient to use an index vs scanning the entire table. If the collected statistics drift too far from reality, the query planner may make poor decisions. To avoid the risk, you can periodically `analyze` tables. + +```sql +analyze customers; +``` + +--- + +By following this guide, you'll be able to discern where indexes can optimize queries and enhance your PostgreSQL performance. Remember that each database is unique, so always consider the specific context and use case of your queries. + +export const Page = ({ children }) => + +export default Page From 511b5ec9773544eca35eb04799c72d846e069763 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Mon, 7 Aug 2023 09:01:46 -0500 Subject: [PATCH 004/163] index-advisor error types --- apps/docs/pages/guides/database/extensions/index_advisor.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/database/extensions/index_advisor.mdx b/apps/docs/pages/guides/database/extensions/index_advisor.mdx index 8ba0e81140b..a21016ca8ba 100644 --- a/apps/docs/pages/guides/database/extensions/index_advisor.mdx +++ b/apps/docs/pages/guides/database/extensions/index_advisor.mdx @@ -136,7 +136,8 @@ from ## Limitations -Currently, index_advisor will only recommend single column, B-tree indexes. This restriction will be relaxed in future releases. +- index_advisor will only recommend single column, B-tree indexes. This restriction will be relaxed in future releases. +- when a generic argument's type is not discernible from context, an error is returned in the `errors` field. To resolve those errors add explicit type casting to uses of the generic argument e.g. `$1::int`. ## Resources From 4769f8b136bd3466eaf8776b63f0f84258ff3baf Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Mon, 7 Aug 2023 09:06:42 -0500 Subject: [PATCH 005/163] remove version from install instructions so latest is req --- apps/docs/pages/guides/database/extensions/index_advisor.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/database/extensions/index_advisor.mdx b/apps/docs/pages/guides/database/extensions/index_advisor.mdx index a21016ca8ba..40a9c661a43 100644 --- a/apps/docs/pages/guides/database/extensions/index_advisor.mdx +++ b/apps/docs/pages/guides/database/extensions/index_advisor.mdx @@ -26,8 +26,7 @@ Then, install index_advisor by running ```sql select dbdev.install('olirice-index_advisor'); create extension if not exists hypopg; -create extension "olirice-index_advisor" - version '0.1.0'; +create extension "olirice-index_advisor"; ``` ## API From 82eccef9bad53501b3cf1f48321280ec669f3099 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 8 Aug 2023 17:21:09 +0800 Subject: [PATCH 006/163] Shift patch notifications to RQ mutation --- .../NotificationsPopover/index.tsx | 36 +++++++-------- .../notifications-update-mutation.ts | 46 +++++++++++++++++++ 2 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 studio/data/notifications/notifications-update-mutation.ts diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index d11b4f510a2..5d55ab90777 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -8,16 +8,17 @@ import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { useRouter } from 'next/router' import { Fragment, useState } from 'react' +import { Alert, Button, IconArrowRight, IconBell, IconInbox, Popover } from 'ui' import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' import { useNotificationsQuery } from 'data/notifications/notifications-query' +import { useNotificationsUpdateMutation } from 'data/notifications/notifications-update-mutation' import { getProjectDetail } from 'data/projects/project-detail-query' import { setProjectPostgrestStatus } from 'data/projects/projects-query' import { useStore } from 'hooks' import { delete_, patch, post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { Project } from 'types' -import { Alert, Button, IconArrowRight, IconBell, IconInbox, Popover } from 'ui' import NotificationRow from './NotificationRow' interface NotificationsPopoverProps { @@ -28,34 +29,31 @@ const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => { const queryClient = useQueryClient() const router = useRouter() const { meta, ui } = useStore() - const { data: notifications, refetch } = useNotificationsQuery() + const { data: notifications } = useNotificationsQuery() + const { mutate: updateNotifications } = useNotificationsUpdateMutation({ + onError: () => console.error('Failed to update notifications'), + }) const [projectToRestart, setProjectToRestart] = useState() const [projectToApplyMigration, setProjectToApplyMigration] = useState() const [projectToRollbackMigration, setProjectToRollbackMigration] = useState() const [projectToFinalizeMigration, setProjectToFinalizeMigration] = useState() - const [targetNotification, setTargetNotification] = useState() - if (!notifications || !Array.isArray(notifications)) return null - - const newNotifications = notifications?.filter( - (notification) => notification.notification_status === NotificationStatus.New - ) + const newNotifications = + notifications?.filter( + (notification) => notification.notification_status === NotificationStatus.New + ) ?? [] const hasNewNotifications = newNotifications.length > 0 const onOpenChange = async (open: boolean) => { - // TODO(alaister): move this to a mutation + // Mark notifications as seen if (!open) { - // Mark notifications as seen - const notificationIds = notifications - .filter((notification) => notification.notification_status === NotificationStatus.New) - .map((notification) => notification.id) - if (notificationIds.length > 0) { - const { error } = await patch(`${API_URL}/notifications`, { ids: notificationIds }) - if (error) console.error('Failed to update notifications', error) - refetch() - } + const notificationIds = + notifications + ?.filter((notification) => notification.notification_status === NotificationStatus.New) + .map((notification) => notification.id) ?? [] + if (notificationIds.length > 0) updateNotifications({ ids: notificationIds }) } } @@ -180,6 +178,8 @@ const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => { setProjectToFinalizeMigration(undefined) } + if (!notifications || !Array.isArray(notifications)) return null + return ( <> > + +export const useNotificationsUpdateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + return useMutation( + (vars) => updateNotifications(vars), + { + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries(notificationKeys.list()) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to update notifications: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} From 53399c8af2082922a9e2d011f25eb70035c41905 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:42:33 +0200 Subject: [PATCH 007/163] chore: update cli reference doc --- spec/cli_v1_commands.yaml | 269 +++++++++++++++++++++++++++++++++++++- 1 file changed, 265 insertions(+), 4 deletions(-) diff --git a/spec/cli_v1_commands.yaml b/spec/cli_v1_commands.yaml index eb1d12e44bb..69eeda41b75 100644 --- a/spec/cli_v1_commands.yaml +++ b/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.82.6 + version: 1.83.7 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -1137,9 +1137,13 @@ commands: summary: Apply pending migrations to local database tags: [] links: [] - usage: supabase migration up + usage: supabase migration up [flags] subcommands: [] - flags: [] + flags: + - id: include-all + name: --include-all + description: Include all migrations not found on remote history table. + default_value: 'false' - id: supabase-migration-squash title: supabase migration squash summary: Squash migrations to a single file @@ -1424,6 +1428,24 @@ commands: - id: supabase-inspect-db-vacuum-stats title: supabase inspect db vacuum-stats summary: Show statistics related to vacuum operations per table + description: |2 + + This shows you stats about the vacuum activities for each table. Due to Postgres' [MVCC](https://www.postgresql.org/docs/current/mvcc.html) when data is updated or deleted new rows are created and old rows are made invisible and marked as "dead tuples". Usually the [autovaccum](https://supabase.com/docs/guides/platform/database-size#vacuum-operations) process will aysnchronously clean the dead tuples. + + The command lists when the last vacuum and last auto vacuum took place, the row count on the table as well as the count of dead rows and whether autovacuum is expected to run or not. If the number of dead rows is much higher than the row count, or if an autovacuum is expected but has not been performed for some time, this can indicate that autovacuum is not able to keep up and that your vacuum settings need to be tweaked or that you require more compute or disk IOPS to allow autovaccum to complete. + + + ``` + SCHEMA │ TABLE │ LAST VACUUM │ LAST AUTO VACUUM │ ROW COUNT │ DEAD ROW COUNT │ EXPECT AUTOVACUUM? + ──────────────────────┼──────────────────────────────────┼─────────────┼──────────────────┼──────────────────────┼────────────────┼───────────────────── + auth │ users │ │ 2023-06-26 12:34 │ 18,030 │ 0 │ no + public │ profiles │ │ 2023-06-26 23:45 │ 13,420 │ 28 │ no + public │ logs │ │ 2023-06-26 01:23 │ 1,313,033 │ 3,318,228 │ yes + storage │ objects │ │ │ No stats │ 0 │ no + storage │ buckets │ │ │ No stats │ 0 │ no + supabase_migrations │ schema_migrations │ │ │ No stats │ 0 │ no + + ``` tags: [] links: [] usage: supabase inspect db vacuum-stats @@ -1436,6 +1458,15 @@ commands: - id: supabase-inspect-db-unused-indexes title: supabase inspect db unused-indexes summary: Show indexes with low usage + description: |2 + + This command displays indexes that have < 50 scans recorded against them, and are greater than 5 pages in size, ordered by size relative to the number of index scans. This command is generally useful for discovering indexes that are unused. Indexes can impact write performance, as well as read performance should they occupy space in memory, its a good idea to remove indexes that are not needed or being used. + + ``` + TABLE │ INDEX │ INDEX SIZE │ INDEX SCANS + ─────────────────────┼────────────────────────────────────────────┼────────────┼────────────── + public.users │ user_id_created_at_idx │ 97 MB │ 0 + ``` tags: [] links: [] usage: supabase inspect db unused-indexes @@ -1448,6 +1479,17 @@ commands: - id: supabase-inspect-db-total-table-sizes title: supabase inspect db total-table-sizes summary: Show total table sizes, including table index sizes + description: |2- + + This command displays the total size of each table in the database. It is the sum of the values that `pg_table_size()` and `pg_indexes_size()` gives for each table. + + ``` + NAME │ SIZE + ───────────────────────────────────┼───────────── + job_run_details │ 395 MB + slack_msgs │ 648 kB + emails │ 640 kB + ``` tags: [] links: [] usage: supabase inspect db total-table-sizes @@ -1460,6 +1502,15 @@ commands: - id: supabase-inspect-db-total-index-size title: supabase inspect db total-index-size summary: Show total size of all indexes + description: |2- + + This command displays the total size of all indexes on the database. It is calculated by taking the number of pages (reported in `relpages`) and multiplying it by the page size (8192 bytes). + + ``` + SIZE + ───────── + 12 MB + ``` tags: [] links: [] usage: supabase inspect db total-index-size @@ -1473,6 +1524,20 @@ commands: title: supabase inspect db table-sizes summary: | Show table sizes of individual tables without their index sizes + description: |2- + + This command displays the size of each table in the database. It is calculated by using the system administration function `pg_table_size()`, which includes the size of the main data fork, free space map, visibility map and TOAST data. It does not include the size of the table's indexes. + + + ``` + NAME │ SIZE + ───────────────────────────────────┼───────────── + job_run_details │ 385 MB + emails │ 584 kB + job │ 40 kB + sessions │ 0 bytes + prod_resource_notifications_meta │ 0 bytes + ``` tags: [] links: [] usage: supabase inspect db table-sizes @@ -1485,6 +1550,19 @@ commands: - id: supabase-inspect-db-table-record-counts title: supabase inspect db table-record-counts summary: Show estimated number of rows per table + description: |2- + + This command displays an estimated count of rows per table, descending by estimated count. The estimated count is derived from `n_live_tup`, which is updated by vacuum operations. Due to the way `n_live_tup` is populated, sparse vs. dense pages can result in estimations that are significantly out from the real count of rows. + + + ``` + NAME │ ESTIMATED COUNT + ─────────────┼────────────────── + logs │ 322943 + emails │ 1103 + job │ 1 + migrations │ 0 + ``` tags: [] links: [] usage: supabase inspect db table-record-counts @@ -1497,6 +1575,19 @@ commands: - id: supabase-inspect-db-table-index-sizes title: supabase inspect db table-index-sizes summary: Show index sizes of individual tables + description: |2 + + This command displays the total size of indexes for each table. It is calculated by using the system administration function `pg_indexes_size()`. + + ``` + TABLE │ INDEX SIZE + ───────────────────────────────────┼───────────── + job_run_details │ 10104 kB + users │ 128 kB + job │ 32 kB + instances │ 8192 bytes + http_request_queue │ 0 bytes + ``` tags: [] links: [] usage: supabase inspect db table-index-sizes @@ -1509,6 +1600,21 @@ commands: - id: supabase-inspect-db-seq-scans title: supabase inspect db seq-scans summary: Show number of sequential scans recorded against all tables + description: |2+ + + This command displays the number of sequential scans recorded against all tables, descending by count of sequential scans. Tables that have very high numbers of sequential scans may be underindexed, and it may be worth investigating queries that read from these tables. + + + ``` + NAME │ COUNT + ───────────────────────────────────┼───────── + emails │ 182435 + users │ 25063 + job_run_details │ 60 + schema_migrations │ 0 + migrations │ 0 + ``` + tags: [] links: [] usage: supabase inspect db seq-scans @@ -1521,6 +1627,39 @@ commands: - id: supabase-inspect-db-role-connections title: supabase inspect db role-connections summary: Show number of active connections for all database roles + description: |2 + + This command shows the number of active connections for each database roles to see which specific role might be consuming more connections than expected. + + This is a Supabase specific command. You can see this breakdown on the dashboard as well: + https://app.supabase.com/project/_/database/roles + + The maximum number of active connections depends [on your instance size](https://supabase.com/docs/guides/platform/compute-add-ons). You can [manually overwrite](https://supabase.com/docs/guides/platform/performance#allowing-higher-number-of-connections) the allowed number of connection but it is not advised. + + ``` + + + ROLE NAME │ ACTIVE CONNCTION + ────────────────────────────┼─────────────────── + authenticator │ 5 + postgres │ 5 + supabase_admin │ 1 + pgbouncer │ 1 + anon │ 0 + authenticated │ 0 + service_role │ 0 + dashboard_user │ 0 + supabase_auth_admin │ 0 + supabase_storage_admin │ 0 + supabase_functions_admin │ 0 + pgsodium_keyholder │ 0 + pg_read_all_data │ 0 + pg_write_all_data │ 0 + pg_monitor │ 0 + + Active connections 12/90 + + ``` tags: [] links: [] usage: supabase inspect db role-connections @@ -1533,6 +1672,19 @@ commands: - id: supabase-inspect-db-replication-slots title: supabase inspect db replication-slots summary: Show information about replication slots on the database + description: |2- + + This command shows information about [logical replication slots](https://www.postgresql.org/docs/current/logical-replication.html) that are setup on the database. It shows if the slot is active, the state of the WAL sender process ('startup', 'catchup', 'streaming', 'backup', 'stopping') the replication client address and the replication lag in GB. + + This command is useful to check that the amount of replication lag is as low as possible, replication lag can occur due to network latency issues, slow disk I/O, long running transactions or lack of ability for the subscriber to consume WAL fast enough. + + + ``` + NAME │ ACTIVE │ STATE │ REPLICATION CLIENT ADDRESS │ REPLICATION LAG GB + ─────────────────────────────────────────────┼────────┼─────────┼────────────────────────────┼───────────────────── + supabase_realtime_replication_slot │ t │ N/A │ N/A │ 0 + datastream │ t │ catchup │ 24.201.24.106 │ 45 + ``` tags: [] links: [] usage: supabase inspect db replication-slots @@ -1546,6 +1698,22 @@ commands: title: supabase inspect db outliers summary: | Show queries from pg_stat_statements ordered by total execution time + description: |2 + + This command displays statements, obtained from `pg_stat_statements`, ordered by the amount of time to execute in aggregate. This includes the statement itself, the total execution time for that statement, the proportion of total execution time for all statements that statement has taken up, the number of times that statement has been called, and the amount of time that statement spent on synchronous I/O (reading/writing from the file system). + + Typically, an efficient query will have an appropriate ratio of calls to total execution time, with as little time spent on I/O as possible. Queries that have a high total execution time but low call count should be investigated to improve their performance. Queries that have a high proportion of execution time being spent on synchronous I/O should also be investigated. + + ``` + + QUERY │ EXECUTION TIME │ PROPORTION OF EXEC TIME │ NUMBER CALLS │ SYNC IO TIME + ─────────────────────────────────────────┼──────────────────┼─────────────────────────┼──────────────┼─────────────── + SELECT * FROM archivable_usage_events.. │ 154:39:26.431466 │ 72.2% │ 34,211,877 │ 00:00:00 + COPY public.archivable_usage_events (.. │ 50:38:33.198418 │ 23.6% │ 13 │ 13:34:21.00108 + COPY public.usage_events (id, reporte.. │ 02:32:16.335233 │ 1.2% │ 13 │ 00:34:19.784318 + INSERT INTO usage_events (id, retaine.. │ 01:42:59.436532 │ 0.8% │ 12,328,187 │ 00:00:00 + SELECT * FROM usage_events WHERE (alp.. │ 01:18:10.754354 │ 0.6% │ 102,114,301 │ 00:00:00 + ``` tags: [] links: [] usage: supabase inspect db outliers @@ -1559,6 +1727,17 @@ commands: title: supabase inspect db long-running-queries summary: | Show currently running queries running for longer than 5 minutes + description: |2 + + This command displays currently running queries, that have been running for longer than 5 minutes, descending by duration. Very long running queries can be a source of multiple issues, such as preventing DDL statements completing or vacuum being unable to update `relfrozenxid`. + + ``` + PID │ DURATION │ QUERY + ───────┼─────────────────┼─────────────────────────────────────────────────────────────────────────────────────── + 19578 | 02:29:11.200129 | EXPLAIN SELECT "students".* FROM "students" WHERE "students"."id" = 1450645 LIMIT 1 + 19465 | 02:26:05.542653 | EXPLAIN SELECT "students".* FROM "students" WHERE "students"."id" = 1889881 LIMIT 1 + 19632 | 02:24:46.962818 | EXPLAIN SELECT "students".* FROM "students" WHERE "students"."id" = 1581884 LIMIT 1 + ``` tags: [] links: [] usage: supabase inspect db long-running-queries @@ -1572,6 +1751,17 @@ commands: title: supabase inspect db locks summary: | Show queries which have taken out an exclusive lock on a relation + description: |2 + + This command displays queries that have taken out an exclusive lock on a relation. Exclusive locks typically prevent other operations on that relation from taking place, and can be a cause of "hung" queries that are waiting for a lock to be granted. + + If you see a query that is hanging for a very long time or causing blocking issues you may consider killing the query by connecting to the database and running `SELECT pg_cancel_backend(PID);` to cancel the query. If the query still does not stop you can force a hard stop by running `SELECT pg_terminate_backend(PID);` + + ``` + PID │ RELNAME │ TRANSACTION ID │ GRANTED │ QUERY │ AGE + ─────────┼─────────┼────────────────┼─────────┼─────────────────────────────────────────┼─────────── + 328112 │ null │ 0 │ t │ SELECT * FROM logs; │ 00:04:20 + ``` tags: [] links: [] usage: supabase inspect db locks @@ -1584,6 +1774,7 @@ commands: - id: supabase-inspect-db-index-usage title: supabase inspect db index-usage summary: Show information about the efficiency of indexes + description: "\n\nThis command provides information on the efficiency of indexes, represented as what percentage of total scans were index scans. A low percentage can indicate under indexing, or wrong data being indexed.\n\n```\n TABLE NAME │ PERCENTAGE OF TIMES INDEX USED │ ROWS IN TABLE\n ────────────────────┼────────────────────────────────┼────────────────\n user_events │ 99 │ 4225318 \n user_feed │ 99 │ 3581573\n unindexed_table │ 0 │ 322911\n job │ 100 │ 33242\n schema_migrations │ 97 │ 0\n migrations │ Insufficient data │ 0\n```" tags: [] links: [] usage: supabase inspect db index-usage @@ -1596,6 +1787,20 @@ commands: - id: supabase-inspect-db-index-sizes title: supabase inspect db index-sizes summary: Show index sizes of individual indexes + description: |2- + + This command displays the size of each each index in the database. It is calculated by taking the number of pages (reported in `relpages`) and multiplying it by the page size (8192 bytes). + + ``` + NAME │ SIZE + ──────────────────────────────┼───────────── + user_events_index │ 2082 MB + job_run_details_pkey │ 3856 kB + schema_migrations_pkey │ 16 kB + refresh_tokens_token_unique │ 8192 bytes + users_instance_id_idx │ 0 bytes + buckets_pkey │ 0 bytes + ``` tags: [] links: [] usage: supabase inspect db index-sizes @@ -1609,6 +1814,21 @@ commands: title: supabase inspect db calls summary: | Show queries from pg_stat_statements ordered by total times called + description: |2 + + This command is much like the `supabase inspect db outliers` command, but ordered by the number of times a statement has been called. + + You can use this information to see which queries are called most often, which can potentially be good candidates for optimisation. + + ``` + + QUERY │ TOTAL EXECUTION TIME │ PROPORTION OF TOTAL EXEC TIME │ NUMBER CALLS │ SYNC IO TIME + ─────────────────────────────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────┼────────────────── + SELECT * FROM users WHERE id = $1 │ 14:50:11.828939 │ 89.8% │ 183,389,757 │ 00:00:00.002018 + SELECT * FROM user_events │ 01:20:23.466633 │ 1.4% │ 78,325 │ 00:00:00 + INSERT INTO users (email, name) VALUES ($1, $2)│ 00:40:11.616882 │ 0.8% │ 54,003 │ 00:00:00.000322 + + ``` tags: [] links: [] usage: supabase inspect db calls @@ -1621,6 +1841,20 @@ commands: - id: supabase-inspect-db-cache-hit title: supabase inspect db cache-hit summary: Show cache hit rates for tables and indices + description: |2- + + This command provides information on the efficiency of the buffer cache and how often your queries have to go hit the disk rather than reading from memory. Information on both index reads (`index hit rate`) as well as table reads (`table hit rate`) are shown. In general, databases with low cache hit rates perform worse as it is slower to go to disk than retrieve data from memory. If your table hit rate is low, this can indicate that you do not have enough RAM and you may benefit from upgrading to a larger compute addon with more memory. If your index hit rate is low, this may indicate that there is scope to add more appropriate indexes. + + The hit rates are calculated as a ratio of number of table or index blocks fetched from the postgres buffer cache against the sum of cached blocks and uncached blocks read from disk. + + On smaller compute plans (free, small, medium), a ratio of below 99% can indicate a problem. On larger plans the hit rates may be lower but performance will remain constant as the data may use the OS cache rather than Postgres buffer cache. + + ``` + NAME │ RATIO + ─────────────────┼─────────── + index hit rate │ 0.996621 + table hit rate │ 0.999341 + ``` tags: [] links: [] usage: supabase inspect db cache-hit @@ -1634,6 +1868,15 @@ commands: title: supabase inspect db blocking summary: | Show queries that are holding locks and the queries that are waiting for them to be released + description: |2 + + This command shows you statements that are currently holding locks and blocking, as well as the statement that is being blocked. This can be used in conjunction with `inspect db locks` to determine which statements need to be terminated in order to resolve lock contention. + + ``` + BLOCKED PID │ BLOCKING STATEMENT │ BLOCKING DURATION │ BLOCKING PID │ BLOCKED STATEMENT │ BLOCKED DURATION + ──────────────┼──────────────────────────────┼───────────────────┼──────────────┼────────────────────────────────────────────────────────────────────────────────────────┼─────────────────── + 253 │ select count(*) from mytable │ 00:00:03.838314 │ 13495 │ UPDATE "mytable" SET "updated_at" = '2023─08─03 14:07:04.746688' WHERE "id" = 83719341 │ 00:00:03.821826 + ``` tags: [] links: [] usage: supabase inspect db blocking @@ -1647,6 +1890,20 @@ commands: title: supabase inspect db bloat summary: | Estimates space allocated to a relation that is full of dead tuples + description: |2 + + This command displays an estimation of table "bloat" - Due to Postgres' [MVCC](https://www.postgresql.org/docs/current/mvcc.html) when data is updated or deleted new rows are created and old rows are made invisible and marked as "dead tuples". Usually the [autovaccum](https://supabase.com/docs/guides/platform/database-size#vacuum-operations) process will asynchronously clean the dead tuples. Sometimes the autovaccum is unable to work fast enough to reduce or prevent tables from becoming bloated. High bloat can slow down queries, cause excessive IOPS and waste space in your database. + + Tables with a high bloat ratio should be investigated to see if there are vacuuming is not quick enough or there are other issues. + + ``` + TYPE │ SCHEMA NAME │ OBJECT NAME │ BLOAT │ WASTE + ────────┼─────────────┼────────────────────────────┼───────┼───────────── + table │ public │ very_bloated_table │ 41.0 │ 700 MB + table │ public │ my_table │ 4.0 │ 76 MB + table │ public │ happy_table │ 1.0 │ 1472 kB + index │ public │ happy_table::my_nice_index │ 0.7 │ 880 kB + ``` tags: [] links: [] usage: supabase inspect db bloat @@ -2131,6 +2388,10 @@ commands: description: | Print the migrations that would be applied, but don't actually apply them. default_value: 'false' + - id: include-all + name: --include-all + description: Include all migrations not found on remote history table. + default_value: 'false' - id: include-roles name: --include-roles description: Include custom roles from supabase/roles.sql. @@ -2221,7 +2482,7 @@ commands: Requires your local project to be linked to a remote database by running `supabase link`. For self-hosted databases, you can pass in the connection parameters using `--db-url` flag. - Runs `pg_dump` in a container with additional flags to exclude Supabase managed schemas. The ignored schemas include auth, stroage, and those created by extensions. + Runs `pg_dump` in a container with additional flags to exclude Supabase managed schemas. The ignored schemas include auth, storage, and those created by extensions. The default dump does not contain any data or custom roles. To dump those contents explicitly, specify either the `--data-only` and `--role-only` flag. examples: From 6b2763fc1959f3826a58b5cb28d57597fef0fb29 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 8 Aug 2023 23:32:56 +0800 Subject: [PATCH 008/163] Use RQ mutation for update default payment method + delete payment method in Org V1 Billing settings --- .../BillingSettings/BillingSettings.tsx | 8 +- .../BillingSettings/PaymentMethods.tsx | 97 ++++++++++--------- ...zation-customer-profile-update-mutation.ts | 10 +- ...nization-payment-method-delete-mutation.ts | 6 +- 4 files changed, 67 insertions(+), 54 deletions(-) diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index c18d3297dfd..b453da92d71 100644 --- a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -1,19 +1,19 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' import { useEffect, useState } from 'react' -import { useParams } from 'common/hooks' +import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' import { useProjectsQuery } from 'data/projects/projects-query' import { useCheckPermissions, useFlag, useSelectedOrganization, useStore } from 'hooks' -import { PermissionAction } from '@supabase/shared-types/out/constants' import { get } from 'lib/common/fetch' import { API_URL } from 'lib/constants' +import OrganizationBillingMigrationPanel from '../GeneralSettings/OrganizationBillingMigrationPanel' import BillingAddress from './BillingAddress/BillingAddress' import BillingEmail from './BillingEmail' import CreditBalance from './CreditBalance' import PaymentMethods from './PaymentMethods' import ProjectsSummary from './ProjectsSummary' import TaxID from './TaxID/TaxID' -import OrganizationBillingMigrationPanel from '../GeneralSettings/OrganizationBillingMigrationPanel' -import { ScaffoldContainer, ScaffoldContainerLegacy } from 'components/layouts/Scaffold' const BillingSettings = () => { const { ui } = useStore() diff --git a/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx index 39076b09d92..666ecb682b3 100644 --- a/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx @@ -17,9 +17,10 @@ import { import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' import NoPermission from 'components/ui/NoPermission' import Panel from 'components/ui/Panel' +import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' +import { useOrganizationPaymentMethodDeleteMutation } from 'data/organizations/organization-payment-method-delete-mutation' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { delete_, patch } from 'lib/common/fetch' -import { API_URL, BASE_PATH } from 'lib/constants' +import { BASE_PATH } from 'lib/constants' import { getURL } from 'lib/helpers' export interface PaymentMethodsProps { @@ -46,7 +47,33 @@ const PaymentMethods = ({ const [selectedMethodForDefault, setSelectedMethodForDefault] = useState() const [selectedMethodToDelete, setSelectedMethodToDelete] = useState() const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false) - const [isUpdatingPaymentMethod, setIsUpdatingPaymentMethod] = useState(false) + + const { mutate: updateCustomerProfile, isLoading: isUpdating } = + useOrganizationCustomerProfileUpdateMutation({ + onSuccess: (updatedCustomer) => { + onDefaultMethodUpdated(updatedCustomer) + setSelectedMethodForDefault(undefined) + ui.setNotification({ + category: 'success', + message: 'Successfully updated default payment method', + }) + }, + onError: (error) => { + ui.setNotification({ + category: 'error', + message: `Failed to make payment method default: ${error.message}`, + }) + }, + }) + + const { mutate: deletePaymentMethod, isLoading: isDeleting } = + useOrganizationPaymentMethodDeleteMutation({ + onSuccess: () => { + onPaymentMethodsDeleted() + setSelectedMethodToDelete(undefined) + ui.setNotification({ category: 'success', message: 'Successfully deleted payment method' }) + }, + }) const canReadPaymentMethods = useCheckPermissions( PermissionAction.BILLING_READ, @@ -58,48 +85,16 @@ const PaymentMethods = ({ ) const onConfirmMakeDefaultPaymentMethod = async () => { - try { - setIsUpdatingPaymentMethod(true) - const updatedCustomer = await patch(`${API_URL}/organizations/${orgSlug}/customer`, { - invoice_settings: { - default_payment_method: selectedMethodForDefault.id, - }, - }) - if (updatedCustomer.error) throw updatedCustomer.error - onDefaultMethodUpdated(updatedCustomer) - setSelectedMethodForDefault(undefined) - ui.setNotification({ - category: 'success', - message: 'Successfully updated default payment method', - }) - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to make payment method default: ${error.message}`, - }) - } finally { - setIsUpdatingPaymentMethod(false) - } + updateCustomerProfile({ + slug: orgSlug, + invoice_settings: { + default_payment_method: selectedMethodForDefault.id, + }, + }) } const onConfirmDetachPaymentMethod = async () => { - try { - setIsUpdatingPaymentMethod(true) - const { error } = await delete_(`${API_URL}/organizations/${orgSlug}/payments`, { - card_id: selectedMethodToDelete.id, - }) - if (error) throw error - onPaymentMethodsDeleted() - setSelectedMethodToDelete(undefined) - ui.setNotification({ category: 'success', message: 'Successfully deleted payment method' }) - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to delete payment method: ${error.message}`, - }) - } finally { - setIsUpdatingPaymentMethod(false) - } + deletePaymentMethod({ slug: orgSlug, cardId: selectedMethodToDelete.id }) } const onLocalPaymentMethodAdded = () => { @@ -271,12 +266,17 @@ const PaymentMethods = ({ onCancel={() => setSelectedMethodForDefault(undefined)} customFooter={
- diff --git a/studio/data/organizations/organization-customer-profile-update-mutation.ts b/studio/data/organizations/organization-customer-profile-update-mutation.ts index 41f9b3724f8..47f3cd7a942 100644 --- a/studio/data/organizations/organization-customer-profile-update-mutation.ts +++ b/studio/data/organizations/organization-customer-profile-update-mutation.ts @@ -67,7 +67,7 @@ export const useOrganizationCustomerProfileUpdateMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to update billing address: ${data.message}`) + toast.error(`Failed to update customer profile: ${data.message}`) } else { onError(data, variables, context) } From d03f7e260f611c3753a9b1183f96c641d98772fd Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 9 Aug 2023 00:31:37 +0800 Subject: [PATCH 010/163] Shift patch database password to RQ mutation --- .../DatabaseSettings/ResetDbPassword.tsx | 56 +++++++++---------- .../database-password-reset-mutation.ts | 52 +++++++++++++++++ 2 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 studio/data/database/database-password-reset-mutation.ts diff --git a/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx b/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx index 769987cc06f..c89ac2398ee 100644 --- a/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx +++ b/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx @@ -1,20 +1,19 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' import generator from 'generate-password' import { debounce } from 'lodash' import { useEffect, useRef, useState } from 'react' import { Button, Input, Modal } from 'ui' -import { useParams } from 'common/hooks' -import { useCheckPermissions, useStore } from 'hooks' -import { patch } from 'lib/common/fetch' -import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants' -import { passwordStrength } from 'lib/helpers' - import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import Panel from 'components/ui/Panel' import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' +import { useDatabasePasswordResetMutation } from 'data/database/database-password-reset-mutation' import { getProjectDetail } from 'data/projects/project-detail-query' +import { useCheckPermissions, useStore } from 'hooks' +import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants' +import { passwordStrength } from 'lib/helpers' const ResetDbPassword = ({ disabled = false }) => { const { ref } = useParams() @@ -24,16 +23,28 @@ const ResetDbPassword = ({ disabled = false }) => { const canResetDbPassword = useCheckPermissions(PermissionAction.UPDATE, 'projects') const [showResetDbPass, setShowResetDbPass] = useState(false) - const [isUpdatingPassword, setIsUpdatingPassword] = useState(false) const [password, setPassword] = useState('') const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('') const [passwordStrengthWarning, setPasswordStrengthWarning] = useState('') const [passwordStrengthScore, setPasswordStrengthScore] = useState(0) + const { mutate: resetDatabasePassword, isLoading: isUpdatingPassword } = + useDatabasePasswordResetMutation({ + onSuccess: async () => { + const project = await getProjectDetail({ ref }) + if (project) meta.setProjectDetails(project) + + ui.setNotification({ + category: 'success', + message: 'Successfully updated database password', + }) + setShowResetDbPass(false) + }, + }) + useEffect(() => { if (showResetDbPass) { - setIsUpdatingPassword(false) setPassword('') setPasswordStrengthMessage('') setPasswordStrengthWarning('') @@ -62,26 +73,10 @@ const ResetDbPassword = ({ disabled = false }) => { } const confirmResetDbPass = async () => { - if (!ref) return + if (!ref) return console.error('Project ref is required') if (passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH) { - setIsUpdatingPassword(true) - const res = await patch(`${API_URL}/projects/${ref}/db-password`, { password }) - if (!res.error) { - const project = await getProjectDetail({ ref }) - if (project) { - meta.setProjectDetails(project) - } - - ui.setNotification({ category: 'success', message: res.message }) - setShowResetDbPass(false) - } else { - ui.setNotification({ - category: 'error', - message: `Failed to reset password: ${res.error.message}`, - }) - } - setIsUpdatingPassword(false) + resetDatabasePassword({ ref, password }) } } @@ -147,7 +142,6 @@ const ResetDbPassword = ({ disabled = false }) => { hideFooter header={
Reset database password
} confirmText="Reset password" - alignFooter="right" size="medium" visible={showResetDbPass} loading={isUpdatingPassword} @@ -175,8 +169,12 @@ const ResetDbPassword = ({ disabled = false }) => { -
-
- +
-
+

The project "{project?.name ?? ''}" is currently paused.

@@ -105,6 +105,18 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { You can also prevent project pausing in the future by upgrading to Pro.

)} + {!isFreePlan && ( +

+ Unpaused projects count towards compute usage. For every hour your instance is + active, we'll bill you based on the instance size of your project. See{' '} + + + Compute Instance Usage Billing + + {' '} + for more details. +

+ )}
diff --git a/studio/data/organizations/organization-migrate-billing-preview-query.ts b/studio/data/organizations/organization-migrate-billing-preview-query.ts index 8f9a117d5e6..ef5e6551319 100644 --- a/studio/data/organizations/organization-migrate-billing-preview-query.ts +++ b/studio/data/organizations/organization-migrate-billing-preview-query.ts @@ -10,12 +10,12 @@ export type OrganizationBillingMigrationPreviewVariables = { export type OrganizationBillingMigrationPreviewResponse = { added_credits: number - addons_to_be_removed?: { + addons_to_be_removed: { projectRef: string projectName: string addons: { variant: string; name: string; type: string }[] }[] - monthly_invoice_breakdown?: { + monthly_invoice_breakdown: { description: string unit_price: number quantity: number diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index 70f25698dde..4842dfbac91 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -467,20 +467,26 @@ const Wizard: NextPageWithLayout = () => { description={

- This organization uses the new organization-level billing and is on the{' '} + This organization uses the new organization-level-billing and is on the{' '} {orgSubscription?.plan?.name} plan.

{/* Show info when launching a new project in a paid org that already has at least one project */} {orgSubscription?.plan?.id !== 'free' && orgProjectCount > 0 && ( -

- Launching another project incurs additional compute costs (starting at $10 - per month). -

+
+

+ Launching another project incurs additional compute costs, starting at + $0.01344 per hour (~$10/month). +

+

+ You can also create a new organization under the free plan (unless you + have exceeded your 2 free project limit). +

+
)}
- +
- +
-
+

The project "{project?.name ?? ''}" is currently paused.

@@ -105,6 +105,18 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { You can also prevent project pausing in the future by upgrading to Pro.

)} + {!isFreePlan && ( +

+ Unpaused projects count towards compute usage. For every hour your instance is + active, we'll bill you based on the instance size of your project. See{' '} + + + Compute Instance Usage Billing + + {' '} + for more details. +

+ )}
diff --git a/studio/data/organizations/organization-migrate-billing-preview-query.ts b/studio/data/organizations/organization-migrate-billing-preview-query.ts index 8f9a117d5e6..ef5e6551319 100644 --- a/studio/data/organizations/organization-migrate-billing-preview-query.ts +++ b/studio/data/organizations/organization-migrate-billing-preview-query.ts @@ -10,12 +10,12 @@ export type OrganizationBillingMigrationPreviewVariables = { export type OrganizationBillingMigrationPreviewResponse = { added_credits: number - addons_to_be_removed?: { + addons_to_be_removed: { projectRef: string projectName: string addons: { variant: string; name: string; type: string }[] }[] - monthly_invoice_breakdown?: { + monthly_invoice_breakdown: { description: string unit_price: number quantity: number diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index 70f25698dde..4842dfbac91 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -467,20 +467,26 @@ const Wizard: NextPageWithLayout = () => { description={

- This organization uses the new organization-level billing and is on the{' '} + This organization uses the new organization-level-billing and is on the{' '} {orgSubscription?.plan?.name} plan.

{/* Show info when launching a new project in a paid org that already has at least one project */} {orgSubscription?.plan?.id !== 'free' && orgProjectCount > 0 && ( -

- Launching another project incurs additional compute costs (starting at $10 - per month). -

+
+

+ Launching another project incurs additional compute costs, starting at + $0.01344 per hour (~$10/month). +

+

+ You can also create a new organization under the free plan (unless you + have exceeded your 2 free project limit). +

+
)}
- + - {isPaused || !canPauseProject || !isProjectActive ? ( + {project !== undefined && (isPaused || !canPauseProject || !isProjectActive) ? ( diff --git a/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx b/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx index 0c3bd1bd5c8..f05bb989f35 100644 --- a/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx +++ b/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx @@ -87,7 +87,7 @@ const RestartServerButton = () => {
- {(!canRestartProject || !isProjectActive) && ( + {project !== undefined && (!canRestartProject || !isProjectActive) && ( diff --git a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 90d627cc098..72d71362d2c 100644 --- a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -159,8 +159,8 @@ interface ContentWrapperProps { * [TODO] Next iteration should scrape long polling and just listen to the project's status */ const ContentWrapper = ({ isLoading, children }: ContentWrapperProps) => { - const selectedProject = useSelectedProject() const router = useRouter() + const selectedProject = useSelectedProject() const requiresDbConnection: boolean = (!router.pathname.includes('/project/[ref]/settings') && diff --git a/studio/components/layouts/ProjectLayout/RestoringState.tsx b/studio/components/layouts/ProjectLayout/RestoringState.tsx index 5902f68d075..700f2a23bdf 100644 --- a/studio/components/layouts/ProjectLayout/RestoringState.tsx +++ b/studio/components/layouts/ProjectLayout/RestoringState.tsx @@ -1,15 +1,19 @@ +import { useQueryClient } from '@tanstack/react-query' +import { useParams } from 'common' import Link from 'next/link' import { useEffect, useRef, useState } from 'react' import { Button, IconAlertCircle, IconCheckCircle, IconLoader } from 'ui' -import { getProjectDetail } from 'data/projects/project-detail-query' +import { getProjectDetail, invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useStore } from 'hooks' import { getWithTimeout } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' import { useProjectContext } from './ProjectContext' const RestoringState = () => { + const { ref } = useParams() const { meta } = useStore() + const queryClient = useQueryClient() const { project } = useProjectContext() const checkServerInterval = useRef() @@ -45,9 +49,8 @@ const RestoringState = () => { setLoading(true) const projectDetail = await getProjectDetail({ ref: project?.ref }) - if (projectDetail) { - meta.setProjectDetails(projectDetail) - } + if (projectDetail) meta.setProjectDetails(projectDetail) + if (ref) await invalidateProjectDetailsQuery(queryClient, ref) } return ( diff --git a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx index dc7e4cd26cc..d978e253b6f 100644 --- a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx +++ b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx @@ -17,15 +17,17 @@ import { import { useParams } from 'common/hooks' import { useProjectUpgradingStatusQuery } from 'data/config/project-upgrade-status-query' -import { getProjectDetail } from 'data/projects/project-detail-query' +import { getProjectDetail, invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useStore } from 'hooks' import { IS_PLATFORM } from 'lib/constants' import { useProjectContext } from '../ProjectContext' import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants' +import { useQueryClient } from '@tanstack/react-query' const UpgradingState = () => { const { ref } = useParams() const { meta } = useStore() + const queryClient = useQueryClient() const { project } = useProjectContext() const [loading, setLoading] = useState(false) const [isExpanded, setIsExpanded] = useState(false) @@ -55,9 +57,8 @@ const UpgradingState = () => { setLoading(true) const projectDetail = await getProjectDetail({ ref }) - if (projectDetail) { - meta.setProjectDetails(projectDetail) - } + if (projectDetail) meta.setProjectDetails(projectDetail) + if (ref) await invalidateProjectDetailsQuery(queryClient, ref) } const subject = 'Upgrade%20failed%20for%20project' diff --git a/studio/data/projects/project-detail-query.ts b/studio/data/projects/project-detail-query.ts index 4d7408cf4e5..cab2475d06c 100644 --- a/studio/data/projects/project-detail-query.ts +++ b/studio/data/projects/project-detail-query.ts @@ -1,5 +1,4 @@ -import { QueryClient, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' -import { useCallback } from 'react' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, isResponseOk } from 'lib/common/fetch' import { API_URL } from 'lib/constants' From b34146d7c451f4ff241285cbf6ce6763e461192f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 14:56:43 +0800 Subject: [PATCH 162/163] Revert changes in passwordStrengthCheck --- studio/lib/helpers.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts index a03468b00de..815e16a0bfc 100644 --- a/studio/lib/helpers.ts +++ b/studio/lib/helpers.ts @@ -1,4 +1,3 @@ -import { checkPasswordStrength } from 'data/profile/password-check-mutation' import { post } from 'lib/common/fetch' import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH, PASSWORD_STRENGTH } from 'lib/constants' import { toast } from 'react-hot-toast' @@ -190,8 +189,10 @@ export async function passwordStrength(value: string) { message = `${PASSWORD_STRENGTH[0]} Maximum length of password exceeded` warning = `Password should be less than 100 characters` } else { - try { - const { result } = await checkPasswordStrength({ password: value }) + // [Joshen] Unable to use RQ atm due to our Jest tests being in JS + const response = await post(`${API_URL}/profile/password-check`, { password: value }) + if (!response.error) { + const { result } = response const resultScore = result?.score ?? 0 const score = (PASSWORD_STRENGTH as any)[resultScore] @@ -208,8 +209,8 @@ export async function passwordStrength(value: string) { result?.feedback?.warning ? result?.feedback?.warning + '.' : '' } You need a stronger password.` } - } catch (error: any) { - toast.error(`Failed to check password strength: ${error.message}`) + } else { + toast.error(`Failed to check password strength: ${response.error.message}`) } } } From b936fa12b58b40007d2c193d8eed982cacd465a9 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 15:12:59 +0800 Subject: [PATCH 163/163] Add loading state when enabling hooks --- studio/components/to-be-cleaned/ProductEmptyState.tsx | 9 ++++++++- studio/pages/project/[ref]/database/hooks/index.tsx | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/studio/components/to-be-cleaned/ProductEmptyState.tsx b/studio/components/to-be-cleaned/ProductEmptyState.tsx index 09eaff00b57..ab85f4820cb 100644 --- a/studio/components/to-be-cleaned/ProductEmptyState.tsx +++ b/studio/components/to-be-cleaned/ProductEmptyState.tsx @@ -9,6 +9,7 @@ interface ProductEmptyStateProps { infoButtonLabel?: string infoButtonUrl?: string onClickCta?: () => void + loading?: boolean disabled?: boolean disabledMessage?: string } @@ -21,6 +22,7 @@ const ProductEmptyState = ({ infoButtonLabel = '', infoButtonUrl = '', onClickCta = () => {}, + loading = false, disabled = false, disabledMessage = '', }: PropsWithChildren) => { @@ -39,7 +41,12 @@ const ProductEmptyState = ({ {ctaButtonLabel && onClickCta && ( - diff --git a/studio/pages/project/[ref]/database/hooks/index.tsx b/studio/pages/project/[ref]/database/hooks/index.tsx index 71f7f6bdf0d..86ccc236c4b 100644 --- a/studio/pages/project/[ref]/database/hooks/index.tsx +++ b/studio/pages/project/[ref]/database/hooks/index.tsx @@ -30,7 +30,7 @@ const HooksPage: NextPageWithLayout = () => { const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') - const { mutate: enableHooks } = useHooksEnableMutation({ + const { mutate: enableHooks, isLoading: isEnablingHooks } = useHooksEnableMutation({ onSuccess: () => { meta.schemas.load() ui.setNotification({ @@ -85,8 +85,11 @@ const HooksPage: NextPageWithLayout = () => { title="Database Webhooks" ctaButtonLabel="Enable webhooks" onClickCta={() => enableHooksForProject()} - disabled={!canCreateWebhooks} - disabledMessage="You need additional permissions to enable webhooks" + loading={isEnablingHooks} + disabled={isEnablingHooks || !canCreateWebhooks} + disabledMessage={ + !canCreateWebhooks ? 'You need additional permissions to enable webhooks' : undefined + } >

Database Webhooks can be used to trigger serverless functions or send requests to an