mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 23:29:08 +03:00
46 files changed
+9348
-7353
No files matched your search
@@ -213,6 +213,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
- [Turkish / Türkçe](/i18n/README.tr.md)
|
||||
- [Ukrainian / Українська](/i18n/README.uk.md)
|
||||
- [Vietnamese / Tiếng Việt](/i18n/README.vi-vn.md)
|
||||
- [Bangla / বাংলা](/i18n/README.bn.md)
|
||||
- [List of translations](/i18n/languages.md) <!--- Keep only this -->
|
||||
|
||||
---
|
||||
|
||||
@@ -69,7 +69,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
label: 'Careers',
|
||||
to: '/careers',
|
||||
to: 'https://boards.greenhouse.io/supabase',
|
||||
activeBasePath: '/careers',
|
||||
position: 'left',
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function Home() {
|
||||
'button hero--button button--md button--primary responsive-button',
|
||||
styles.button
|
||||
)}
|
||||
to={'/careers'}
|
||||
to={'https://boards.greenhouse.io/supabase'}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
Join the team →
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
image: supabase/gotrue:v2.2.12
|
||||
image: supabase/gotrue:v2.5.8
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<p align="center">
|
||||
<img width="300" src="https://raw.githubusercontent.com/supabase/supabase/master/web/static/supabase-light-rounded-corner-background.svg"/>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
# Supabase
|
||||
|
||||
[Supabase](https://supabase.com) একটি ওপেন সোর্স ফায়ারবেস বিকল্প। আমরা এন্টারপ্রাইজ-গ্রেড ওপেন সোর্স টুল ব্যবহার করে Firebase-এর বৈশিষ্ট্য তৈরি করছি।
|
||||
|
||||
- [x] হোস্ট করা পোস্টগ্রেস ডাটাবেস
|
||||
- [x] রিয়েলটাইম সদস্যতা
|
||||
- [x] প্রমাণীকরণ এবং অনুমোদন
|
||||
- [x] স্বয়ংক্রিয়ভাবে তৈরি APIs
|
||||
- [x] ড্যাশবোর্ড
|
||||
- [x] স্টোরেজ
|
||||
- [ ] ফাংশন (শীঘ্রই আসছে)
|
||||
|
||||
## ডকুমেন্টেশন
|
||||
|
||||
সম্পূর্ণ ডকুমেন্টেশনের জন্য, দেখুন [supabase.io/docs](https://supabase.com/docs)
|
||||
|
||||
কিভাবে অবদান রাখতে হয় তা দেখতে, পরিদর্শন করুন [Getting Started](./DEVELOPERS.md)
|
||||
|
||||
## কমিউনিটি ও সাপোর্ট
|
||||
|
||||
- [কমিউনিটি ফোরাম](https://github.com/supabase/supabase/discussions). এর জন্য সর্বোত্তম: তৈরিতে সহায়তা, ডাটাবেস সেরা অনুশীলন সম্পর্কে আলোচনা।
|
||||
- [গিঠাব ইস্যু](https://github.com/supabase/supabase/issues). এর জন্য সেরা: সুপাবেস ব্যবহার করে আপনি যে বাগ এবং ত্রুটির সম্মুখীন হন।
|
||||
- [ইমেইল সাপোর্ট](https://supabase.com/docs/support#business-support). এর জন্য সেরা: আপনার ডাটাবেস বা অবকাঠামো নিয়ে সমস্যা।
|
||||
- [ডিসকোর্ড](https://discord.supabase.com). এর জন্য সর্বোত্তম: আপনার অ্যাপ্লিকেশনগুলি ভাগ করা এবং সম্প্রদায়ের সাথে হ্যাঙ্গআউট করা৷
|
||||
|
||||
## স্ট্যাটাস
|
||||
|
||||
- [x] আলফা: আমরা গ্রাহকদের একটি বন্ধ সেটের সাথে সুপাবেস পরীক্ষা করছি
|
||||
- [x] পাবলিক আলফা: যে কেউ [app.supabase.io](https://app.supabase.io) এ সাইন আপ করতে পারেন। কিন্তু আমাদের উপর সহজ যান, কয়েক kinks আছে
|
||||
- [x] পাবলিক বিটা: বেশিরভাগ নন-এন্টারপ্রাইজ ব্যবহারের ক্ষেত্রে যথেষ্ট স্থিতিশীল
|
||||
- [ ] পাবলিক: উত্পাদন প্রস্তুত
|
||||
|
||||
আমরা বর্তমানে পাবলিক বিটাতে আছি। বড় আপডেটের বিজ্ঞপ্তি পেতে এই রেপোর "রিলিজ" দেখুন।
|
||||
|
||||
<kbd><img src="https://gitcdn.link/repo/supabase/supabase/master/web/static/watch-repo.gif" alt="এই রেপো দেখুন"/></kbd>
|
||||
|
||||
---
|
||||
|
||||
## কিভাবে এটা কাজ করে
|
||||
|
||||
সুপাবেস হল ওপেন সোর্স টুলের সংমিশ্রণ। আমরা এন্টারপ্রাইজ-গ্রেড, ওপেন সোর্স পণ্য ব্যবহার করে Firebase-এর বৈশিষ্ট্য তৈরি করছি। যদি MIT, Apache 2, বা সমতুল্য ওপেন লাইসেন্স সহ টুল এবং সম্প্রদায়গুলি বিদ্যমান থাকে, আমরা সেই টুলটি ব্যবহার করব এবং সমর্থন করব। যদি টুলটি বিদ্যমান না থাকে, তাহলে আমরা নিজেরাই এটি তৈরি এবং ওপেন সোর্স করি। সুপাবেস ফায়ারবেসের 1-থেকে-1 ম্যাপিং নয়। ওপেন সোর্স টুল ব্যবহার করে ডেভেলপারদের ফায়ারবেসের মতো ডেভেলপার অভিজ্ঞতা দেওয়াই আমাদের লক্ষ্য।
|
||||
|
||||
**স্থাপত্য**
|
||||
|
||||
সুপাবেস হল একটি [হোস্ট করা প্ল্যাটফর্ম](https://app.supabase.io)। আপনি সাইন আপ করতে পারেন এবং কিছু ইনস্টল না করে সুপাবেস ব্যবহার শুরু করতে পারেন।
|
||||
এছাড়াও আপনি [স্ব-হোস্ট](https://supabase.com/docs/guides/self-hosting) এবং [স্থানীয়ভাবে বিকাশ](https://supabase.com/docs/guides/local-development) করতে পারেন।
|
||||
|
||||

|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org/) হল একটি অবজেক্ট-রিলেশনাল ডাটাবেস সিস্টেম যার 30 বছরের বেশি সক্রিয় বিকাশ রয়েছে যা এটিকে নির্ভরযোগ্যতা, বৈশিষ্ট্যের দৃঢ়তা এবং কর্মক্ষমতার জন্য একটি শক্তিশালী খ্যাতি অর্জন করেছে।
|
||||
- [রিয়েলটাইম](https://github.com/supabase/realtime) হল একটি Elixir সার্ভার যা আপনাকে ওয়েবসকেট ব্যবহার করে PostgreSQL সন্নিবেশ, আপডেট এবং মুছে ফেলা শুনতে দেয়। ডাটাবেস পরিবর্তনের জন্য রিয়েলটাইম পোল পোস্টগ্রেসের অন্তর্নির্মিত প্রতিলিপি কার্যকারিতা, পরিবর্তনগুলিকে JSON-এ রূপান্তরিত করে, তারপর অনুমোদিত ক্লায়েন্টদের কাছে ওয়েবসকেটের মাধ্যমে JSON সম্প্রচার করে।
|
||||
- [PostgREST](http://postgrest.org/) একটি ওয়েব সার্ভার যা আপনার PostgreSQL ডাটাবেসকে সরাসরি একটি RESTful API-তে পরিণত করে
|
||||
- [স্টোরেজ](https://github.com/supabase/storage-api) অনুমতিগুলি পরিচালনা করতে Postgres ব্যবহার করে S3-এ সঞ্চিত ফাইলগুলি পরিচালনা করার জন্য একটি RESTful ইন্টারফেস প্রদান করে।
|
||||
- [postgres-meta](https://github.com/supabase/postgres-meta) হল আপনার পোস্টগ্রেস পরিচালনা করার জন্য একটি RESTful API, যা আপনাকে টেবিল আনতে, ভূমিকা যোগ করতে এবং কোয়েরি চালানোর অনুমতি দেয়।
|
||||
- [GoTrue](https://github.com/netlify/gotrue) ব্যবহারকারীদের পরিচালনা এবং SWT টোকেন ইস্যু করার জন্য একটি SWT ভিত্তিক API।
|
||||
- [কং](https://github.com/Kong/kong) হল একটি ক্লাউড-নেটিভ API গেটওয়ে।
|
||||
|
||||
#### ক্লায়েন্ট লাইব্রেরি
|
||||
|
||||
ক্লায়েন্ট লাইব্রেরির জন্য আমাদের পদ্ধতি মডুলার। প্রতিটি উপ-লাইব্রেরি একটি একক বহিরাগত সিস্টেমের জন্য একটি স্বতন্ত্র বাস্তবায়ন। এটি এমন একটি উপায় যা আমরা বিদ্যমান সরঞ্জামগুলিকে সমর্থন করি৷
|
||||
|
||||
<table style="table-layout:fixed; white-space: nowrap;">৷
|
||||
<tr>
|
||||
<th>ভাষা</th>
|
||||
<th>ক্লায়েন্ট</th>
|
||||
<th colspan="4">ফিচার-ক্লায়েন্ট (সুপাবেস ক্লায়েন্টে বান্ডিল)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Supabase</th>
|
||||
<th><a href="https://github.com/postgrest/postgrest" target="_blank" rel="noopener noreferrer">PostgREST</a></th>
|
||||
<th><a href="https://github.com/supabase/gotrue" target="_blank" rel="noopener noreferrer">GoTrue</a></th>
|
||||
<th><a href="https://github.com/supabase/realtime" target="_blank" rel="noopener noreferrer">রিয়েলটাইম</a></th>
|
||||
<th><a href="https://github.com/supabase/storage-api" target="_blank" rel="noopener noreferrer">স্টোরেজ</a></th>
|
||||
</tr>
|
||||
<!-- নতুন সারির জন্য টেমপ্লেট -->
|
||||
<!-- সারি শুরু করুন
|
||||
<tr>
|
||||
<td>lang</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-lang" target="_blank" rel="noopener noreferrer">supabase-lang</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-lang" target="_blank" rel="noopener noreferrer">postgrest-lang</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-lang" target="_blank" rel="noopener noreferrer">gotrue-lang</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-lang" target="_blank" rel="noopener noreferrer">রিয়েলটাইম-ল্যাং</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-lang" target="_blank" rel="noopener noreferrer">স্টোরেজ-ল্যাং</a></td>
|
||||
</tr>
|
||||
শেষ সারি -->
|
||||
<th colspan="6">⚡️ অফিসিয়াল ⚡️</th>
|
||||
<tr>
|
||||
<td>জাভাস্ক্রিপ্ট (টাইপস্ক্রিপ্ট)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-js" target="_blank" rel="noopener noreferrer">postgrest-js</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">gotrue-js</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-js" target="_blank" rel="noopener noreferrer">রিয়েলটাইম-জেএস</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
|
||||
</tr>
|
||||
<th colspan="6">💚 সম্প্রদায় 💚</th>
|
||||
<tr>
|
||||
<td>C#</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-csharp" target="_blank" rel="noopener noreferrer">supabase-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-csharp" target="_blank" rel="noopener noreferrer">postgrest-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-csharp" target="_blank" rel="noopener noreferrer">gotrue-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-csharp" target="_blank" rel="noopener noreferrer">রিয়েলটাইম-csharp</a></td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ডার্ট (ফ্লাটার)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-dart" target="_blank" rel="noopener noreferrer">supabase-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">রিয়েলটাইম-ডার্ট</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>গো</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-go" target="_blank" rel="noopener noreferrer">postgrest-go</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>জাভা</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-java" target="_blank" rel="noopener noreferrer">gotrue-java</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>কোটলিন</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-kt" target="_blank" rel="noopener noreferrer">postgrest-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-kt" target="_blank" rel="noopener noreferrer">gotrue-kt</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>পাইথন</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-py" target="_blank" rel="noopener noreferrer">supabase-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>রুবি</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-rb" target="_blank" rel="noopener noreferrer">supabase-rb</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-rb" target="_blank" rel="noopener noreferrer">postgrest-rb</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>রাস্ট</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-rs" target="_blank" rel="noopener noreferrer">postgrest-rs</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>সুইফট</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">রিয়েলটাইম-সুইফট</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">স্টোরেজ-সুইফট</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## অনুবাদ
|
||||
|
||||
- [অনুবাদের তালিকা](/i18n/languages.md) <!--- Keep only this -->
|
||||
|
||||
## স্পনসর
|
||||
|
||||
[](https://github.com/sponsors/supabase)
|
||||
@@ -36,3 +36,4 @@
|
||||
- [Turkish / Türkçe](/i18n/README.tr.md)
|
||||
- [Ukrainian / Українська](/i18n/README.uk.md)
|
||||
- [Vietnamese / Tiếng Việt](/i18n/README.vi-vn.md)
|
||||
- [Bangla / বাংলা](/i18n/README.bn.md)
|
||||
@@ -11,10 +11,12 @@ import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { SubscriptionStats } from 'hooks'
|
||||
|
||||
interface Props {
|
||||
project: any
|
||||
subscription: StripeSubscription
|
||||
subscriptionStats: SubscriptionStats
|
||||
paygStats?: Dictionary<number>
|
||||
loading?: boolean
|
||||
showProjectName?: boolean
|
||||
@@ -25,6 +27,7 @@ interface Props {
|
||||
const Subscription: FC<Props> = ({
|
||||
project,
|
||||
subscription,
|
||||
subscriptionStats,
|
||||
paygStats,
|
||||
loading = false,
|
||||
showProjectName = false,
|
||||
@@ -59,7 +62,11 @@ const Subscription: FC<Props> = ({
|
||||
</Typography.Title>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
<UpgradeButton projectRef={project.ref} paid={paid} />
|
||||
<UpgradeButton
|
||||
subscriptionStats={subscriptionStats}
|
||||
projectRef={project.ref}
|
||||
paid={paid}
|
||||
/>
|
||||
</div>
|
||||
{paid && (
|
||||
<div className="px-6 pt-4">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useReducer, useState } from 'react'
|
||||
import { FC, useReducer, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { includes, without } from 'lodash'
|
||||
import { Button, Modal, Input, Divider, Typography, IconCheckCircle } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { SubscriptionStats, useStore } from 'hooks'
|
||||
import { API_URL, DEFAULT_FREE_PROJECTS_LIMIT } from 'lib/constants'
|
||||
import { getURL } from 'lib/helpers'
|
||||
import { post } from 'lib/common/fetch'
|
||||
@@ -16,7 +16,13 @@ import { CancellationReasons } from './SubcriptionCancellation.constants'
|
||||
* @param {Boolean} props.paid Tier name
|
||||
*/
|
||||
|
||||
export default function UpgradeButton({ projectRef, paid }: any) {
|
||||
interface Props {
|
||||
projectRef: string
|
||||
paid: boolean
|
||||
subscriptionStats: SubscriptionStats
|
||||
}
|
||||
|
||||
const UpgradeButton: FC<Props> = ({ projectRef, paid, subscriptionStats }) => {
|
||||
const router = useRouter()
|
||||
const { ui, app } = useStore()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -24,7 +30,7 @@ export default function UpgradeButton({ projectRef, paid }: any) {
|
||||
const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState<boolean>(false)
|
||||
|
||||
const freeProjectsLimit = ui?.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT
|
||||
const freeProjectsOwned = ui?.profile?.total_free_projects ?? 0
|
||||
const freeProjectsOwned = subscriptionStats.total_free_projects ?? 0
|
||||
const isOrgOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
/**
|
||||
@@ -49,7 +55,7 @@ export default function UpgradeButton({ projectRef, paid }: any) {
|
||||
|
||||
function handleUpgradeButton() {
|
||||
if (paid) {
|
||||
if (freeProjectsOwned >= DEFAULT_FREE_PROJECTS_LIMIT) {
|
||||
if (freeProjectsOwned >= freeProjectsLimit) {
|
||||
setShowFreeProjectLimitWarning(true)
|
||||
} else {
|
||||
setExitSurveyVisible(true)
|
||||
@@ -106,6 +112,8 @@ export default function UpgradeButton({ projectRef, paid }: any) {
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeButton
|
||||
|
||||
function UnsubcribeExitSurvey({ visible, setVisible, handleDowngrade }: any) {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
|
||||
@@ -64,9 +64,9 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
if (timestampDigits < 16) {
|
||||
// pad unix timestamp with additional 0 and then forward
|
||||
const paddedTimestamp = String(timestamp) + '0'.repeat(16 - timestampDigits)
|
||||
router.push(`/project/${ref}/settings/logs/rest?te=${paddedTimestamp}&s=${search}`)
|
||||
router.push(`/project/${ref}/settings/logs/api?te=${paddedTimestamp}`)
|
||||
} else {
|
||||
router.push(`/project/${ref}/settings/logs/rest?te=${timestamp}&s=${search}`)
|
||||
router.push(`/project/${ref}/settings/logs/api?te=${timestamp}`)
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import dayjs from 'dayjs'
|
||||
import { get as _get, maxBy } from 'lodash'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useStore, useSubscriptionStats } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { get, post } from 'lib/common/fetch'
|
||||
@@ -18,6 +18,8 @@ interface Props {
|
||||
|
||||
const ProjectSubscription: FC<Props> = ({ project }) => {
|
||||
const { ui } = useStore()
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [subscription, setSubscription] = useState<StripeSubscription>()
|
||||
const [paygStats, setPaygStats] = useState<Dictionary<number>>()
|
||||
@@ -81,6 +83,7 @@ const ProjectSubscription: FC<Props> = ({ project }) => {
|
||||
// @ts-ignore
|
||||
subscription={subscription}
|
||||
paygStats={paygStats}
|
||||
subscriptionStats={subscriptionStats}
|
||||
// @ts-ignore
|
||||
currentPeriodStart={subscription?.billing.current_period_start}
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { Button, Loading, Typography, IconFileText, IconDownload } from '@supabase/ui'
|
||||
import {
|
||||
Button,
|
||||
Loading,
|
||||
Typography,
|
||||
IconFileText,
|
||||
IconDownload,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
|
||||
const PAGE_LIMIT = 10
|
||||
|
||||
interface Props {
|
||||
organization: any
|
||||
}
|
||||
@@ -13,35 +23,38 @@ interface Props {
|
||||
const InvoicesSettings: FC<Props> = ({ organization }) => {
|
||||
const { ui } = useStore()
|
||||
const [loading, setLoading] = useState<any>(false)
|
||||
const [error, setError] = useState<any>(null)
|
||||
const [stripeAccount, setStripeAccount] = useState<any>(null)
|
||||
const { stripe_customer_id } = organization
|
||||
|
||||
/**
|
||||
* Get stripe account to populate page
|
||||
*/
|
||||
const getStripeAccount = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const response = await post(`${API_URL}/stripe/customer`, {
|
||||
stripe_customer_id: stripe_customer_id,
|
||||
})
|
||||
setStripeAccount(response)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to get Stripe account: ${error.message}`,
|
||||
})
|
||||
setError(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const [page, setPage] = useState(1)
|
||||
const [count, setCount] = useState(0)
|
||||
const [invoices, setInvoices] = useState<any>([])
|
||||
|
||||
const { stripe_customer_id } = organization
|
||||
const offset = (page - 1) * PAGE_LIMIT
|
||||
|
||||
useEffect(() => {
|
||||
getStripeAccount()
|
||||
}, [stripe_customer_id])
|
||||
let cancel = false
|
||||
|
||||
const fetchInvoices = async () => {
|
||||
setLoading(true)
|
||||
const res = await get(
|
||||
`${API_URL}/stripe/invoices?offset=${offset}&limit=${PAGE_LIMIT}&customer=${stripe_customer_id}`
|
||||
)
|
||||
if (!cancel) {
|
||||
if (res.error) {
|
||||
ui.setNotification({ category: 'error', message: res.error.message })
|
||||
} else {
|
||||
setInvoices(res.invoices)
|
||||
setCount(res.count)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchInvoices()
|
||||
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}, [stripe_customer_id, page])
|
||||
|
||||
return (
|
||||
<div className="my-4 container max-w-4xl space-y-1">
|
||||
@@ -55,45 +68,72 @@ const InvoicesSettings: FC<Props> = ({ organization }) => {
|
||||
<Table.th key="header-download" className="text-right"></Table.th>,
|
||||
]}
|
||||
body={
|
||||
!stripeAccount || (stripeAccount && stripeAccount.invoices?.data <= 0) ? (
|
||||
invoices.length === 0 ? (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={5} className="p-3 py-12 text-center">
|
||||
<Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{loading ? 'Checking for invoices' : 'No invoices for this organization yet'}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
) : (
|
||||
stripeAccount.invoices?.data.map((x: any) => {
|
||||
return (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.td>
|
||||
<IconFileText size="xxl" />
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>
|
||||
{new Date(x.period_end * 1000).toLocaleString()}
|
||||
<>
|
||||
{invoices.map((x: any) => {
|
||||
return (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.td>
|
||||
<IconFileText size="xxl" />
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>
|
||||
{new Date(x.period_end * 1000).toLocaleString()}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>${x.subtotal / 100}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>{x.number}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="align-right">
|
||||
<div className="flex items-center space-x-2 justify-end">
|
||||
<a href={x.hosted_invoice_url} target="_blank">
|
||||
<Button type="outline">View invoice</Button>
|
||||
</a>
|
||||
<form method="get" action={x.invoice_pdf}>
|
||||
<Button type="outline" icon={<IconDownload />} htmlType="submit" />
|
||||
</form>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})}
|
||||
<Table.tr key="navigation">
|
||||
<Table.td colSpan={5}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text type="secondary" small>
|
||||
Showing {offset + 1} to {offset + invoices.length} out of {count} invoices
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>${x.subtotal / 100}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>{x.number}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className="align-right">
|
||||
<div className="flex items-center space-x-2 justify-end">
|
||||
<a href={x.hosted_invoice_url} target="_blank">
|
||||
<Button type="outline">View invoice</Button>
|
||||
</a>
|
||||
<form method="get" action={x.invoice_pdf}>
|
||||
<Button type="outline" icon={<IconDownload />} htmlType="submit" />
|
||||
</form>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
icon={<IconChevronLeft />}
|
||||
type="secondary"
|
||||
size="tiny"
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage(page - 1)}
|
||||
/>
|
||||
<Button
|
||||
icon={<IconChevronRight />}
|
||||
type="secondary"
|
||||
size="tiny"
|
||||
disabled={page * PAGE_LIMIT >= count}
|
||||
onClick={() => setPage(page + 1)}
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,63 @@
|
||||
import { LogTemplate } from '.'
|
||||
|
||||
export const TEMPLATES: LogTemplate[] = [
|
||||
{ label: 'Recent Errors', mode: 'simple', searchString: '[Ee]rror|\\s[45][0-9][0-9]\\s' },
|
||||
{
|
||||
label: 'Recent Errors',
|
||||
mode: 'simple',
|
||||
searchString: '[Ee]rror|\\s[45][0-9][0-9]\\s',
|
||||
for: ['api'],
|
||||
},
|
||||
{
|
||||
label: 'Commits',
|
||||
mode: 'simple',
|
||||
searchString: 'COMMIT',
|
||||
for: ['database'],
|
||||
},
|
||||
|
||||
{
|
||||
label: '10 Minutes Ago',
|
||||
mode: 'simple',
|
||||
searchString: 'timestamp < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 10 MINUTE)',
|
||||
for: ['database', 'api'],
|
||||
},
|
||||
{
|
||||
label: 'POST or PATCH',
|
||||
mode: 'custom',
|
||||
searchString:
|
||||
"REGEXP_CONTAINS(event_message, 'POST') OR REGEXP_CONTAINS(event_message, 'PATCH')",
|
||||
for: ['api'],
|
||||
},
|
||||
{
|
||||
label: 'Metadata IP',
|
||||
mode: 'custom',
|
||||
searchString: `SELECT timestamp, h.x_real_ip
|
||||
FROM edge_logs
|
||||
LEFT JOIN UNNEST(metadata) as m ON TRUE
|
||||
LEFT JOIN UNNEST(m.request) AS r ON TRUE
|
||||
LEFT JOIN UNNEST(r.headers) AS h ON TRUE
|
||||
WHERE h.x_real_ip IS NOT NULL
|
||||
`,
|
||||
for: ['database'],
|
||||
},
|
||||
{
|
||||
label: 'Requests by Country',
|
||||
mode: 'custom',
|
||||
searchString: `SELECT
|
||||
cf.country, count(*) as count
|
||||
FROM edge_logs
|
||||
LEFT JOIN UNNEST(metadata) as m ON TRUE
|
||||
LEFT JOIN UNNEST(m.request) AS r ON TRUE
|
||||
LEFT JOIN UNNEST(r.cf) AS cf ON TRUE
|
||||
GROUP BY
|
||||
cf.country
|
||||
ORDER BY
|
||||
DESC count
|
||||
`,
|
||||
for: ['api'],
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_QUERY = 'SELECT timestamp, event_message FROM postgres_logs;'
|
||||
export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
|
||||
api: 'API',
|
||||
database: 'Database',
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
interface Metadata {
|
||||
[key: string]: string | number | Object | Object[]
|
||||
}
|
||||
export type LogSearchCallback = (filters: { query: string; from?: string, fromMicro?: number }) => void
|
||||
export type LogSearchCallback = (filters: {
|
||||
query: string
|
||||
from?: string
|
||||
fromMicro?: number
|
||||
}) => void
|
||||
export interface LogData {
|
||||
id: string
|
||||
timestamp: number
|
||||
@@ -12,6 +16,7 @@ export interface LogData {
|
||||
export interface LogTemplate {
|
||||
label?: string
|
||||
mode: 'custom' | 'simple'
|
||||
for?: string[]
|
||||
searchString: string
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -87,6 +87,7 @@ const ColumnForeignKeyInformation: FC<{
|
||||
}> = ({ columnName, foreignKey, onSelectEditRelation, onSelectRemoveRelation }) => {
|
||||
return (
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconLink />}
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -125,6 +126,7 @@ const ColumnForeignKeyAdded: FC<{
|
||||
}> = ({ columnName, foreignKey, onSelectEditRelation, onSelectRemoveRelation }) => {
|
||||
return (
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconLink />}
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -165,6 +167,7 @@ const ColumnForeignKeyRemoved: FC<{
|
||||
}> = ({ columnName, originalForeignKey, onSelectEditRelation }) => {
|
||||
return (
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconLink />}
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -210,6 +213,7 @@ const ColumnForeignKeyUpdated: FC<{
|
||||
}) => {
|
||||
return (
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconLink />}
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
+1
@@ -126,6 +126,7 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconHelpCircle size="large" strokeWidth={1.5} />}
|
||||
title="What are foreign keys?"
|
||||
description={
|
||||
|
||||
+1
@@ -178,6 +178,7 @@ const ColumnManagement: FC<Props> = ({
|
||||
|
||||
{primaryKeyColumns.length > 1 && (
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconKey className="text-white" size="large" />}
|
||||
title="Composite primary key selected"
|
||||
description="The columns that you've selected will grouped as a primary key, and will serve
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { LOG_TYPE_LABEL_MAPPING } from 'components/interfaces/Settings/Logs'
|
||||
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
|
||||
import { LOG_TYPE_LABEL_MAPPING } from 'lib/constants'
|
||||
|
||||
export const generateSettingsMenu = (ref: string): ProductMenuGroup[] => {
|
||||
const logTypes: string[] = ['database', 'api']
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Project settings',
|
||||
@@ -20,15 +22,12 @@ export const generateSettingsMenu = (ref: string): ProductMenuGroup[] => {
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
// items: ['database', 'auth', 'realtime', 'rest', 'storage'].map((type: string) => {
|
||||
items: ['database', 'rest'].map((type: string) => {
|
||||
return {
|
||||
name: LOG_TYPE_LABEL_MAPPING[type],
|
||||
key: `logs-${type}`,
|
||||
url: `/project/${ref}/settings/logs/${type}`,
|
||||
items: [],
|
||||
}
|
||||
}),
|
||||
items: logTypes.map((type: string) => ({
|
||||
name: LOG_TYPE_LABEL_MAPPING[type],
|
||||
key: `logs-${type}`,
|
||||
url: `/project/${ref}/settings/logs/${type}`,
|
||||
items: [],
|
||||
})),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -9,6 +9,9 @@ interface Props {
|
||||
urlLabel?: string
|
||||
defaultVisibility?: boolean
|
||||
hideCollapse?: boolean
|
||||
className?: string
|
||||
block?: boolean
|
||||
size?: 'tiny' | 'small' | 'normal' | 'large'
|
||||
}
|
||||
|
||||
const InformationBox: FC<Props> = ({
|
||||
@@ -19,10 +22,19 @@ const InformationBox: FC<Props> = ({
|
||||
urlLabel = 'Read more',
|
||||
defaultVisibility = false,
|
||||
hideCollapse = false,
|
||||
className = '',
|
||||
block = false,
|
||||
size = 'normal',
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(defaultVisibility)
|
||||
|
||||
const padding = { tiny: 'py-1', small: 'py-2', normal: 'py-3', large: 'py-4' }[size]
|
||||
return (
|
||||
<div className="block w-full bg-gray-100 dark:bg-gray-600 py-3 border border-gray-600 dark:border-gray-500 border-opacity-20 rounded">
|
||||
<div
|
||||
className={`${
|
||||
block ? 'block w-full' : ''
|
||||
} bg-gray-100 dark:bg-gray-600 ${padding} border border-gray-600 dark:border-gray-500 border-opacity-20 rounded ${className}`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-between px-3">
|
||||
<div className="flex space-x-3 w-full lg:items-center">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useOrganizationDetail'
|
||||
export * from './useProfile'
|
||||
export * from './useProjectAuthConfig'
|
||||
export * from './useSubscriptionStats'
|
||||
@@ -22,7 +22,6 @@ export function useProfile(returning?: 'minimal') {
|
||||
{
|
||||
first_name: updatedUser.first_name,
|
||||
last_name: updatedUser.last_name,
|
||||
total_free_projects: updatedUser.total_free_projects,
|
||||
},
|
||||
revalidate ?? true
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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_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_pro_projects: data?.total_pro_projects ?? 0,
|
||||
total_payg_projects: data?.total_payg_projects ?? 0,
|
||||
isLoading: !anyError && !data,
|
||||
isError: !!anyError,
|
||||
}
|
||||
}
|
||||
@@ -88,14 +88,6 @@ export const POLICY_MODAL_VIEWS = {
|
||||
REVIEW: 'REVIEW',
|
||||
}
|
||||
|
||||
export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
|
||||
rest: 'API',
|
||||
realtime: 'Realtime',
|
||||
auth: 'Auth',
|
||||
storage: 'Storage',
|
||||
database: 'Database',
|
||||
}
|
||||
|
||||
export const GOTRUE_ERRORS = {
|
||||
UNVERIFIED_GITHUB_USER: 'Error sending confirmation mail',
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { isUndefined } from 'lodash'
|
||||
import { Typography } from '@supabase/ui'
|
||||
|
||||
import { Project } from 'types'
|
||||
import { useProfile, useStore, withAuth } from 'hooks'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { post, delete_ } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { AccountLayout } from 'components/layouts'
|
||||
@@ -22,7 +22,6 @@ const Home: NextPage = () => {
|
||||
const { profile } = ui
|
||||
|
||||
const router = useRouter()
|
||||
const { mutateProfile } = useProfile()
|
||||
|
||||
const [isDeletingProject, setIsDeletingProject] = useState<boolean>(false)
|
||||
const [selectedProjectToDelete, setSelectedProjectToDelete] = useState<Project>()
|
||||
@@ -60,7 +59,7 @@ const Home: NextPage = () => {
|
||||
return ui.setNotification({ category: 'error', message: response.error.message })
|
||||
}
|
||||
|
||||
app.onProjectDeleted(response, mutateProfile)
|
||||
app.onProjectDeleted(response)
|
||||
ui.setNotification({ category: 'success', message: `Deleted ${project.name} successfully!` })
|
||||
|
||||
setIsDeletingProject(false)
|
||||
|
||||
+28
-14
@@ -7,7 +7,7 @@ import { useRef, useState, useEffect } from 'react'
|
||||
import { debounce, isUndefined, values } from 'lodash'
|
||||
import { toJS } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Typography, Listbox, IconUsers, IconAlertCircle } from '@supabase/ui'
|
||||
import { Button, Typography, Listbox, IconUsers, IconAlertCircle, Loading } from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
@@ -30,6 +30,7 @@ import Panel from 'components/to-be-cleaned/Panel'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { passwordStrength } from 'lib/helpers'
|
||||
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
|
||||
import { useSubscriptionStats } from 'hooks'
|
||||
|
||||
interface StripeCustomer {
|
||||
paymentMethods: any
|
||||
@@ -60,6 +61,8 @@ export const Wizard = observer(() => {
|
||||
const { slug } = router.query
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
|
||||
const [projectName, setProjectName] = useState('')
|
||||
const [dbPass, setDbPass] = useState('')
|
||||
const [dbRegion, setDbRegion] = useState(REGIONS_DEFAULT)
|
||||
@@ -74,7 +77,7 @@ export const Wizard = observer(() => {
|
||||
const currentOrg = organizations.find((o: any) => o.slug === slug)
|
||||
const stripeCustomerId = currentOrg?.stripe_customer_id
|
||||
|
||||
const totalFreeProjects = ui.profile?.total_free_projects ?? 0
|
||||
const totalFreeProjects = subscriptionStats.total_free_projects
|
||||
const freeProjectsLimit = ui.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT
|
||||
|
||||
const isEmptyOrganizations = organizations.length <= 0
|
||||
@@ -86,7 +89,10 @@ export const Wizard = observer(() => {
|
||||
const isSelectFreeTier = dbPricingPlan === PRICING_PLANS.FREE
|
||||
|
||||
const canCreateProject =
|
||||
currentOrg?.is_owner && (!isSelectFreeTier || (isSelectFreeTier && !isOverFreeProjectLimit))
|
||||
currentOrg?.is_owner &&
|
||||
!subscriptionStats.isError &&
|
||||
!subscriptionStats.isLoading &&
|
||||
(!isSelectFreeTier || (isSelectFreeTier && !isOverFreeProjectLimit))
|
||||
|
||||
const canSubmit =
|
||||
projectName != '' &&
|
||||
@@ -95,11 +101,6 @@ export const Wizard = observer(() => {
|
||||
dbPricingPlan != '' &&
|
||||
(isSelectFreeTier || (!isSelectFreeTier && !isEmptyPaymentMethod))
|
||||
|
||||
const passwordErrorMessage =
|
||||
dbPass != '' && passwordStrengthScore < DEFAULT_MINIMUM_PASSWORD_STRENGTH
|
||||
? 'You need a stronger password'
|
||||
: undefined
|
||||
|
||||
const delayedCheckPasswordStrength = useRef(
|
||||
debounce((value) => checkPasswordStrength(value), 300)
|
||||
).current
|
||||
@@ -319,12 +320,23 @@ export const Wizard = observer(() => {
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
{isSelectFreeTier && isOverFreeProjectLimit && <FreeProjectLimitWarning />}
|
||||
{isSelectFreeTier && isOverFreeProjectLimit && (
|
||||
<FreeProjectLimitWarning limit={freeProjectsLimit} />
|
||||
)}
|
||||
{!isSelectFreeTier && isEmptyPaymentMethod && (
|
||||
<EmptyPaymentMethodWarning stripeCustomerId={stripeCustomerId} />
|
||||
)}
|
||||
</Panel.Content>
|
||||
)}
|
||||
|
||||
{subscriptionStats.isLoading && (
|
||||
<Panel.Content>
|
||||
<div className="py-10">
|
||||
{/* @ts-ignore */}
|
||||
<Loading active={true} />
|
||||
</div>
|
||||
</Panel.Content>
|
||||
)}
|
||||
</>
|
||||
</Panel>
|
||||
</WizardLayout>
|
||||
@@ -335,6 +347,7 @@ const NotOrganizationOwnerWarning = () => {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
|
||||
defaultVisibility={true}
|
||||
hideCollapse
|
||||
@@ -342,8 +355,8 @@ const NotOrganizationOwnerWarning = () => {
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm leading-normal">
|
||||
Only the organization owner can create new projects. Contact your organization owner to
|
||||
create a new project for this organization.
|
||||
Only the organization owner can create new projects. Contact your organization owner
|
||||
to create a new project for this organization.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@@ -352,10 +365,11 @@ const NotOrganizationOwnerWarning = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const FreeProjectLimitWarning = () => {
|
||||
const FreeProjectLimitWarning = ({ limit }: { limit: number }) => {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
|
||||
defaultVisibility={true}
|
||||
hideCollapse
|
||||
@@ -363,8 +377,7 @@ const FreeProjectLimitWarning = () => {
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm leading-normal">
|
||||
Your account can only have a maximum of 2 free projects. You can only choose paid
|
||||
pricing plan.
|
||||
{`Your account can only have up to ${limit} free projects - to create another free project, you'll need to delete an existing free project first.`}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@@ -411,6 +424,7 @@ const EmptyPaymentMethodWarning = observer(
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
|
||||
defaultVisibility={true}
|
||||
hideCollapse
|
||||
|
||||
@@ -157,12 +157,12 @@ const Settings = () => {
|
||||
])}
|
||||
model={{
|
||||
MAILER_SECURE_EMAIL_CHANGE_ENABLED: model.MAILER_SECURE_EMAIL_CHANGE_ENABLED,
|
||||
SMTP_ADMIN_EMAIL: isCustomSMTPEnabled ? model.SMTP_ADMIN_EMAIL : "",
|
||||
SMTP_HOST: isCustomSMTPEnabled ? model.SMTP_HOST : "",
|
||||
SMTP_PORT: isCustomSMTPEnabled ? model.SMTP_PORT : "",
|
||||
SMTP_USER: isCustomSMTPEnabled ? model.SMTP_USER : "",
|
||||
SMTP_PASS: isCustomSMTPEnabled ? model.SMTP_PASS : "",
|
||||
SMTP_SENDER_NAME: isCustomSMTPEnabled ? model.SMTP_SENDER_NAME : "",
|
||||
SMTP_ADMIN_EMAIL: isCustomSMTPEnabled ? model.SMTP_ADMIN_EMAIL : '',
|
||||
SMTP_HOST: isCustomSMTPEnabled ? model.SMTP_HOST : '',
|
||||
SMTP_PORT: isCustomSMTPEnabled ? model.SMTP_PORT : '',
|
||||
SMTP_USER: isCustomSMTPEnabled ? model.SMTP_USER : '',
|
||||
SMTP_PASS: isCustomSMTPEnabled ? model.SMTP_PASS : '',
|
||||
SMTP_SENDER_NAME: isCustomSMTPEnabled ? model.SMTP_SENDER_NAME : '',
|
||||
RATE_LIMIT_EMAIL_SENT: isCustomSMTPEnabled ? model.RATE_LIMIT_EMAIL_SENT : 30,
|
||||
}}
|
||||
onSubmit={(model: any) =>
|
||||
@@ -210,19 +210,19 @@ const Settings = () => {
|
||||
<label className="">Enable Custom SMTP</label>
|
||||
<div className="form-control flex items-center">
|
||||
<Toggle
|
||||
onToggle={(value : any) => {
|
||||
onToggle={(value: any) => {
|
||||
/*
|
||||
* temporary solution
|
||||
* clear the values of SMTP when toggling
|
||||
*/
|
||||
if(!value) {
|
||||
*/
|
||||
if (!value) {
|
||||
onFormSubmit({
|
||||
SMTP_ADMIN_EMAIL: "",
|
||||
SMTP_HOST: "",
|
||||
SMTP_PORT: "",
|
||||
SMTP_USER: "",
|
||||
SMTP_PASS: "",
|
||||
SMTP_SENDER_NAME: "",
|
||||
SMTP_ADMIN_EMAIL: '',
|
||||
SMTP_HOST: '',
|
||||
SMTP_PORT: '',
|
||||
SMTP_USER: '',
|
||||
SMTP_PASS: '',
|
||||
SMTP_SENDER_NAME: '',
|
||||
RATE_LIMIT_EMAIL_SENT: 30,
|
||||
})
|
||||
}
|
||||
@@ -273,12 +273,12 @@ const Settings = () => {
|
||||
'SMS_TEXTLOCAL_SENDER',
|
||||
'SMS_VONAGE_API_KEY',
|
||||
'SMS_VONAGE_API_SECRET',
|
||||
'SMS_VONAGE_FROM'
|
||||
'SMS_VONAGE_FROM',
|
||||
])}
|
||||
model={{
|
||||
SMS_PROVIDER: model.SMS_PROVIDER,
|
||||
SMS_TEXTLOCAL_API_KEY: model.SMS_TEXTLOCAL_API_KEY,
|
||||
SMS_TEXTLOCAL_SENDER: model.SMS_TEXTLOCAL_SENDER,
|
||||
SMS_TEXTLOCAL_API_KEY: model.SMS_TEXTLOCAL_API_KEY || undefined,
|
||||
SMS_TEXTLOCAL_SENDER: model.SMS_TEXTLOCAL_SENDER || undefined,
|
||||
SMS_TWILIO_ACCOUNT_SID: model.SMS_TWILIO_ACCOUNT_SID || undefined,
|
||||
SMS_TWILIO_AUTH_TOKEN: model.SMS_TWILIO_AUTH_TOKEN || undefined,
|
||||
SMS_TWILIO_MESSAGE_SERVICE_SID: model.SMS_TWILIO_MESSAGE_SERVICE_SID || undefined,
|
||||
@@ -323,7 +323,7 @@ const Settings = () => {
|
||||
errorMessage="Please enter the messagebird originator."
|
||||
/>
|
||||
</>
|
||||
) : (smsProviderModel?.SMS_PROVIDER === 'textlocal' ? (
|
||||
) : smsProviderModel?.SMS_PROVIDER === 'textlocal' ? (
|
||||
<>
|
||||
<AutoField
|
||||
name="SMS_TEXTLOCAL_API_KEY"
|
||||
@@ -336,7 +336,7 @@ const Settings = () => {
|
||||
errorMessage="Please enter the vonage auth token."
|
||||
/>
|
||||
</>
|
||||
) : (smsProviderModel?.SMS_PROVIDER === 'vonage' ? (
|
||||
) : smsProviderModel?.SMS_PROVIDER === 'vonage' ? (
|
||||
<>
|
||||
<AutoField
|
||||
name="SMS_VONAGE_API_KEY"
|
||||
@@ -372,7 +372,7 @@ const Settings = () => {
|
||||
errorMessage="Please enter the twilio message service sid."
|
||||
/>
|
||||
</>
|
||||
)))}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<UIToggle
|
||||
@@ -383,7 +383,7 @@ const Settings = () => {
|
||||
// "Enable phone confirmations" should be toggled off.
|
||||
handleToggle('SMS_AUTOCONFIRM', !value)
|
||||
}}
|
||||
//
|
||||
//
|
||||
checked={!model.SMS_AUTOCONFIRM}
|
||||
descriptionText={authConfig.properties.SMS_AUTOCONFIRM.help}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { get as _get, maxBy } from 'lodash'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { useStore, useSubscriptionStats, withAuth } from 'hooks'
|
||||
import { post, get } from 'lib/common/fetch'
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
@@ -51,6 +51,8 @@ const Settings: FC<SettingsProps> = ({ project }) => {
|
||||
const { ui } = useStore()
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [subscription, setSubscription] = useState<StripeSubscription>()
|
||||
const [paygStats, setPaygStats] = useState<Dictionary<number>>()
|
||||
@@ -121,6 +123,7 @@ const Settings: FC<SettingsProps> = ({ project }) => {
|
||||
project={project}
|
||||
subscription={subscription}
|
||||
paygStats={paygStats}
|
||||
subscriptionStats={subscriptionStats}
|
||||
currentPeriodStart={subscription?.billing.current_period_start}
|
||||
currentPeriodEnd={subscription?.billing.current_period_end}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers'
|
||||
import { post, delete_ } from 'lib/common/fetch'
|
||||
import { useProfile, useStore, withAuth } from 'hooks'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
@@ -173,7 +173,6 @@ const GeneralSettings = observer(() => {
|
||||
|
||||
const ProjectDeleteModal = ({ project }: any) => {
|
||||
const router = useRouter()
|
||||
const { mutateProfile } = useProfile()
|
||||
const { ui, app } = useStore()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -190,7 +189,7 @@ const ProjectDeleteModal = ({ project }: any) => {
|
||||
try {
|
||||
const response = await delete_(`${API_URL}/projects/${project.ref}/remove`)
|
||||
if (response.error) throw response.error
|
||||
app.onProjectDeleted(response, mutateProfile)
|
||||
app.onProjectDeleted(response)
|
||||
ui.setNotification({ category: 'success', message: `Successfully deleted ${project.name}` })
|
||||
router.push(`/`)
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
import { withAuth } from 'hooks'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL, LOG_TYPE_LABEL_MAPPING } from 'lib/constants'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { SettingsLayout } from 'components/layouts/'
|
||||
import CodeEditor from 'components/ui/CodeEditor'
|
||||
import {
|
||||
@@ -28,13 +28,14 @@ import {
|
||||
TEMPLATES,
|
||||
LogData,
|
||||
LogSearchCallback,
|
||||
LOG_TYPE_LABEL_MAPPING,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { isUndefined } from 'lodash'
|
||||
import Flag from 'components/ui/Flag/Flag'
|
||||
import { useFlag } from 'hooks'
|
||||
import dayjs from 'dayjs'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
/**
|
||||
* Acts as a container component for the entire log display
|
||||
@@ -45,11 +46,10 @@ import dayjs from 'dayjs'
|
||||
* params used are:
|
||||
* - `q` for the editor query.
|
||||
* - `s` for search query.
|
||||
* - `ts` for timestamp start value.
|
||||
* - `te` for timestamp start value.
|
||||
*/
|
||||
export const LogPage: NextPage = () => {
|
||||
const logsQueryParamsSyncing = useFlag('logsQueryParamsSyncing')
|
||||
const logsCustomSql = useFlag('logsCustomSql')
|
||||
const router = useRouter()
|
||||
const { ref, type, q, s, te } = router.query
|
||||
const [editorId, setEditorId] = useState<string>(uuidv4())
|
||||
@@ -65,8 +65,10 @@ export const LogPage: NextPage = () => {
|
||||
timestamp_start: '',
|
||||
timestamp_end: '',
|
||||
})
|
||||
const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as string]}`
|
||||
const isSelectQuery = logsCustomSql && editorValue.toLowerCase().includes('select') ? true : false
|
||||
const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as keyof typeof LOG_TYPE_LABEL_MAPPING]}`
|
||||
const checkIfSelectQuery = (value: string) =>
|
||||
value.toLowerCase().includes('select') ? true : false
|
||||
const isSelectQuery = checkIfSelectQuery(editorValue)
|
||||
|
||||
useEffect(() => {
|
||||
setParams({ ...params, type: type as string })
|
||||
@@ -181,8 +183,12 @@ export const LogPage: NextPage = () => {
|
||||
setEditorValue(template.searchString)
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
where: isSelectQuery ? '' : template.searchString,
|
||||
sql: isSelectQuery ? template.searchString : '',
|
||||
where: checkIfSelectQuery(template.searchString)
|
||||
? ''
|
||||
: cleanEditorValue(template.searchString),
|
||||
sql: checkIfSelectQuery(template.searchString)
|
||||
? cleanEditorValue(template.searchString)
|
||||
: '',
|
||||
search_query: '',
|
||||
timestamp_end: '',
|
||||
}))
|
||||
@@ -192,8 +198,8 @@ export const LogPage: NextPage = () => {
|
||||
const handleEditorSubmit = () => {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
where: isSelectQuery ? '' : editorValue,
|
||||
sql: isSelectQuery ? editorValue : '',
|
||||
where: isSelectQuery ? '' : cleanEditorValue(editorValue),
|
||||
sql: isSelectQuery ? cleanEditorValue(editorValue) : '',
|
||||
search_query: '',
|
||||
}))
|
||||
if (!logsQueryParamsSyncing) return
|
||||
@@ -228,7 +234,10 @@ export const LogPage: NextPage = () => {
|
||||
})
|
||||
setEditorValue('')
|
||||
}
|
||||
|
||||
const cleanEditorValue = (value: string) => {
|
||||
if (typeof value !== 'string') return value
|
||||
return value.replace(/\n/g, ' ')
|
||||
}
|
||||
return (
|
||||
<SettingsLayout title={title}>
|
||||
<div className="h-full flex flex-col flex-grow">
|
||||
@@ -238,7 +247,7 @@ export const LogPage: NextPage = () => {
|
||||
isCustomQuery={mode === 'custom'}
|
||||
isLoading={isValidating}
|
||||
newCount={newCount}
|
||||
templates={TEMPLATES}
|
||||
templates={TEMPLATES.filter((template) => template.for?.includes(type as string))}
|
||||
onRefresh={handleRefresh}
|
||||
onSearch={handleSearch}
|
||||
defaultSearchValue={params.search_query}
|
||||
@@ -259,19 +268,17 @@ export const LogPage: NextPage = () => {
|
||||
onInputRun={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end p-2 w-full">
|
||||
<Flag name="logsCustomSql">
|
||||
{isSelectQuery && (
|
||||
<div className="flex flex-grow flex-row items-center gap-x-1">
|
||||
{/* // we don't have a slim Alert component yet */}
|
||||
<IconInfo size="tiny" />
|
||||
<Typography.Text small={true} type="secondary">
|
||||
Custom queries are restricted to a {type === 'database' ? '2 hour' : '7 day'}{' '}
|
||||
querying window.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</Flag>
|
||||
<div className="flex flex-row justify-between items-center px-2 py-1 w-full">
|
||||
{isSelectQuery && (
|
||||
<InformationBox
|
||||
block
|
||||
size="tiny"
|
||||
icon={<IconInfo size="tiny" />}
|
||||
title={`Custom queries are restricted to a ${
|
||||
type === 'database' ? '2 hour' : '7 day'
|
||||
} querying window.`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row gap-x-2 justify-end p-2">
|
||||
{editorValue && (
|
||||
<Button
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface IAppStore {
|
||||
organizations: OrganizationStore
|
||||
database: IDatabaseStore
|
||||
onProjectUpdated: (project: any) => void
|
||||
onProjectDeleted: (project: any, mutateProfile: Function) => void
|
||||
onProjectDeleted: (project: any) => void
|
||||
onOrgAdded: (org: any) => void
|
||||
onOrgUpdated: (org: any) => void
|
||||
onOrgDeleted: (org: any) => void
|
||||
@@ -52,22 +52,12 @@ export default class AppStore implements IAppStore {
|
||||
}
|
||||
}
|
||||
|
||||
onProjectDeleted(project: any, mutateProfile: Function) {
|
||||
onProjectDeleted(project: any) {
|
||||
if (project && project.id) {
|
||||
const profile = this.rootStore.ui.profile
|
||||
const projectMetadata = this.projects.find((proj: any) => proj.id === project.id)
|
||||
|
||||
// cleanup project saved queries
|
||||
localStorage.removeItem(`supabase-queries-state-${project.ref}`)
|
||||
localStorage.removeItem(`supabase_${project.ref}`)
|
||||
delete this.projects.data[project.id]
|
||||
|
||||
if (profile && projectMetadata?.subscription_tier_prod_id === STRIPE_PRODUCT_IDS.FREE) {
|
||||
mutateProfile({
|
||||
...profile,
|
||||
total_free_projects: profile.total_free_projects - 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ test('custom sql querying', async () => {
|
||||
expect(editor).toBeTruthy()
|
||||
})
|
||||
editor = container.querySelector('.monaco-editor')
|
||||
userEvent.type(editor, 'select count(*) as my_count from edge_logs')
|
||||
userEvent.type(editor, 'select \ncount(*) as my_count \nfrom edge_logs')
|
||||
// should show sandbox warning alert
|
||||
await waitFor(() => screen.getByText(/restricted to a 7 day querying window/))
|
||||
|
||||
@@ -305,6 +305,7 @@ test('custom sql querying', async () => {
|
||||
userEvent.click(screen.getByText('Run'))
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(get).not.toHaveBeenCalledWith(expect.stringContaining(encodeURI('\n')))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('sql='))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('select'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('edge_logs'))
|
||||
|
||||
@@ -36,7 +36,6 @@ export interface User {
|
||||
last_name: string
|
||||
is_alpha_user: boolean
|
||||
free_project_limit: number
|
||||
total_free_projects: number
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
id: snaplet
|
||||
title: 'Snaplet'
|
||||
description: 'Get started with Supabase and Snaplet, a developer tool for working with safe, versioned, up-to-date production-like data'
|
||||
---
|
||||
|
||||
This step-by-step guide explains how to use Snaplet to clone your production Supabase project into another development database.
|
||||
|
||||
[Snaplet](https://snaplet.dev/) is a developer tool that copies a Postgres database, transforming personal information, so that you can **safely code against actual data.** This functionality makes it possible to easily achieve environment parity in Supabase.
|
||||
|
||||
Let's get started!
|
||||
|
||||
Follow along in the video below as the founder of Snaplet, Peter Pistorius, takes you through the entire process. Otherwise, you can skip the video and dive into the step-by-step guide.
|
||||
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/oPtMMhdhEP4?rel=0"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; modestbranding; showinfo=0"
|
||||
allowfullscreen
|
||||
width="100%"
|
||||
height="400px"
|
||||
></iframe>
|
||||
|
||||
## Step 1: Prerequisites
|
||||
|
||||
1. **A production Supabase project's connection string:** These can be found in Supabase via `Organization > Project > Database > Connection Pooling > Connection string`
|
||||
2. **A development Supabase project's connection string:** Same steps as above, but a different project/environment
|
||||
3. **A read-only role** in Production (recommended): This can be done by running the following statements on Supabase via `Organization > SQL Editor > + New Query`
|
||||
|
||||
> To create a read-only role across all schemas you can checkout the Snaplet [docs](https://docs.snaplet.dev/postgresql/create-read-only-role)
|
||||
|
||||
4. **Superuser access** for the development project. This can be done in Supabase via `Organization > SQL Editor > + New Query` and running this statement:
|
||||
`ALTER USER postgres WITH superuser;`
|
||||
|
||||
## Step 2: Copying your production database
|
||||
|
||||
### 2.1. Connect your data source
|
||||
|
||||
Navigate to [https://www.snaplet.dev/](https://www.snaplet.dev/) and sign up for a new account (it’s free). Once you have successfully signed up for a new account, create a team, and start by connecting to your Supabase project.
|
||||
|
||||

|
||||
|
||||
Enter the credentials of your production Supabase project. Find the "Connection string" in Supabase via `Organization > Project > Settings > Database > Connection string` (at the bottom of the page).
|
||||
|
||||
The password is the same password you used when creating the Supabase project.
|
||||
|
||||

|
||||
|
||||
You’ll have to confirm providing Snaplet access to your database. Snaplet will prompt you to only provide `read-only` access to your database. Snaplet has a guide in their documentation on how to do so [here](https://docs.snaplet.dev/postgresql/create-read-only-role).
|
||||
|
||||
> Note that whatever connection string you provide here will be that of your Data Source – essentially the production database in a real-life scenario
|
||||
|
||||

|
||||
|
||||
### 2.2. Transform your data
|
||||
|
||||

|
||||
|
||||
The next step is to exclude any schemas that you do not require. You are able to exclude an entire schema by clicking on the drop-down at the top, selecting the schema you would like to exclude and clicking ‘Exclude schema’. Alternatively, you can select a given schema and exclude only specific tables from that particular schema. Exclude any non-required table data (such as logs) and extensions and view your columns.
|
||||
|
||||
At this point, Snaplet will automatically detect any columns that have Personally Identifiable Information (PII) and mark them in purple. If there are any additional columns that hold data you would like to anonymise, you can click on the respective column name and provide a replacement value for the data in that column. To complete the onboarding, click on `Review and Save` and proceed to the dashboard.
|
||||
|
||||

|
||||
|
||||
### 2.3. Create a Snapshot
|
||||
|
||||
Create a snapshot of your production database. This is what you’re going to restore later into your data target (more on that later in the guide).
|
||||
|
||||
## Step 3: Pasting into your development database
|
||||
|
||||
### 3.1. Create a data target on Supabase
|
||||
|
||||
Your data target is where you want Snaplet to restore the captured snapshot of your production project. This would most likely be either your staging or developer Supabase project. If you don’t already have a developer database setup on Supabase, create a new data target by setting up a new project on Supabase. To create a new project, follow the steps below:
|
||||
|
||||
1. Go to [app.supabase.io](https://app.supabase.io/)
|
||||
2. Click on “new project”
|
||||
3. Enter your project details
|
||||
4. Wait for the new database to launch
|
||||
|
||||
> Remember the password you use when creating the project. You’ll need this password to connect your database to Snaplet later.
|
||||
|
||||
### 3.2. Make your `postgres` user a superuser
|
||||
|
||||
Snaplet requires the ability to drop the database schemas whilst restoring a snapshot. In order to do that, Snaplet requires `superuser` privileges.
|
||||
|
||||
1. Navigate to the `SQL Editor` in your Supabase console
|
||||
2. Click on `new query`
|
||||
3. Paste `alter user postgres with superuser;` into the SQL editor
|
||||
4. Run the query
|
||||
|
||||
### 3.3. Install the Snaplet CLI
|
||||
|
||||
1. Open your terminal and run `curl -sL [https://app.snaplet.dev/get-cli/](https://app.snaplet.dev/get-cli/) | bash`
|
||||
2. Run `snaplet auth`
|
||||
3. Navigate to `[https://app.snaplet.dev/access-token/cli](https://app.snaplet.dev/access-token/cli)` to get your access token
|
||||
4. Paste your access token in the terminal
|
||||
|
||||
### 3.4. Restore to the data target
|
||||
|
||||
You're now ready to restore your production snapshot into your Supabase development project.
|
||||
|
||||
1. Navigate to your project directory
|
||||
2. Run `snaplet setup` – you will be prompted to enter your database credentials. These are the database credentials of your **data target.** This could be your staging or development database
|
||||
|
||||
Once completed, you will be presented with a list of databases that are connected to your Snaplet account.
|
||||
|
||||
1. Select a data source from the list
|
||||
2. Run `snaplet restore`
|
||||
|
||||
## All done!
|
||||
|
||||
As a Supabase user, you can see how this solves an issue developers all typically experience when attempting to create multiple development environments and populating each of those environments with data that can be worked with. Snaplet simplifies this process down to creating the respective Supabase projects, connecting the data source (The production database) to Snaplet and telling Snaplet where to restore that data (staging and development databases).
|
||||
|
||||
If you want to learn more about Snaplet, you can explore the Snaplet [docs](https://docs.snaplet.dev/). If you have any questions, feel free to [reach out on Discord](https://discord.com/invite/6HUuajc866).
|
||||
Generated
+1
@@ -18035,6 +18035,7 @@
|
||||
},
|
||||
"@kiwicopple/prism-react-renderer": {
|
||||
"version": "git+ssh://git@github.com/kiwicopple/prism-react-renderer.git#4a09100a587bce2d94d7ac8ed3564a61c6e70781",
|
||||
"integrity": "sha512-QluokmJkMoHOwahIBN34hTG5n03kFyJ0REDq4mkf0o05JUC61ikXQsPNXtynASqoZRW7A+bKVNT65kCtUfgZlQ==",
|
||||
"from": "@kiwicopple/prism-react-renderer@https://github.com/kiwicopple/prism-react-renderer.git",
|
||||
"requires": {
|
||||
"prismjs": "^1.22.0"
|
||||
|
||||
@@ -33,7 +33,7 @@ module.exports = {
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Storage',
|
||||
items: ['reference/javascript/storage-createbucket', 'reference/javascript/storage-getbucket', 'reference/javascript/storage-listbuckets', 'reference/javascript/storage-updatebucket', 'reference/javascript/storage-deletebucket', 'reference/javascript/storage-emptybucket', 'reference/javascript/storage-from-upload', 'reference/javascript/storage-from-download', 'reference/javascript/storage-from-list', 'reference/javascript/storage-from-update', 'reference/javascript/storage-from-move', 'reference/javascript/storage-from-remove', 'reference/javascript/storage-from-createsignedurl', 'reference/javascript/storage-from-getpublicurl'],
|
||||
items: ['reference/javascript/storage-createbucket', 'reference/javascript/storage-getbucket', 'reference/javascript/storage-listbuckets', 'reference/javascript/storage-updatebucket', 'reference/javascript/storage-deletebucket', 'reference/javascript/storage-emptybucket', 'reference/javascript/storage-from-upload', 'reference/javascript/storage-from-download', 'reference/javascript/storage-from-list', 'reference/javascript/storage-from-update', 'reference/javascript/storage-from-move', 'reference/javascript/storage-from-remove', 'reference/javascript/storage-from-createsignedurl', 'reference/javascript/storage-from-createsignedurls', 'reference/javascript/storage-from-getpublicurl'],
|
||||
collapsed: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -174,6 +174,7 @@ module.exports = {
|
||||
'guides/integrations/auth0',
|
||||
'guides/integrations/draftbit',
|
||||
'guides/integrations/prisma',
|
||||
'guides/integrations/snaplet',
|
||||
'guides/integrations/vercel',
|
||||
],
|
||||
},
|
||||
|
||||
+4345
-3588
File diff suppressed because it is too large.
Load diff
@@ -370,6 +370,7 @@ pages:
|
||||
- By default, Supabase projects will return a maximum of 1,000 rows. This setting can be changed in Project API Settings. It's recommended that you keep it low to limit the payload size of accidental or malicious requests. You can use `range()` queries to paginate through your data.
|
||||
- `select()` can be combined with [Modifiers](/docs/reference/dart/using-modifiers)
|
||||
- `select()` can be combined with [Filters](/docs/reference/dart/using-filters)
|
||||
- If using the Supabase hosted platform `apikey` is technically a reserved keyword, since the API gateway will pluck it out for authentication. [It should be avoided as a column name](https://github.com/supabase/supabase/issues/5465).
|
||||
examples:
|
||||
- name: Getting your data
|
||||
isSpotlight: true
|
||||
|
||||
+2701
-2262
File diff suppressed because it is too large.
Load diff
+733
-294
File diff suppressed because it is too large.
Load diff
+911
-1032
File diff suppressed because it is too large.
Load diff
+21
-1
@@ -167,6 +167,7 @@ info:
|
||||
- storage.from.move()
|
||||
- storage.from.remove()
|
||||
- storage.from.createSignedUrl()
|
||||
- storage.from.createSignedUrls()
|
||||
- storage.from.getPublicUrl()
|
||||
- name: 'Modifiers'
|
||||
items:
|
||||
@@ -230,7 +231,7 @@ pages:
|
||||
//or
|
||||
<script src="https://unpkg.com/@supabase/supabase-js"></script>
|
||||
```
|
||||
|
||||
|
||||
Initializing:
|
||||
$ref: '@supabase/supabase-js."SupabaseClient".SupabaseClient.constructor'
|
||||
description: |
|
||||
@@ -948,6 +949,7 @@ pages:
|
||||
- By default, Supabase projects will return a maximum of 1,000 rows. This setting can be changed in Project API Settings. It's recommended that you keep it low to limit the payload size of accidental or malicious requests. You can use `range()` queries to paginate through your data.
|
||||
- `select()` can be combined with [Modifiers](/docs/reference/javascript/using-modifiers)
|
||||
- `select()` can be combined with [Filters](/docs/reference/javascript/using-filters)
|
||||
- If using the Supabase hosted platform `apikey` is technically a reserved keyword, since the API gateway will pluck it out for authentication. [It should be avoided as a column name](https://github.com/supabase/supabase/issues/5465).
|
||||
examples:
|
||||
- name: Getting your data
|
||||
isSpotlight: true
|
||||
@@ -1614,6 +1616,24 @@ pages:
|
||||
.createSignedUrl('folder/avatar1.png', 60)
|
||||
```
|
||||
|
||||
storage.from.createSignedUrls():
|
||||
title: 'from.createSignedUrls()'
|
||||
$ref: '@supabase/storage-js."lib/StorageFileApi".StorageFileApi.createSignedUrls'
|
||||
notes: |
|
||||
- Policy permissions required:
|
||||
- `buckets` permissions: none
|
||||
- `objects` permissions: `select`
|
||||
examples:
|
||||
- name: Create Signed URLs
|
||||
isSpotlight: true
|
||||
js: |
|
||||
```js
|
||||
const { data, error } = await supabase
|
||||
.storage
|
||||
.from('avatars')
|
||||
.createSignedUrls(['folder/avatar1.png', 'folder/avatar2.png'], 60)
|
||||
```
|
||||
|
||||
storage.from.getPublicUrl():
|
||||
title: 'from.getPublicUrl()'
|
||||
$ref: '@supabase/storage-js."lib/StorageFileApi".StorageFileApi.getPublicUrl'
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
Reference in new issue
Block a user