diff --git a/README.md b/README.md index 3e3aeece00a..533a2cd1e4f 100644 --- a/README.md +++ b/README.md @@ -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) --- diff --git a/about/docusaurus.config.js b/about/docusaurus.config.js index 7bf635b5c72..47382d37765 100755 --- a/about/docusaurus.config.js +++ b/about/docusaurus.config.js @@ -69,7 +69,7 @@ module.exports = { }, { label: 'Careers', - to: '/careers', + to: 'https://boards.greenhouse.io/supabase', activeBasePath: '/careers', position: 'left', }, diff --git a/about/src/pages/index.js b/about/src/pages/index.js index 54c0c16499b..474473ed03f 100755 --- a/about/src/pages/index.js +++ b/about/src/pages/index.js @@ -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 → diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index cab0c58c8b2..cdb3fb7727b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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 diff --git a/i18n/README.bn.md b/i18n/README.bn.md new file mode 100644 index 00000000000..7f68d584a1b --- /dev/null +++ b/i18n/README.bn.md @@ -0,0 +1,183 @@ +

+ +

+ +--- + +# 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] পাবলিক বিটা: বেশিরভাগ নন-এন্টারপ্রাইজ ব্যবহারের ক্ষেত্রে যথেষ্ট স্থিতিশীল +- [ ] পাবলিক: উত্পাদন প্রস্তুত + +আমরা বর্তমানে পাবলিক বিটাতে আছি। বড় আপডেটের বিজ্ঞপ্তি পেতে এই রেপোর "রিলিজ" দেখুন। + +এই রেপো দেখুন + +--- + +## কিভাবে এটা কাজ করে + +সুপাবেস হল ওপেন সোর্স টুলের সংমিশ্রণ। আমরা এন্টারপ্রাইজ-গ্রেড, ওপেন সোর্স পণ্য ব্যবহার করে 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 গেটওয়ে। + +#### ক্লায়েন্ট লাইব্রেরি + +ক্লায়েন্ট লাইব্রেরির জন্য আমাদের পদ্ধতি মডুলার। প্রতিটি উপ-লাইব্রেরি একটি একক বহিরাগত সিস্টেমের জন্য একটি স্বতন্ত্র বাস্তবায়ন। এটি এমন একটি উপায় যা আমরা বিদ্যমান সরঞ্জামগুলিকে সমর্থন করি৷ + +৷ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ভাষাক্লায়েন্টফিচার-ক্লায়েন্ট (সুপাবেস ক্লায়েন্টে বান্ডিল)
SupabasePostgRESTGoTrueরিয়েলটাইমস্টোরেজ
⚡️ অফিসিয়াল ⚡️
জাভাস্ক্রিপ্ট (টাইপস্ক্রিপ্ট)supabase-jspostgrest-jsgotrue-jsরিয়েলটাইম-জেএসstorage-js
💚 সম্প্রদায় 💚
C#supabase-csharppostgrest-csharpgotrue-csharpরিয়েলটাইম-csharp-
ডার্ট (ফ্লাটার)supabase-dartpostgrest-dartgotrue-dartরিয়েলটাইম-ডার্টstorage-dart
গো-postgrest-go---
জাভা--gotrue-java--
কোটলিন-postgrest-ktgotrue-kt--
পাইথনsupabase-pypostgrest-pygotrue-pyrealtime-py-
রুবিsupabase-rbpostgrest-rb---
রাস্ট-postgrest-rs---
সুইফটsupabase-swiftpostgrest-swiftgotrue-swiftরিয়েলটাইম-সুইফটস্টোরেজ-সুইফট
+ +## অনুবাদ + +- [অনুবাদের তালিকা](/i18n/languages.md) + +## স্পনসর + +[![নতুন স্পনসর](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase) diff --git a/i18n/languages.md b/i18n/languages.md index 4ca01e9e74c..460de3275eb 100644 --- a/i18n/languages.md +++ b/i18n/languages.md @@ -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) diff --git a/studio/components/interfaces/Billing/Subscription/Subscription.tsx b/studio/components/interfaces/Billing/Subscription/Subscription.tsx index 615c5900279..c120e543587 100644 --- a/studio/components/interfaces/Billing/Subscription/Subscription.tsx +++ b/studio/components/interfaces/Billing/Subscription/Subscription.tsx @@ -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 loading?: boolean showProjectName?: boolean @@ -25,6 +27,7 @@ interface Props { const Subscription: FC = ({ project, subscription, + subscriptionStats, paygStats, loading = false, showProjectName = false, @@ -59,7 +62,11 @@ const Subscription: FC = ({ {/* @ts-ignore */} - + {paid && (
diff --git a/studio/components/interfaces/Billing/Subscription/UpgradeButton.tsx b/studio/components/interfaces/Billing/Subscription/UpgradeButton.tsx index 388f540cdc3..4fcce39815d 100644 --- a/studio/components/interfaces/Billing/Subscription/UpgradeButton.tsx +++ b/studio/components/interfaces/Billing/Subscription/UpgradeButton.tsx @@ -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 = ({ projectRef, paid, subscriptionStats }) => { const router = useRouter() const { ui, app } = useStore() const [loading, setLoading] = useState(false) @@ -24,7 +30,7 @@ export default function UpgradeButton({ projectRef, paid }: any) { const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(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 diff --git a/studio/components/interfaces/Home/ProjectUsage.tsx b/studio/components/interfaces/Home/ProjectUsage.tsx index f9e510fbb13..7ca67a69d93 100644 --- a/studio/components/interfaces/Home/ProjectUsage.tsx +++ b/studio/components/interfaces/Home/ProjectUsage.tsx @@ -64,9 +64,9 @@ const ProjectUsage: FC = ({ 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 ( diff --git a/studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx b/studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx index 54624131313..c6c7d604b37 100644 --- a/studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx @@ -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 = ({ project }) => { const { ui } = useStore() + const subscriptionStats = useSubscriptionStats() + const [loading, setLoading] = useState(true) const [subscription, setSubscription] = useState() const [paygStats, setPaygStats] = useState>() @@ -81,6 +83,7 @@ const ProjectSubscription: FC = ({ project }) => { // @ts-ignore subscription={subscription} paygStats={paygStats} + subscriptionStats={subscriptionStats} // @ts-ignore currentPeriodStart={subscription?.billing.current_period_start} // @ts-ignore diff --git a/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx index f4fec38f672..fbf8190fe35 100644 --- a/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx +++ b/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx @@ -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 = ({ organization }) => { const { ui } = useStore() const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [stripeAccount, setStripeAccount] = useState(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([]) + + 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 (
@@ -55,45 +68,72 @@ const InvoicesSettings: FC = ({ organization }) => { , ]} body={ - !stripeAccount || (stripeAccount && stripeAccount.invoices?.data <= 0) ? ( + invoices.length === 0 ? ( - + {loading ? 'Checking for invoices' : 'No invoices for this organization yet'} ) : ( - stripeAccount.invoices?.data.map((x: any) => { - return ( - - - - - - - {new Date(x.period_end * 1000).toLocaleString()} + <> + {invoices.map((x: any) => { + return ( + + + + + + + {new Date(x.period_end * 1000).toLocaleString()} + + + + ${x.subtotal / 100} + + + {x.number} + + +
+ + + +
+
+
+
+ ) + })} + + +
+ + Showing {offset + 1} to {offset + invoices.length} out of {count} invoices - - - ${x.subtotal / 100} - - - {x.number} - - -
- - - -
-
-
- - ) - }) +
+
+
+ ) } /> diff --git a/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 78b3c542e9c..78c41406bae 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -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', +} diff --git a/studio/components/interfaces/Settings/Logs/Logs.types.ts b/studio/components/interfaces/Settings/Logs/Logs.types.ts index f93d5c747fe..877bcd3d77a 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.types.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.types.ts @@ -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 } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnForeignKey.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnForeignKey.tsx index 9d5b4c70c10..e36cfe5186f 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnForeignKey.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnForeignKey.tsx @@ -87,6 +87,7 @@ const ColumnForeignKeyInformation: FC<{ }> = ({ columnName, foreignKey, onSelectEditRelation, onSelectRemoveRelation }) => { return ( } title={
@@ -125,6 +126,7 @@ const ColumnForeignKeyAdded: FC<{ }> = ({ columnName, foreignKey, onSelectEditRelation, onSelectRemoveRelation }) => { return ( } title={
@@ -165,6 +167,7 @@ const ColumnForeignKeyRemoved: FC<{ }> = ({ columnName, originalForeignKey, onSelectEditRelation }) => { return ( } title={
@@ -210,6 +213,7 @@ const ColumnForeignKeyUpdated: FC<{ }) => { return ( } title={
diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx index 924384e642e..7716ddf3b0a 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx @@ -126,6 +126,7 @@ const ForeignKeySelector: FC = ({ >
} title="What are foreign keys?" description={ diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx index f48fa8fa067..cb5e070743e 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx @@ -178,6 +178,7 @@ const ColumnManagement: FC = ({ {primaryKeyColumns.length > 1 && ( } title="Composite primary key selected" description="The columns that you've selected will grouped as a primary key, and will serve diff --git a/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts b/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts index a1547dfa076..1c6f13d71b2 100644 --- a/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts +++ b/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts @@ -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: [], + })), }, ] } diff --git a/studio/components/ui/InformationBox.tsx b/studio/components/ui/InformationBox.tsx index 1281218e201..accf153a641 100644 --- a/studio/components/ui/InformationBox.tsx +++ b/studio/components/ui/InformationBox.tsx @@ -9,6 +9,9 @@ interface Props { urlLabel?: string defaultVisibility?: boolean hideCollapse?: boolean + className?: string + block?: boolean + size?: 'tiny' | 'small' | 'normal' | 'large' } const InformationBox: FC = ({ @@ -19,10 +22,19 @@ const InformationBox: FC = ({ urlLabel = 'Read more', defaultVisibility = false, hideCollapse = false, + className = '', + block = false, + size = 'normal', }) => { const [isExpanded, setIsExpanded] = useState(defaultVisibility) + + const padding = { tiny: 'py-1', small: 'py-2', normal: 'py-3', large: 'py-4' }[size] return ( -
+
diff --git a/studio/hooks/queries/index.ts b/studio/hooks/queries/index.ts index 2e1effbdaf7..34c5ae8d600 100644 --- a/studio/hooks/queries/index.ts +++ b/studio/hooks/queries/index.ts @@ -1,3 +1,4 @@ export * from './useOrganizationDetail' export * from './useProfile' export * from './useProjectAuthConfig' +export * from './useSubscriptionStats' diff --git a/studio/hooks/queries/useProfile.ts b/studio/hooks/queries/useProfile.ts index 62782c163c8..3d5802391f8 100644 --- a/studio/hooks/queries/useProfile.ts +++ b/studio/hooks/queries/useProfile.ts @@ -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 ) diff --git a/studio/hooks/queries/useSubscriptionStats.ts b/studio/hooks/queries/useSubscriptionStats.ts new file mode 100644 index 00000000000..3557413d527 --- /dev/null +++ b/studio/hooks/queries/useSubscriptionStats.ts @@ -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(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, + } +} diff --git a/studio/lib/constants/index.ts b/studio/lib/constants/index.ts index eda02a194b6..d76636cd817 100644 --- a/studio/lib/constants/index.ts +++ b/studio/lib/constants/index.ts @@ -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', } diff --git a/studio/pages/index.tsx b/studio/pages/index.tsx index 5fd19797cc7..1754fbd8c53 100644 --- a/studio/pages/index.tsx +++ b/studio/pages/index.tsx @@ -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(false) const [selectedProjectToDelete, setSelectedProjectToDelete] = useState() @@ -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) diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index f5c1c6d1450..56a6e08074c 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -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(() => { ))} - {isSelectFreeTier && isOverFreeProjectLimit && } + {isSelectFreeTier && isOverFreeProjectLimit && ( + + )} {!isSelectFreeTier && isEmptyPaymentMethod && ( )} )} + + {subscriptionStats.isLoading && ( + +
+ {/* @ts-ignore */} + +
+
+ )} @@ -335,6 +347,7 @@ const NotOrganizationOwnerWarning = () => { return (
} defaultVisibility={true} hideCollapse @@ -342,8 +355,8 @@ const NotOrganizationOwnerWarning = () => { description={

- 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.

} @@ -352,10 +365,11 @@ const NotOrganizationOwnerWarning = () => { ) } -const FreeProjectLimitWarning = () => { +const FreeProjectLimitWarning = ({ limit }: { limit: number }) => { return (
} defaultVisibility={true} hideCollapse @@ -363,8 +377,7 @@ const FreeProjectLimitWarning = () => { description={

- 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.`}

} @@ -411,6 +424,7 @@ const EmptyPaymentMethodWarning = observer( return (
} defaultVisibility={true} hideCollapse diff --git a/studio/pages/project/[ref]/auth/settings.tsx b/studio/pages/project/[ref]/auth/settings.tsx index 1810263dead..8a9c3a652e5 100644 --- a/studio/pages/project/[ref]/auth/settings.tsx +++ b/studio/pages/project/[ref]/auth/settings.tsx @@ -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 = () => {
{ + 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' ? ( <> { errorMessage="Please enter the vonage auth token." /> - ) : (smsProviderModel?.SMS_PROVIDER === 'vonage' ? ( + ) : smsProviderModel?.SMS_PROVIDER === 'vonage' ? ( <> { errorMessage="Please enter the twilio message service sid." /> - )))} + )} )} { // "Enable phone confirmations" should be toggled off. handleToggle('SMS_AUTOCONFIRM', !value) }} - // + // checked={!model.SMS_AUTOCONFIRM} descriptionText={authConfig.properties.SMS_AUTOCONFIRM.help} /> diff --git a/studio/pages/project/[ref]/settings/billing.tsx b/studio/pages/project/[ref]/settings/billing.tsx index 01fdaaa8482..02162eb8933 100644 --- a/studio/pages/project/[ref]/settings/billing.tsx +++ b/studio/pages/project/[ref]/settings/billing.tsx @@ -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 = ({ project }) => { const { ui } = useStore() const projectRef = ui.selectedProject?.ref + const subscriptionStats = useSubscriptionStats() + const [loading, setLoading] = useState(true) const [subscription, setSubscription] = useState() const [paygStats, setPaygStats] = useState>() @@ -121,6 +123,7 @@ const Settings: FC = ({ project }) => { project={project} subscription={subscription} paygStats={paygStats} + subscriptionStats={subscriptionStats} currentPeriodStart={subscription?.billing.current_period_start} currentPeriodEnd={subscription?.billing.current_period_end} /> diff --git a/studio/pages/project/[ref]/settings/general.tsx b/studio/pages/project/[ref]/settings/general.tsx index 1f72cc3c688..3b8f330b4df 100644 --- a/studio/pages/project/[ref]/settings/general.tsx +++ b/studio/pages/project/[ref]/settings/general.tsx @@ -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) { diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx index 3eff0222c7a..b70ae7dc39e 100644 --- a/studio/pages/project/[ref]/settings/logs/[type].tsx +++ b/studio/pages/project/[ref]/settings/logs/[type].tsx @@ -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(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 (
@@ -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} />
-
- - {isSelectQuery && ( -
- {/* // we don't have a slim Alert component yet */} - - - Custom queries are restricted to a {type === 'database' ? '2 hour' : '7 day'}{' '} - querying window. - -
- )} -
+
+ {isSelectQuery && ( + } + title={`Custom queries are restricted to a ${ + type === 'database' ? '2 hour' : '7 day' + } querying window.`} + /> + )}
{editorValue && (