Merge branch 'master' into chore/storage-download-multiple-files-as-zip

This commit is contained in:
Joshen Lim authored and GitHub committed 2023-01-09 21:34:46 +08:00
commit 58adb54180
105 files changed
+10908 -8533

No files matched your search

+1 -1
View File
@@ -142,7 +142,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td>-</td>
<td><a href="https://github.com/supabase-community/gotrue-java" target="_blank" rel="noopener noreferrer">gotrue-java</a></td>
<td>-</td>
<td><a href="https://github.com/supabase-community/storage-java" target="_blanke" rel="noopener noreferrer">storage-java</a></td>
<td><a href="https://github.com/supabase-community/storage-java" target="_blank" rel="noopener noreferrer">storage-java</a></td>
<td>-</td>
</tr>
<tr>
@@ -204,6 +204,11 @@ export const menuItems: NavMenu = {
url: '/guides/database/extensions/plv8',
items: [],
},
{
name: 'PostGIS: Geo queries',
url: '/guides/database/extensions/postgis',
items: [],
},
{
name: 'uuid-ossp: Unique Identifiers',
url: '/guides/database/extensions/uuid-ossp',
@@ -42,13 +42,14 @@ export const gettingstarted = {
items: [
{ name: 'Features', url: '/guides/getting-started/features', items: [] },
{ name: 'Architecture', url: '/guides/getting-started/architecture', items: [] },
// {
// name: 'Quickstarts',
// items: [
// { name: 'NextJS', url: '/guides/getting-started/quickstarts/nextjs', items: [] },
// { name: 'ReactJS', url: '/guides/getting-started/quickstarts/reactjs', items: [] },
// ],
// },
{
name: 'Framework Quickstarts',
items: [
{ name: 'ReactJS', url: '/guides/getting-started/quickstarts/reactjs', items: [] },
{ name: 'NextJS', url: '/guides/getting-started/quickstarts/nextjs', items: [] },
{ name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter', items: [] },
],
},
{
name: 'Web app tutorials',
items: [
@@ -389,6 +390,11 @@ export const database = {
url: '/guides/database/extensions/pgnet',
items: [],
},
{
name: 'PostGIS: Geo queries',
url: '/guides/database/extensions/postgis',
items: [],
},
{ name: 'pgTAP: Unit Testing', url: '/guides/database/extensions/pgtap', items: [] },
{
name: 'uuid-ossp: Unique Identifiers',
@@ -7,10 +7,6 @@ import { IconChevronLeft } from '~/../../packages/ui'
import * as NavItems from './NavigationMenu.constants'
import * as Accordion from '@radix-ui/react-accordion'
interface ImPureProps {
// users: Array<User>
}
const HeaderImage = React.memo(function HeaderImage(props: any) {
const router = useRouter()
const { isDarkMode } = useTheme()
@@ -45,12 +41,6 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
const router = useRouter()
const { isDarkMode } = useTheme()
let subItemMenuOpen = false
if (router.asPath.includes(props.subItem.url)) {
subItemMenuOpen = true
}
const LinkContainer = (props) => {
return (
<Link href={props.url} passHref>
@@ -188,7 +178,7 @@ const Content = (props) => {
return (
<div key={x.name}>
{x.items && x.items.length > 0 ? (
<div>
<div className="flex flex-col gap-1">
{x.items.map((subItem, subItemIndex) => {
return (
<ContentAccordionLink
@@ -148,7 +148,7 @@ const RenderLink = React.memo(function RenderLink(props: any) {
const SideMenuTitle = ({ title }: { title: string }) => {
return (
<span className="font-mono text-xs uppercase text-scale-1200 font-medium tracking-wider mb-3">
<span className="font-mono text-xs uppercase text-scale-1200 font-medium tracking-wider">
{title}
</span>
)
@@ -245,7 +245,7 @@ const Content: React.FC<INavigationMenuRefList> = ({ id, lib, commonSections, sp
<RevVersionDropdown />
</div>
<ul className="function-link-list">
<ul className="function-link-list flex flex-col gap-1">
{sections.map((fn: any, fnIndex) => {
// run allow check
if (isFuncNotInLibraryOrVersion(fn.id, fn.type, allowedKeys)) {
@@ -34,6 +34,10 @@ Make sure to specify the `on delete cascade` clause when referencing `auth.users
</Admonition>
## Deleting Users
You may delete users directly or via the management console at Authentication > Users. Note that deleting a user from the `auth.users` table does not automatically sign out a user. As Supabase makes use of JSON Web Tokens (JWT), a user's JWT will remain "valid" until it has expired. Should you wish to immediately revoke access for a user, do considering making use of a Row Level Security policy as described below.
## Public access
Since Row Level Security is enabled, this table is accessible via the API but no data will be returned unless we set up some Policies.
@@ -6,7 +6,7 @@ export const meta = {
description: 'Add Twitter OAuth to your Supabase project',
}
To enable Twitter Auth for your project, you need to set up a Twitter OAuth application with [elevated access](https://developer.twitter.com/en/portal/products/elevated)and add the application credentials in the Supabase Dashboard.
To enable Twitter Auth for your project, you need to set up a Twitter OAuth application with [elevated access](https://developer.twitter.com/en/portal/products/elevated) and add the application credentials in the Supabase Dashboard.
## Overview
@@ -0,0 +1,302 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'postgis',
title: 'PostGIS: Geo queries',
description: 'Working with geo-spatial data in Postgres',
}
[PostGIS](https://postgis.net/) is a Postgres extension that allows you to interact with Geo data within Postgres. You can sort your data by geographic location, get data within certain geographic boundaries, and do much more with it.
## Overview
While you may be able to store simple lat/long geographic coordinates as a set of decimals, it does not scale very well when you try to query through a large data set. PostGIS comes with special data types that are efficient, and indexable for high scalability.
The additional data types that PostGIS provides include [Point](https://postgis.net/docs/using_postgis_dbmanagement.html#Point), [Polygon](https://postgis.net/docs/using_postgis_dbmanagement.html#Polygon), [Linestring](https://postgis.net/docs/using_postgis_dbmanagement.html#LineString), and many more to represent different types of geographical data. In this guide, we will mainly focus on how to interact with `Point` type, which represents a single set of latitude and longitude. If you are interested in digging deeper, you can learn more about different data types on the [data management section of PostGIS docs](https://postgis.net/docs/using_postgis_dbmanagement.html).
## Usage
### Enable the extension
You can get started with PostGIS by enabling the PostGIS extension in your Supabase dashboard.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "postgis" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
-- Example: enable the "postgis" extension
create extension postgis with schema extensions;
-- Example: disable the "postgis" extension
drop extension if exists postgis;
```
</TabPanel>
</Tabs>
## Examples
Now that we are ready to get started with PostGIS, let’s create a table and see how we can utilize PostGIS for some typical use cases. Let’s imagine we are creating a simple restaurant-searching app.
Let’s create our table. Each row represents a restaurant with its location stored in `location` column as a `Point` type.
```sql
create table if not exists public.restaurants (
id int generated by default as identity primary key,
name text not null,
location geography(POINT) not null
);
```
We can then set a [spatial index](https://postgis.net/docs/using_postgis_dbmanagement.html#build-indexes) on the `location` column of this table.
```sql
create index restaurants_geo_index
on public.restaurants
using GIST (location);
```
### Inserting data
You can insert geographical data through SQL or through our API.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="data"
>
<TabPanel id="data" label="Data">
<h4>Restaurants</h4>
| id | name | location |
| --- | ----------- | -------------------------------- |
| 1 | Supa Burger | lat: 40.807416, long: -73.946823 |
| 2 | Supa Pizza | lat: 40.807475, long: -73.94581 |
| 3 | Supa Taco | lat: 40.80629, long: -73.945826 |
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
insert into public.restaurants
(name, location)
values
('Supa Burger', st_point(-73.946823 40.807416)),
('Supa Pizza', st_point(-73.94581 40.807475)),
('Supa Taco', st_point(-73.945826 40.80629));
```
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { error } = await supabase.from('restaurants').insert([
{
name: 'Supa Burger',
location: 'POINT(-73.946823 40.807416)',
},
{
name: 'Supa Pizza',
location: 'POINT(-73.94581 40.807475)',
},
{
name: 'Supa Taco',
location: 'POINT(-73.945826 40.80629)',
},
])
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
await supabase.from('restaurants').insert([
{
'name': 'Supa Burger',
'location': 'POINT(-73.946823 40.807416)',
},
{
'name': 'Supa Pizza',
'location': 'POINT(-73.94581 40.807475)',
},
{
'name': 'Supa Taco',
'location': 'POINT(-73.945826 40.80629)',
},
]);
```
</TabPanel>
</Tabs>
Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is that there is no comma between the two values, just a single space.
At this point, if you go into your Supabase dashboard and look at the data, you will notice that the value of the `location` column looks something like this.
```
0101000020E6100000A4DFBE0E9C91614044FAEDEBC0494240
```
We can query the `restaurants` table directly, but it will return the `location` column in the format you see above. We will create [database functions](https://supabase.com/docs/guides/database/functions) so that we can use the [st_astext()](https://postgis.net/docs/ST_AsText.html) function to convert it back to a human-readable format like `POINT(-73.946713 40.807313)`.
### Order by distance
Sorting datasets from closest to farthest, sometimes called nearest-neighbor sort, is a very common use case in Geo-queries. PostGIS can handle it very easily with the use of the [`<->`](https://postgis.net/docs/geometry_distance_knn.html) operator. `<->` operator returns the two-dimensional distance between two geometries and will utilize the spatial index when used within `order by` clause. You can create the following database function to sort the restaurants from closest to farthest by passing the current locations as parameters.
```sql
create or replace function nearby_restaurants(lat float, long float)
returns setof record
language sql
as $$
select id, name, st_astext(location) as location, st_distance(location, st_point(long, lat)::geography) as dist_meters
from public.restaurants
order by location <-> st_point(long, lat)::geography;
$$;
```
You can call this function from your client using `rpc()` like this:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.rpc('nearby_restaurants', {
lat: 40.807313,
long: -73.946713,
})
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.rpc('nearby_restaurants',params: {
'lat': 40.807313,
'long': -73.946713,
});
```
</TabPanel>
<TabPanel id="result" label="Result">
```json
[
{
"id": 1,
"name": "Supa Burger",
"location": "POINT(-73.946823 40.807416)",
"dist_meters": 14.73033739
},
{
"id": 2,
"name": "Supa Pizza",
"location": "POINT(-73.94581 40.807475)",
"dist_meters": 78.28980007
},
{
"id": 3,
"name": "Supa Taco",
"location": "POINT(-73.945826 40.80629)",
"dist_meters": 136.04329002
}
]
```
</TabPanel>
</Tabs>
### Finding all data points within a bounding box
![Searching within a bounding box of a map](/docs/img/guides/database/extensions/postgis/map.png)
When you are working on a map-based application where the user scrolls through your map, you might want to load the that lies within the bounding box of the map every time your users scroll. PostGIS can return the rows that are within the bounding box just by supplying the bottom left and the top right coordinates. Let’s look at what the function would look like.
```sql
create or replace function restaurants_in_view(min_lat float, min_long float, max_lat float, max_long float)
returns setof record
language sql
as $$
select id, name, st_astext(location) as location
from public.restaurants
where location && ST_SetSRID(ST_MakeBox2D(ST_Point(min_long, min_lat), ST_Point(max_long, max_lat)),4326)
$$;
```
[`&&`](https://postgis.net/docs/geometry_overlaps.html) operator used in the `where` statement here returns a boolean of whether the bounding box of the two geometries intersect or not. We basically are creating a bounding box from the two points and finding those points that fall under the bounding box. We are also utilizing a few different PostGIS functions here.
- [ST_MakeBox2D](https://postgis.net/docs/ST_MakeBox2D.html): Creates a 2-dimensional box from two points.
- [ST_SetSRID](https://postgis.net/docs/ST_SetSRID.html): Sets the [SRID](https://postgis.net/docs/manual-dev/using_postgis_dbmanagement.html#spatial_ref_sys), which is an identifier of what coordinate system to use, for the geometry. 4326 the standard longitude and latitude coordinate systems.
You can call this function from your client using `rpc()` like this:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.rpc('restaurants_in_view', {
min_lat: 40.807,
min_long: -73.946,
max_lat: 40.808,
max_long: -73.945,
})
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.rpc('restaurants_in_view', params: {
'min_lat': 40.807,
'min_long': -73.946,
'max_lat': 40.808,
'max_long': -73.945,
});
```
</TabPanel>
<TabPanel id="result" label="Result">
```json
[
{
"id": 2,
"name": "Supa Pizza",
"location": "POINT(-73.94581 40.807475)"
}
]
```
</TabPanel>
</Tabs>
## Resources
- [Official PostGIS documentation](https://postgis.net/documentation/)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,196 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Use Supabase with Flutter',
subtitle:
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.',
breadcrumb: 'Framework Quickstarts',
}
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Set up a Supabase project with sample data">
[Create a new project](https://app.supabase.com) in the Supabase Dashboard.
After your project is ready, create a table in your Supabase database using the [SQL Editor](https://app.supabase.com/project/_/sql) in the Dashboard. Use the following SQL statement to create a `countries` table with some sample data.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql SQL_EDITOR
-- Create the table
CREATE TABLE countries (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
-- Insert some sample data into the table
INSERT INTO countries (name) VALUES ('United States');
INSERT INTO countries (name) VALUES ('Canada');
INSERT INTO countries (name) VALUES ('Mexico');
````
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Create a Flutter app">
Create a Flutter app using the `flutter create` command. You can skip this step if you already have a working app.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
flutter create my_app
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Install the Supabase client library">
The fastest way to get started is to use the [`supabase_flutter`](https://pub.dev/packages/supabase_flutter) client library which provides a convenient interface for working with Supabase from a Flutter app.
Open the `pubspec.yaml` file inside your Flutter app and add `supabase_flutter` as a dependency.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```yaml pubspec.yaml
supabase_flutter: ^1.2.2
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Initialize the Supabase client">
Open `lib/main.dart` and edit the main function to initialize Supabase using your [project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```dart lib/main.dart
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
);
runApp(MyApp());
}
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Query data from the app">
Use a `FutureBuilder` to fetch the data when the home page loads and display the query result in a `ListView`.
Replace the default `MyApp` and `MyHomePage` classes with the following code.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```dart lib/main.dart
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Countries',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _future = Supabase.instance.client
.from('countries')
.select<List<Map<String, dynamic>>>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final countries = snapshot.data!;
return ListView.builder(
itemCount: countries.length,
itemBuilder: ((context, index) {
final country = countries[index];
return ListTile(
title: Text(country['name']),
);
}),
);
},
),
);
}
}
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details title="Start the app">
Run your app on a platform of your choosing! By default an app should launch in your web browser.
Note that `supabase_flutter` is compatible with web, iOS, Android, macOS, and Windows apps.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
flutter run
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
export default Page
@@ -3,7 +3,8 @@ import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Use Supabase with NextJS',
subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a NextJS app.',
subtitle:
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a NextJS app.',
breadcrumb: 'Framework Quickstarts',
}
@@ -58,7 +59,7 @@ export const meta = {
<StepHikeCompact.Details title="Install the Supabase client library">
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a NextJS app.
Navigate to the NextJS app and install `supabase-js`.
</StepHikeCompact.Details>
@@ -95,8 +96,8 @@ export const meta = {
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Query data from the app">
Use a `getInitialProps` method to fetch the data when the page loads and display the query result as a simple list.
Use the `getServerSideProps` method to fetch the data server-side and display the query result as a simple list.
Replace the existing function in your index file in the `pages` directory with the following code.
</StepHikeCompact.Details>
@@ -116,11 +117,13 @@ export const meta = {
);
}
Page.getInitialProps = async () => {
const { data, error } = await supabase.from('countries').select()
export async function getServerSideProps() {
let { data } = await supabase.from('countries').select()
return {
countries: data
props: {
countries: data
},
}
}
@@ -3,7 +3,8 @@ import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Use Supabase with React',
subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a React app.',
subtitle:
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a React app.',
breadcrumb: 'Framework Quickstarts',
}
@@ -58,7 +59,7 @@ export const meta = {
<StepHikeCompact.Details title="Install the Supabase client library">
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a React app.
Navigate to the React app and install `supabase-js`.
</StepHikeCompact.Details>
@@ -73,11 +74,11 @@ export const meta = {
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Query data from the app">
In `index.js`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
Add a `getCountries` function to fetch the data and log the query result to the browser console.
</StepHikeCompact.Details>
@@ -90,7 +91,7 @@ export const meta = {
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
async function getCountries() {
const countries = await supabase.from('countries').select('*')
const countries = await supabase.from('countries').select()
console.log(countries)
}
@@ -101,7 +102,7 @@ export const meta = {
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Start the app">
Start the app, go to http://localhost:3000 in a browser, and open the browser console and you should see the list of countries.
@@ -129,6 +129,7 @@ Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component
import { useState } from 'react'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
import { AppProps } from 'next/app'
function MyApp({
Component,
@@ -13,7 +13,7 @@ In this guide we will build a simple expense tracker web application using Stytc
We will leverage Supabase to store and authorize access to user data. Supabase makes it simple to set up Row Level Security (RLS) policies which ensure users can only read and write data that they are authorized to do so. If you do not already have a Supabase account, you will need to create one.
This guide will use [Next.js](https://nextjs.org/) which is a web application framework built on top of React. Stytch provides a [Node.js library](https://github.com/stytchauth/stytch-node) and a [React library](https://github.com/stytchauth/stytch-react) which makes building Next.js apps super easy.
This guide will use [Next.js](https://nextjs.org/) which is a web application framework built on top of React. Stytch provides a [Node.js library](https://github.com/stytchauth/stytch-node) and a [React library](https://stytch.com/docs/sdks/javascript-sdk) which makes building Next.js apps super easy.
> Note: You can find a completed version of this project on [Github](https://github.com/stytchauth/stytch-nextjs-supabase).
@@ -8,18 +8,18 @@ export const meta = {
Every project on the Supabase Platform comes with its own dedicated Postgres instance running inside a virtual machine (VM). The following table describes the base instance with additional compute add-ons available if you need extra performance when scaling up Supabase.
| Plan | Pricing | CPU | Memory | Disk IO Bandwidth | Connections: Direct | Connections: Pooler |
| --------------- | ------- | ----------------------- | ------ | ----------------- | ------------------- | -------------------- |
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | Up to 2,606 Mbps | 10 (recommended) | 50 (recommended) |
| Small | $5 | 2-core ARM (shared) | 2 GB | Up to 2,606 Mbps | 30 (recommended) | 75 (recommended) |
| Medium | $50 | 2-core ARM (shared) | 4 GB | Up to 2,606 Mbps | 50 (recommended) | 150 (recommended) |
| Large | $100 | 2-core ARM (dedicated) | 8 GB | Up to 4,750 Mbps | 100 (recommended) | 300 (recommended) |
| XL | $200 | 4-core ARM (dedicated) | 16 GB | Up to 4,750 Mbps | 200 (recommended) | 600 (recommended) |
| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | Up to 4,750 Mbps | 350 (recommended) | 1200 (recommended) |
| 4XL | $950 | 16-core ARM (dedicated) | 64 GB | Up to 4,750 Mbps | 420 (recommended) | 2800 (recommended) |
| 8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | Up to 9,000 Mbps | 450 (recommended) | 5600 (recommended) |
| 12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | Up to 13,500 Mbps | 480 (recommended) | 8600 (recommended) |
| 16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | Up to 19,000 Mbps | 500 (recommended) | 11,600 (recommended) |
| Plan | Pricing | CPU | Memory | Maximum Disk IO Bandwidth | Baseline Disk IO Bandwidth | Connections: Direct (recommended) | Connections: Pooler (recommended) |
| --------------- | ------- | ----------------------- | ------ | ------------------------- | -------------------------- | --------------------------------- | --------------------------------- |
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | 2,606 Mbps | 87 Mbps | 10 | 50 |
| Small | $5 | 2-core ARM (shared) | 2 GB | 2,606 Mbps | 174 Mbps | 30 | 75 |
| Medium | $50 | 2-core ARM (shared) | 4 GB | 2,606 Mbps | 347 Mbps | 50 | 150 |
| Large | $100 | 2-core ARM (dedicated) | 8 GB | 4,750 Mbps | 630 Mbps | 100 | 300 |
| XL | $200 | 4-core ARM (dedicated) | 16 GB | 4,750 Mbps | 1,188 Mbps | 200 | 600 |
| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | 4,750 Mbps | 2,375 Mbps | 350 | 1200 |
| 4XL | $950 | 16-core ARM (dedicated) | 64 GB | 4,750 Mbps | 4,750 Mbps | 420 | 2800 |
| 8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | 9,500 Mbps | 9,500 Mbps | 450 | 5600 |
| 12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | 14,250 Mbps | 14,250 Mbps | 480 | 8600 |
| 16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | 19,000 Mbps | 19,000 Mbps | 500 | 11,600 |
[Contact us](https://supabase.com/contact/enterprise) if you require a custom plan.
@@ -33,7 +33,9 @@ When considering compute upgrades, assess whether your bottlenecks are hardware-
## Disk IO bandwidth
Disk IO bandwidth can be measured in IOPS, a unit of measurement representing the number of input/output operations per second. These operations are calculated in Megabits per second. Depending on the read/write load of your application, you may need to upgrade to a larger instance capable of a higher number of IOPS. If you're unsure of how many IOPS your application requires, you can load test your project and inspect these [metrics in the Dashboard](https://app.supabase.com/project/_/reports). Alternatively, you can start on a smaller instance size and upgrade later as needed.
SSD Disks are attached to your servers and the disk performance of your workload is determined by the Disk IO bandwidth of this connection. Smaller compute instances can burst up to the maximum disk IO bandwidth for 30 minutes in a day. Beyond that, the performance reverts to the baseline disk IO bandwidth. For example, the free tier can burst up to 2,606 Mbps for 30 minutes a day and reverts to the baseline performance of 87 Mbps. If you need consistent disk performance, choose the 4XL or larger compute add-on which has the same baseline and maximum disk IO bandwidth.
If you're unsure of how many IOPS your application requires, you can load test your project and inspect these [metrics in the Dashboard](https://app.supabase.com/project/_/reports). If the `Daily Disk IO Budget % Remaining` stat is less than 100%, it indicates that your workload has burst beyond the baseline IO throughput during the day. If this metric drops to zero, the workload has used up all the burst IO throughput minutes during the day and is running at the baseline performance. These projects are good candidates for upgrading to a larger compute add with higher baseline throughput.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
+11 -7
View File
@@ -11,16 +11,15 @@ export const meta = {
<h2 className="text-base max-w-xl m-0">
Supabase is an open source Firebase alternative providing all the backend features you need to
build a product. [Learn more](/docs/guides/getting-started/architecture) about Supabase, start
with the [tutorials](/docs/guides/getting-started#tutorials), or dive straight into the different
products and APIs.
build a product. [Learn more](/docs/guides/getting-started/architecture) about Supabase, follow a
quickstart for an overview, or dive straight into the different products and APIs.
</h2>
{/* start container */}
<div className="flex flex-col gap-12 my-12">
<div className="space-y-8 hidden">
<div className="space-y-8">
<div className="
border border-scale-400
relative overflow-hidden
@@ -63,16 +62,21 @@ export const meta = {
<Button size="small">Get started</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-6">
<Link href={`/getting-started/quickstarts/reactjs`} passHref>
<Link href={`/guides/getting-started/quickstarts/reactjs`} passHref>
<a className="no-underline">
<IconPanel title="ReactJS" icon="/docs/img/icons/react-icon" />
</a>
</Link>
<Link href={`/getting-started/quickstarts/nextjs`} passHref>
<Link href={`/guides/getting-started/quickstarts/nextjs`} passHref>
<a className="no-underline">
<IconPanel title="NextJS" icon="/docs/img/icons/nextjs-icon" />
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/flutter`} passHref>
<a className="no-underline">
<IconPanel title="Flutter" icon="/docs/img/icons/flutter-icon" />
</a>
</Link>
</div>
</div>
</div>
@@ -106,7 +110,7 @@ export const meta = {
md:items-center md:gap-3
">
<h2 className="text-sm lg:text-base text-scale-1200 m-0">Ready to make a fully working app?</h2>
<p className="text-sm lg:text-base text-scale-1100 m-0">Follow the step-by-step <Link href="/guides/tutorials"><a className="m-0 text-scale-1200 underline">tutorials</a></Link>.</p>
<p className="text-sm lg:text-base text-scale-1100 m-0">Follow the step-by-step <Link href="/guides/getting-started#tutorials"><a className="m-0 text-scale-1200 underline">tutorials</a></Link>.</p>
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

@@ -39,7 +39,7 @@ Supa SQL App turns human language into SQL queries. Simply tell it what you want
### Winner
[HTTP Sense](https://github.com/http-sense/http-sense) - by [@tchyut_p](https://twitter.com/tchyut_p).
[HTTP Sense](https://github.com/http-sense/http-sense) - by [@tchyut_p](https://twitter.com/tchyut_p) and [@nkitsaini](https://github.com/nkitsaini).
HTTP Sense is a reverse proxy and network monitoring tool that provides comprehensive network monitoring for your backend servers and microservices. It provides a secure and high-performance gateway that can be used to monitor incoming traffic.
@@ -0,0 +1,121 @@
---
title: Supabase Beta December 2022
description: This month the Beta Update is a Launch Week 6 Special, where we review the cascade of announcements.
author: ant_wilson
image: 2023-01-05-december-beta-update/monthly-update-december2022.jpg
thumb: 2023-01-05-december-beta-update/monthly-update-december2022.jpg
tags:
- release-notes
date: '2023-01-05'
toc_depth: 3
---
Happy New Year! Hope you are off to a great start. We closed 2022 on a high note with [Launch Week 6](https://supabase.com/launch-week) and a cascade of announcements. Here is everything we shipped so you can try them all 💻
## Day 1: New Docs
![Day 1: New Docs](/images/blog/2023-01-05-december-beta-update/docs.jpg)
Documentation is super important to us, it's part of the product, and the Supabase docs needed a refresh. That's why we worked on a new redesign, better navigation, better reference docs, and we rebuilt the site using Next.js.
- [Blog Post](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
- [Video announcement](https://www.youtube.com/watch?v=Q1Amk6iDlF8)
## Day 2: Storage v2 - Image resizing and Smart CDN
![Day 2: Storage v2 - Image resizing and Smart CDN](/images/blog/2023-01-05-december-beta-update/storage-v2-image-rezising.jpg)
We introduced three new features for Supabase Storage: Image resizing, webhooks, and a Smart CDN. These features are designed to work together to deliver a next-gen image resizing system.
- [Blog Post](https://supabase.com/blog/storage-image-resizing-smart-cdn)
- [Video announcement](https://www.youtube.com/watch?v=NpEl20iuOtg)
## Day 3 - Multi-factor Authentication via Row Level Security Enforcement
![Day 3 - Multi-factor Authentication via Row Level Security Enforcement](/images/blog/2023-01-05-december-beta-update/multi-factor-authentication.png)
We released Multi-factor Authentication (MFA) for everyone, including the option to enforce it via RLS 🔐. Plus, in preparation for releasing SAML, we're "dogfooding" the feature with the introduction of Single Sign On (SSO) on our dashboard.
- [Blog post](https://supabase.com/blog/mfa-auth-via-rls)
- [Video announcement](https://www.youtube.com/watch?v=He7LI2mv9v0)
## Day 4 - Supabase Wrappers, a Postgres FDW framework
![Day 4 - Supabase Wrappers, a Postgres FDW framework](/images/blog/2023-01-05-december-beta-update/supabase-wrappers.jpg)
Foreign Data Wrappers are a core feature of PostgreSQL. With Supabase Wrappers, we've extended this feature to query other databases or any other external system (including third-party APIs), using SQL.
- [Blog post](https://supabase.com/blog/postgres-foreign-data-wrappers-rust)
- [Video announcement](https://www.youtube.com/watch?v=QA2qC5F-4OU)
## Day 5 - The Vault is now in Beta
![Day 5 - The Vault is now in Beta](/images/blog/2023-01-05-december-beta-update/vault.jpg)
The Vault is a new Postgres extension that makes it safe and easy to store encrypted secrets and encrypt other stored data in your database.
- [Blog post](https://supabase.com/blog/vault-now-in-beta)
- [Video announcement](https://www.youtube.com/watch?v=QHLPNDrdN2w)
## Community Day
![Community Day](/images/blog/2023-01-05-december-beta-update/community.jpg)
It's starting to feel like Supabase is really just a "community of communities" 🤗. Far too much has happened in the last 3 months for us to possibly feature it all here, so here's a collection of the highlights!
[Blog post](https://supabase.com/blog/launch-week-6-community-day)
## Postgres Ecosystem
We are all in on Postgres. That is why contributing to the ecosystem is always a priority. Launch Week 6 was no exception, with several announcements:
![Postgres Ecosystem](/images/blog/2023-01-05-december-beta-update/postgres-ecosystem.jpg)
- [pg_graphql v1.0](https://supabase.com/blog/pg-graphql-v1)
- [PostgREST 11 pre-release](https://supabase.com/blog/postgrest-11-prerelease)
- [What's new in Postgres 15\?](https://supabase.com/blog/new-in-postgres-15)
- [pg_crdt - an experimental CRDT extension for Postgres](https://supabase.com/blog/postgres-crdt)
## Custom Domains and Point-in-Time Recovery
![Custom Domains and Point-in-Time Recovery](/images/blog/2023-01-05-december-beta-update/custom-domains-pitr.jpg)
Two key features to level up your projects are now available: Custom domains allow you to use your own domain for your Supabase project, while PITR provides the ability for a database to be restored at any specified point in time.
- [Custom Domains](https://supabase.com/blog/custom-domain-names)
- [Point in Time Recovery](https://supabase.com/blog/postgres-point-in-time-recovery)
## Hackathon Winners
![Hackathon Winners](/images/blog/2023-01-05-december-beta-update/lw-6-hackathon.jpg)
It was great to see so many high-quality projects. The decision wasn't easy, but in the end, the panel of judges chose Depulso as the winner of the Best Overall Project.
Congratulations 👏 to [@_iamkarthikeyan](https://twitter.com/_iamkarthikeyan) and [@SindhuMansi](https://twitter.com/SindhuMansi) who get each a Supabase Darkmode Keyboard and the $1500 Github sponsorship!
- [Full list of Winners](https://supabase.com/blog/launch-week-6-hackathon-winners)
- [See all the submissions](https://www.madewithsupabase.com/launch-week-6)
## Extended Community Highlights
![Community](/images/blog/2022-june/community.jpg)
- FlutterFlow and Supabase integration. [Video](https://www.youtube.com/watch?v=hw9Q-NjASbU)
- ClickHouse and PostgreSQL - a match made in data heaven. [Article](https://clickhouse.com/blog/migrating-data-between-clickhouse-postgres)
- Build any Front-end like Admin Panel or CRM for your Supabase. [Article](https://blog.jetadmin.io/how-to-build-any-front-end-such-as-admin-panel-or-crm-for-your-supabase-in-minutes/)
- Everything Svelte: a course with all you need to know to build a modern web application. [Course](https://www.everythingsvelte.com/)
- Edgy Edge Functions: a look at a new Edge Function per week. [Youtube Playlist](https://www.youtube.com/playlist?list=PL5S4mPUpp4OulD3olUW8Eq1IYKpUbk5Ob)
- Pixels, an app that provides canvas to create pixel art. [App](https://pixels.refine.dev/) | [Github Repo](https://github.com/refinedev/refine/tree/next/examples/pixels)
- Supabase Angular authentication with RxJS Observables. [Tutorial](https://dev.to/kylerummens/supabase-angular-authentication-with-rxjs-observables-3d0f)
- Create Calendar Events in React With Google Calendar API and Supabase. [Video Tutorial](https://www.youtube.com/watch?v=tgcCl52EN84)
- Going Serverless with Supabase. [Part 1](https://dev.to/davepar/going-serverless-with-supabase-103h)
- Supabase a Complete Backend Solution. [Video](https://www.youtube.com/watch?v=Ve0qE55fw0Y)
- Supabase OAuth with SvelteKit (Discord, Google, GitHub). [Video](https://www.youtube.com/watch?v=KfezTtt2GsA)
## Meme Zone
As always, one of our favorite memes from last month. [Follow us on Twitter](https://twitter.com/supabase) for more.
![Supabase Beta December Meme](/images/blog/2023-01-05-december-beta-update/beta-update-december-meme.jpeg)
See you next month!
+1 -2
View File
@@ -24,8 +24,7 @@ const Favicons = () => {
/>
<link rel="manifest" href={`${basePath}/favicon/site.webmanifest`} />
{/* <link rel="mask-icon" href={`${basePath}/favicon/safari-pinned-tab.svg`} color="#333333" /> */}
{/* <link rel="shortcut icon" href={`${basePath}/favicon/favicon.ico`} /> */}
<link rel="shortcut icon" href={`${basePath}/favicon/favicon-lw.svg`} />
<link rel="shortcut icon" href={`${basePath}/favicon/favicon.ico`} />
<meta name="msapplication-TileColor" content="#1E1E1E" />
<meta name="msapplication-config" content={`${basePath}/favicon/browserconfig.xml`} />
<meta name="theme-color" content="#1E1E1E" />
+116 -53
View File
@@ -14,16 +14,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$0" },
{ "key": "cpu", "title": "CPU", "value": "2-core ARM (shared)" },
{ "key": "memory", "title": "Memory", "value": "1 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,606 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "2,606 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "87 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "10 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "10"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "50 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "50"
}
]
},
@@ -33,16 +38,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$5" },
{ "key": "cpu", "title": "CPU", "value": "2-core ARM (shared)" },
{ "key": "memory", "title": "Memory", "value": "2 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,606 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "2,606 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "174 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "30 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "30"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "75 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "75"
}
]
},
@@ -52,16 +62,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$50" },
{ "key": "cpu", "title": "CPU", "value": "2-core ARM (shared)" },
{ "key": "memory", "title": "Memory", "value": "4 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,606 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "2,606 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "347 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "50 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "50"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "150 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "150"
}
]
},
@@ -71,16 +86,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$100" },
{ "key": "cpu", "title": "CPU", "value": "2-core ARM (dedicated)" },
{ "key": "memory", "title": "Memory", "value": "8 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 4,750 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "4,750 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "630 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "100 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "100"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "300 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "300"
}
]
},
@@ -90,16 +110,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$200" },
{ "key": "cpu", "title": "CPU", "value": "4-core ARM (dedicated)" },
{ "key": "memory", "title": "Memory", "value": "16 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 4,750 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "4,750 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "1,188 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "200 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "200"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "600 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "600"
}
]
},
@@ -109,16 +134,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$400" },
{ "key": "cpu", "title": "CPU", "value": "8-core ARM (dedicated)" },
{ "key": "memory", "title": "Memory", "value": "32 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 4,750 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "4,750 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "2,375 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "350 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "350"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "1200 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "1200"
}
]
},
@@ -128,16 +158,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$950" },
{ "key": "cpu", "title": "CPU", "value": "16-core ARM (dedicated)" },
{ "key": "memory", "title": "Memory", "value": "64 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 4,750 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "4,750 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "4,750 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "420 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "420"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "2800 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "2800"
}
]
},
@@ -147,16 +182,21 @@
{ "key": "pricing", "title": "Pricing", "value": "$1,860" },
{ "key": "cpu", "title": "CPU", "value": "32-core ARM (dedicated)" },
{ "key": "memory", "title": "Memory", "value": "128 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 9,000 Mbps" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "9,500 Mbps" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "9,500 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "450 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "450"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "5600 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "5600"
}
]
},
@@ -166,16 +206,25 @@
{ "key": "pricing", "title": "Pricing", "value": "$2,790" },
{ "key": "cpu", "title": "CPU", "value": "48-core ARM (dedicated)" },
{ "key": "memory", "title": "Memory", "value": "192 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 13,500 Mbps" },
{
"key": "maxDiskIO",
"title": "Maximum Disk IO Bandwidth",
"value": "14,250 Mbps"
},
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "14,250 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "480 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "480"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "8600 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "8600"
}
]
},
@@ -185,16 +234,25 @@
{ "key": "pricing", "title": "Pricing", "value": "$3,720" },
{ "key": "cpu", "title": "CPU", "value": "64-core ARM (dedicated)" },
{ "key": "memory", "title": "Memory", "value": "256 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 19,000 Mbps" },
{
"key": "maxDiskIO",
"title": "Maximum Disk IO Bandwidth",
"value": "19,000 Mbps"
},
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "19,000 Mbps"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"value": "500 (recommended)"
"title": "Connections: Direct (recommended)",
"value": "500"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"value": "11,600 (recommended)"
"title": "Connections: Pooler (recommended)",
"value": "11,600"
}
]
},
@@ -204,15 +262,20 @@
{ "key": "pricing", "title": "Pricing", "value": "Contact Us" },
{ "key": "cpu", "title": "CPU", "value": "Contact Us" },
{ "key": "memory", "title": "Memory", "value": "Contact Us" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Contact Us" },
{ "key": "maxDiskIO", "title": "Maximum Disk IO Bandwidth", "value": "Contact Us" },
{
"key": "baseDiskIO",
"title": "Baseline Disk IO Bandwidth",
"value": "Contact Us"
},
{
"key": "directConnections",
"title": "Connections: Direct",
"title": "Connections: Direct (recommended)",
"value": "Contact Us"
},
{
"key": "poolerConnections",
"title": "Connections: Pooler",
"title": "Connections: Pooler (recommended)",
"value": "Contact Us"
}
]
-15
View File
@@ -1217,26 +1217,11 @@ module.exports = [
source: '/docs/reference/cli/supabase-db-commit',
destination: '/docs/reference/cli/usage',
},
{
permanent: true,
source: '/docs/reference/cli/supabase-db-reset',
destination: '/docs/reference/cli/usage',
},
{
permanent: true,
source: '/docs/reference/cli/supabase-db-remote-set',
destination: '/docs/reference/cli/usage',
},
{
permanent: true,
source: '/docs/reference/cli/supabase-db-remote-commit',
destination: '/docs/reference/cli/usage',
},
{
permanent: true,
source: '/docs/reference/cli/supabase-db-push',
destination: '/docs/reference/cli/usage',
},
{
permanent: true,
source: '/docs/reference/cli/supabase-functions-delete',
+6
View File
@@ -1,5 +1,11 @@
Pricing Changelog
### 09 Jan 2023
----
Database Compute Add ons
- Clarified iops values to the latest instance types
### 02 Nov 2022
----
-19
View File
@@ -1,19 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.48403" y="1.48403" width="29.0319" height="29.0319" rx="4.33084" fill="url(#paint0_linear_920_70794)"/>
<rect x="1.48403" y="1.48403" width="29.0319" height="29.0319" rx="4.33084" stroke="url(#paint1_linear_920_70794)" stroke-width="0.577445"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9125 7.63694C15.9013 6.89111 14.9598 6.57102 14.4954 7.1552L7.48827 15.9706C6.6612 17.011 7.40299 18.5455 8.73305 18.5455H15.9793L16.0645 24.2263C16.0757 24.9721 17.0172 25.2922 17.4815 24.7081L24.4887 15.8927C25.3157 14.8522 24.5739 13.3177 23.2439 13.3177H15.9498L15.9125 7.63694Z" fill="url(#paint2_linear_920_70794)"/>
<defs>
<linearGradient id="paint0_linear_920_70794" x1="10.1877" y1="9.76698" x2="25.4827" y2="42.2286" gradientUnits="userSpaceOnUse">
<stop stop-color="#14292C"/>
<stop offset="1" stop-color="#121212"/>
</linearGradient>
<linearGradient id="paint1_linear_920_70794" x1="19.9762" y1="1.25667" x2="28.1178" y2="12.0371" gradientUnits="userSpaceOnUse">
<stop stop-color="#76A891" stop-opacity="0.11"/>
<stop offset="1" stop-color="#132525" stop-opacity="0.35"/>
</linearGradient>
<linearGradient id="paint2_linear_920_70794" x1="3.62028" y1="6.85546" x2="32.7486" y2="6.85546" gradientUnits="userSpaceOnUse">
<stop stop-color="#F4FFFA"/>
<stop offset="1" stop-color="#D8FFEC" stop-opacity="0.6"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

+2
View File
@@ -18,6 +18,8 @@ services:
db:
restart: 'no'
volumes:
# Always use a fresh database when developing
- /var/lib/postgresql/data
# Seed data should be inserted last (alphabetical order)
- ./dev/data.sql:/docker-entrypoint-initdb.d/seed.sql
storage:
+17 -15
View File
@@ -47,7 +47,7 @@ services:
auth:
container_name: supabase-auth
image: supabase/gotrue:v2.31.0
image: supabase/gotrue:v2.40.1
depends_on:
db: # Disable this if you are using an external Postgres database
condition: service_healthy
@@ -104,29 +104,30 @@ services:
PGRST_DB_USE_LEGACY_GUCS: "false"
realtime:
container_name: supabase-realtime
image: supabase/realtime:v0.25.1
container_name: realtime-dev.supabase-realtime
image: supabase/realtime:v2.0.2
depends_on:
db: # Disable this if you are using an external Postgres database
condition: service_healthy
restart: unless-stopped
environment:
PORT: 4000
DB_HOST: ${POSTGRES_HOST}
DB_PORT: ${POSTGRES_PORT}
DB_NAME: ${POSTGRES_DB}
DB_USER: supabase_admin
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_SSL: "false"
PORT: 4000
JWT_SECRET: ${JWT_SECRET}
REPLICATION_MODE: RLS
REPLICATION_POLL_INTERVAL: 100
SECURE_CHANNELS: "true"
SLOT_NAME: supabase_realtime_rls
TEMPORARY_SLOT: "true"
DB_NAME: ${POSTGRES_DB}
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
DB_ENC_KEY: supabaserealtime
API_JWT_SECRET: ${JWT_SECRET}
FLY_ALLOC_ID: fly123
FLY_APP_NAME: realtime
SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
ERL_AFLAGS: -proto_dist inet_tcp
ENABLE_TAILSCALE: "false"
DNS_NODES: "''"
command: >
bash -c "./prod/rel/realtime/bin/realtime eval Realtime.Release.migrate
&& ./prod/rel/realtime/bin/realtime start"
sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
storage:
container_name: supabase-storage
@@ -208,5 +209,6 @@ services:
PGDATABASE: ${POSTGRES_DB}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/realtime.sql
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/roles.sql
- ./volumes/db/data:/var/lib/postgresql/data
+1 -1
View File
@@ -124,7 +124,7 @@ services:
## Secure Realtime routes
- name: realtime-v1
_comment: "Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*"
url: http://realtime:4000/socket/
url: http://realtime-dev.supabase-realtime:4000/socket/
routes:
- name: realtime-v1-all
strip_path: true
+4
View File
@@ -0,0 +1,4 @@
\set pguser `echo "$POSTGRES_USER"`
create schema if not exists _realtime;
alter schema _realtime owner to :pguser;
+6 -6
View File
@@ -2180,9 +2180,9 @@
"dev": true
},
"node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -4922,9 +4922,9 @@
"dev": true
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -7093,9 +7093,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@@ -17212,9 +17212,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonc-parser": {
@@ -2364,9 +2364,9 @@
"dev": true
},
"node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -5219,9 +5219,9 @@
"dev": true
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -4385,9 +4385,9 @@
}
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@@ -10873,9 +10873,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonc-parser": {
@@ -4140,9 +4140,9 @@
}
},
"node_modules/cache-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -6823,9 +6823,9 @@
}
},
"node_modules/html-webpack-plugin/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -7642,9 +7642,9 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
@@ -9446,9 +9446,9 @@
}
},
"node_modules/postcss-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -12440,9 +12440,9 @@
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA=="
},
"node_modules/vue-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -12591,9 +12591,9 @@
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA=="
},
"node_modules/vue-style-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -13407,9 +13407,9 @@
}
},
"node_modules/webpack/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -17088,9 +17088,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -19177,9 +19177,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -19762,9 +19762,9 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"jsonfile": {
"version": "6.1.0",
@@ -21177,9 +21177,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -23561,9 +23561,9 @@
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA=="
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -23686,9 +23686,9 @@
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA=="
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -24190,9 +24190,9 @@
}
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -4273,9 +4273,9 @@
}
},
"node_modules/babel-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -9521,9 +9521,9 @@
}
},
"node_modules/html-webpack-plugin/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -12595,12 +12595,9 @@
"integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA=="
},
"node_modules/json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"dependencies": {
"minimist": "^1.2.5"
},
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
@@ -13133,9 +13130,9 @@
}
},
"node_modules/mini-css-extract-plugin/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -14942,9 +14939,9 @@
}
},
"node_modules/postcss-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -16818,9 +16815,9 @@
}
},
"node_modules/resolve-url-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -19194,9 +19191,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -20999,9 +20996,9 @@
}
},
"node_modules/webpack/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -24968,9 +24965,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -29143,9 +29140,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -31411,12 +31408,9 @@
"integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA=="
},
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"requires": {
"minimist": "^1.2.5"
}
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"jsonfile": {
"version": "6.1.0",
@@ -31836,9 +31830,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -33259,9 +33253,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -34764,9 +34758,9 @@
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -36653,9 +36647,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -37523,9 +37517,9 @@
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -1320,9 +1320,9 @@
}
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@@ -2631,9 +2631,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"merge-anything": {
+36 -36
View File
@@ -14540,9 +14540,9 @@
}
},
"node_modules/css-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -19585,9 +19585,9 @@
}
},
"node_modules/html-webpack-plugin/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -22588,9 +22588,9 @@
}
},
"node_modules/js-yaml-loader/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -22734,9 +22734,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
@@ -34683,9 +34683,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -36622,9 +36622,9 @@
}
},
"node_modules/webpack/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -48428,9 +48428,9 @@
}
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -52827,9 +52827,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -55190,9 +55190,9 @@
}
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -55297,9 +55297,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"jsonc-parser": {
"version": "3.2.0",
@@ -64542,9 +64542,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
@@ -66059,9 +66059,9 @@
"integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}
+386 -304
View File
@@ -338,165 +338,6 @@
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
}
},
"/v1/projects/{ref}/secrets": {
"get": {
"operationId": "getSecrets",
"summary": "List all secrets",
"description": "Returns all secrets you've previously added to the specified project.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/SecretResponse" }
}
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to retrieve project's secrets" }
},
"tags": ["secrets"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
},
"post": {
"operationId": "createSecrets",
"summary": "Bulk create secrets",
"description": "Creates multiple secrets and adds them to the specified project.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/CreateSecretBody" }
}
}
}
},
"responses": {
"201": { "description": "" },
"403": { "description": "" },
"500": { "description": "Failed to create project's secrets" }
},
"tags": ["secrets"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
},
"delete": {
"operationId": "deleteSecrets",
"summary": "Bulk delete secrets",
"description": "Deletes all secrets with the given names from the specified project",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": { "schema": { "type": "array", "items": { "type": "string" } } }
}
},
"responses": {
"200": {
"description": "",
"content": { "application/json": { "schema": { "type": "object" } } }
},
"403": { "description": "" },
"500": { "description": "Failed to delete secrets with given names" }
},
"tags": ["secrets"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
}
},
"/v1/projects/{ref}/postgrest": {
"get": {
"operationId": "getPostgRESTConfig",
"summary": "Gets project's postgrest config",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PostgrestConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to retrieve project's postgrest config" }
},
"tags": ["services"],
"security": [{ "bearer": [] }]
},
"patch": {
"operationId": "updatePostgRESTConfig",
"summary": "Updates project's postgrest config",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UpdatePostgrestConfigBody" }
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PostgrestConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to update project's postgrest config" }
},
"tags": ["services"],
"security": [{ "bearer": [] }]
}
},
"/v1/projects/{ref}/custom-hostname": {
"get": {
"operationId": "getCustomHostnameConfig",
@@ -641,106 +482,6 @@
"security": [{ "bearer": [] }]
}
},
"/v1/projects/{ref}/pgsodium": {
"get": {
"operationId": "getConfig",
"summary": "Gets project's pgsodium config",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PgsodiumConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to retrieve project's pgsodium config" }
},
"tags": ["pgsodium (beta)"],
"security": [{ "bearer": [] }]
},
"put": {
"operationId": "updateConfig",
"summary": "Updates project's pgsodium config. Updating the root_key can cause all data encrypted with the older key to become inaccessible.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UpdatePgsodiumConfigBody" }
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PgsodiumConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to update project's pgsodium config" }
},
"tags": ["pgsodium (beta)"],
"security": [{ "bearer": [] }]
}
},
"/v1/projects/{ref}/types/typescript": {
"get": {
"operationId": "getTypescriptTypes",
"summary": "Generate TypeScript types",
"description": "Returns the TypeScript types of your schema for use with supabase-js.",
"parameters": [
{
"name": "included_schemas",
"required": false,
"in": "query",
"schema": { "default": "public", "type": "string" }
},
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/TypescriptResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to generate TypeScript types" }
},
"tags": ["projects"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
}
},
"/v1/projects/{ref}/network-bans/retrieve": {
"post": {
"operationId": "getNetworkBans",
@@ -866,6 +607,329 @@
"security": [{ "bearer": [] }]
}
},
"/v1/projects/{ref}/pgsodium": {
"get": {
"operationId": "getConfig",
"summary": "Gets project's pgsodium config",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PgsodiumConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to retrieve project's pgsodium config" }
},
"tags": ["pgsodium (beta)"],
"security": [{ "bearer": [] }]
},
"put": {
"operationId": "updateConfig",
"summary": "Updates project's pgsodium config. Updating the root_key can cause all data encrypted with the older key to become inaccessible.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UpdatePgsodiumConfigBody" }
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PgsodiumConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to update project's pgsodium config" }
},
"tags": ["pgsodium (beta)"],
"security": [{ "bearer": [] }]
}
},
"/v1/projects/{ref}/postgrest": {
"get": {
"operationId": "getPostgRESTConfig",
"summary": "Gets project's postgrest config",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PostgrestConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to retrieve project's postgrest config" }
},
"tags": ["services"],
"security": [{ "bearer": [] }]
},
"patch": {
"operationId": "updatePostgRESTConfig",
"summary": "Updates project's postgrest config",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UpdatePostgrestConfigBody" }
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PostgrestConfigResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to update project's postgrest config" }
},
"tags": ["services"],
"security": [{ "bearer": [] }]
}
},
"/v1/projects/{ref}/secrets": {
"get": {
"operationId": "getSecrets",
"summary": "List all secrets",
"description": "Returns all secrets you've previously added to the specified project.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/SecretResponse" }
}
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to retrieve project's secrets" }
},
"tags": ["secrets"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
},
"post": {
"operationId": "createSecrets",
"summary": "Bulk create secrets",
"description": "Creates multiple secrets and adds them to the specified project.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/CreateSecretBody" }
}
}
}
},
"responses": {
"201": { "description": "" },
"403": { "description": "" },
"500": { "description": "Failed to create project's secrets" }
},
"tags": ["secrets"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
},
"delete": {
"operationId": "deleteSecrets",
"summary": "Bulk delete secrets",
"description": "Deletes all secrets with the given names from the specified project",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": { "schema": { "type": "array", "items": { "type": "string" } } }
}
},
"responses": {
"200": {
"description": "",
"content": { "application/json": { "schema": { "type": "object" } } }
},
"403": { "description": "" },
"500": { "description": "Failed to delete secrets with given names" }
},
"tags": ["secrets"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
}
},
"/v1/projects/{ref}/ssl-enforcement": {
"get": {
"operationId": "getSslEnforcementConfig",
"summary": "Get project's SSL enforcement configuration.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SslEnforcementResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to retrieve project's SSL enforcement config" }
},
"tags": ["ssl enforcement (beta)"],
"security": [{ "bearer": [] }]
},
"put": {
"operationId": "updateSslEnforcementConfig",
"summary": "Update project's SSL enforcement configuration.",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SslEnforcementRequest" }
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SslEnforcementResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to update project's SSL enforcement configuration." }
},
"tags": ["ssl enforcement (beta)"],
"security": [{ "bearer": [] }]
}
},
"/v1/projects/{ref}/types/typescript": {
"get": {
"operationId": "getTypescriptTypes",
"summary": "Generate TypeScript types",
"description": "Returns the TypeScript types of your schema for use with supabase-js.",
"parameters": [
{
"name": "included_schemas",
"required": false,
"in": "query",
"schema": { "default": "public", "type": "string" }
},
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/TypescriptResponse" }
}
}
},
"403": { "description": "" },
"500": { "description": "Failed to generate TypeScript types" }
},
"tags": ["projects"],
"security": [{ "apiKeyHeader": [], "apiKeyParam": [] }, { "bearer": [] }]
}
},
"/v1/projects/{ref}/vanity-subdomain": {
"get": {
"operationId": "getVanitySubdomainConfig",
@@ -1100,36 +1164,6 @@
"verify_jwt": { "type": "boolean" }
}
},
"SecretResponse": {
"type": "object",
"properties": { "name": { "type": "string" }, "value": { "type": "string" } },
"required": ["name", "value"]
},
"CreateSecretBody": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "string", "pattern": "/^(?!SUPABASE_).*/" }
},
"required": ["name", "value"]
},
"PostgrestConfigResponse": {
"type": "object",
"properties": {
"max_rows": { "type": "integer" },
"db_schema": { "type": "string" },
"db_extra_search_path": { "type": "string" }
},
"required": ["max_rows", "db_schema", "db_extra_search_path"]
},
"UpdatePostgrestConfigBody": {
"type": "object",
"properties": {
"max_rows": { "type": "integer", "minimum": 0, "maximum": 1000000 },
"db_extra_search_path": { "type": "string" },
"db_schema": { "type": "string" }
}
},
"UpdateCustomHostnameResponse": {
"type": "object",
"properties": {
@@ -1153,21 +1187,6 @@
"properties": { "custom_hostname": { "type": "string" } },
"required": ["custom_hostname"]
},
"PgsodiumConfigResponse": {
"type": "object",
"properties": { "root_key": { "type": "string" } },
"required": ["root_key"]
},
"UpdatePgsodiumConfigBody": {
"type": "object",
"properties": { "root_key": { "type": "string" } },
"required": ["root_key"]
},
"TypescriptResponse": {
"type": "object",
"properties": { "types": { "type": "string" } },
"required": ["types"]
},
"NetworkBanResponse": {
"type": "object",
"properties": {
@@ -1194,6 +1213,69 @@
},
"required": ["entitlement", "config", "status"]
},
"PgsodiumConfigResponse": {
"type": "object",
"properties": { "root_key": { "type": "string" } },
"required": ["root_key"]
},
"UpdatePgsodiumConfigBody": {
"type": "object",
"properties": { "root_key": { "type": "string" } },
"required": ["root_key"]
},
"PostgrestConfigResponse": {
"type": "object",
"properties": {
"max_rows": { "type": "integer" },
"db_schema": { "type": "string" },
"db_extra_search_path": { "type": "string" }
},
"required": ["max_rows", "db_schema", "db_extra_search_path"]
},
"UpdatePostgrestConfigBody": {
"type": "object",
"properties": {
"max_rows": { "type": "integer", "minimum": 0, "maximum": 1000000 },
"db_extra_search_path": { "type": "string" },
"db_schema": { "type": "string" }
}
},
"SecretResponse": {
"type": "object",
"properties": { "name": { "type": "string" }, "value": { "type": "string" } },
"required": ["name", "value"]
},
"CreateSecretBody": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "string", "pattern": "/^(?!SUPABASE_).*/" }
},
"required": ["name", "value"]
},
"SslEnforcements": {
"type": "object",
"properties": { "database": { "type": "boolean" } },
"required": ["database"]
},
"SslEnforcementResponse": {
"type": "object",
"properties": {
"currentConfig": { "$ref": "#/components/schemas/SslEnforcements" },
"appliedSuccessfully": { "type": "boolean" }
},
"required": ["currentConfig", "appliedSuccessfully"]
},
"SslEnforcementRequest": {
"type": "object",
"properties": { "requestedConfig": { "$ref": "#/components/schemas/SslEnforcements" } },
"required": ["requestedConfig"]
},
"TypescriptResponse": {
"type": "object",
"properties": { "types": { "type": "string" } },
"required": ["types"]
},
"VanitySubdomainConfigResponse": {
"type": "object",
"properties": {
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+45 -21
View File
@@ -809,7 +809,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 315,
"line": 327,
"character": 2
}
],
@@ -821,7 +821,7 @@
"kindString": "Call signature",
"flags": {},
"comment": {
"shortText": "Listen to messages."
"shortText": "Creates an event handler that listens to changes"
},
"parameters": [
{
@@ -830,6 +830,9 @@
"kind": 32768,
"kindString": "Parameter",
"flags": {},
"comment": {
"shortText": "Realtime feature (broadcast, presence, or postgres_changes)"
},
"type": {
"type": "literal",
"value": "broadcast"
@@ -841,6 +844,9 @@
"kind": 32768,
"kindString": "Parameter",
"flags": {},
"comment": {
"shortText": "Pre-defined and custom values specific to the Realtime feature"
},
"type": {
"type": "reflection",
"declaration": {
@@ -856,10 +862,13 @@
"kind": 1024,
"kindString": "Property",
"flags": {},
"comment": {
"shortText": "Event to listen to. If listening to postgres_changes type\nthen this value is one of INSERT, UPDATE, DELETE, *"
},
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 317,
"line": 329,
"character": 14
}
],
@@ -885,6 +894,9 @@
"kind": 32768,
"kindString": "Parameter",
"flags": {},
"comment": {
"shortText": "Function to be invoked when event handler is triggered\n"
},
"type": {
"type": "reflection",
"declaration": {
@@ -925,7 +937,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 320,
"line": 332,
"character": 6
}
],
@@ -943,7 +955,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 319,
"line": 331,
"character": 6
}
],
@@ -1045,7 +1057,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 326,
"line": 338,
"character": 14
}
],
@@ -1144,7 +1156,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 331,
"line": 343,
"character": 14
}
],
@@ -1257,7 +1269,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 336,
"line": 348,
"character": 14
}
],
@@ -1911,7 +1923,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 367,
"line": 379,
"character": 2
}
],
@@ -1947,7 +1959,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 368,
"line": 380,
"character": 15
}
],
@@ -2298,7 +2310,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 407,
"line": 419,
"character": 2
}
],
@@ -2443,7 +2455,7 @@
"sources": [
{
"fileName": "RealtimeChannel.ts",
"line": 394,
"line": 406,
"character": 2
}
],
@@ -3246,7 +3258,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 218,
"line": 228,
"character": 2
}
],
@@ -3330,7 +3342,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 198,
"line": 208,
"character": 2
}
],
@@ -3424,7 +3436,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 162,
"line": 165,
"character": 2
}
],
@@ -3435,6 +3447,9 @@
"kind": 4096,
"kindString": "Call signature",
"flags": {},
"comment": {
"shortText": "Returns all created channels"
},
"type": {
"type": "array",
"elementType": {
@@ -3455,7 +3470,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 214,
"line": 224,
"character": 2
}
],
@@ -3485,7 +3500,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 191,
"line": 201,
"character": 2
}
],
@@ -3553,7 +3568,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 236,
"line": 246,
"character": 2
}
],
@@ -3607,7 +3622,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 177,
"line": 187,
"character": 2
}
],
@@ -3618,6 +3633,9 @@
"kind": 4096,
"kindString": "Call signature",
"flags": {},
"comment": {
"shortText": "Unsubscribes and removes all channels"
},
"type": {
"type": "reference",
"typeArguments": [
@@ -3646,7 +3664,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 166,
"line": 173,
"character": 2
}
],
@@ -3657,6 +3675,9 @@
"kind": 4096,
"kindString": "Call signature",
"flags": {},
"comment": {
"shortText": "Unsubscribes and removes a single channel"
},
"parameters": [
{
"id": 354,
@@ -3664,6 +3685,9 @@
"kind": 32768,
"kindString": "Parameter",
"flags": {},
"comment": {
"shortText": "A RealtimeChannel instance\n"
},
"type": {
"type": "reference",
"id": 55,
@@ -3696,7 +3720,7 @@
"sources": [
{
"fileName": "RealtimeClient.ts",
"line": 263,
"line": 273,
"character": 2
}
],
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -3594,7 +3594,7 @@
"isOptional": true
},
"comment": {
"shortText": "Automatically refreshes the token for logged in users."
"shortText": "Automatically refreshes the token for logged-in users. Defaults to true."
},
"sources": [
{
@@ -3617,7 +3617,7 @@
"isOptional": true
},
"comment": {
"shortText": "Detect a session from the URL. Used for OAuth login callbacks."
"shortText": "Detect a session from the URL. Used for OAuth login callbacks. Defaults to true."
},
"sources": [
{
@@ -3640,7 +3640,7 @@
"isOptional": true
},
"comment": {
"shortText": "Whether to persist a logged in session to storage."
"shortText": "Whether to persist a logged-in session to storage. Defaults to true."
},
"sources": [
{
@@ -3663,7 +3663,7 @@
"isOptional": true
},
"comment": {
"shortText": "A storage provider. Used to store the logged in session."
"shortText": "A storage provider. Used to store the logged-in session."
},
"sources": [
{
@@ -3694,7 +3694,7 @@
"isOptional": true
},
"comment": {
"shortText": "Optional key name used for storing tokens in local storage"
"shortText": "Optional key name used for storing tokens in local storage."
},
"sources": [
{
@@ -3735,7 +3735,7 @@
"isOptional": true
},
"comment": {
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to `public`."
},
"sources": [
{
@@ -283,7 +283,7 @@
"isOptional": true
},
"comment": {
"shortText": "Automatically refreshes the token for logged in users."
"shortText": "Automatically refreshes the token for logged-in users. Defaults to true."
},
"sources": [
{
@@ -306,7 +306,7 @@
"isOptional": true
},
"comment": {
"shortText": "Detect a session from the URL. Used for OAuth login callbacks."
"shortText": "Detect a session from the URL. Used for OAuth login callbacks. Defaults to true."
},
"sources": [
{
@@ -329,7 +329,7 @@
"isOptional": true
},
"comment": {
"shortText": "Whether to persist a logged in session to storage."
"shortText": "Whether to persist a logged-in session to storage. Defaults to true."
},
"sources": [
{
@@ -352,7 +352,7 @@
"isOptional": true
},
"comment": {
"shortText": "A storage provider. Used to store the logged in session."
"shortText": "A storage provider. Used to store the logged-in session."
},
"sources": [
{
@@ -383,7 +383,7 @@
"isOptional": true
},
"comment": {
"shortText": "Optional key name used for storing tokens in local storage"
"shortText": "Optional key name used for storing tokens in local storage."
},
"sources": [
{
@@ -424,7 +424,7 @@
"isOptional": true
},
"comment": {
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to `public`."
},
"sources": [
{
@@ -2598,7 +2598,7 @@
"isOptional": true
},
"comment": {
"shortText": "Automatically refreshes the token for logged in users."
"shortText": "Automatically refreshes the token for logged-in users. Defaults to true."
},
"sources": [
{
@@ -2621,7 +2621,7 @@
"isOptional": true
},
"comment": {
"shortText": "Detect a session from the URL. Used for OAuth login callbacks."
"shortText": "Detect a session from the URL. Used for OAuth login callbacks. Defaults to true."
},
"sources": [
{
@@ -2644,7 +2644,7 @@
"isOptional": true
},
"comment": {
"shortText": "Whether to persist a logged in session to storage."
"shortText": "Whether to persist a logged-in session to storage. Defaults to true."
},
"sources": [
{
@@ -2667,7 +2667,7 @@
"isOptional": true
},
"comment": {
"shortText": "A storage provider. Used to store the logged in session."
"shortText": "A storage provider. Used to store the logged-in session."
},
"sources": [
{
@@ -2698,7 +2698,7 @@
"isOptional": true
},
"comment": {
"shortText": "Optional key name used for storing tokens in local storage"
"shortText": "Optional key name used for storing tokens in local storage."
},
"sources": [
{
@@ -2739,7 +2739,7 @@
"isOptional": true
},
"comment": {
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to `public`."
},
"sources": [
{
@@ -3702,7 +3702,7 @@
"isOptional": true
},
"comment": {
"shortText": "Automatically refreshes the token for logged in users."
"shortText": "Automatically refreshes the token for logged-in users. Defaults to true."
},
"sources": [
{
@@ -3725,7 +3725,7 @@
"isOptional": true
},
"comment": {
"shortText": "Detect a session from the URL. Used for OAuth login callbacks."
"shortText": "Detect a session from the URL. Used for OAuth login callbacks. Defaults to true."
},
"sources": [
{
@@ -3748,7 +3748,7 @@
"isOptional": true
},
"comment": {
"shortText": "Whether to persist a logged in session to storage."
"shortText": "Whether to persist a logged-in session to storage. Defaults to true."
},
"sources": [
{
@@ -3771,7 +3771,7 @@
"isOptional": true
},
"comment": {
"shortText": "A storage provider. Used to store the logged in session."
"shortText": "A storage provider. Used to store the logged-in session."
},
"sources": [
{
@@ -3802,7 +3802,7 @@
"isOptional": true
},
"comment": {
"shortText": "Optional key name used for storing tokens in local storage"
"shortText": "Optional key name used for storing tokens in local storage."
},
"sources": [
{
@@ -3843,7 +3843,7 @@
"isOptional": true
},
"comment": {
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to `public`."
},
"sources": [
{
@@ -4151,7 +4151,7 @@
"isOptional": true
},
"comment": {
"shortText": "Automatically refreshes the token for logged in users."
"shortText": "Automatically refreshes the token for logged-in users. Defaults to true."
},
"sources": [
{
@@ -4174,7 +4174,7 @@
"isOptional": true
},
"comment": {
"shortText": "Detect a session from the URL. Used for OAuth login callbacks."
"shortText": "Detect a session from the URL. Used for OAuth login callbacks. Defaults to true."
},
"sources": [
{
@@ -4197,7 +4197,7 @@
"isOptional": true
},
"comment": {
"shortText": "Whether to persist a logged in session to storage."
"shortText": "Whether to persist a logged-in session to storage. Defaults to true."
},
"sources": [
{
@@ -4220,7 +4220,7 @@
"isOptional": true
},
"comment": {
"shortText": "A storage provider. Used to store the logged in session."
"shortText": "A storage provider. Used to store the logged-in session."
},
"sources": [
{
@@ -4251,7 +4251,7 @@
"isOptional": true
},
"comment": {
"shortText": "Optional key name used for storing tokens in local storage"
"shortText": "Optional key name used for storing tokens in local storage."
},
"sources": [
{
@@ -4292,7 +4292,7 @@
"isOptional": true
},
"comment": {
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to `public`."
},
"sources": [
{
@@ -5531,7 +5531,7 @@
"isOptional": true
},
"comment": {
"shortText": "Automatically refreshes the token for logged in users."
"shortText": "Automatically refreshes the token for logged-in users. Defaults to true."
},
"sources": [
{
@@ -5554,7 +5554,7 @@
"isOptional": true
},
"comment": {
"shortText": "Detect a session from the URL. Used for OAuth login callbacks."
"shortText": "Detect a session from the URL. Used for OAuth login callbacks. Defaults to true."
},
"sources": [
{
@@ -5577,7 +5577,7 @@
"isOptional": true
},
"comment": {
"shortText": "Whether to persist a logged in session to storage."
"shortText": "Whether to persist a logged-in session to storage. Defaults to true."
},
"sources": [
{
@@ -5600,7 +5600,7 @@
"isOptional": true
},
"comment": {
"shortText": "A storage provider. Used to store the logged in session."
"shortText": "A storage provider. Used to store the logged-in session."
},
"sources": [
{
@@ -5631,7 +5631,7 @@
"isOptional": true
},
"comment": {
"shortText": "Optional key name used for storing tokens in local storage"
"shortText": "Optional key name used for storing tokens in local storage."
},
"sources": [
{
@@ -5672,7 +5672,7 @@
"isOptional": true
},
"comment": {
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."
"shortText": "The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to `public`."
},
"sources": [
{
+51 -4
View File
@@ -11,11 +11,9 @@ info:
title: 'GoTrue' # {string} A readable name.
source: 'https://github.com/supabase/gotrue' # {string} Where developers can find the source code.
bugs: 'https://github.com/supabase/gotrue/issues' # {string} Where developers can file bugs.
spec: 'https://github.com/supabase/supabase/blob/master/web/spec/cli_config_001.yml' # {string} Where developers can find this spec (to link directly in the docs).
spec: 'https://github.com/supabase/supabase/blob/master/spec/gotrue_v1_config.yaml' # {string} Where developers can find this spec (to link directly in the docs).
description: |
A `config.toml` file is generated after running `supabase init`.
This file is located in the `supabase` folder under `supabase/config.toml`.
A `config.toml` file is generated after running `supabase init`. This file is located in the `supabase` folder under `supabase/config.toml`.
tags:
- id: general
title: General
@@ -30,3 +28,52 @@ parameters:
# default: '5432'
description: |
A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`.
- id: 'GOTRUE_EXTERNAL_GITHUB' # {string} A unique identifier for this param.
title: 'auth.external.github' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
# default: '5432'
description: |
Describes whether the Github provider is enabled or not.
- id: 'GOTRUE_SITE_URL' # {string} A unique identifier for this param.
title: 'auth.site_url' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
The base URL of your website. Used as an allow-list for redirects and for constructing URLs used in emails.
- id: 'GOTRUE_URI_ALLOW_LIST' # {string} A unique identifier for this param.
title: 'auth.additional_redirect_urls' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
- id: 'GOTRUE_JWT_EXP' # {string} A unique identifier for this param.
title: 'auth.jwt_expiry' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).
- id: 'GOTRUE_DISABLE_SIGNUP' # {string} A unique identifier for this param.
title: 'auth.enable_signup' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
Allow/disallow new user signups to your project.
- id: 'GOTRUE_EXTERNAL_EMAIL_ENABLED' # {string} A unique identifier for this param.
title: 'auth.email.enable_signup' # {string} Any name.
tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
Allow/disallow new user signups via email to your project.
- id: 'GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED' # {string} A unique identifier for this param.
title: 'auth.email.double_confirm_changes' # {string} Any name.
tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
If enabled, a user will be required to confirm any email change on both the old, and new email addresses. If disabled, only the new email is required to confirm.
- id: 'GOTRUE_MAILER_AUTOCONFIRM' # {string} A unique identifier for this param.
title: 'auth.email.enable_confirmations' # {string} Any name.
tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
If enabled, users need to confirm their email address before signing in.
+2 -2
View File
@@ -643,7 +643,7 @@ functions:
- Defaults to return 50 users per page.
examples:
- id: get-a-full-list-of-users
name: Get a full list of users
name: Get a page of users
isSpotlight: true
code: |
```js
@@ -1062,7 +1062,7 @@ functions:
If you're in a situation where your tables are **NOT** directly
related, but instead are joined by a _join table_, you can still use
the `select()` method to query the related data. The join table needs
to have the foreign keys as part of its composite primary key.
to have the foreign keys as part of its composite primary key.
hideCodeBlock: true
- id: query-the-same-foreign-table-multiple-times
name: Query the same foreign table multiple times
File diff suppressed because it is too large. Load diff
@@ -64,10 +64,10 @@ const AddPaymentMethodForm: FC<Props> = ({ returnUrl, onCancel }) => {
<Modal.Separator />
<Modal.Content>
<div className="flex items-center space-x-2 pt-2">
<Button block htmlType="submit" type="primary" loading={isSaving}>
<Button block htmlType="submit" type="primary" loading={isSaving} disabled={isSaving}>
Save
</Button>
<Button htmlType="button" type="default" onClick={onCancel} block>
<Button htmlType="button" type="default" onClick={onCancel} block disabled={isSaving}>
Cancel
</Button>
</div>
@@ -15,6 +15,9 @@ interface Props {
const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan }) => {
const formattedTiers = formatTierOptions(tiers)
// [Kevin] TODO Remove after team plan is generally available
const gridCols = formattedTiers.length === 3 ? 'gap-8 xl:grid-cols-3' : 'gap-4 xl:grid-cols-4'
return (
<Transition
show={visible}
@@ -22,13 +25,12 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
enterFrom="transform opacity-0 -translate-x-10"
enterTo="transform opacity-100 translate-x-0"
>
<div className="space-y-8">
<h4 className="text-lg">Change your project's subscription</h4>
{/* FE will make a call to fetch all plans first at the page level */}
<div className="grid lg:grid-cols-3 gap-8 py-8">
<div>
<h4 className="text-lg mb-8">Change your project's subscription</h4>
<div className={`grid py-8 grid-cols-2 ${gridCols}`}>
<Plans plans={formattedTiers} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
</div>
<div className="flex justify-center items-center">
<div className="flex justify-center items-center mt-4">
<Link href="https://supabase.com/pricing">
<a
target="_blank"
@@ -14,8 +14,13 @@ export const formatTierOptions = (tiers: any[]) => {
}
// Fix the order of plans here
return tiers
.filter((tier: any) => tier.id === STRIPE_PRODUCT_IDS.FREE)
.concat(tiers.filter((tier: any) => tier.id === STRIPE_PRODUCT_IDS.PRO))
.concat([enterpriseOption])
return (
tiers
.filter((tier: any) => tier.id === STRIPE_PRODUCT_IDS.FREE)
.concat(tiers.filter((tier: any) => tier.id === STRIPE_PRODUCT_IDS.PRO))
.concat(tiers.filter((tier: any) => tier.id === STRIPE_PRODUCT_IDS.TEAM))
.concat([enterpriseOption])
// Team plan might not be enabled yet, make sure to filter out null/undefined
.filter(Boolean)
)
}
@@ -16,10 +16,18 @@ interface Props {
const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const getButtonType = (plan: any) => {
if (plan.name === 'Enterprise' || plan.id === STRIPE_PRODUCT_IDS.FREE) {
const getButtonType = (plan: any, currentPlan: any) => {
if (['Enterprise', 'Free tier'].includes(plan.name)) {
// Enterprise/Free are always default
return 'default'
} else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.FREE) {
// If the current plan is free, other plans are primary
return 'primary'
} else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM && plan.name !== 'Team tier') {
// Non-free plans are default (pro), when team tier is selected
return 'default'
}
return 'primary'
}
@@ -37,6 +45,8 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
if (plan.id === STRIPE_PRODUCT_IDS.PRO) {
if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.FREE) {
return 'Upgrade to Pro'
} else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM) {
return 'Downgrade to Pro'
} else if (
currentPlan.prod_id === STRIPE_PRODUCT_IDS.PRO ||
currentPlan.prod_id === STRIPE_PRODUCT_IDS.PAYG
@@ -46,6 +56,20 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
return 'Contact sales'
}
}
if (plan.id === STRIPE_PRODUCT_IDS.TEAM) {
if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM) {
return 'Edit plan configuration'
} else if (
[STRIPE_PRODUCT_IDS.FREE, STRIPE_PRODUCT_IDS.PRO, STRIPE_PRODUCT_IDS.PAYG].includes(
currentPlan.prod_id
)
) {
return 'Upgrade to Team'
} else {
return 'Contact sales'
}
}
}
if (!currentPlan)
@@ -57,7 +81,7 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
</div>
)
const type = getButtonType(plan)
const type = getButtonType(plan, currentPlan)
const ctaText = getButtonText(plan, currentPlan)
const disabled =
plan.id === STRIPE_PRODUCT_IDS.FREE && currentPlan.prod_id === STRIPE_PRODUCT_IDS.FREE
@@ -34,7 +34,7 @@ export const PRICING_META = {
href: '#',
from: true,
priceMonthly: 25,
warning: '+ additional use',
warning: '+ any additional usage',
description: 'For production applications with the option to scale.',
features: [
'8GB database & 100GB file storage',
@@ -52,10 +52,34 @@ export const PRICING_META = {
],
scale: 'Additional fees apply for usage and storage beyond the limits above.',
shutdown: '',
preface: 'Everything below included in the base plan',
preface: 'Everything in the base plan, plus',
additional: 'Need more? Turn off your spend cap to Pay As You Grow ',
cta: 'Get Started',
},
[STRIPE_PRODUCT_IDS.TEAM]: {
name: 'Team',
href: '#',
from: true,
priceMonthly: 599,
warning: '+ usage',
description: 'For collaborative teams with different permissions and access.',
features: [
'Usage-based pricing',
'100,000 monthly active users included',
'Organization member roles (ABAC)',
'Standardised Security Questionnaire',
'SOC2',
'SSO for Supabase Dashboard',
'Priority email support & SLAs',
'1 XS compute instance',
'14 day backups',
'28 day log retention',
],
scale: '',
shutdown: '',
additional: '',
cta: 'Get Started',
},
Enterprise: {
name: 'Enterprise',
href: '/contact/enterprise',
@@ -6,17 +6,11 @@ import { StripeProduct } from 'components/interfaces/Billing'
import PlanCTAButton from './PlanCTAButton'
import { PRICING_META } from './Plans.Constants'
interface Props {
plans: any[]
currentPlan?: StripeProduct
onSelectPlan: (plan: any) => void
}
const AnimatedGradientBackground = () => (
<div
className={[
'absolute top-0 left-0 h-full w-full animate-sway',
'bg-gradient-to-b from-transparent via-transparent to-green-800',
'bg-gradient-to-b from-transparent via-transparent to-green-900',
].join(' ')}
/>
)
@@ -34,6 +28,12 @@ const CurrentSubscriptionBanner = () => (
</div>
)
interface Props {
plans: any[]
currentPlan?: StripeProduct
onSelectPlan: (plan: any) => void
}
/**
* JSX below for the mapped plans is also very similar to www/pages/pricing
* TO DO: move to use shared components for these
@@ -44,6 +44,7 @@ const CurrentSubscriptionBanner = () => (
const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
return (
<>
{/* Existing available plans */}
{plans.map((plan) => {
const isCurrentPlan =
plan.id === currentPlan?.prod_id ||
@@ -59,7 +60,7 @@ const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
)}
<div className="flex h-full flex-col overflow-hidden rounded border">
{isCurrentPlan && <CurrentSubscriptionBanner />}
<div className="bg-white px-8 py-6 dark:bg-scale-300">
<div className="bg-white px-6 py-6 dark:bg-scale-300">
<span
className="text-cd inline-flex rounded-full text-base font-normal tracking-wide text-scale-1200"
id="tier-standard"
@@ -70,20 +71,18 @@ const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
<div className="flex space-x-2">
{PRICING_META[plan.id].priceMonthly !== undefined ? (
<>
<div className="flex items-end gap-1">
<div>
{PRICING_META[plan.id].from && (
<span className="text-base font-medium text-scale-1200">From</span>
<span className="text-sm font-medium text-scale-1200 mr-1">From</span>
)}
<div>
<span className="text-2xl">${PRICING_META[plan.id].priceMonthly}</span>
<span className="ml-1 text-xl font-medium text-scale-900">/mo</span>
</div>
<span className="text-2xl">${PRICING_META[plan.id].priceMonthly}</span>
<span className="text-xl font-medium text-scale-900">/mo</span>
</div>
</>
) : (
<span className="text-2xl">Contact Us</span>
)}
<div className="flex h-8">
<div className="flex h-8 -mt-[2px]">
{PRICING_META[plan.id].warning && (
<div className="mt-2 rounded-md bg-brand-300 bg-opacity-30 px-2 py-1 text-xs text-brand-1000">
{PRICING_META[plan.id].warning}
@@ -106,26 +105,30 @@ const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
'flex-1 flex-col justify-between',
'space-y-6 border-t dark:border-scale-400',
'bg-scale-100 dark:bg-scale-300',
'hidden h-full px-8 py-6 lg:flex',
'hidden h-full px-6 py-6 lg:flex',
].join(' ')}
>
{PRICING_META[plan.id].preface && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].preface}</p>
)}
<ul role="list" className="divide-y dark:divide-scale-400">
{PRICING_META[plan.id].features.map((feature) => (
<li key={feature} className="flex items-center py-2">
<IconCheck
className="h-3 w-3 text-brand-900 "
aria-hidden="true"
strokeWidth={3}
/>
<p className="mb-0 ml-3 text-xs text-scale-1100 dark:text-scale-1200">
{feature}
</p>
</li>
))}
</ul>
<div className="space-y-6">
{PRICING_META[plan.id].preface && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].preface}</p>
)}
<ul role="list" className="divide-y dark:divide-scale-400">
{PRICING_META[plan.id].features.map((feature) => (
<li key={feature} className="flex py-2">
<div className="w-[12px]">
<IconCheck
className="h-3 w-3 text-brand-900 translate-y-[2.5px]"
aria-hidden="true"
strokeWidth={3}
/>
</div>
<p className="mb-0 ml-3 text-xs text-scale-1100 dark:text-scale-1200">
{feature}
</p>
</li>
))}
</ul>
</div>
<div className="flex flex-col gap-6">
<div className="space-y-2">
{PRICING_META[plan.id].additional && (
@@ -0,0 +1,348 @@
import { FC, useEffect, useRef, useState } from 'react'
import { Transition } from '@headlessui/react'
import { useRouter } from 'next/router'
import { useStore, useFlag } from 'hooks'
import { post, patch } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { getURL } from 'lib/helpers'
import Divider from 'components/ui/Divider'
import {
PaymentSummaryPanel,
ComputeSizeSelection,
PITRDurationSelection,
CustomDomainSelection,
StripeSubscription,
AddNewPaymentMethodModal,
} from './'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import UpdateSuccess from './UpdateSuccess'
import { PaymentMethod, SubscriptionPreview } from './Billing.types'
import { formSubscriptionUpdatePayload, getCurrentAddons } from './Billing.utils'
import { SubscriptionAddon } from './AddOns/AddOns.types'
import {
formatComputeSizes,
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 {
products: { tiers: any[]; addons: SubscriptionAddon[] }
paymentMethods?: PaymentMethod[]
currentSubscription: StripeSubscription
isLoadingPaymentMethods: boolean
onSelectBack: () => void
}
const TeamUpgrade: FC<Props> = ({
products,
paymentMethods,
currentSubscription,
isLoadingPaymentMethods,
onSelectBack,
}) => {
const { app, ui } = useStore()
const router = useRouter()
const isCustomDomainsEnabled = useFlag('customDomains')
const isPITRSelfServeEnabled = useFlag('pitrSelfServe')
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const captchaRef = useRef<HCaptcha>(null)
const { addons } = products
const computeSizes = formatComputeSizes(addons)
const pitrDurationOptions = formatPITROptions(addons)
const customDomainOptions = formatCustomDomainOptions(addons)
const currentAddons = getCurrentAddons(currentSubscription, addons)
const projectId = ui.selectedProject?.id ?? -1
const projectRef = ui.selectedProject?.ref
const projectRegion = ui.selectedProject?.region ?? ''
const [isRefreshingPreview, setIsRefreshingPreview] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccessful, setIsSuccessful] = useState(false)
const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false)
// [Joshen TODO] Ideally we just have a state to hold all the add ons selection, rather than individual
// Even better if we can just use the <Form> component to handle all of these. Mainly to reduce the amount
// of unnecessary state management on this complex page.
const [selectedComputeSize, setSelectedComputeSize] = useState<SubscriptionAddon>(
currentAddons.computeSize
)
const [selectedPITRDuration, setSelectedPITRDuration] = useState<SubscriptionAddon>(
currentAddons.pitrDuration
)
const [selectedCustomDomainOption, setSelectedCustomDomainOption] = useState<SubscriptionAddon>(
currentAddons.customDomains
)
// [Joshen TODO] Future - We may need to also include any add ons outside of
// compute size, pitr, custom domain and support plan, although we dont have any now
const nonChangeableAddons = [currentAddons.supportPlan].filter(
(x) => x !== undefined
) as SubscriptionAddon[]
// [Joshen] Scaffolded here
const selectedAddons = {
computeSize: selectedComputeSize,
pitrDuration: selectedPITRDuration,
customDomains: selectedCustomDomainOption,
}
const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState<string>('')
const [subscriptionPreview, setSubscriptionPreview] = useState<SubscriptionPreview>()
const selectedTier = products?.tiers.find((tier: any) => tier.id === STRIPE_PRODUCT_IDS.TEAM)
const isManagingTeamSubscription = currentSubscription.tier.prod_id === STRIPE_PRODUCT_IDS.TEAM
const isChangingComputeSize = currentAddons.computeSize?.id !== selectedAddons.computeSize.id
useEffect(() => {
if (!isLoadingPaymentMethods && paymentMethods && paymentMethods.length > 0) {
setSelectedPaymentMethodId(paymentMethods[0].id)
}
}, [isLoadingPaymentMethods, paymentMethods])
useEffect(() => {
getSubscriptionPreview()
}, [selectedComputeSize, selectedPITRDuration, selectedCustomDomainOption])
const getSubscriptionPreview = async () => {
const payload = formSubscriptionUpdatePayload(
currentSubscription,
selectedTier,
selectedAddons,
nonChangeableAddons,
selectedPaymentMethodId,
projectRegion
)
setIsRefreshingPreview(true)
const preview = await post(`${API_URL}/projects/${projectRef}/subscription/preview`, payload)
if (preview.error) {
ui.setNotification({
category: 'error',
message: `Failed to fetch subscription preview: ${preview.error.message}`,
})
}
setSubscriptionPreview(preview)
setIsRefreshingPreview(false)
}
const resetCaptcha = () => {
setCaptchaToken(null)
captchaRef.current?.resetCaptcha()
}
const beforeConfirmPayment = async (): Promise<boolean> => {
setIsSubmitting(true)
let token = captchaToken
try {
if (!token) {
const captchaResponse = await captchaRef.current?.execute({ async: true })
token = captchaResponse?.response ?? null
setCaptchaToken(token)
}
} catch (error) {
setIsSubmitting(false)
return false
}
setIsSubmitting(false)
return true
}
const teamTierEnabled = useFlag('teamTier')
const onConfirmPayment = async () => {
if (!teamTierEnabled) {
return ui.setNotification({
category: 'error',
message: 'Team Plan is not enabled yet.',
})
}
const payload = formSubscriptionUpdatePayload(
currentSubscription,
selectedTier,
selectedAddons,
nonChangeableAddons,
selectedPaymentMethodId,
projectRegion
)
const res = await patch(`${API_URL}/projects/${projectRef}/subscription`, payload)
resetCaptcha()
if (res?.error) {
ui.setNotification({
category: 'error',
message: `Failed to update subscription: ${res?.error?.message}`,
})
} else {
if (isChangingComputeSize) {
app.onProjectStatusUpdated(projectId, PROJECT_STATUS.RESTORING)
ui.setNotification({
category: 'success',
message:
'Your project has been updated and is currently restarting to update its instance size',
duration: 8000,
})
router.push(`/project/${projectRef}`)
} else {
setIsSuccessful(true)
}
}
setIsSubmitting(false)
}
if (!isSubmitting && isSuccessful) {
return (
<UpdateSuccess
projectRef={projectRef || ''}
title="Your project has been updated!"
message="Let us know if you have any feedback at sales@supabase.io."
/>
)
}
return (
<>
<Transition
show
enter="transition ease-out duration-300"
enterFrom="transform opacity-0 translate-x-10"
enterTo="transform opacity-100 translate-x-0"
className="flex items-start justify-between w-full"
>
<div className="flex-grow mt-10">
<div className="relative space-y-4">
<div className="px-32 mx-auto space-y-4 2xl:min-w-5xl">
<BackButton onClick={() => onSelectBack()} />
<h4 className="text-lg text-scale-900 !mb-8">Change your project's subscription</h4>
</div>
<div
className="px-32 pb-8 mx-auto space-y-8 overflow-y-auto 2xl:min-w-5xl"
style={{ height: 'calc(100vh - 9rem - 57px)' }}
>
<div className="space-y-2">
{!isManagingTeamSubscription ? (
<>
<h3 className="text-xl">
Welcome to <span className="text-brand-900">Team</span>
</h3>
<p className="text-base text-scale-1100">
Your new subscription will begin immediately after payment
</p>
</>
) : (
<>
<h3 className="text-3xl">
Managing your <span className="text-brand-900">Team</span> plan
</h3>
</>
)}
</div>
{projectRegion !== 'af-south-1' && (
<>
{currentAddons.supportPlan !== undefined && (
<>
<Divider light />
<SupportPlan currentOption={currentAddons.supportPlan} />
</>
)}
{isCustomDomainsEnabled && customDomainOptions.length > 0 && (
<>
<Divider light />
<CustomDomainSelection
options={customDomainOptions}
currentOption={
isManagingTeamSubscription ? currentAddons.customDomains : undefined
}
selectedOption={selectedAddons.customDomains}
onSelectOption={setSelectedCustomDomainOption}
/>
</>
)}
{isPITRSelfServeEnabled && pitrDurationOptions.length > 0 && (
<>
<Divider light />
<PITRDurationSelection
pitrDurationOptions={pitrDurationOptions}
currentPitrDuration={
isManagingTeamSubscription ? currentAddons.pitrDuration : undefined
}
selectedPitrDuration={selectedAddons.pitrDuration}
onSelectOption={setSelectedPITRDuration}
/>
</>
)}
<Divider light />
<ComputeSizeSelection
computeSizes={computeSizes || []}
currentComputeSize={currentAddons.computeSize}
selectedComputeSize={selectedAddons.computeSize}
onSelectOption={setSelectedComputeSize}
/>
</>
)}
</div>
</div>
</div>
<div className="w-[34rem]">
<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>
</Transition>
<AddNewPaymentMethodModal
visible={showAddPaymentMethodModal}
returnUrl={`${getURL()}/project/${projectRef}/settings/billing/update/team`}
onCancel={() => setShowAddPaymentMethodModal(false)}
/>
</>
)
}
export default TeamUpgrade
@@ -1,21 +1,19 @@
import { useContext, useEffect } from 'react'
import { useEffect } from 'react'
import { Form, Input } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useStore, checkPermissions } from 'hooks'
import { useStore, checkPermissions, useParams } from 'hooks'
import { API_URL } from 'lib/constants'
import { patch } from 'lib/common/fetch'
import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms'
import { PageContext } from 'pages/org/[slug]/settings'
const BillingEmail = () => {
const { ui } = useStore()
const PageState: any = useContext(PageContext)
const { app, ui } = useStore()
const { slug } = useParams()
const { name, billing_email } = ui.selectedOrganization ?? {}
const formId = 'org-billing-email'
const initialValues = {
billing_email: PageState.organization?.billing_email ?? '',
billing_email: billing_email ?? '',
}
const canUpdateOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
@@ -30,9 +28,9 @@ const BillingEmail = () => {
}
setSubmitting(true)
const response = await patch(`${API_URL}/organizations/${PageState.organization.slug}`, {
const response = await patch(`${API_URL}/organizations/${slug}`, {
...values,
name: PageState.organization.name,
name,
})
if (response.error) {
ui.setNotification({
@@ -46,7 +44,7 @@ const BillingEmail = () => {
initialValues: { billing_email },
})
PageState.onOrgUpdated(response)
app.onOrgUpdated(response)
ui.setNotification({
category: 'success',
message: 'Successfully saved settings',
@@ -66,9 +64,9 @@ const BillingEmail = () => {
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
useEffect(() => {
const values = { billing_email: PageState.organization?.billing_email ?? '' }
const values = { billing_email: billing_email ?? '' }
resetForm({ values, initialValues: values })
}, [PageState.organization.slug])
}, [slug])
return (
<FormPanel
@@ -1,6 +1,8 @@
import { FC, useState, useEffect } from 'react'
import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useStore } from 'hooks'
import { Project } from 'types'
import { useParams, useStore } from 'hooks'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
@@ -11,14 +13,12 @@ import BillingAddress from './BillingAddress/BillingAddress'
import TaxID from './TaxID/TaxID'
import BillingEmail from './BillingEmail'
interface Props {
organization: any
projects: any[]
}
const BillingSettings = () => {
const { app, ui } = useStore()
const { slug } = useParams()
const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
const { ui } = useStore()
const { slug } = organization
const organization = ui.selectedOrganization
const projects = app.projects.list((x: Project) => x.organization_id == organization?.id) || []
const [customer, setCustomer] = useState<any>(null)
const [isLoadingCustomer, setIsLoadingCustomer] = useState(false)
@@ -122,4 +122,4 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
)
}
export default BillingSettings
export default observer(BillingSettings)
@@ -14,7 +14,7 @@ import {
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useStore, useFlag } from 'hooks'
import { checkPermissions, useStore } from 'hooks'
import { delete_, patch } from 'lib/common/fetch'
import { getURL } from 'lib/helpers'
import { API_URL } from 'lib/constants'
@@ -39,7 +39,6 @@ const PaymentMethods: FC<Props> = ({
}) => {
const { ui } = useStore()
const orgSlug = ui.selectedOrganization?.slug ?? ''
const isOwner = ui.selectedOrganization?.is_owner
const [selectedMethodForDefault, setSelectedMethodForDefault] = useState<any>()
const [selectedMethodToDelete, setSelectedMethodToDelete] = useState<any>()
@@ -244,7 +243,7 @@ const PaymentMethods: FC<Props> = ({
<AddNewPaymentMethodModal
visible={showAddPaymentMethodModal}
returnUrl={`${getURL()}/org/${orgSlug}/settings`}
returnUrl={`${getURL()}/org/${orgSlug}/billing`}
onCancel={() => setShowAddPaymentMethodModal(false)}
/>
@@ -48,8 +48,8 @@ const ProjectSummary: FC<ProjectSummaryProps> = ({ project }) => {
<div className="flex w-[15%] items-center justify-end">
<Link href={`/project/${project.ref}/settings/billing/subscription`}>
<a className="group flex items-center space-x-2">
<p className="text-sm opacity-0 transition group-hover:opacity-100">View details</p>
<IconChevronRight />
<p className="text-xs opacity-0 transition group-hover:opacity-100">View details</p>
<IconChevronRight strokeWidth={1.5} />
</a>
</Link>
</div>
@@ -7,14 +7,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useStore, checkPermissions } from 'hooks'
import { delete_ } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { PageContext } from 'pages/org/[slug]/settings'
const DeleteOrganizationButton = observer(() => {
const PageState: any = useContext(PageContext)
const router = useRouter()
const { ui } = useStore()
const { app, ui } = useStore()
const { slug: orgSlug, name: orgName } = PageState.organization
const { slug: orgSlug, name: orgName } = ui.selectedOrganization || {}
const [isOpen, setIsOpen] = useState(false)
const [value, setValue] = useState('')
@@ -49,7 +47,7 @@ const DeleteOrganizationButton = observer(() => {
})
setSubmitting(false)
} else {
PageState.onOrgDeleted(PageState.organization)
app.onOrgDeleted(ui.selectedOrganization)
setSubmitting(false)
router.push('/')
ui.setNotification({
@@ -62,7 +60,7 @@ const DeleteOrganizationButton = observer(() => {
return (
<>
<div className="mt-2">
<Button onClick={() => setIsOpen(true)} type="danger">
<Button loading={!orgSlug} onClick={() => setIsOpen(true)} type="danger">
Delete organization
</Button>
</div>
@@ -1,9 +1,9 @@
import { useContext, useEffect } from 'react'
import { useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { Form, Input } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useStore, checkPermissions } from 'hooks'
import { useStore, checkPermissions, useParams } from 'hooks'
import { API_URL } from 'lib/constants'
import { patch } from 'lib/common/fetch'
@@ -16,14 +16,12 @@ import {
FormSectionLabel,
} from 'components/ui/Forms'
import { PageContext } from 'pages/org/[slug]/settings'
const GeneralSettings = observer(() => {
const { ui } = useStore()
const PageState: any = useContext(PageContext)
const GeneralSettings = () => {
const { app, ui } = useStore()
const { slug } = useParams()
const formId = 'org-general-settings'
const initialValues = { name: PageState.organization.name }
const initialValues = { name: ui.selectedOrganization?.name ?? '' }
const canUpdateOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
const canDeleteOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
@@ -37,9 +35,9 @@ const GeneralSettings = observer(() => {
setSubmitting(true)
const response = await patch(`${API_URL}/organizations/${PageState.organization.slug}`, {
const response = await patch(`${API_URL}/organizations/${slug}`, {
...values,
billing_email: PageState.organization?.billing_email ?? '',
billing_email: ui.selectedOrganization?.billing_email ?? '',
})
if (response.error) {
@@ -54,7 +52,7 @@ const GeneralSettings = observer(() => {
initialValues: { name },
})
PageState.onOrgUpdated(response)
app.onOrgUpdated(response)
ui.setNotification({
category: 'success',
message: 'Successfully saved settings',
@@ -70,9 +68,9 @@ const GeneralSettings = observer(() => {
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
useEffect(() => {
const values = { name: PageState.organization.name }
const values = { name: ui.selectedOrganization?.name ?? '' }
resetForm({ values, initialValues: values })
}, [PageState.organization.slug])
}, [ui.selectedOrganization?.slug])
return (
<FormPanel
@@ -105,6 +103,6 @@ const GeneralSettings = observer(() => {
{canDeleteOrganization && <OrganizationDeletePanel />}
</div>
)
})
}
export default GeneralSettings
export default observer(GeneralSettings)
@@ -1,4 +1,4 @@
import { FC, useState, useEffect } from 'react'
import { useState, useEffect } from 'react'
import { Button, Loading, IconFileText, IconDownload, IconChevronLeft, IconChevronRight } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
@@ -10,15 +10,9 @@ import NoPermission from 'components/ui/NoPermission'
const PAGE_LIMIT = 10
/**
* Eventually deprecate this - as we move on to show invoices by project on the organization billing page
*/
// Refactor: Split invoices by projects so it's easier for users to identify
interface Props {
organization: any
}
const InvoicesSettings: FC<Props> = ({ organization }) => {
const InvoicesSettings = () => {
const { ui } = useStore()
const [loading, setLoading] = useState<any>(false)
@@ -26,7 +20,7 @@ const InvoicesSettings: FC<Props> = ({ organization }) => {
const [count, setCount] = useState(0)
const [invoices, setInvoices] = useState<any>([])
const { stripe_customer_id } = organization
const { stripe_customer_id } = ui.selectedOrganization ?? {}
const offset = (page - 1) * PAGE_LIMIT
const canReadInvoices = checkPermissions(PermissionAction.READ, 'invoices')
@@ -1,16 +1,15 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { FC, useContext, useState } from 'react'
import { FC, useState } from 'react'
import { Button, Dropdown, IconMoreHorizontal, IconTrash } from 'ui'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
import { checkPermissions, useOrganizationDetail, useStore } from 'hooks'
import { checkPermissions, useOrganizationDetail, useParams, useStore } from 'hooks'
import { delete_, post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { Member, Role } from 'types'
import { PageContext } from 'pages/org/[slug]/settings'
import { isInviteExpired } from '../Organization.utils'
import { getRolesManagementPermissions } from './TeamSettings.utils'
@@ -21,12 +20,10 @@ interface Props {
}
const MemberActions: FC<Props> = ({ members, member, roles }) => {
const PageState: any = useContext(PageContext)
const { id, slug, name: orgName } = PageState.organization
const { ui } = useStore()
const { slug } = useParams()
const { rolesRemovable } = getRolesManagementPermissions(roles)
const { app, ui } = useStore()
const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '')
const { mutateOrgMembers } = useOrganizationDetail(slug || '')
const [loading, setLoading] = useState(false)
@@ -72,35 +69,6 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
})
}
// [Joshen] This will be deprecated after ABAC is fully rolled out
const handleTransferOwnership = async () => {
setLoading(true)
const response = await post(`${API_URL}/organizations/${slug}/transfer`, {
org_id: id,
member_id: member.id,
})
if (response.error) {
ui.setNotification({
category: 'error',
message: `Failed to transfer ownership: ${response.error.message}`,
})
setLoading(false)
} else {
const updatedMembers = members.map((m: any) => {
if (m.is_owner) return { ...m, is_owner: false }
if (m.id === member.id) return { ...m, is_owner: true }
else return { ...m }
})
mutateOrgMembers(updatedMembers)
ui.setNotification({ category: 'success', message: 'Successfully transferred organization' })
}
app.organizations.load()
}
async function handleResendInvite(member: Member) {
setLoading(true)
@@ -1,11 +1,11 @@
import Image from 'next/image'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useState, useContext, Fragment } from 'react'
import { useState, FC, Fragment } from 'react'
import { observer } from 'mobx-react-lite'
import { Badge, Button, Loading, Listbox, IconUser, Modal } from 'ui'
import { Badge, Button, Loading, Listbox, IconUser, Modal, IconAlertCircle } from 'ui'
import { Member, Role } from 'types'
import { useStore, useOrganizationDetail } from 'hooks'
import { useStore, useOrganizationDetail, useParams } from 'hooks'
import { patch } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
@@ -13,7 +13,6 @@ import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
import Table from 'components/to-be-cleaned/Table'
import MemberActions from './MemberActions'
import RolesHelperModal from './RolesHelperModal/RolesHelperModal'
import { PageContext } from 'pages/org/[slug]/settings'
import { getRolesManagementPermissions } from './TeamSettings.utils'
interface SelectedMember extends Member {
@@ -21,20 +20,42 @@ interface SelectedMember extends Member {
newRoleId: number
}
const MembersView = () => {
const PageState: any = useContext(PageContext)
const { roles }: { roles: Role[] } = PageState
const { rolesAddable, rolesRemovable } = getRolesManagementPermissions(roles)
interface Props {
roles: Role[]
members: Member[]
searchString: string
}
const MembersView: FC<Props> = ({ searchString, roles, members }) => {
const { ui } = useStore()
const slug = ui.selectedOrganization?.slug || ''
const { slug } = useParams()
const { mutateOrgMembers } = useOrganizationDetail(slug)
const user = ui.profile
const { mutateOrgMembers } = useOrganizationDetail(slug ?? '')
const { rolesAddable, rolesRemovable } = getRolesManagementPermissions(roles)
const [loading, setLoading] = useState(false)
const [selectedMember, setSelectedMember] = useState<SelectedMember>()
const [userRoleChangeModalVisible, setUserRoleChangeModalVisible] = useState(false)
if (!members) return <div />
const filteredMembers = (
!searchString
? members
: members.filter((x: any) => {
if (x.invited_at) {
return x.primary_email.includes(searchString)
}
if (x.id || x.gotrue_id) {
return x.username.includes(searchString) || x.primary_email.includes(searchString)
}
})
)
.slice()
.sort((a: any, b: any) => a.username.localeCompare(b.username))
const getRoleNameById = (id: number | undefined) => {
if (!roles) return id
return roles.find((x: any) => x.id === id)?.name
@@ -55,7 +76,7 @@ const MembersView = () => {
message: `Failed to update role for ${getUserDisplayName(selectedMember)}`,
})
} else {
const updatedMembers = PageState.members.map((member: Member) => {
const updatedMembers = members.map((member: Member) => {
if (member.gotrue_id === selectedMember.gotrue_id) {
return { ...member, role_ids: [newRoleId] }
} else {
@@ -76,7 +97,7 @@ const MembersView = () => {
return (
<>
<div className="rounded">
<Loading active={!PageState.filteredMembers}>
<Loading active={!filteredMembers}>
<Table
head={[
<Table.th key="header-user">User</Table.th>,
@@ -88,10 +109,10 @@ const MembersView = () => {
<Table.th key="header-action"></Table.th>,
]}
body={[
PageState.filteredMembers.map((x: Member, i: number) => {
...filteredMembers.map((x: Member, i: number) => {
const [memberRoleId] = x.role_ids ?? []
const role = (roles || []).find((role) => role.id === memberRoleId)
const memberIsUser = x.primary_email == PageState.user.primary_email
const memberIsUser = x.primary_email == user?.primary_email
const memberIsPendingInvite = !!x.invited_id
const canRemoveRole = rolesRemovable.includes(memberRoleId)
const disableRoleEdit = !canRemoveRole || memberIsUser || memberIsPendingInvite
@@ -216,22 +237,38 @@ const MembersView = () => {
</Table.td>
<Table.td>
{!memberIsUser && (
<MemberActions members={PageState.members} member={x} roles={roles} />
<MemberActions members={members} member={x} roles={roles} />
)}
</Table.td>
</Table.tr>
</Fragment>
)
}),
...(searchString.length > 0 && filteredMembers.length === 0
? [
<Table.tr
key="no-results"
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
>
<Table.td colSpan={12}>
<div className="flex items-center space-x-3 opacity-75">
<IconAlertCircle size={16} strokeWidth={2} />
<p className="text-scale-1100">
No users matched the search query "{searchString}"
</p>
</div>
</Table.td>
</Table.tr>,
]
: []),
<Table.tr
key="footer"
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
>
<Table.td colSpan={4}>
<p className="text-scale-1100">
{PageState.membersFilterString ? `${PageState.filteredMembers.length} of ` : ''}
{PageState.members.length || '0'}{' '}
{PageState.members.length == 1 ? 'user' : 'users'}
{searchString ? `${filteredMembers.length} of ` : ''}
{members.length || '0'} {members.length == 1 ? 'user' : 'users'}
</p>
</Table.td>
</Table.tr>,
@@ -1,9 +1,9 @@
import { useState, useContext } from 'react'
import { useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, Input, IconSearch } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useStore } from 'hooks'
import { useStore, useFlag, useOrganizationRoles, useOrganizationDetail, useParams } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import InviteMemberButton from './InviteMemberButton'
@@ -11,26 +11,28 @@ import MembersView from './MembersView'
import { getRolesManagementPermissions, hasMultipleOwners } from './TeamSettings.utils'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
import { PageContext } from 'pages/org/[slug]/settings'
const TeamSettings = observer(() => {
const PageState: any = useContext(PageContext)
const { user, members, roles } = PageState
const { rolesAddable } = getRolesManagementPermissions(roles)
const TeamSettings = () => {
const { ui } = useStore()
const slug = ui.selectedOrganization?.slug ?? ''
const { slug } = useParams()
const user = ui.profile
const isOwner = ui.selectedOrganization?.is_owner
const { members } = useOrganizationDetail(slug || '')
const { roles: allRoles } = useOrganizationRoles(slug)
const enableBillingOnlyReadOnly = useFlag('enableBillingOnlyReadOnlyRoles')
const roles = enableBillingOnlyReadOnly
? allRoles
: (allRoles ?? []).filter((role) => ['Owner', 'Administrator', 'Developer'].includes(role.name))
const { rolesAddable } = getRolesManagementPermissions(roles)
const [isLeaving, setIsLeaving] = useState(false)
const [searchString, setSearchString] = useState('')
const canAddMembers = rolesAddable.length > 0
const canLeave = !isOwner || (isOwner && hasMultipleOwners(members, roles))
function onFilterMemberChange(e: any) {
PageState.membersFilterString = e.target.value
}
const leaveTeam = async () => {
setIsLeaving(true)
try {
@@ -63,14 +65,14 @@ const TeamSettings = observer(() => {
<Input
icon={<IconSearch size="tiny" />}
size="small"
value={PageState.membersFilterString}
onChange={onFilterMemberChange}
value={searchString}
onChange={(e: any) => setSearchString(e.target.value)}
name="email"
id="email"
placeholder="Filter members"
/>
<div className="flex items-center space-x-4">
{canAddMembers && (
{canAddMembers && user !== undefined && (
<div>
<InviteMemberButton
user={user}
@@ -113,10 +115,10 @@ const TeamSettings = observer(() => {
</div>
</div>
<div className="container my-4 max-w-4xl space-y-8">
<MembersView />
<MembersView searchString={searchString} roles={roles} members={members} />
</div>
</>
)
})
}
export default TeamSettings
export default observer(TeamSettings)
@@ -1,6 +1,8 @@
import dayjs from 'dayjs'
import { DatetimeHelper, FilterTableSet, LogTemplate } from '.'
export const LOGS_EXPLORER_DOCS_URL = "https://supabase.com/docs/guides/platform/logs#querying-with-the-logs-explorer"
export const LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD = 4
export const TEMPLATES: LogTemplate[] = [
@@ -631,5 +633,6 @@ export const TIER_QUERY_LIMITS: {
FREE: { text: '1 day', value: 1, unit: 'day', promptUpgrade: true },
PRO: { text: '7 days', value: 7, unit: 'day', promptUpgrade: true },
PAYG: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
TEAM: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
ENTERPRISE: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
}
@@ -173,16 +173,22 @@ export const genSingleLogQuery = (table: LogsTableName, id: string) =>
*/
export const maybeShowUpgradePrompt = (
from: string | null | undefined,
tierKey?: StripeSubscription['tier']["key"]
tierKey?: StripeSubscription['tier']['key']
) => {
const day = Math.abs(dayjs().diff(dayjs(from), 'day'))
return (day > 1 && tierKey === 'FREE') || (day > 7 && tierKey === 'PRO') || day > 90 && tierKey === 'ENTERPRISE'
return (
(day > 1 && tierKey === 'FREE') ||
(day > 7 && tierKey === 'PRO') ||
(day > 28 && tierKey === 'TEAM') ||
(day > 90 && tierKey === 'ENTERPRISE')
)
}
export const genCountQuery = (table: LogsTableName, filters: Filters): string => {
const where = _genWhereStatement(table, filters)
return `SELECT count(*) as count FROM ${table} ${where}`}
return `SELECT count(*) as count FROM ${table} ${where}`
}
/** calculates how much the chart start datetime should be offset given the current datetime filter params */
export const calcChartStart = (params: Partial<LogsEndpointParams>): [Dayjs, string] => {
@@ -224,10 +230,11 @@ SELECT
FROM
${table} t
cross join unnest(t.metadata) as metadata
${where
${
where
? where + ` and t.timestamp > '${startOffset.toISOString()}'`
: `where t.timestamp > '${startOffset.toISOString()}'`
}
}
GROUP BY
timestamp
ORDER BY
@@ -6,6 +6,7 @@ import {
EXPLORER_DATEPICKER_HELPERS,
LogsTableName,
LogsWarning,
LOGS_EXPLORER_DOCS_URL,
LOGS_SOURCE_DESCRIPTION,
LogTemplate,
} from '.'
@@ -13,6 +14,7 @@ import DatePickers from './Logs.DatePickers'
import Link from 'next/link'
import React from 'react'
import { checkPermissions, useStore } from 'hooks'
import { IconBookOpen } from '@supabase/ui'
interface Props {
templates?: LogTemplate[]
@@ -130,6 +132,11 @@ const LogsQueryPanel: React.FC<Props> = ({
<div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<a target="_blank" href={LOGS_EXPLORER_DOCS_URL}>
<Button type="default" icon={<IconBookOpen size={12} />}>
Docs
</Button>
</a>
<Button type="default" onClick={onClear}>
Clear query
</Button>
@@ -3,6 +3,7 @@ import { Button, Modal } from 'ui'
import { TIER_QUERY_LIMITS } from '.'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useFlag } from 'hooks'
interface Props {
show: boolean
@@ -13,6 +14,8 @@ const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt }) => {
const router = useRouter()
const { ref } = router.query
const teamTierEnabled = useFlag('teamTier')
return (
<Modal
hideFooter
@@ -43,6 +46,12 @@ const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt }) => {
<p className="w-[40%] text-sm">Pro</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.PRO.text}</p>
</div>
{teamTierEnabled && (
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Team</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.TEAM.text}</p>
</div>
)}
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Enterprise</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.ENTERPRISE.text}</p>
@@ -23,6 +23,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
const projectHasNoLimits =
subscriptionTier === PRICING_TIER_PRODUCT_IDS.PAYG ||
subscriptionTier === PRICING_TIER_PRODUCT_IDS.TEAM ||
subscriptionTier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
const showUsageExceedMessage = subscriptionTier !== undefined && !projectHasNoLimits
@@ -31,6 +32,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
[PRICING_TIER_PRODUCT_IDS.FREE]: 'Free',
[PRICING_TIER_PRODUCT_IDS.PRO]: 'Pro',
[PRICING_TIER_PRODUCT_IDS.PAYG]: 'Pro',
[PRICING_TIER_PRODUCT_IDS.TEAM]: 'Team',
[PRICING_TIER_PRODUCT_IDS.ENTERPRISE]: 'Enterprise',
}
@@ -20,7 +20,8 @@ const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter
if (
subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PAYG ||
subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE ||
subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
) {
return <></>
}
@@ -59,6 +59,7 @@ const SupportForm: FC<Props> = ({ setSentCategory }) => {
[PRICING_TIER_PRODUCT_IDS.FREE]: 'Free',
[PRICING_TIER_PRODUCT_IDS.PRO]: 'Pro',
[PRICING_TIER_PRODUCT_IDS.PAYG]: 'Pro',
[PRICING_TIER_PRODUCT_IDS.TEAM]: 'Team',
[PRICING_TIER_PRODUCT_IDS.ENTERPRISE]: 'Enterprise',
}
@@ -37,7 +37,7 @@ const AccountLayout: FC<Props> = ({ children, title, breadcrumbs }) => {
isActive:
router.pathname.startsWith('/org/') && ui.selectedOrganization?.slug === organization.slug,
label: organization.name,
href: `/org/${organization.slug}/settings`,
href: `/org/${organization.slug}/general`,
key: organization.slug,
}))
.sort((a, b) => a.label.localeCompare(b.label))
@@ -0,0 +1,23 @@
import { useStore } from 'hooks'
import { observer } from 'mobx-react-lite'
import { FC, ReactNode } from 'react'
import { AccountLayout } from './'
interface Props {
children: ReactNode
}
const OrganizationLayout: FC<Props> = ({ children }) => {
const { ui } = useStore()
return (
<AccountLayout
title={ui.selectedOrganization?.name ?? 'Supabase'}
breadcrumbs={[{ key: `org-settings`, label: 'Settings' }]}
>
{children}
</AccountLayout>
)
}
export default observer(OrganizationLayout)
@@ -23,7 +23,8 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
const resourcesExceededLimits = getResourcesExceededLimits(usage)
const projectHasNoLimits =
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.PAYG ||
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE ||
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.TEAM
const showOverUsageBadge =
selectedProject?.subscription_tier !== undefined &&
!projectHasNoLimits &&
@@ -40,7 +40,7 @@ const OrgDropdown = () => {
}
router.push({
pathname: `/org/[slug]/settings`,
pathname: `/org/[slug]/general`,
query: { slug },
hash: router.asPath.split('#')[1]?.toLowerCase(),
})
+2
View File
@@ -16,6 +16,7 @@ import VercelIntegrationLayout from './VercelIntegrationLayout'
import BillingLayout from './BillingLayout'
import LogsLayout from './LogsLayout/LogsLayout'
import ReportsLayout from './ReportsLayout/ReportsLayout'
import OrganizationLayout from './OrganizationLayout'
export {
ProjectLayoutWithAuth,
@@ -36,6 +37,7 @@ export {
BillingLayout,
LogsLayout,
ReportsLayout,
OrganizationLayout,
}
export default ProjectLayout
@@ -30,7 +30,8 @@ const OveragesBanner: FC<Props> = ({ tier, minimal }) => {
error ||
(!isApproachingUsageLimits && !isOverUsageLimits) ||
tier === PRICING_TIER_PRODUCT_IDS.PAYG ||
tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE ||
tier === PRICING_TIER_PRODUCT_IDS.TEAM
) {
return <></>
}
-1
View File
@@ -7,6 +7,5 @@ export * from './useProfile'
export * from './useProjectAuthConfig'
export * from './useProjectSettings'
export * from './useProjectSubscription'
export * from './useSubscriptionStats'
export * from './useProjectUsage'
export * from './useProjectPostgrestConfig'
@@ -1,29 +0,0 @@
import useSWR, { mutate } from 'swr'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
export interface SubscriptionStats {
total_paid_projects: number
total_free_projects: number
total_active_free_projects: number
total_paused_free_projects: number
total_pro_projects: number
total_payg_projects: number
}
export function useSubscriptionStats() {
const url = `${API_URL}/profile/subscriptions`
const { data, error } = useSWR<any>(url, get)
const anyError = data?.error || error
return {
total_paid_projects: data?.total_paid_projects ?? 0,
total_free_projects: data?.total_free_projects ?? 0,
total_active_free_projects: data?.total_active_free_projects ?? 0,
total_paused_free_projects: data?.total_paused_free_projects ?? 0,
total_pro_projects: data?.total_pro_projects ?? 0,
total_payg_projects: data?.total_payg_projects ?? 0,
isLoading: !anyError && !data,
isError: !!anyError,
}
}
+2
View File
@@ -30,6 +30,7 @@ export const PRICING_TIER_PRODUCT_IDS = {
FREE: 'tier_free',
PRO: 'tier_pro',
PAYG: 'tier_payg',
TEAM: 'tier_team',
ENTERPRISE: 'tier_enterprise',
}
@@ -112,6 +113,7 @@ export const STRIPE_PRODUCT_IDS: { [x: string]: string } = {
FREE: process?.env?.NEXT_PUBLIC_STRIPE_FREE_TIER_ID || 'prod_Ip4vqwv3EJ7Mi0',
PRO: process?.env?.NEXT_PUBLIC_STRIPE_PRO_TIER_ID || 'prod_IsRLOp58Z7V4XN',
PAYG: process?.env?.NEXT_PUBLIC_STRIPE_PAYG_TIER_ID || 'prod_JlTbw91xcM6NY4',
TEAM: process?.env?.NEXT_PUBLIC_STRIPE_TEAM_TIER_ID || 'prod_MeUrsWN6pj0oXM',
}
export const DEFAULT_MINIMUM_PASSWORD_STRENGTH = 4
+5
View File
@@ -115,6 +115,11 @@ const nextConfig = {
destination: '/project/:ref/logs/explorer',
permanent: true,
},
{
source: '/org/:slug/settings',
destination: '/org/:slug/general',
permanent: true,
},
]
},
async headers() {
+1 -1
View File
@@ -1,6 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next'
import apiWrapper from 'lib/api/apiWrapper'
import { Member } from 'types'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
@@ -27,6 +26,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
total_paid_projects: 0,
total_free_projects: 0,
total_pro_projects: 0,
total_team_projects: 0,
total_payg_projects: 0,
},
}
+1 -7
View File
@@ -25,7 +25,6 @@ import {
useStore,
useFlag,
withAuth,
useSubscriptionStats,
checkPermissions,
useFreeProjectLimitCheck,
} from 'hooks'
@@ -47,7 +46,6 @@ const Wizard: NextPageWithLayout = () => {
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const kpsEnabled = useFlag('initWithKps')
const subscriptionStats = useSubscriptionStats()
const { membersExceededLimit, isLoading: isLoadingFreeProjectLimitCheck } =
useFreeProjectLimitCheck(slug as string)
@@ -74,10 +72,7 @@ const Wizard: NextPageWithLayout = () => {
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
const canCreateProject =
isAdmin &&
!subscriptionStats.isError &&
!subscriptionStats.isLoading &&
(!isSelectFreeTier || (isSelectFreeTier && !hasMembersExceedingFreeTierLimit))
isAdmin && (!isSelectFreeTier || (isSelectFreeTier && !hasMembersExceedingFreeTierLimit))
const canSubmit =
projectName !== '' &&
@@ -204,7 +199,6 @@ const Wizard: NextPageWithLayout = () => {
hideHeaderStyling
loading={
!app.organizations.isInitialized ||
subscriptionStats.isLoading ||
isLoadingFreeProjectLimitCheck
}
title={
+55
View File
@@ -0,0 +1,55 @@
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Tabs } from 'ui'
import { NextPageWithLayout } from 'types'
import { useParams, useStore } from 'hooks'
import Loading from 'components/ui/Loading'
import { OrganizationLayout } from 'components/layouts'
import { BillingSettings } from 'components/interfaces/Organization'
const OrgBillingSettings: NextPageWithLayout = () => {
const { ui } = useStore()
const { slug } = useParams()
const router = useRouter()
return (
<>
{ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? (
<Loading />
) : (
<div className="p-4 pt-0">
<div className="space-y-3">
<section className="mt-4">
<h1 className="text-3xl">
{ui.selectedOrganization?.name ?? 'Organization'} settings
</h1>
</section>
<nav>
<Tabs
size="small"
type="underlined"
activeId="billing"
onChange={(id: any) => {
if (id !== 'billing') router.push(`/org/${slug}/${id}`)
}}
>
<Tabs.Panel id="general" label="General" />
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
</Tabs>
</nav>
</div>
<div className="mb-8">
<BillingSettings />
</div>
</div>
)}
</>
)
}
OrgBillingSettings.getLayout = (page) => <OrganizationLayout>{page}</OrganizationLayout>
export default observer(OrgBillingSettings)
+55
View File
@@ -0,0 +1,55 @@
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Tabs } from 'ui'
import { NextPageWithLayout } from 'types'
import { useParams, useStore } from 'hooks'
import Loading from 'components/ui/Loading'
import { OrganizationLayout } from 'components/layouts'
import { GeneralSettings } from 'components/interfaces/Organization'
const OrgGeneralSettings: NextPageWithLayout = () => {
const { ui } = useStore()
const { slug } = useParams()
const router = useRouter()
return (
<>
{ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? (
<Loading />
) : (
<div className="p-4 pt-0">
<div className="space-y-3">
<section className="mt-4">
<h1 className="text-3xl">
{ui.selectedOrganization?.name ?? 'Organization'} settings
</h1>
</section>
<nav>
<Tabs
size="small"
type="underlined"
activeId="general"
onChange={(id: any) => {
if (id !== 'general') router.push(`/org/${slug}/${id}`)
}}
>
<Tabs.Panel id="general" label="General" />
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
</Tabs>
</nav>
</div>
<div className="mb-8">
<GeneralSettings />
</div>
</div>
)}
</>
)
}
OrgGeneralSettings.getLayout = (page) => <OrganizationLayout>{page}</OrganizationLayout>
export default observer(OrgGeneralSettings)
+55
View File
@@ -0,0 +1,55 @@
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Tabs } from 'ui'
import { NextPageWithLayout } from 'types'
import { useParams, useStore } from 'hooks'
import Loading from 'components/ui/Loading'
import { OrganizationLayout } from 'components/layouts'
import { InvoicesSettings } from 'components/interfaces/Organization'
const OrgInvoices: NextPageWithLayout = () => {
const { ui } = useStore()
const { slug } = useParams()
const router = useRouter()
return (
<>
{ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? (
<Loading />
) : (
<div className="p-4 pt-0">
<div className="space-y-3">
<section className="mt-4">
<h1 className="text-3xl">
{ui.selectedOrganization?.name ?? 'Organization'} settings
</h1>
</section>
<nav>
<Tabs
size="small"
type="underlined"
activeId="invoices"
onChange={(id: any) => {
if (id !== 'invoices') router.push(`/org/${slug}/${id}`)
}}
>
<Tabs.Panel id="general" label="General" />
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
</Tabs>
</nav>
</div>
<div className="mb-8">
<InvoicesSettings />
</div>
</div>
)}
</>
)
}
OrgInvoices.getLayout = (page) => <OrganizationLayout>{page}</OrganizationLayout>
export default observer(OrgInvoices)
-189
View File
@@ -1,189 +0,0 @@
import { Tabs } from 'ui'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { Member, NextPageWithLayout, Organization, Project, Role, User } from 'types'
import {
useFlag,
useOrganizationDetail,
useOrganizationRoles,
useStore,
withAuth,
useParams,
} from 'hooks'
import { AccountLayoutWithoutAuth } from 'components/layouts'
import {
GeneralSettings,
TeamSettings,
BillingSettings,
InvoicesSettings,
} from 'components/interfaces/Organization'
export const PageContext = createContext(null)
const OrgSettingsLayout = withAuth(
observer<PropsWithChildren<{}>>(({ children }) => {
const { app, ui } = useStore()
const router = useRouter()
const { slug } = useParams()
const { roles: allRoles } = useOrganizationRoles(slug)
const enableBillingOnlyReadOnly = useFlag('enableBillingOnlyReadOnlyRoles')
const roles = enableBillingOnlyReadOnly
? allRoles
: (allRoles ?? []).filter((role) =>
['Owner', 'Administrator', 'Developer'].includes(role.name)
)
// [Refactor] Eventually move away from useLocalObservable
const PageState: any = useLocalObservable(() => ({
user: {} as User,
organization: {} as Organization,
projects: [] as Project[],
members: [] as Member[],
roles: [] as Role[],
membersFilterString: '',
get filteredMembers() {
const temp = !this.membersFilterString
? this.members
: this.members.filter((x: any) => {
if (x.invited_at) {
return x.primary_email.includes(this.membersFilterString)
}
if (x.id || x.gotrue_id) {
return (
x.username.includes(this.membersFilterString) ||
x.primary_email.includes(this.membersFilterString)
)
}
})
return temp.slice().sort((a: any, b: any) => a.username.localeCompare(b.username))
},
initData(organization: Organization, user: User, projects: Project[], roles: Role[]) {
this.user = user
this.projects = projects
this.organization = organization
this.roles = roles
},
onOrgUpdated(updatedOrg: any) {
app.onOrgUpdated(updatedOrg)
},
onOrgDeleted() {
app.onOrgDeleted(this.organization)
},
}))
useEffect(() => {
// User added a new payment method
if (router.query.setup_intent && router.query.redirect_status) {
ui.setNotification({
category: 'success',
message: 'Successfully added new payment method',
})
}
}, [router.query])
useEffect(() => {
const user = ui.profile
const organization = ui.selectedOrganization
const projects = app.projects.list((x: Project) => x.organization_id == organization?.id)
PageState.initData(organization, user, projects, roles)
}, [ui.selectedOrganization, ui.profile, roles])
return (
<AccountLayoutWithoutAuth
title={PageState.organization?.name || 'Supabase'}
breadcrumbs={[
{
key: `org-settings`,
label: 'Settings',
},
]}
>
<PageContext.Provider value={PageState}>{children}</PageContext.Provider>
</AccountLayoutWithoutAuth>
)
})
)
const OrganizationSettings: NextPageWithLayout = () => {
const router = useRouter()
const { slug } = useParams()
const PageState: any = useContext(PageContext)
const { ui, app } = useStore()
const [selectedTab, setSelectedTab] = useState('GENERAL')
const { members, isError: isOrgDetailError } = useOrganizationDetail(slug || '')
const hash = router.asPath.split('#')[1]?.toUpperCase()
useEffect(() => {
if (hash) {
setSelectedTab(hash)
}
}, [hash])
function handleChangeTab(id: string) {
if (!slug) {
// The user should not see this error as the page should
// be rerendered with the value of slug before they can click.
// It is just here in case they are the flash.
return ui.setNotification({
category: 'error',
message: 'Could not navigate to organization settings, please try again or contact support',
})
}
setSelectedTab(id)
router.push({
pathname: `/org/[slug]/settings`,
query: { slug },
hash: id.toLowerCase(),
})
}
useEffect(() => {
if (!isOrgDetailError) {
PageState.members = members ?? []
}
}, [members, isOrgDetailError])
if (!ui.selectedOrganization || !PageState.organization) return <div />
const organization = ui.selectedOrganization
const projects = app.projects.list((x: Project) => x.organization_id == organization?.id)
return (
<div className="p-4 pt-0">
<div className="space-y-3">
<section className="mt-4">
<h1 className="text-3xl">{organization?.name || 'Organization'} settings</h1>
</section>
<nav className="">
<Tabs size="small" onChange={(id: string) => handleChangeTab(id)} type="underlined">
<Tabs.Panel id="GENERAL" label="General" />
<Tabs.Panel id="TEAM" label="Team" />
<Tabs.Panel id="BILLING" label="Billing" />
<Tabs.Panel id="INVOICES" label="Invoices" />
</Tabs>
</nav>
</div>
<div className="mb-8">
{selectedTab == 'GENERAL' ? (
<GeneralSettings />
) : selectedTab == 'TEAM' ? (
<TeamSettings />
) : selectedTab == 'BILLING' ? (
<BillingSettings organization={organization} projects={projects} />
) : selectedTab == 'INVOICES' ? (
<InvoicesSettings organization={organization} />
) : null}
</div>
</div>
)
}
OrganizationSettings.getLayout = (page) => <OrgSettingsLayout>{page}</OrgSettingsLayout>
export default observer(OrganizationSettings)
+55
View File
@@ -0,0 +1,55 @@
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Tabs } from 'ui'
import { NextPageWithLayout } from 'types'
import { useParams, useStore } from 'hooks'
import Loading from 'components/ui/Loading'
import { OrganizationLayout } from 'components/layouts'
import { TeamSettings } from 'components/interfaces/Organization'
const OrgTeamSettings: NextPageWithLayout = () => {
const { ui } = useStore()
const { slug } = useParams()
const router = useRouter()
return (
<>
{ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? (
<Loading />
) : (
<div className="p-4 pt-0">
<div className="space-y-3">
<section className="mt-4">
<h1 className="text-3xl">
{ui.selectedOrganization?.name ?? 'Organization'} settings
</h1>
</section>
<nav>
<Tabs
size="small"
type="underlined"
activeId="team"
onChange={(id: any) => {
if (id !== 'team') router.push(`/org/${slug}/${id}`)
}}
>
<Tabs.Panel id="general" label="General" />
<Tabs.Panel id="team" label="Team" />
<Tabs.Panel id="billing" label="Billing" />
<Tabs.Panel id="invoices" label="Invoices" />
</Tabs>
</nav>
</div>
<div className="mb-8">
<TeamSettings />
</div>
</div>
)}
</>
)
}
OrgTeamSettings.getLayout = (page) => <OrganizationLayout>{page}</OrganizationLayout>
export default observer(OrgTeamSettings)
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Button, Modal } from 'ui'
import { useFreeProjectLimitCheck, useStore } from 'hooks'
import { useFlag, useFreeProjectLimitCheck, useStore } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL, PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
@@ -85,6 +85,8 @@ const BillingUpdate: NextPageWithLayout = () => {
const onSelectPlan = (plan: any) => {
if (plan.id === STRIPE_PRODUCT_IDS.PRO) {
router.push(`/project/${projectRef}/settings/billing/update/pro`)
} else if (plan.id === STRIPE_PRODUCT_IDS.TEAM) {
router.push(`/project/${projectRef}/settings/billing/update/team`)
} else if (plan.id === STRIPE_PRODUCT_IDS.FREE) {
setSelectedPlan(plan)
setShowConfirmDowngrade(true)
@@ -109,12 +111,18 @@ const BillingUpdate: NextPageWithLayout = () => {
)
}
const teamTierEnabled = useFlag('teamTier')
const productTiers = (products?.tiers ?? []).filter(
(tier) => teamTierEnabled || tier.id !== STRIPE_PRODUCT_IDS.TEAM
)
return (
<>
<div className="mx-auto my-10 max-w-6xl px-6">
<div className={`mx-auto my-10 ${teamTierEnabled ? 'max-w-[90vw]' : 'max-w-[80vw]'}`}>
<PlanSelection
visible={!selectedPlan || (selectedPlan && showConfirmDowngrade)}
tiers={products?.tiers ?? []}
tiers={productTiers}
currentPlan={subscription?.tier}
onSelectPlan={onSelectPlan}
/>
@@ -0,0 +1,132 @@
import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore, useFlag } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { NextPageWithLayout } from 'types'
import { BillingLayout } from 'components/layouts'
import Connecting from 'components/ui/Loading/Loading'
import { StripeSubscription } from 'components/interfaces/Billing'
import { PaymentMethod } from 'components/interfaces/Billing/Billing.types'
import { SubscriptionAddon } from 'components/interfaces/Billing/AddOns/AddOns.types'
import TeamUpgrade from 'components/interfaces/Billing/TeamUpgrade'
const BillingUpdateTeam: NextPageWithLayout = () => {
const { ui } = useStore()
const router = useRouter()
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[]>()
const [isLoadingPaymentMethods, setIsLoadingPaymentMethods] = useState(false)
const isEnterprise =
subscription && subscription.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
useEffect(() => {
// User added a new payment method
if (router.query.setup_intent && router.query.redirect_status) {
ui.setNotification({ category: 'success', message: 'Successfully added new payment method' })
}
}, [])
useEffect(() => {
if (projectUpdateDisabled) {
router.push(`/project/${projectRef}/settings/billing/update`)
} else if (projectRef) {
getStripeProducts()
getSubscription()
}
}, [projectRef])
useEffect(() => {
if (orgSlug) {
getPaymentMethods()
}
}, [orgSlug])
useEffect(() => {
if (isEnterprise) {
router.push(`/project/${projectRef}/settings/billing/update/enterprise`)
}
}, [subscription])
const getStripeProducts = async () => {
try {
const products = await get(`${API_URL}/stripe/products`)
setProducts(products)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to get products: ${error.message}`,
})
}
}
const getPaymentMethods = async () => {
const orgSlug = ui.selectedOrganization?.slug ?? ''
try {
setIsLoadingPaymentMethods(true)
const { data: paymentMethods, error } = await get(
`${API_URL}/organizations/${orgSlug}/payments`
)
if (error) throw error
setIsLoadingPaymentMethods(false)
setPaymentMethods(paymentMethods)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to get available payment methods: ${error.message}`,
})
}
}
const getSubscription = async () => {
try {
if (!ui.selectedProject?.subscription_id) {
throw new Error('Unable to get subscription ID of project')
}
const subscription = await get(`${API_URL}/projects/${projectRef}/subscription`)
if (subscription.error) throw subscription.error
setSubscription(subscription)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to get subscription: ${error.message}`,
})
}
}
if (!products || !subscription || isEnterprise)
return (
<div className="flex h-full w-full items-center justify-center">
<Connecting />
</div>
)
return (
<TeamUpgrade
products={products}
currentSubscription={subscription}
isLoadingPaymentMethods={isLoadingPaymentMethods}
paymentMethods={paymentMethods || []}
onSelectBack={() => router.push(`/project/${projectRef}/settings/billing/update`)}
/>
)
}
BillingUpdateTeam.getLayout = (page) => <BillingLayout>{page}</BillingLayout>
export default observer(BillingUpdateTeam)
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more