mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 07:39:07 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
a1ba4e0d08
41 files changed
+593
-301
No files matched your search
@@ -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} />
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ author_image_url: https://avatars0.githubusercontent.com/u/26374889?s=400&u=f5e3
|
||||
authorURL: https://github.com/dragarcia
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '07-09-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ author_image_url: https://avatars0.githubusercontent.com/u/26374889?s=400&u=f5e3
|
||||
authorURL: https://github.com/dragarcia
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '07-07-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ author_image_url: https://avatars0.githubusercontent.com/u/26374889?s=400&u=f5e3
|
||||
authorURL: https://github.com/dragarcia
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '08-02-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ authorURL: https://github.com/dragarcia
|
||||
image: postgres-views.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '11-18-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ thumb: elephants.png
|
||||
tags:
|
||||
- sql
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '02-27-2021'
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ image: supabase-postgres-cron.png
|
||||
thumb: supabase-postgres-cron.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '03-05-2021'
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ image: postgres-hooks/og-postgres-hooks.jpg
|
||||
thumb: postgres-hooks/cover-postgres-hooks.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2021-07-02'
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ image: supa-audit/postgres_auditing_in_150_lines_of_SQL.png
|
||||
thumb: supa-audit/postgres_auditing_in_150_lines_of_SQL.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-03-08'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -7,6 +7,7 @@ thumb: partial-dumps/og-partial-dumps-with-rls.png
|
||||
tags:
|
||||
- postgresql
|
||||
- data
|
||||
- planetpg
|
||||
date: '2022-06-28'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: seen-by/seen-by-postgresql-thumb.png
|
||||
thumb: seen-by/seen-by-postgresql-thumb.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-07-18'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: primary-keys/postgres-primary-key.jpg
|
||||
thumb: primary-keys/postgres-primary-key.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-09-08'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: postgres-wasm/postgres-wasm.jpg
|
||||
thumb: postgres-wasm/postgres-wasm.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-10-03'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: postgres-fts-vs-the-rest/postgres-full-text-search.jpg
|
||||
thumb: postgres-fts-vs-the-rest/postgres-full-text-search.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-10-14'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: what-is-postgres-commitfest/what-is-postgres-commitfest.jpg
|
||||
thumb: what-is-postgres-commitfest/what-is-postgres-commitfest.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-10-27'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: 2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg
|
||||
thumb: 2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-11-24'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: transparent-column-encryption-with-postgres.jpg
|
||||
thumb: transparent-column-encryption-with-postgres.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-12-01'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -7,6 +7,7 @@ thumb: lw6-community/pg15.png
|
||||
tags:
|
||||
- postgres
|
||||
- launch-week
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ tags:
|
||||
- graphql
|
||||
- postgres
|
||||
- launch-week
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ tags:
|
||||
- launch-week
|
||||
- postgres
|
||||
- backups
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -7,6 +7,7 @@ thumb: lw6-community/postgrest.png
|
||||
tags:
|
||||
- postgres
|
||||
- launch-week
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ tags:
|
||||
- launch-week
|
||||
- postgres
|
||||
- encryption
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
+116
-53
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -19,11 +19,17 @@ export async function getStaticProps() {
|
||||
const allPostsData = getSortedPosts('_blog', undefined, undefined, '** BLOG PAGE **')
|
||||
const categories = getAllCategories('_blog')
|
||||
const rss = generateRss(allPostsData)
|
||||
// @ts-ignore
|
||||
const planetPgPosts = allPostsData.filter((post) => post.tags.includes('planetpg'))
|
||||
const planetPgrss = generateRss(planetPgPosts)
|
||||
|
||||
// create a rss feed in public directory
|
||||
// rss feed is added via <Head> component in render return
|
||||
fs.writeFileSync('./public/rss.xml', rss)
|
||||
|
||||
// create separate feed for just posts promoted in PlanetPG
|
||||
fs.writeFileSync('./public/planetpg-rss.xml', planetPgrss)
|
||||
|
||||
return {
|
||||
props: {
|
||||
blogs: allPostsData,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
Pricing Changelog
|
||||
|
||||
### 09 Jan 2023
|
||||
----
|
||||
|
||||
Database Compute Add ons
|
||||
- Clarified iops values to the latest instance types
|
||||
|
||||
### 02 Nov 2022
|
||||
----
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+16
@@ -11040,6 +11040,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.6.61",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.6.61.tgz",
|
||||
"integrity": "sha512-3zBN4zSTXUTz0vFzSnVV76a1tNVBj5cH6rSRDGpYzY95P6JQkepN0puXgtUqLHMTsnfGbwnpG1yVO93RH3xQ1g==",
|
||||
"engines": {
|
||||
"deno": ">=1.0.0",
|
||||
"node": ">=16.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
@@ -37585,6 +37594,7 @@
|
||||
"@supabase/supabase-js": "^2.1.1",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-query-devtools": "^4.16.1",
|
||||
"@zip.js/zip.js": "^2.6.61",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
@@ -45664,6 +45674,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
|
||||
},
|
||||
"@zip.js/zip.js": {
|
||||
"version": "2.6.61",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.6.61.tgz",
|
||||
"integrity": "sha512-3zBN4zSTXUTz0vFzSnVV76a1tNVBj5cH6rSRDGpYzY95P6JQkepN0puXgtUqLHMTsnfGbwnpG1yVO93RH3xQ1g=="
|
||||
},
|
||||
"abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
@@ -63391,6 +63406,7 @@
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"@zip.js/zip.js": "^2.6.61",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"autoprefixer": "10.4.2",
|
||||
|
||||
@@ -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.
|
||||
@@ -12,9 +12,9 @@ interface ReportsLayoutProps {
|
||||
|
||||
const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps>) => {
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const project = ui.selectedProject
|
||||
const page = router.pathname.split('/')[4] || ''
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,7 @@ import { WizardLayoutWithoutAuth } from './WizardLayout'
|
||||
import VercelIntegrationLayout from './VercelIntegrationLayout'
|
||||
import BillingLayout from './BillingLayout'
|
||||
import LogsLayout from './LogsLayout/LogsLayout'
|
||||
import ReportsLayout from './ReportsLayout/ReportsLayout'
|
||||
import OrganizationLayout from './OrganizationLayout'
|
||||
|
||||
export {
|
||||
@@ -35,6 +36,7 @@ export {
|
||||
VercelIntegrationLayout,
|
||||
BillingLayout,
|
||||
LogsLayout,
|
||||
ReportsLayout,
|
||||
OrganizationLayout,
|
||||
}
|
||||
|
||||
|
||||
+78
-40
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { FC, useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { compact, debounce, isEqual } from 'lodash'
|
||||
import {
|
||||
Button,
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
IconList,
|
||||
} from 'ui'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants.ts'
|
||||
import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants'
|
||||
|
||||
const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }) => {
|
||||
const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }: any) => {
|
||||
return (
|
||||
<div
|
||||
className={`group ${!loading ? 'cursor-pointer' : ''}`}
|
||||
@@ -46,11 +46,11 @@ const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }) => {
|
||||
const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }: any) => {
|
||||
// Max 5 crumbs, otherwise replace middle segment with ellipsis and only
|
||||
// have the first 2 and last 2 crumbs visible
|
||||
const ellipsis = '...'
|
||||
const breadcrumbsWithIndexes = breadcrumbs.map((name, index) => {
|
||||
const breadcrumbsWithIndexes = breadcrumbs.map((name: string, index: number) => {
|
||||
return { name, index }
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@ const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-3 flex items-center">
|
||||
{formattedBreadcrumbs.map((crumb, idx) => (
|
||||
{formattedBreadcrumbs.map((crumb: any, idx: number) => (
|
||||
<div className="flex items-center" key={crumb.name}>
|
||||
{idx !== 0 && <IconChevronRight size={10} strokeWidth={2} className="mx-3" />}
|
||||
<p
|
||||
@@ -91,37 +91,52 @@ const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const FileExplorerHeader = ({
|
||||
view = STORAGE_VIEWS.COLUMNS,
|
||||
sortBy = STORAGE_SORT_BY.NAME,
|
||||
sortByOrder = STORAGE_SORT_BY_ORDER.ASC,
|
||||
loading = {},
|
||||
breadcrumbs = [],
|
||||
backDisabled = false,
|
||||
isSearching = false,
|
||||
interface Props {
|
||||
isSearching: boolean
|
||||
itemSearchString: string
|
||||
setIsSearching: (value: boolean) => void
|
||||
setItemSearchString: (value: string) => void
|
||||
onFilesUpload: (event: any, columnIndex: number) => void
|
||||
}
|
||||
|
||||
const FileExplorerHeader: FC<Props> = ({
|
||||
itemSearchString = '',
|
||||
setItemSearchString = () => {},
|
||||
onSetPathByString = () => {},
|
||||
onChangeView = () => {},
|
||||
onChangeSortBy = () => {},
|
||||
onChangeSortByOrder = () => {},
|
||||
onToggleSearch = () => {},
|
||||
onFilesUpload = () => {},
|
||||
onSelectBack = () => {},
|
||||
onSelectCreateFolder = () => {},
|
||||
onSelectBreadcrumb = () => {},
|
||||
}) => {
|
||||
const debounceDuration = 300
|
||||
const [isEditingPath, setIsEditingPath] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [pathString, setPathString] = useState('')
|
||||
const [searchString, setSearchString] = useState('')
|
||||
const [loading, setLoading] = useState({ isLoading: false, message: '' })
|
||||
|
||||
const uploadButtonRef = useRef(null)
|
||||
const previousBreadcrumbs = useRef(null)
|
||||
const [isEditingPath, setIsEditingPath] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
|
||||
const uploadButtonRef: any = useRef(null)
|
||||
const previousBreadcrumbs: any = useRef(null)
|
||||
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { refetchAllOpenedFolders } = storageExplorerStore
|
||||
const {
|
||||
view,
|
||||
setView,
|
||||
columns,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
sortByOrder,
|
||||
setSortByOrder,
|
||||
popColumn,
|
||||
popColumnAtIndex,
|
||||
popOpenedFolders,
|
||||
fetchFoldersByPath,
|
||||
refetchAllOpenedFolders,
|
||||
addNewFolderPlaceholder,
|
||||
clearOpenedFolders,
|
||||
closeFilePreview,
|
||||
} = storageExplorerStore
|
||||
|
||||
const breadcrumbs = columns.map((column: any) => column.name)
|
||||
const backDisabled = columns.length <= 1
|
||||
|
||||
useEffect(() => {
|
||||
if (itemSearchString) setSearchString(itemSearchString)
|
||||
@@ -137,11 +152,18 @@ const FileExplorerHeader = ({
|
||||
}, [breadcrumbs])
|
||||
|
||||
const searchInputHandler = useCallback(debounce(setItemSearchString, debounceDuration), [])
|
||||
const onSearchInputUpdate = (event) => {
|
||||
const onSearchInputUpdate = (event: any) => {
|
||||
setSearchString(event.target.value)
|
||||
// @ts-ignore
|
||||
searchInputHandler(event.target.value)
|
||||
}
|
||||
|
||||
const onSelectBack = () => {
|
||||
popColumn()
|
||||
popOpenedFolders()
|
||||
closeFilePreview()
|
||||
}
|
||||
|
||||
const onSelectUpload = () => {
|
||||
if (uploadButtonRef.current) {
|
||||
uploadButtonRef.current.click()
|
||||
@@ -155,11 +177,11 @@ const FileExplorerHeader = ({
|
||||
if (isSearching) onCancelSearch()
|
||||
}
|
||||
|
||||
const onUpdatePathString = (event) => {
|
||||
const onUpdatePathString = (event: any) => {
|
||||
setPathString(event.target.value)
|
||||
}
|
||||
|
||||
const navigateByPathString = (event) => {
|
||||
const navigateByPathString = (event: any) => {
|
||||
if (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -168,6 +190,19 @@ const FileExplorerHeader = ({
|
||||
onSetPathByString(compact(pathString.split('/')))
|
||||
}
|
||||
|
||||
const onSetPathByString = async (paths: any[]) => {
|
||||
if (paths.length === 0) {
|
||||
popColumnAtIndex(0)
|
||||
clearOpenedFolders()
|
||||
closeFilePreview()
|
||||
} else {
|
||||
const pathString = paths.join('/')
|
||||
setLoading({ isLoading: true, message: `Navigating to ${pathString}...` })
|
||||
await fetchFoldersByPath(paths)
|
||||
setLoading({ isLoading: false, message: '' })
|
||||
}
|
||||
}
|
||||
|
||||
const cancelSetPathString = () => {
|
||||
setIsEditingPath(false)
|
||||
}
|
||||
@@ -177,17 +212,19 @@ const FileExplorerHeader = ({
|
||||
// Searching for column view requires much more thinking
|
||||
const toggleSearch = () => {
|
||||
setIsEditingPath(false)
|
||||
onToggleSearch(true)
|
||||
setIsSearching(true)
|
||||
}
|
||||
|
||||
const onCancelSearch = () => {
|
||||
setSearchString('')
|
||||
onToggleSearch(false)
|
||||
setIsSearching(false)
|
||||
setItemSearchString('')
|
||||
}
|
||||
|
||||
/** Methods for breadcrumbs */
|
||||
|
||||
const selectBreadcrumb = (columnIndex) => {
|
||||
onSelectBreadcrumb(columnIndex)
|
||||
const selectBreadcrumb = (columnIndex: number) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
@@ -278,7 +315,7 @@ const FileExplorerHeader = ({
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlay={[
|
||||
<Dropdown.RadioGroup key="viewOptions" value={view} onChange={onChangeView}>
|
||||
<Dropdown.RadioGroup key="viewOptions" value={view} onChange={setView}>
|
||||
<Dropdown.Radio value={STORAGE_VIEWS.COLUMNS}>Columns</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_VIEWS.LIST}>List</Dropdown.Radio>
|
||||
</Dropdown.RadioGroup>,
|
||||
@@ -295,14 +332,14 @@ const FileExplorerHeader = ({
|
||||
}
|
||||
type="text"
|
||||
disabled={breadcrumbs.length === 0}
|
||||
onChange={onChangeView}
|
||||
onChange={setView}
|
||||
>
|
||||
View as
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
overlay={[
|
||||
<Dropdown.RadioGroup key="sortOptions" value={sortBy} onChange={onChangeSortBy}>
|
||||
<Dropdown.RadioGroup key="sortOptions" value={sortBy} onChange={setSortBy}>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY.NAME}>Name</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY.CREATED_AT}>Time created</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY.UPDATED_AT}>Time modified</Dropdown.Radio>
|
||||
@@ -326,7 +363,7 @@ const FileExplorerHeader = ({
|
||||
<Dropdown.RadioGroup
|
||||
key="sortOrderOptions"
|
||||
value={sortByOrder}
|
||||
onChange={onChangeSortByOrder}
|
||||
onChange={setSortByOrder}
|
||||
>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY_ORDER.ASC}>Ascending</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY_ORDER.DESC}>Descending</Dropdown.Radio>
|
||||
@@ -352,6 +389,7 @@ const FileExplorerHeader = ({
|
||||
<div className="h-6 border-r border-panel-border-light dark:border-panel-border-dark" />
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="hidden">
|
||||
{/* @ts-ignore */}
|
||||
<input ref={uploadButtonRef} type="file" multiple onChange={onFilesUpload} />
|
||||
</div>
|
||||
<Button
|
||||
@@ -366,7 +404,7 @@ const FileExplorerHeader = ({
|
||||
icon={<IconFolderPlus size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
disabled={breadcrumbs.length === 0}
|
||||
onClick={() => onSelectCreateFolder()}
|
||||
onClick={() => addNewFolderPlaceholder(-1)}
|
||||
>
|
||||
Create folder
|
||||
</Button>
|
||||
@@ -381,7 +419,7 @@ const FileExplorerHeader = ({
|
||||
<Input
|
||||
size="tiny"
|
||||
autoFocus
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
icon={<IconSearch size={'tiny'} strokeWidth={2} />}
|
||||
actions={[
|
||||
<IconX
|
||||
+22
-12
@@ -1,31 +1,41 @@
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { Button, IconDownload, IconTrash2, IconMove, IconX } from 'ui'
|
||||
|
||||
const FileExplorerHeaderSelection = ({
|
||||
selectedItems = [],
|
||||
onSelectItemsDownload = () => {},
|
||||
onSelectItemsDelete = () => {},
|
||||
onSelectItemsMove = () => {},
|
||||
onUnselectAllItems = () => {},
|
||||
}) => {
|
||||
const FileExplorerHeaderSelection = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
selectedItems,
|
||||
downloadFile,
|
||||
downloadSelectedFiles,
|
||||
clearSelectedItems,
|
||||
setSelectedItemsToDelete,
|
||||
setSelectedItemsToMove,
|
||||
} = storageExplorerStore
|
||||
|
||||
return (
|
||||
<div className="z-10 flex h-[40px] items-center rounded-t-md bg-brand-700 px-2 py-1 shadow dark:bg-brand-600">
|
||||
<Button
|
||||
icon={<IconX size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
shadow={false}
|
||||
onClick={onUnselectAllItems}
|
||||
onClick={() => clearSelectedItems()}
|
||||
/>
|
||||
<div className="ml-4 flex items-center space-x-3">
|
||||
<p className="mb-0 text-sm text-scale-1200">
|
||||
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{selectedItems.length}</span> items
|
||||
selected
|
||||
</p>
|
||||
{/* [Joshen] Disabled multi download for now */}
|
||||
<Button
|
||||
icon={<IconDownload size={16} strokeWidth={2} />}
|
||||
type="primary"
|
||||
shadow={false}
|
||||
onClick={onSelectItemsDownload}
|
||||
onClick={async () => {
|
||||
if (selectedItems.length === 1) {
|
||||
await downloadFile(selectedItems[0])
|
||||
} else {
|
||||
await downloadSelectedFiles(selectedItems)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
@@ -34,7 +44,7 @@ const FileExplorerHeaderSelection = ({
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
type="primary"
|
||||
shadow={false}
|
||||
onClick={onSelectItemsDelete}
|
||||
onClick={() => setSelectedItemsToDelete(selectedItems)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -42,7 +52,7 @@ const FileExplorerHeaderSelection = ({
|
||||
icon={<IconMove size={16} strokeWidth={2} />}
|
||||
type="primary"
|
||||
shadow={false}
|
||||
onClick={onSelectItemsMove}
|
||||
onClick={() => setSelectedItemsToMove(selectedItems)}
|
||||
>
|
||||
Move
|
||||
</Button>
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
CONTEXT_MENU_KEYS,
|
||||
} from '../Storage.constants.ts'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const RowIcon = ({ view, status, fileType, mimeType }) => {
|
||||
if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) {
|
||||
@@ -85,6 +86,9 @@ const FileExplorerRow = ({
|
||||
onSelectItemRename = () => {},
|
||||
onSelectItemMove = () => {},
|
||||
}) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { downloadFolder } = storageExplorerStore
|
||||
|
||||
const itemWithColumnIndex = { ...item, columnIndex }
|
||||
const isSelected = find(selectedItems, item) !== undefined
|
||||
const isOpened =
|
||||
@@ -146,8 +150,22 @@ const FileExplorerRow = ({
|
||||
? [{ name: 'Delete', onClick: () => onSelectItemDelete(itemWithColumnIndex) }]
|
||||
: item.type === STORAGE_ROW_TYPES.FOLDER
|
||||
? [
|
||||
{ name: 'Rename', onClick: () => onSelectItemRename(itemWithColumnIndex) },
|
||||
{ name: 'Delete', onClick: () => onSelectItemDelete(itemWithColumnIndex) },
|
||||
{
|
||||
name: 'Rename',
|
||||
icon: <IconEdit size="tiny" />,
|
||||
onClick: () => onSelectItemRename(itemWithColumnIndex),
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: () => downloadFolder(itemWithColumnIndex),
|
||||
},
|
||||
{ name: 'Separator' },
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash size="tiny" />,
|
||||
onClick: () => onSelectItemDelete(itemWithColumnIndex),
|
||||
},
|
||||
]
|
||||
: [
|
||||
...(!item.isCorrupted
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { Menu, Item } from 'react-contexify'
|
||||
import 'react-contexify/dist/ReactContexify.css'
|
||||
|
||||
const FolderContextMenu = ({ id = '', onRenameFolder = () => {}, onDeleteFolder = () => {} }) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { downloadFolder } = storageExplorerStore
|
||||
|
||||
return (
|
||||
<Menu id={id} animation="fade">
|
||||
<Item onClick={({ props }) => onRenameFolder(props.item)}>Rename</Item>
|
||||
<Item onClick={({ props }) => downloadFolder(props.item)}>Download</Item>
|
||||
<Item onClick={({ props }) => onDeleteFolder(props.item)}>Delete</Item>
|
||||
</Menu>
|
||||
)
|
||||
|
||||
+17
-15
@@ -1,3 +1,4 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
import {
|
||||
Button,
|
||||
IconX,
|
||||
@@ -11,13 +12,13 @@ import SVG from 'react-inlinesvg'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import Link from 'next/link'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
const PreviewFile = ({ mimeType, previewUrl }: { mimeType: string; previewUrl: string }) => {
|
||||
if (!mimeType || !previewUrl) {
|
||||
return (
|
||||
<SVG
|
||||
src={'/img/file-filled.svg'}
|
||||
alt={'No preview'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
|
||||
}
|
||||
@@ -36,7 +37,6 @@ const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<SVG
|
||||
src={'/img/file-filled.svg'}
|
||||
alt={'No preview'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
|
||||
}
|
||||
@@ -80,7 +80,6 @@ const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
return (
|
||||
<SVG
|
||||
src={'/img/file-filled.svg'}
|
||||
alt={'No preview'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
|
||||
}
|
||||
@@ -88,14 +87,17 @@ const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const PreviewPane = ({
|
||||
isOpen = false,
|
||||
file = {},
|
||||
width = 400,
|
||||
onCopyFileURL = () => {},
|
||||
onSelectFileDelete = () => {},
|
||||
onClosePreviewPane = () => {},
|
||||
}) => {
|
||||
const PreviewPane = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
selectedFilePreview: file,
|
||||
copyFileURLToClipboard,
|
||||
closeFilePreview,
|
||||
setSelectedItemsToDelete,
|
||||
} = storageExplorerStore
|
||||
|
||||
const width = 450
|
||||
const isOpen = !isEmpty(file)
|
||||
const size = file.metadata ? formatBytes(file.metadata.size) : null
|
||||
const mimeType = file.metadata ? file.metadata.mimetype : null
|
||||
const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
|
||||
@@ -123,7 +125,7 @@ const PreviewPane = ({
|
||||
className="cursor-pointer"
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
onClick={onClosePreviewPane}
|
||||
onClick={() => closeFilePreview()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +184,7 @@ const PreviewPane = ({
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconClipboard size={16} strokeWidth={2} />}
|
||||
onClick={() => onCopyFileURL(file)}
|
||||
onClick={async () => await copyFileURLToClipboard(file)}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Copy URL
|
||||
@@ -193,7 +195,7 @@ const PreviewPane = ({
|
||||
shadow={false}
|
||||
size="tiny"
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
onClick={() => onSelectFileDelete(file)}
|
||||
onClick={() => setSelectedItemsToDelete([file])}
|
||||
>
|
||||
Delete file
|
||||
</Button>
|
||||
@@ -27,10 +27,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
clearSelectedItemsToDelete,
|
||||
openedFolders,
|
||||
pushOpenedFolderAtIndex,
|
||||
clearOpenedFolders,
|
||||
popColumn,
|
||||
popColumnAtIndex,
|
||||
popOpenedFolders,
|
||||
popOpenedFoldersAtIndex,
|
||||
setSelectedItemToRename,
|
||||
selectedItemsToMove,
|
||||
@@ -38,9 +35,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
clearSelectedItemsToMove,
|
||||
view,
|
||||
setView,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
sortByOrder,
|
||||
setSortByOrder,
|
||||
currentBucketName,
|
||||
copyFileURLToClipboard,
|
||||
@@ -51,27 +46,22 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
addNewFolder,
|
||||
fetchFolderContents,
|
||||
fetchMoreFolderContents,
|
||||
fetchFoldersByPath,
|
||||
renameFolder,
|
||||
deleteFolder,
|
||||
uploadFiles,
|
||||
deleteFiles,
|
||||
downloadFile,
|
||||
downloadSelectedFiles,
|
||||
renameFile,
|
||||
moveFiles,
|
||||
} = storageExplorerStore
|
||||
|
||||
const storageExplorerRef = useRef(null)
|
||||
const [loading, setLoading] = useState({ isLoading: false, message: '' })
|
||||
|
||||
// This state exists outside of the header because FileExplorerColumn needs to listen to these as well
|
||||
// I'm keeping them outside of the mobx store as I feel that the store should contain persistent data
|
||||
// Things like showing results from a search filter is "temporary", hence we use react state to manage
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [itemSearchString, setItemSearchString] = useState('')
|
||||
|
||||
const previewPaneWidth = 450
|
||||
// Requires a fixed height to ensure that explorer is constrained to the viewport
|
||||
const fileExplorerHeight = window.innerHeight - 122
|
||||
|
||||
@@ -121,24 +111,12 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
|
||||
const onSelectFolder = async (columnIndex, folder) => {
|
||||
closeFilePreview()
|
||||
clearSelectedItems(columnIndex + 1)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
pushOpenedFolderAtIndex(folder, columnIndex)
|
||||
await fetchFolderContents(folder.id, folder.name, columnIndex)
|
||||
}
|
||||
|
||||
const onSetPathByString = async (paths) => {
|
||||
if (paths.length === 0) {
|
||||
popColumnAtIndex(0)
|
||||
clearOpenedFolders()
|
||||
closeFilePreview()
|
||||
} else {
|
||||
const pathString = paths.join('/')
|
||||
setLoading({ isLoading: true, message: `Navigating to ${pathString}...` })
|
||||
await fetchFoldersByPath(paths)
|
||||
setLoading({ isLoading: false, message: '' })
|
||||
}
|
||||
}
|
||||
|
||||
/** Checkbox selection methods */
|
||||
/** [Joshen] We'll only support checkbox selection for files ONLY */
|
||||
|
||||
@@ -152,9 +130,11 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
}
|
||||
|
||||
const onSelectAllItemsInColumn = (columnIndex) => {
|
||||
const columnFiles = columns[columnIndex].items.filter(
|
||||
(item) => item.type === STORAGE_ROW_TYPES.FILE
|
||||
)
|
||||
const columnFiles = columns[columnIndex].items
|
||||
.filter((item) => item.type === STORAGE_ROW_TYPES.FILE)
|
||||
.map((item) => {
|
||||
return { ...item, columnIndex }
|
||||
})
|
||||
const columnFilesId = compact(columnFiles.map((item) => item.id))
|
||||
const selectedItemsFromColumn = selectedItems.filter((item) => columnFilesId.includes(item.id))
|
||||
|
||||
@@ -183,10 +163,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
renameFolder(folder, newName, columnIndex)
|
||||
}
|
||||
|
||||
const onSelectItemsDelete = () => {
|
||||
setSelectedItemsToDelete(selectedItems)
|
||||
}
|
||||
|
||||
const onSelectItemDelete = (file) => {
|
||||
setSelectedItemsToDelete([file])
|
||||
}
|
||||
@@ -195,18 +171,10 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
setSelectedItemToRename(file)
|
||||
}
|
||||
|
||||
const onSelectItemsMove = () => {
|
||||
setSelectedItemsToMove(selectedItems)
|
||||
}
|
||||
|
||||
const onSelectItemMove = (file) => {
|
||||
setSelectedItemsToMove([file])
|
||||
}
|
||||
|
||||
const onSelectItemsDownload = async () => {
|
||||
await downloadSelectedFiles()
|
||||
}
|
||||
|
||||
const onCopyFileURL = async (file) => {
|
||||
await copyFileURLToClipboard(file)
|
||||
}
|
||||
@@ -250,17 +218,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
}
|
||||
|
||||
/** Misc UI methods */
|
||||
|
||||
const onSelectBack = () => {
|
||||
popColumn()
|
||||
popOpenedFolders()
|
||||
closeFilePreview()
|
||||
}
|
||||
|
||||
const onSelectBreadcrumb = (columnIndex) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
}
|
||||
|
||||
const onSelectColumnEmptySpace = (columnIndex) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
@@ -274,13 +231,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
|
||||
const onChangeSortByOrder = (sortByOrder) => setSortByOrder(sortByOrder)
|
||||
|
||||
const onToggleSearch = (bool) => {
|
||||
setIsSearching(bool)
|
||||
if (bool === false) {
|
||||
setItemSearchString('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={storageExplorerRef}
|
||||
@@ -291,33 +241,12 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
>
|
||||
{selectedItems.length === 0 ? (
|
||||
<FileExplorerHeader
|
||||
view={view}
|
||||
sortBy={sortBy}
|
||||
sortByOrder={sortByOrder}
|
||||
loading={loading}
|
||||
breadcrumbs={columns.map((column) => column.name)}
|
||||
backDisabled={columns.length <= 1}
|
||||
isSearching={isSearching}
|
||||
itemSearchString={itemSearchString}
|
||||
setItemSearchString={setItemSearchString}
|
||||
onChangeView={onChangeView}
|
||||
onChangeSortBy={onChangeSortBy}
|
||||
onChangeSortByOrder={onChangeSortByOrder}
|
||||
onToggleSearch={onToggleSearch}
|
||||
onFilesUpload={onFilesUpload}
|
||||
onSelectBack={onSelectBack}
|
||||
onSelectCreateFolder={onSelectCreateFolder}
|
||||
onSetPathByString={onSetPathByString}
|
||||
onSelectBreadcrumb={onSelectBreadcrumb}
|
||||
/>
|
||||
) : (
|
||||
<FileExplorerHeaderSelection
|
||||
selectedItems={selectedItems}
|
||||
onSelectItemsDownload={onSelectItemsDownload}
|
||||
onSelectItemsDelete={onSelectItemsDelete}
|
||||
onSelectItemsMove={onSelectItemsMove}
|
||||
onUnselectAllItems={clearSelectedItems}
|
||||
/>
|
||||
<FileExplorerHeaderSelection />
|
||||
)}
|
||||
<div className="flex h-full" style={{ height: fileExplorerHeight }}>
|
||||
<FileExplorer
|
||||
@@ -348,14 +277,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
fetchMoreFolderContents(index, column, itemSearchString)
|
||||
}
|
||||
/>
|
||||
<PreviewPane
|
||||
isOpen={!isEmpty(selectedFilePreview)}
|
||||
file={selectedFilePreview}
|
||||
width={previewPaneWidth}
|
||||
onCopyFileURL={onCopyFileURL}
|
||||
onSelectFileDelete={onSelectItemDelete}
|
||||
onClosePreviewPane={closeFilePreview}
|
||||
/>
|
||||
<PreviewPane />
|
||||
</div>
|
||||
<ConfirmDeleteModal
|
||||
visible={selectedItemsToDelete.length > 0}
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { createContext, useContext } from 'react'
|
||||
import { makeAutoObservable } from 'mobx'
|
||||
import {
|
||||
find,
|
||||
compact,
|
||||
isEqual,
|
||||
isNull,
|
||||
isNil,
|
||||
isUndefined,
|
||||
has,
|
||||
some,
|
||||
chunk,
|
||||
get,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import toast from 'react-hot-toast'
|
||||
import { find, compact, isEqual, isNil, has, some, chunk, get, uniq } from 'lodash'
|
||||
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants'
|
||||
import {
|
||||
STORAGE_VIEWS,
|
||||
STORAGE_ROW_TYPES,
|
||||
STORAGE_ROW_STATUS,
|
||||
STORAGE_SORT_BY,
|
||||
} from 'components/to-be-cleaned/Storage/Storage.constants.ts'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants'
|
||||
|
||||
/**
|
||||
* This is a preferred method rather than React Context and useStorageExplorerStore().
|
||||
@@ -50,7 +39,6 @@ const LIMIT = 200
|
||||
const OFFSET = 0
|
||||
const DEFAULT_EXPIRY = 10 * 365 * 24 * 60 * 60 // in seconds, default to 1 year
|
||||
const PREVIEW_SIZE_LIMIT = 10000000 // 10MB
|
||||
// const BATCH_SIZE = 10
|
||||
const BATCH_SIZE = 2
|
||||
const EMPTY_FOLDER_PLACEHOLDER_FILE_NAME = '.emptyFolderPlaceholder'
|
||||
const STORAGE_PROGRESS_INFO_TEXT = "Please do not close the browser until it's completed"
|
||||
@@ -288,8 +276,12 @@ class StorageExplorerStore {
|
||||
this.selectedItems = items
|
||||
}
|
||||
|
||||
clearSelectedItems = () => {
|
||||
this.selectedItems = []
|
||||
clearSelectedItems = (columnIndex) => {
|
||||
if (columnIndex !== undefined) {
|
||||
this.selectedItems = this.selectedItems.filter((item) => item.columnIndex !== columnIndex)
|
||||
} else {
|
||||
this.selectedItems = []
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedItemsToDelete = (items) => {
|
||||
@@ -319,10 +311,8 @@ class StorageExplorerStore {
|
||||
addNewFolder = async (folderName, columnIndex) => {
|
||||
const autofix = false
|
||||
const formattedName = this.sanitizeNameForDuplicateInColumn(folderName, autofix, columnIndex)
|
||||
if (formattedName === null) return
|
||||
|
||||
if (isNull(formattedName)) {
|
||||
return
|
||||
}
|
||||
/**
|
||||
* todo: move this to a util file, as renameFolder() uses same logic
|
||||
*/
|
||||
@@ -374,7 +364,7 @@ class StorageExplorerStore {
|
||||
|
||||
const fetchedAt = get(cachedPreview, ['fetchedAt'], null)
|
||||
const expiresIn = get(cachedPreview, ['expiresIn'], null)
|
||||
const existsInCache = !isNull(fetchedAt) && !isNull(expiresIn)
|
||||
const existsInCache = fetchedAt !== null && expiresIn !== null
|
||||
const isExpired = existsInCache ? fetchedAt + expiresIn * 1000 < Date.now() : true
|
||||
|
||||
if (!isExpired) {
|
||||
@@ -556,7 +546,10 @@ class StorageExplorerStore {
|
||||
|
||||
// https://stackoverflow.com/a/53058574
|
||||
getFilesDataTransferItems = async (items) => {
|
||||
const toastId = toast.loading('Retrieving items to upload...')
|
||||
const toastId = this.ui.setNotification({
|
||||
category: 'loading',
|
||||
message: 'Retrieving items to upload...',
|
||||
})
|
||||
const files = []
|
||||
const queue = []
|
||||
for (const item of items) {
|
||||
@@ -566,7 +559,7 @@ class StorageExplorerStore {
|
||||
const entry = queue.shift() || {}
|
||||
if (entry.isFile) {
|
||||
const file = await this.getFile(entry)
|
||||
if (!isUndefined(file)) {
|
||||
if (file !== undefined) {
|
||||
file.path = entry.fullPath.slice(1)
|
||||
files.push(file)
|
||||
}
|
||||
@@ -637,9 +630,9 @@ class StorageExplorerStore {
|
||||
return file
|
||||
})
|
||||
|
||||
const uploadedTopLevelFolders = []
|
||||
this.uploadProgress = 0
|
||||
let numberOfFilesToUpload = formattedFilesToUpload.length
|
||||
const uploadedTopLevelFolders = []
|
||||
const numberOfFilesToUpload = formattedFilesToUpload.length
|
||||
let numberOfFilesUploadedSuccess = 0
|
||||
let numberOfFilesUploadedFail = 0
|
||||
|
||||
@@ -746,28 +739,34 @@ class StorageExplorerStore {
|
||||
if (numberOfFilesToUpload === 0) {
|
||||
toast.dismiss(toastId)
|
||||
} else if (numberOfFilesUploadedFail === numberOfFilesToUpload) {
|
||||
toast.error(
|
||||
`Failed to upload ${numberOfFilesToUpload} file${numberOfFilesToUpload > 1 ? 's' : ''}!`,
|
||||
{ id: toastId }
|
||||
)
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'error',
|
||||
message: `Failed to upload ${numberOfFilesToUpload} file${
|
||||
numberOfFilesToUpload > 1 ? 's' : ''
|
||||
}!`,
|
||||
})
|
||||
} else if (numberOfFilesUploadedSuccess === numberOfFilesToUpload) {
|
||||
toast.success(
|
||||
`Successfully uploaded ${numberOfFilesToUpload} file${
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully uploaded ${numberOfFilesToUpload} file${
|
||||
numberOfFilesToUpload > 1 ? 's' : ''
|
||||
}!`,
|
||||
{ id: toastId }
|
||||
)
|
||||
})
|
||||
} else {
|
||||
toast.success(
|
||||
`Successfully uploaded ${numberOfFilesUploadedSuccess} out of ${numberOfFilesToUpload} file${
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully uploaded ${numberOfFilesUploadedSuccess} out of ${numberOfFilesToUpload} file${
|
||||
numberOfFilesToUpload > 1 ? 's' : ''
|
||||
}!`,
|
||||
{ id: toastId }
|
||||
)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
error: e,
|
||||
message: 'Failed to upload files',
|
||||
category: 'error',
|
||||
})
|
||||
@@ -943,33 +942,153 @@ class StorageExplorerStore {
|
||||
}
|
||||
}
|
||||
|
||||
downloadSelectedFiles = async () => {
|
||||
const showIndividualToast = false
|
||||
const toastId = toast.loading(`Retrieving ${this.selectedItems.length} files...`, {
|
||||
autoClose: false,
|
||||
hideProgressBar: true,
|
||||
downloadFolder = async (folder) => {
|
||||
let progress = 0
|
||||
const toastId = this.ui.setNotification({
|
||||
category: 'loading',
|
||||
message: 'Retrieving files from folder...',
|
||||
})
|
||||
|
||||
const res = await Promise.all(
|
||||
this.selectedItems.map(async (item) => await this.downloadFile(item, showIndividualToast))
|
||||
)
|
||||
|
||||
const numberOfSuccessfullyDownloadedFiles = res.filter((x) => x === true).length
|
||||
toast.success(`Downloaded ${numberOfSuccessfullyDownloadedFiles} files`, {
|
||||
const files = await this.getAllItemsAlongFolder(folder)
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} files...`,
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
progress: 0,
|
||||
})
|
||||
|
||||
const promises = files.map((file) => {
|
||||
const fileMimeType = file.metadata?.mimetype ?? null
|
||||
return () => {
|
||||
return new Promise(async (resolve) => {
|
||||
const { data, error } = await this.supabaseClient.storage
|
||||
.from(this.selectedBucket.name)
|
||||
.download(`${file.prefix}/${file.name}`)
|
||||
|
||||
progress = progress + 1 / files.length
|
||||
|
||||
if (!error) {
|
||||
resolve({
|
||||
name: file.name,
|
||||
prefix: file.prefix,
|
||||
blob: new Blob([data], { type: fileMimeType }),
|
||||
})
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const batchedPromises = chunk(promises, 10)
|
||||
const downloadedFiles = await batchedPromises.reduce(async (previousPromise, nextBatch) => {
|
||||
const previousResults = await previousPromise
|
||||
const batchResults = await Promise.allSettled(nextBatch.map((batch) => batch()))
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} file${files.length > 1 ? 's' : ''}...`,
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
progress: progress * 100,
|
||||
})
|
||||
return (previousResults ?? []).concat(batchResults.map((x) => x.value).filter(Boolean))
|
||||
}, Promise.resolve())
|
||||
|
||||
const zipFileWriter = new BlobWriter('application/zip')
|
||||
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true })
|
||||
downloadedFiles.forEach((file) => {
|
||||
zipWriter.add(`${file.prefix}/${file.name}`, new BlobReader(file.blob))
|
||||
})
|
||||
|
||||
const blobURL = URL.createObjectURL(await zipWriter.close())
|
||||
const link = document.createElement('a')
|
||||
link.href = blobURL
|
||||
link.setAttribute('download', `${folder.name}.zip`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.parentNode.removeChild(link)
|
||||
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully downloaded folder "${folder.name}"`,
|
||||
})
|
||||
}
|
||||
|
||||
downloadFile = async (file, showToast = true) => {
|
||||
let toastId
|
||||
downloadSelectedFiles = async (files) => {
|
||||
const lowestColumnIndex = Math.min(...files.map((file) => file.columnIndex))
|
||||
|
||||
const formattedFilesWithPrefix = files.map((file) => {
|
||||
const { name, columnIndex } = file
|
||||
const pathToFile = this.openedFolders
|
||||
.slice(lowestColumnIndex, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${name}` : name
|
||||
return { ...file, formattedPathToFile }
|
||||
})
|
||||
|
||||
let progress = 0
|
||||
const returnBlob = true
|
||||
const showIndividualToast = false
|
||||
const toastId = this.ui.setNotification({
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} files...`,
|
||||
})
|
||||
|
||||
const promises = formattedFilesWithPrefix.map((file) => {
|
||||
return () => {
|
||||
return new Promise(async (resolve) => {
|
||||
const data = await this.downloadFile(file, showIndividualToast, returnBlob)
|
||||
progress = progress + 1 / formattedFilesWithPrefix.length
|
||||
resolve({ ...data, name: file.formattedPathToFile })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const batchedPromises = chunk(promises, 10)
|
||||
const downloadedFiles = await batchedPromises.reduce(async (previousPromise, nextBatch) => {
|
||||
const previousResults = await previousPromise
|
||||
const batchResults = await Promise.allSettled(nextBatch.map((batch) => batch()))
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} file${files.length > 1 ? 's' : ''}...`,
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
progress: progress * 100,
|
||||
})
|
||||
return (previousResults ?? []).concat(batchResults.map((x) => x.value).filter(Boolean))
|
||||
}, Promise.resolve())
|
||||
|
||||
const zipFileWriter = new BlobWriter('application/zip')
|
||||
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true })
|
||||
downloadedFiles.forEach((file) => {
|
||||
zipWriter.add(file.name, new BlobReader(file.blob))
|
||||
})
|
||||
|
||||
const blobURL = URL.createObjectURL(await zipWriter.close())
|
||||
const link = document.createElement('a')
|
||||
link.href = blobURL
|
||||
link.setAttribute('download', `supabase-files.zip`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.parentNode.removeChild(link)
|
||||
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully downloaded ${downloadedFiles.length} files`,
|
||||
})
|
||||
}
|
||||
|
||||
downloadFile = async (file, showToast = true, returnBlob = false) => {
|
||||
const fileName = file.name
|
||||
const fileMimeType = get(file, ['metadata', 'mimetype'], null)
|
||||
|
||||
if (showToast) {
|
||||
toastId = toast.loading(`Retrieving ${fileName}...`, {
|
||||
autoClose: false,
|
||||
})
|
||||
}
|
||||
const toastId = showToast
|
||||
? this.ui.setNotification({ category: 'loading', message: `Retrieving ${fileName}...` })
|
||||
: undefined
|
||||
|
||||
const pathToFile = this.openedFolders
|
||||
.slice(0, file.columnIndex)
|
||||
@@ -984,8 +1103,10 @@ class StorageExplorerStore {
|
||||
if (!error) {
|
||||
const blob = data
|
||||
const newBlob = new Blob([blob], { type: fileMimeType })
|
||||
const blobUrl = window.URL.createObjectURL(newBlob)
|
||||
|
||||
if (returnBlob) return { name: fileName, blob: newBlob }
|
||||
|
||||
const blobUrl = window.URL.createObjectURL(newBlob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.setAttribute('download', `${fileName}`)
|
||||
@@ -994,16 +1115,20 @@ class StorageExplorerStore {
|
||||
link.parentNode.removeChild(link)
|
||||
window.URL.revokeObjectURL(blob)
|
||||
if (toastId) {
|
||||
toast.success(`Downloading ${fileName}`, {
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Downloading ${fileName}`,
|
||||
})
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
console.error('Failed to download:', fileName)
|
||||
if (toastId) {
|
||||
toast.error(`Failed to download ${fileName}`, {
|
||||
this.ui.setNotification({
|
||||
error,
|
||||
id: toastId,
|
||||
category: 'error',
|
||||
message: `Failed to download ${fileName}`,
|
||||
})
|
||||
}
|
||||
return false
|
||||
@@ -1333,7 +1458,7 @@ class StorageExplorerStore {
|
||||
let formattedPathToFolder = ''
|
||||
const { name, columnIndex, prefix } = folder
|
||||
|
||||
if (isUndefined(prefix)) {
|
||||
if (prefix === undefined) {
|
||||
const pathToFolder = this.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
@@ -1361,8 +1486,8 @@ class StorageExplorerStore {
|
||||
}
|
||||
}
|
||||
|
||||
const subfolders = folderContents?.filter((item) => isNull(item.id)) ?? []
|
||||
const folderItems = folderContents?.filter((item) => !isNull(item.id)) ?? []
|
||||
const subfolders = folderContents?.filter((item) => item.id === null) ?? []
|
||||
const folderItems = folderContents?.filter((item) => item.id !== null) ?? []
|
||||
|
||||
folderItems.forEach((item) => items.push({ ...item, prefix: formattedPathToFolder }))
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@supabase/supabase-js": "^2.1.1",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-query-devtools": "^4.16.1",
|
||||
"@zip.js/zip.js": "^2.6.61",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import dayjs from 'dayjs'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -30,6 +29,8 @@ import LogsExplorerHeader from 'components/ui/Logs/LogsExplorerHeader'
|
||||
|
||||
export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
|
||||
const { ref, q, ite, its } = router.query
|
||||
const [editorId, setEditorId] = useState<string>(uuidv4())
|
||||
const [editorValue, setEditorValue] = useState<string>('')
|
||||
@@ -221,12 +222,19 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
if (error) throw error
|
||||
setSubmitting(false)
|
||||
setSaveModalOpen(false)
|
||||
toast.success(`Saved "${values.name}" log query`)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Saved "${values.name}" log query`,
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
setSubmitting(false)
|
||||
setSaveModalOpen(false)
|
||||
toast.error(error.message)
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to save query: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Presets } from 'components/interfaces/Reports/Reports.types'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ReportsLayout from 'components/layouts'
|
||||
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import { hooksFactory, usePresetReport } from 'components/interfaces/Reports/Reports.utils'
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
renderRequestsPathsTable,
|
||||
renderStatusCodesChart,
|
||||
renderBotScores,
|
||||
renderUserAgents
|
||||
renderUserAgents,
|
||||
} from 'components/interfaces/Reports/renderers/ApiRenderers'
|
||||
|
||||
export const ApiReport: NextPageWithLayout = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Presets } from 'components/interfaces/Reports/Reports.types'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ReportsLayout from 'components/layouts'
|
||||
import { hooksFactory, usePresetReport } from 'components/interfaces/Reports/Reports.utils'
|
||||
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
|
||||
Reference in new issue
Block a user