Merge pull request #5501 from supabase/master

Prod deploy for studio
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-02-17 16:25:01 +08:00
commit 870543daa7
46 files changed
+9348 -7353

No files matched your search

+1
View File
@@ -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 -->
---
+1 -1
View File
@@ -69,7 +69,7 @@ module.exports = {
},
{
label: 'Careers',
to: '/careers',
to: 'https://boards.greenhouse.io/supabase',
activeBasePath: '/careers',
position: 'left',
},
+1 -1
View File
@@ -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 →
+1 -1
View File
@@ -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
+183
View File
@@ -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) করতে পারেন।
![আর্কিটেকচার](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
- [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://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase)
+1
View File
@@ -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
}
@@ -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">
@@ -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={
@@ -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: [],
})),
},
]
}
+13 -1
View File
@@ -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
View File
@@ -1,3 +1,4 @@
export * from './useOrganizationDetail'
export * from './useProfile'
export * from './useProjectAuthConfig'
export * from './useSubscriptionStats'
-1
View File
@@ -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,
}
}
-8
View File
@@ -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',
}
+2 -3
View File
@@ -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
View File
@@ -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
+22 -22
View File
@@ -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
+2 -12
View File
@@ -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,
})
}
}
}
+2 -1
View File
@@ -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'))
-1
View File
@@ -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 {
+114
View File
@@ -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.
![connect-your-database](/img/guides/integrations/snaplet/connect-your-database.png)
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.
![supabase-connection-db-info](/img/guides/integrations/snaplet/supabase-connection-db-info.png)
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
![checking-database-credentials](/img/guides/integrations/snaplet/checking-database-credentials.png)
### 2.2. Transform your data
![transform-your-data](/img/guides/integrations/snaplet/transform-your-data.png)
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.
![create-your-first-snapshot](/img/guides/integrations/snaplet/create-your-first-snapshot.png)
### 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).
+1
View File
@@ -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"
+1 -1
View File
@@ -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,
},
{
+1
View File
@@ -174,6 +174,7 @@ module.exports = {
'guides/integrations/auth0',
'guides/integrations/draftbit',
'guides/integrations/prisma',
'guides/integrations/snaplet',
'guides/integrations/vercel',
],
},
+4345 -3588
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+733 -294
View File
File diff suppressed because it is too large. Load diff
+911 -1032
View File
File diff suppressed because it is too large. Load diff
+21 -1
View File
@@ -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