Merge branch 'master' into chore/mono

This commit is contained in:
Jonathan Summers-Muir committed 2022-04-26 16:21:04 +08:00
commit 81a55e69c8
134 files changed
+2109 -1320

No files matched your search

+1 -1
View File
@@ -67,7 +67,7 @@ jobs:
if: always()
with:
github_repo: test-reports
allure_results: allure-results
allure_results: tests/allure-results
allure_history: allure-history
subfolder: supabase-integration
keep_reports: 50
+2 -2
View File
@@ -50,7 +50,7 @@ To build Supabase, you clone the source code repository:
### Choosing Directory
Before you start a development server, you must choose if you want to work on the [Supabase Website](https://supabase.com), [Supabase Docs](https://supabase.com/docs/), or [Supabase Studio](https://app.supabase.io).
Before you start a development server, you must choose if you want to work on the [Supabase Website](https://supabase.com), [Supabase Docs](https://supabase.com/docs), or [Supabase Studio](https://app.supabase.io).
1. Go to the [Supabase Website](https://supabase.com) directory
@@ -58,7 +58,7 @@ Before you start a development server, you must choose if you want to work on th
cd www
```
Go to the [Supabase Docs](https://supabase.com/docs/) directory
Go to the [Supabase Docs](https://supabase.com/docs) directory
```sh
cd web
+1 -1
View File
@@ -55,7 +55,7 @@ Supabase is a combination of open source tools. We’re building the features of
**Architecture**
Supabase is a [hosted platform](https://app.supabase.io). You can sign up and start using Supabase without installing anything.
You can also [self-host](https://supabase.com/docs/guides/self-hosting) and [develop locally](https://supabase.com/docs/guides/local-development).
You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and [develop locally](https://supabase.com/docs/guides/local-development).
![Architecture](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -34,7 +34,7 @@ Supabase is hiring Developer Success Engineers. Help us build the easiest databa
## Bonus if you:
- Are a Polyglot developer, with some skill in NodeJS/Javascript.
- Are a Polyglot developer, with some skill in Node.js/JavaScript.
- Have PostgreSQL experience.
## About Supabase
+1 -1
View File
@@ -42,7 +42,7 @@ You've worked in multi disciplinary teams before, working with designers, develo
## Must have skills
- 5+ years experience in web development (Javascript, HTML5, CSS3, SCSS/LESS).
- 5+ years experience in web development (JavaScript, HTML5, CSS3, SCSS/LESS).
- Have an understanding of modern JS (ES6 etc).
- Experience with Node.
- Experience with React (We use NextJS everywhere).
@@ -2,6 +2,6 @@
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
This directory contains JavaScript plugins that you want to run before mounting the root Vue.js application.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
+1 -1
View File
@@ -46,7 +46,7 @@ Supabase عبارة عن مجموعة من الأدوات مفتوحة المص
**الهيكلة الحالية**
(supabase) هي [منصة مستضافة](https://app.supabase.io), يمكنك التسجيل والبدأ باستخدامها دون الحاجة لتثبيت أي شئ. يمكنك أيضا [استضافتها ذاتيا](https://supabase.com/docs/guides/self-hosting) و [تطويرها داخليا](https://supabase.com/docs/guides/local-development).
(supabase) هي [منصة مستضافة](https://app.supabase.io), يمكنك التسجيل والبدأ باستخدامها دون الحاجة لتثبيت أي شئ. يمكنك أيضا [استضافتها ذاتيا](https://supabase.com/docs/guides/hosting/overview) و [تطويرها داخليا](https://supabase.com/docs/guides/local-development).
![Architecture](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -49,7 +49,7 @@
**স্থাপত্য**
সুপাবেস হল একটি [হোস্ট করা প্ল্যাটফর্ম](https://app.supabase.io)। আপনি সাইন আপ করতে পারেন এবং কিছু ইনস্টল না করে সুপাবেস ব্যবহার শুরু করতে পারেন।
এছাড়াও আপনি [স্ব-হোস্ট](https://supabase.com/docs/guides/self-hosting) এবং [স্থানীয়ভাবে বিকাশ](https://supabase.com/docs/guides/local-development) করতে পারেন।
এছাড়াও আপনি [স্ব-হোস্ট](https://supabase.com/docs/guides/hosting/overview) এবং [স্থানীয়ভাবে বিকাশ](https://supabase.com/docs/guides/local-development) করতে পারেন।
![আর্কিটেকচার](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -46,7 +46,7 @@ Supabase és una combinació d’eines de codi obert. Estem construint les funci
**Arquitectura actual**
Supabase és una [plataforma allotjada](https://app.supabase.io). Et pots registrar i començar a utilitzar Supabase sense instal·lar res. També podeu tenir una [_host_ pròpia](https://supabase.com/docs/guides/self-hosting) i [desenvolupar localment](https://supabase.com/docs/guides/local-development).
Supabase és una [plataforma allotjada](https://app.supabase.io). Et pots registrar i començar a utilitzar Supabase sense instal·lar res. També podeu tenir una [_host_ pròpia](https://supabase.com/docs/guides/hosting/overview) i [desenvolupar localment](https://supabase.com/docs/guides/local-development).
![Arquitectura](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -46,7 +46,7 @@ Supabase es una combinación de herramientas de código abierto. Estamos constru
**Arquitectura actual**
Supabase es una [plataforma alojada](https://app.supabase.io). Te puedes registrar y comenzar a utilizar Supabase sin instalar nada. También puedes tener un [_host_ propio](https://supabase.com/docs/guides/self-hosting) y [desarrollar en local](https://supabase.com/docs/guides/local-development).
Supabase es una [plataforma alojada](https://app.supabase.io). Te puedes registrar y comenzar a utilizar Supabase sin instalar nada. También puedes tener un [_host_ propio](https://supabase.com/docs/guides/hosting/overview) y [desarrollar en local](https://supabase.com/docs/guides/local-development).
![Arquitectura](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -49,7 +49,7 @@ Supabase est une combinaison d'outils open source. Nous développons les fonctio
**Architecture actuelle**
Supabase est une [plateforme hébergée](https://app.supabase.io). Vous pouvez vous inscrire et commencer à utiliser Supabase sans rien installer.
Vous pouvez aussi [auto-héberger](https://supabase.com/docs/guides/self-hosting) et [développer localement](https://supabase.com/docs/guides/local-development).
Vous pouvez aussi [auto-héberger](https://supabase.com/docs/guides/hosting/overview) et [développer localement](https://supabase.com/docs/guides/local-development).
![Architecture](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -47,7 +47,7 @@ A Supabase nyílt forráskódú eszközök kombinációja.A Firebase funkcióit
**Jelenlegi architektúra**
Supabase egy [hosztolt platform](https://app.supabase.io). Regisztrálással letöltés nélkül is elkezdheted használni.
De [hosztolhatod magadnak](https://supabase.com/docs/guides/self-hosting) és akár [fejlesztheted helyileg](https://supabase.com/docs/guides/local-development).
De [hosztolhatod magadnak](https://supabase.com/docs/guides/hosting/overview) és akár [fejlesztheted helyileg](https://supabase.com/docs/guides/local-development).
![Architektúra](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -46,7 +46,7 @@ Supabase adalah gabungan alat sumber terbuka. Kami membina ciri Firebase menggun
**Seni bina semasa**
Supabase ialah [platform yang dihoskan](https://app.supabase.io). Anda boleh mendaftar dan mula menggunakan Supabase tanpa memasang apa-apa.
Anda juga boleh [host sendiri](https://supabase.com/docs/guides/self-hosting) dan [lokal](https://supabase.com/docs/guides/local-development).
Anda juga boleh [host sendiri](https://supabase.com/docs/guides/hosting/overview) dan [lokal](https://supabase.com/docs/guides/local-development).
![Seni bina](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+190
View File
@@ -0,0 +1,190 @@
<p align="center">
<img width="300" src="https://raw.githubusercontent.com/supabase/supabase/master/web/static/supabase-light-rounded-corner-background.svg"/>
</p>
---
# சுபாபேஸ்
[சுபாபேஸ்](https://supabase.com) என்பது ஒரு திறந்த மூல ஃபயர்பேஸ் மாற்றாகும். நிறுவன-தர திறந்த மூல கருவிகளைப் பயன்படுத்தி ஃபயர்பேஸ் இன் அம்சங்களை நாங்கள் உருவாக்குகிறோம்.
- [x] ஹோஸ்ட் செய்யப்பட்ட பூஸ்ட்ஜீஆர்இஎஸ் தரவுத்தளம். [ஆவணம்](https://supabase.com/docs/guides/database)
- [x] அங்கீகாரம் மற்றும் அங்கீகாரம்.. [ஆவணம்](https://supabase.com/docs/guides/auth)
- [x] தானாக உருவாக்கப்பட்ட ஏபிஐ கள்.
- [x] ராஸ்ட். [ஆவணம்](https://supabase.com/docs/guides/api#rest-api)
- [x] நிகழ்நேர சந்தாக்கள். [ஆவணம்](https://supabase.com/docs/guides/api#realtime-api)
- [x] கிராப்க்யூஎல் (பீட்டா). [ஆவணம்](https://supabase.com/docs/guides/api#graphql-api)
- [x] செயல்பாடுகள்.
- [x] தரவுத்தளம் செயல்பாடுகள். [ஆவணம்](https://supabase.com/docs/guides/database/functions)
- [x] விளிம்பு செயல்பாடுகள் [ஆவணம்](https://supabase.com/docs/guides/functions)
- [x] கோப்பு சேமிப்பகம். [ஆவணம்](https://supabase.com/docs/guides/storage)
- [x] டாஷ்போர்டு
![Supabase Dashboard](https://raw.githubusercontent.com/supabase/supabase/master/www/public/images/github/supabase-dashboard.png)
## ஆவணப்படுத்தல்
முழு ஆவணங்களுக்கு, பார்வையிடவும் [supabase.com/docs](https://supabase.com/docs)
எவ்வாறு பங்களிப்பது என்பதைப் பார்க்க, பார்வையிடவும் [தொடங்குதல்](./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) இல் பதிவு செய்யலாம், ஆனால் எங்களுக்கு எளிதாக செல்லுங்கள், ஒரு சில கின்க்ஸ் உள்ளன
- [x] பொது பீட்டா: பெரும்பாலான அல்லாத நிறுவன பயன்பாட்டு-வழக்குகளுக்கு போதுமான நிலையானது
- [ ] பொது: உற்பத்தி-தயார்
நாங்கள் தற்போது பொது பீட்டாவில் இருக்கிறோம். முக்கிய துப்பிப்புகளைப் பற்றி அறிவிக்க இந்த ரெப்போவின் "வெளியீடுகளைப்" பாருங்கள்.
<kbd><img src="https://gitcdn.link/repo/supabase/supabase/master/web/static/watch-repo.gif" alt="இந்த ரெப்போவைப் பாருங்கள்"/></kbd>
---
## இது எவ்வாறு செயல்படுகிறது
சுபாபேஸ் என்பது திறந்த மூல கருவிகளின் கலவையாகும். நிறுவன-தர, திறந்த மூல தயாரிப்புகளைப் பயன்படுத்தி ஃபயர்பேஸ் இன் அம்சங்களை நாங்கள் உருவாக்குகிறோம். கருவிகள் மற்றும் சமூகங்கள் இருந்தால், ஒரு எம்ஐடி, அப்பாச்சி 2, அல்லது அதற்கு சமமான திறந்த உரிமத்துடன், நாங்கள் அந்த கருவியைப் பயன்படுத்துவோம் மற்றும் ஆதரிப்போம். கருவி இல்லை என்றால், நாமே அதை உருவாக்கி திறக்கிறோம். சுபாபேஸ் ஃபயர்பேஸின் 1-க்கு-1 மேப்பிங் அல்ல. திறந்த மூல கருவிகளைப் பயன்படுத்தி டெவலப்பர்களுக்கு ஃபயர்பேஸ் போன்ற டெவலப்பர் அனுபவத்தை வழங்குவதே எங்கள் நோக்கம்.
**கட்டிடக்கலை**
சுபாபேஸ் ஒரு [ஹோஸ்ட் செய்யப்பட்ட தளம்](https://app.supabase.io). எதையும் நிறுவாமல் நீங்கள் பதிவுசெய்து Supabase ஐப் பயன்படுத்தத் தொடங்கலாம்.
நீங்கள் [சுய ஹோஸ்ட்](https://supabase.com/docs/guides/hosting/overview) மற்றும் [உள்நாட்டில் உருவாக்க](https://supabase.com/docs/guides/local-development) ஆகியவற்றையும் செய்யலாம்.
![கட்டிடக்கலை](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
- [பூஸ்ட்ஜீஆர்இஎஸ்கியூஎல்](https://www.postgresql.org/) என்பது 30 ஆண்டுகளுக்கும் மேலான செயலில் உள்ள வளர்ச்சியுடன் கூடிய ஒரு பொருள்-தொடர்புடைய தரவுத்தள அமைப்பாகும், இது நம்பகத்தன்மை, அம்ச உறுதிப்பாடு மற்றும் செயல்திறனுக்கான வலுவான நற்பெயரைப் பெற்றுள்ளது.
- [நிகழ்நேரம்](https://github.com/supabase/realtime) என்பது ஒரு எலிக்சர் சேவையகமாகும், இது வெப்சாக்கெட்டுகளைப் பயன்படுத்தி பூஸ்ட்ஜீஆர்இஎஸ்கியூஎல் செருகல்கள், புதுப்பிப்புகள் மற்றும் நீக்குதல்களைக் கேட்க உங்களை அனுமதிக்கிறது. நிகழ்நேர கருத்துக் கணிப்புகள் தரவுத்தள மாற்றங்களுக்கான போஸ்ட்கிரஸின் உள்ளமைக்கப்பட்ட பிரதிபலிப்பு செயல்பாடு, ஜே.எஸ்.ஆனுக்கு மாற்றங்களை மாற்றுகிறது, பின்னர் அங்கீகரிக்கப்பட்ட வாடிக்கையாளர்களுக்கு வெப்சாக்கெட்டுகள் மூலம் ஜே.எஸ்.என் ஐ ஒளிபரப்புகிறது.
- [போஸ்ட்ஜீராஸ்ட்](http://postgrest.org/) என்பது உங்கள் பூஸ்ட்ஜீஆர்இஎஸ்கியூஎல் தரவுத்தளத்தை நேரடியாக ராஸ்ட் ஏபிஐ ஆக மாற்றும் ஒரு வலை சேவையகமாகும்
- [சேமிப்பகம்](https://github.com/supabase/storage-api) அனுமதிகளை நிர்வகிக்க போஸ்ட்கிரெஸைப் பயன்படுத்தி, S3 இல் சேமிக்கப்பட்ட கோப்புகளை நிர்வகிப்பதற்கான ராஸ்ட் இடைமுகத்தை வழங்குகிறது.
- [பூஸ்ட்ஜீஆர்இ-மெட்டா](https://github.com/supabase/postgres-meta) என்பது உங்கள் போஸ்ட்கிரேஸை நிர்வகிப்பதற்கான ஒரு ராஸ்ட் ஏபிஐ ஆகும், இது அட்டவணைகளைப் பெறவும், பாத்திரங்களைச் சேர்க்கவும், கேள்விகளை இயக்கவும் உங்களை அனுமதிக்கிறது.
- [கூட்ரூ](https://github.com/netlify/gotrue) என்பது பயனர்களை நிர்வகிப்பதற்கும் எஸ்டபிள்யூடி டோக்கன்கள் வழங்குவதற்கும் எஸ்டபிள்யூடி அடிப்படையிலான ஏபிஐ ஆகும்.
- [காங்](https://github.com/Kong/kong) என்பது கிளவுட்-நேட்டிவ் ஏபிஐ நுழைவாயில் ஆகும்.
#### வாடிக்கையாளர் நூலகங்கள்
கிளையன்ட் லைப்ரரிகளுக்கான எங்கள் அணுகுமுறை மட்டு. ஒவ்வொரு துணை நூலகமும் ஒரு வெளிப்புற அமைப்பிற்கான ஒரு முழுமையான செயலாக்கமாகும். ஏற்கனவே உள்ள கருவிகளை நாங்கள் ஆதரிக்கும் வழிகளில் இதுவும் ஒன்றாகும்.
<table style="table-layout:fixed; white-space: nowrap;">
<tr>
<th>மொழி</th>
<th>வாடிக்கையாளர்</th>
<th colspan="4">
அம்சம்-வாடிக்கையாளர் (சுபாபேஸ் கிளையண்டில் தொகுக்கப்பட்டுள்ளது)</th>
</tr>
<tr>
<th></th>
<th>சுபாபேஸ்</th>
<th><a href="https://github.com/postgrest/postgrest" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்</a></th>
<th><a href="https://github.com/supabase/gotrue" target="_blank" rel="noopener noreferrer">கூட்ரூ</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>
<!-- TEMPLATE FOR NEW ROW -->
<!-- START ROW
<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">realtime-lang</a></td>
<td><a href="https://github.com/supabase-community/storage-lang" target="_blank" rel="noopener noreferrer">storage-lang</a></td>
</tr>
END ROW -->
<th colspan="6">⚡️ அதிகாரி ⚡️</th>
<tr>
<td>ஜாவாஸ்கிரிப்ட் (டைப்ஸ்கிரிப்ட்)</td>
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">சுபாபேஸ்-ஜேஎஸ்</a></td>
<td><a href="https://github.com/supabase/postgrest-js" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்-ஜேஎஸ்</a></td>
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">கூட்ரூ-ஜேஎஸ்</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">சேமிப்பு-ஜேஎஸ்</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">சுபாபேஸ்-ஸேசர்ப</a></td>
<td><a href="https://github.com/supabase-community/postgrest-csharp" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்-ஸேசர்ப</a></td>
<td><a href="https://github.com/supabase-community/gotrue-csharp" target="_blank" rel="noopener noreferrer">கூட்ரூ-ஸேசர்ப</a></td>
<td><a href="https://github.com/supabase-community/realtime-csharp" target="_blank" rel="noopener noreferrer">நிகழ்நேரம்-ஸேசர்ப</a></td>
<td><a href="https://github.com/supabase-community/storage-csharp" target="_blank" rel="noopener noreferrer">சேமிப்பு-ஸேசர்ப</a></td>
</tr>
<tr>
<td>டார்ட் (ஃப்ளடர்)</td>
<td><a href="https://github.com/supabase/supabase-dart" target="_blank" rel="noopener noreferrer">சுபாபேஸ்-டார்ட்</a></td>
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்-டார்ட்</a></td>
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">கூட்ரூ-டார்ட்</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">சேமிப்பு-டார்ட்</a></td>
</tr>
<tr>
<td>கோ</td>
<td>-</td>
<td><a href="https://github.com/supabase-community/postgrest-go" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்-கோ</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">கூட்ரூ-ஜாவா</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">போஸ்ட்ஜீராஸ்ட்-கேடி</a></td>
<td><a href="https://github.com/supabase-community/gotrue-kt" target="_blank" rel="noopener noreferrer">கூட்ரூ-கேடி</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">சுபாபேஸ்-பை</a></td>
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்-பை</a></td>
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">கூட்ரூ-பை</a></td>
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">நிகழ்நேரம்-பை</a></td>
<td>-</td>
</tr>
<tr>
<td>ராபி</td>
<td><a href="https://github.com/supabase-community/supabase-rb" target="_blank" rel="noopener noreferrer">சுபாபேஸ்-ஆர்பி</a></td>
<td><a href="https://github.com/supabase-community/postgrest-rb" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்-ஆர்பி</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">போஸ்ட்ஜீராஸ்ட்-ஆர்எஸ்</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">சுபாபேஸ்-சஃப்ட்</a></td>
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">போஸ்ட்ஜீராஸ்ட்-சஃப்ட்</a></td>
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">கூட்ரூ-சஃப்ட்</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 -1
View File
@@ -55,7 +55,7 @@ Supabase là sự kết hợp của các công cụ mã nguồn mở. Các tính
**Kiến trúc**
Supabase là một [nền tảng lưu trữ cơ sở dữ liệu](https://app.supabase.io). Bạn có thể đăng ký và bắt đầu sử dụng Supabase mà không cần cài đặt bất kỳ thứ gì.
Bạn cũng có thể [tự quản lý](https://supabase.com/docs/guides/self-hosting) và [phát triển cục bộ](https://supabase.com/docs/guides/local-development).
Bạn cũng có thể [tự quản lý](https://supabase.com/docs/guides/hosting/overview) và [phát triển cục bộ](https://supabase.com/docs/guides/local-development).
![Kiến trúc](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1 -1
View File
@@ -53,7 +53,7 @@ Supabase 是一个开源工具的组合。我们正在使用企业级的开源
**当前架构**
Supabase 是一个[托管平台](https://app.supabase.io)。你可以注册并开始使用 Supabase,而无需安装任何软件。
你也可以[自托管](https://supabase.com/docs/guides/self-hosting)和[本地开发](https://supabase.com/docs/guides/local-development)。
你也可以[自托管](https://supabase.com/docs/guides/hosting/overview)和[本地开发](https://supabase.com/docs/guides/local-development)。
![架构](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
+1
View File
@@ -32,6 +32,7 @@
- [Spanish / Español](/i18n/README.es.md)
- [Simplified Chinese / 简体中文](/i18n/README.zh-cn.md)
- [Swedish / Svenska](/i18n/README.sv.md)
- [Tamil / தமிழ்](/i18n/README.ta.md)
- [Thai / ไทย](/i18n/README.th.md)
- [Traditional Chinese / 繁体中文](/i18n/README.zh-tw.md)
- [Turkish / Türkçe](/i18n/README.tr.md)
@@ -10,12 +10,14 @@ export const chargeableProducts: ChargeableProduct[] = [
attribute: 'total_db_size_bytes',
costPerUnit: 0.125,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
},
{
title: 'Database Egress',
attribute: 'total_db_egress_bytes',
attribute: 'total_egress_modified',
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
},
],
},
@@ -28,12 +30,14 @@ export const chargeableProducts: ChargeableProduct[] = [
attribute: 'total_storage_size_bytes',
costPerUnit: 0.021,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
},
{
title: 'Storage Egress',
attribute: 'total_storage_egress',
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
},
],
},
@@ -3,14 +3,14 @@ import dayjs from 'dayjs'
import { Typography } from '@supabase/ui'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import { ChargeableProduct, ProductFeature } from './PAYGUsage.types'
import { ChargeableProduct, PaygStats, ProductFeature } from './PAYGUsage.types'
import { deriveProductCost } from './PAYGUsage.utils'
import { chargeableProducts } from './PAYGUsage.constants'
import { DATE_FORMAT } from 'lib/constants'
import { Dictionary } from '@supabase/grid'
interface Props {
paygStats: Dictionary<number>
paygStats: PaygStats
}
const PAYGUsage: FC<Props> = ({ paygStats }) => {
@@ -21,6 +21,7 @@ const PAYGUsage: FC<Props> = ({ paygStats }) => {
<div className="flex flex-col">
{chargeableProducts.map((product: ChargeableProduct) => {
const productCost = deriveProductCost(paygStats, product)
return (
<div
key={product.title}
@@ -63,7 +64,7 @@ const PAYGUsage: FC<Props> = ({ paygStats }) => {
endDate={endDate}
interval="1d"
highlight="maximum"
defaultChartStyle="line"
defaultChartStyle="bar"
customDateFormat={'MMM D, YYYY'}
/>
</div>
@@ -9,4 +9,9 @@ export interface ProductFeature {
attribute: string
costPerUnit: number
unitQuantity: number
pricingModel: PricingModel
}
export type PaygStats = { [key: string]: { sum: number; max: number } }
export type PricingModel = 'sum' | 'max'
@@ -1,8 +1,18 @@
import { sum } from 'lodash'
import { ChargeableProduct, ProductFeature } from './PAYGUsage.types'
import { get, maxBy, sum } from 'lodash'
import { ChargeableProduct, PaygStats, ProductFeature } from './PAYGUsage.types'
export const deriveFeatureCost = (paygStats: any, feature: ProductFeature) => {
const maximumValueOfTheMonth = paygStats?.[feature.attribute] ?? 0
/**
*
* Derives the most appropriate cost
* depending on what kind of product feature we are displaying cost for.
*
* @param paygStats
* @param feature
*
* @returns number
*/
export const deriveFeatureCost = (paygStats: PaygStats | undefined, feature: ProductFeature) => {
const maximumValueOfTheMonth = paygStats?.[feature.attribute]?.[feature.pricingModel] ?? 0
return (maximumValueOfTheMonth / feature.unitQuantity) * feature.costPerUnit
}
@@ -12,11 +12,12 @@ import CostBreakdownRow from './CostBreakdownRow'
import { StripeSubscription } from './Subscription.types'
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
import { PaygStats } from '../PAYGUsage/PAYGUsage.types'
interface Props {
project: any
subscription: StripeSubscription
paygStats?: Dictionary<number>
paygStats: PaygStats | undefined
loading?: boolean
showProjectName?: boolean
currentPeriodStart: number
@@ -132,12 +133,16 @@ const Subscription: FC<Props> = ({
{isPayg &&
chargeableProducts.map((product) =>
product.features.map((feature) => {
const cost = deriveFeatureCost(paygStats, feature).toFixed(3)
const derivedCost = deriveFeatureCost(paygStats, feature)
const cost = derivedCost.toFixed(3)
return (
<CostBreakdownRow
key={feature.attribute}
item={feature.title}
amount={formatBytes(paygStats?.[feature.attribute] ?? 0)}
amount={formatBytes(
paygStats?.[feature.attribute]?.[feature.pricingModel] ?? 0
)}
unitPrice={`${feature.costPerUnit}/GB`}
price={cost}
/>
@@ -14,8 +14,6 @@ interface Props {
const FunctionsListItem: FC<Props> = ({ function: item }) => {
const { ui } = useStore()
console.log('_function', item)
const is_functionConfirmed = item.email_confirmed_at || item.phone_confirmed_at
const router = useRouter()
@@ -1,6 +1,6 @@
export const CLIENT_LIBRARIES = [
{
language: 'Javascript',
language: 'JavaScript',
officialSupport: true,
releaseState: undefined,
docsUrl: 'https://supabase.com/docs/reference/javascript/installing',
@@ -1,11 +1,6 @@
import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings'
import { FC } from 'react'
import { Typography } from '@supabase/ui'
import GetStartedPanel from './GetStartedPanel'
import {
DisplayApiSettings,
DisplayConfigSettings,
} from 'components/to-be-cleaned/DisplayProjectSettings'
interface Props {}
@@ -1,15 +1,6 @@
import dayjs from 'dayjs'
import { useEffect, useState, useMemo } from 'react'
import {
Alert,
Button,
IconDownloadCloud,
IconEye,
IconEyeOff,
IconLoader,
Input,
Typography,
} from '@supabase/ui'
import { Alert, Button, IconEye, IconEyeOff, Input } from '@supabase/ui'
import DataGrid from '@supabase/react-data-grid'
import LogSelection from './LogSelection'
@@ -262,52 +253,69 @@ const LogTable = ({
!isLoading ? (
<>
<div className="py-4 w-full h-full flex-col space-y-12">
<div
className={[
'transition-all duration-500 delay-200',
'flex flex-col items-center gap-6 text-center',
'mt-16 opacity-100 scale-100 justify-center',
].join(' ')}
>
<div className="flex flex-col gap-1">
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center"></div>
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center">
<div className="absolute right-1 -bottom-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{!error && (
<div
className={`transition-all
duration-500
delay-200
flex
flex-col
items-center
gap-6
text-center
mt-16
opacity-100
scale-100
justify-center
`}
>
<>
<div className="flex flex-col gap-1">
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center"></div>
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center">
<div className="absolute right-1 -bottom-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-1 px-5">
<h3 className="text-lg text-scale-1200">No results</h3>
<p className="text-sm text-scale-900">
Try another search, or adjusting the filters
</p>
</div>
</>
</div>
<div className="flex flex-col gap-1 px-5">
<h3 className="text-lg text-scale-1200">No results</h3>
<p className="text-sm text-scale-900">
Try another search, or adjusting the filters
</p>
</div>
</div>
)}
{error && (
<div className="flex justify-center px-5">
<Alert
variant="danger"
title="Sorry! An error occured when fetching data."
withIcon
className="max-w-xl"
className="w-1/2"
>
<Input.TextArea
size="small"
value={JSON.stringify(error, null, 2)}
size="tiny"
value={
typeof error === 'string' ? error : JSON.stringify(error, null, 2)
}
borderless
className="font-mono w-full mt-4"
copy
@@ -1,35 +1,41 @@
import { Button, Dropdown, IconClock } from '@supabase/ui'
import { Alert, Button, Dropdown, IconClock } from '@supabase/ui'
import { DatePicker } from 'components/ui/DatePicker'
import { useState, useEffect } from 'react'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { DatetimeHelper, getDefaultHelper, LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from '.'
type ToFrom = { to: string; from: string }
interface Props {
to?: string
from?: string
onChange: ({ to, from }: ToFrom) => void
to: string
from: string
onChange: React.ComponentProps<typeof DatePicker>["onChange"]
helpers: DatetimeHelper[]
}
const HELPERS = [
{ text: 'Last hour', value: '1_hour' },
{ text: 'Last 3 hours', value: '3_hour' },
{ text: 'Last day', value: '1_day' },
]
const DEFAULT_HELPER_VALUE = HELPERS[0].value
const DatePickers: React.FC<Props> = ({ to, from, onChange }) => {
const [helperValue, setHelperValue] = useState<string>('')
const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
const defaultHelper = getDefaultHelper(helpers)
const [helperValue, setHelperValue] = useState<string>(to || from ? '' : defaultHelper.text)
const handleHelperChange = (newValue: string) => {
setHelperValue(newValue)
const toDate = new Date()
const fromDate = {
'1_hour': new Date(toDate.getTime() - 60 * 60 * 1000),
'3_hour': new Date(toDate.getTime() - 3 * 60 * 60 * 1000),
'1_day': new Date(toDate.getTime() - 24 * 60 * 60 * 1000),
}[newValue]
if (onChange) onChange({ to: toDate.toISOString(), from: fromDate?.toISOString() as string })
const selectedHelper = helpers.find((h) => h.text === newValue)
if (onChange && selectedHelper) {
onChange({ to: selectedHelper.calcTo(), from: selectedHelper.calcFrom() })
}
}
const selectedHelper = HELPERS.find((h) => h.value === (helperValue || DEFAULT_HELPER_VALUE))
const selectedHelper = helpers.find((helper) => {
if (to === helper.calcTo() && from === helper.calcFrom()) {
return true
} else if (helper.text === helperValue) {
return true
} else {
return false
}
})
useEffect(() => {
if (selectedHelper && helperValue !== selectedHelper.text) {
setHelperValue(selectedHelper.text)
}
}, [selectedHelper])
return (
<div className="flex items-center">
<Dropdown
@@ -38,12 +44,9 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange }) => {
align="start"
overlay={
<>
<Dropdown.RadioGroup
onChange={handleHelperChange}
value={helperValue || DEFAULT_HELPER_VALUE}
>
{HELPERS.map((helper) => (
<Dropdown.Radio key={helper.value} value={helper.value}>
<Dropdown.RadioGroup onChange={handleHelperChange} value={selectedHelper?.text || ''}>
{helpers.map((helper) => (
<Dropdown.Radio key={helper.text} value={helper.text}>
{helper.text}
</Dropdown.Radio>
))}
@@ -57,18 +60,27 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange }) => {
icon={<IconClock size={12} />}
className="rounded-r-none"
>
{selectedHelper?.text}
{selectedHelper?.text || defaultHelper.text}
</Button>
</Dropdown>
<DatePicker
triggerButtonClassName="rounded-l-none"
triggerButtonType={helperValue ? 'default' : 'secondary'}
onChange={(value: ToFrom) => {
triggerButtonType={selectedHelper ? 'default' : 'secondary'}
onChange={(value) => {
setHelperValue('')
if (onChange) onChange(value)
}}
to={!helperValue ? to : undefined}
from={!helperValue ? from : undefined}
renderFooter={({ to, from }) => {
if (to && from && Math.abs(dayjs(from).diff(dayjs(to), 'day')) > LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD) {
return (
<Alert title={""} variant="warning" className="mx-3 pl-2 pr-2 pt-1 pb-2">
Large ranges may result in memory errors for big projects.
</Alert>
)
}
}}
/>
</div>
)
@@ -15,7 +15,6 @@ const SavedQueriesItem: FC<Props> = ({ item }: Props) => {
const router = useRouter()
const { ref } = router.query
console.log('item in component', item)
return (
<>
<Table.tr key={item.id} className="expandable-tr">
@@ -47,7 +46,9 @@ const SavedQueriesItem: FC<Props> = ({ item }: Props) => {
type="alternative"
iconRight={<IconPlay size={10} />}
onClick={() =>
router.push(`/project/${ref}/logs-explorer?sql=${encodeURIComponent(item.content.sql)}`)
router.push(
`/project/${ref}/logs-explorer?sql=${encodeURIComponent(item.content.sql)}`
)
}
>
Run
@@ -1,4 +1,7 @@
import { FilterTableSet, LogTemplate } from '.'
import dayjs from 'dayjs'
import { DatetimeHelper, FilterTableSet, LogTemplate } from '.'
export const LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD = 4
export const TEMPLATES: LogTemplate[] = [
{
@@ -185,10 +188,8 @@ export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
database: 'Database',
}
const _SQL_FILTER_COMMON = {
'search_query': (value: string) => `regexp_contains(event_message, '${value}')`
search_query: (value: string) => `regexp_contains(event_message, '${value}')`,
}
export const SQL_FILTER_TEMPLATES: any = {
@@ -424,3 +425,43 @@ export const LOGS_TAILWIND_CLASSES = {
log_selection_x_padding: 'px-8',
space_y: 'px-6',
}
export const PREVIEWER_DATEPICKER_HELPERS: DatetimeHelper[] = [
{
text: 'Last hour',
calcFrom: () => dayjs().subtract(1, 'hour').startOf('hour').toISOString(),
calcTo: () => '',
},
{
text: 'Last 3 hours',
calcFrom: () => dayjs().subtract(3, 'hour').startOf('hour').toISOString(),
calcTo: () => '',
},
{
text: 'Last day',
calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
calcTo: () => '',
default: true,
},
]
export const EXPLORER_DATEPICKER_HELPERS: DatetimeHelper[] = [
{
text: 'Last day',
calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
calcTo: () => '',
default: true,
},
{
text: 'Last 3 days',
calcFrom: () => dayjs().subtract(3, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
{
text: 'Last 7 days',
calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
]
export const getDefaultHelper = (helpers: DatetimeHelper[]) =>
helpers.find((helper) => helper.default) || helpers[0]
@@ -1,19 +1,29 @@
import React from "react"
interface Metadata {
[key: string]: string | number | Object | Object[]
}
export type LogSearchCallback = (filters: { query: string; to?: string; from?: string; fromMicro?: number, toMicro?: number }) => void
export type DatePickerToFrom = { to: string | null; from: string | null }
export type LogSearchCallback = (
filters: {
query: string
} & DatePickerToFrom
) => void
export interface LogsWarning {
text: string | React.ReactNode
link?: string
linkText?: string
}
export interface LogsEndpointParams {
// project ref
project: string
// micro unix timestamp
timestamp_start?: string
// micro timestamp
timestamp_end?: string
period_start?: string
period_end?: string
sql: string
rawSql: string
iso_timestamp_start?: string
iso_timestamp_end?: string
sql?: string
rawSql?: string
}
export interface LogData {
@@ -65,7 +75,6 @@ export interface FilterObject {
te?: string
}
export interface FilterSet {
label: string
key: string
@@ -91,3 +100,10 @@ export type Override = {
key: string
value: string | string[] | undefined
}
export interface DatetimeHelper {
text: string
calcTo: () => string
calcFrom: () => string
default?: boolean
}
@@ -1,37 +1,45 @@
import { get } from "lodash"
import { Filters, LogsTableName, SQL_FILTER_TEMPLATES } from "."
import { Filters, LogsTableName, SQL_FILTER_TEMPLATES } from '.'
import dayjs from 'dayjs'
import { get } from 'lodash'
/**
* Convert a micro timestamp from number/string to iso timestamp
*/
export const unixMicroToIsoTimestamp = (unix: string | number): string => {
return dayjs.unix(Number(unix) / 1000).toISOString()
}
/**
* Recursively retrieve all nested object key paths.
*
*
* TODO: move to utils
*
*
* @param obj any object
* @param parent a string representing the parent key
* @returns string[] all dot paths for keys.
*/
const getDotKeys = (obj: { [k: string]: unknown }, parent?: string): string[] => {
const keys = Object.keys(obj).filter(k => obj[k])
return keys.flatMap(k => {
const currKey = parent ? `${parent}.${k}` : k
if (typeof obj[k] === 'object') {
return getDotKeys(obj[k] as any, currKey)
} else {
return [currKey]
}
})
const keys = Object.keys(obj).filter((k) => obj[k])
return keys.flatMap((k) => {
const currKey = parent ? `${parent}.${k}` : k
if (typeof obj[k] === 'object') {
return getDotKeys(obj[k] as any, currKey)
} else {
return [currKey]
}
})
}
/**
* Root keys in the filter object are considered to be AND filters.
* Nested keys under a root key are considered to be OR filters.
*
*
* For example:
* ```
* {my_value: 'something', nested: {id: 123, test: 123 }}
* ```
* This would be converted into `WHERE (my_value = 'something') and (id = 123 or test = 123)
*
*
* The template of the filter determines the actual filter statement. If no template is provided, a generic equality statement will be used.
* This only applies for root keys of the filter.
* For example:
@@ -39,60 +47,71 @@ const getDotKeys = (obj: { [k: string]: unknown }, parent?: string): string[] =>
* {'my.nested.value': 123}
* ```
* with no template, it will be converted into `WHERE (my.nested.value = 123)
*
* @returns a whewhere statement with WHERE clause.
*
* @returns a where statement with WHERE clause.
*/
const _genWhereStatement = (table: LogsTableName, filters: Filters) => {
const keys = Object.keys(filters)
const filterTemplates = SQL_FILTER_TEMPLATES[table]
const _resolveTemplateToStatement = (dotKey: string): string | null => {
const template = filterTemplates[dotKey]
const value = get(filters, dotKey)
if (value !== undefined && typeof template === 'function') {
return template(value)
} else if (template === undefined) {
// resolve unknwon filters (possibly from filter overrides)
// no template, set a default
if (typeof value === 'string') {
return `${dotKey} = '${value}'`
} else {
return `${dotKey} = ${value}`
}
} else if (value === undefined && typeof template === 'function') {
return null
} else {
return template
}
}
const statement = keys.map(rootKey => {
if (typeof filters[rootKey] === 'object') {
// join all statements with an OR
const nestedStatement = getDotKeys(filters[rootKey] as Filters, rootKey).map(_resolveTemplateToStatement).filter(Boolean).join(" or ")
return `(${nestedStatement})`
} else {
const nestedStatement = _resolveTemplateToStatement(rootKey)
if (nestedStatement === null) return null
return `(${nestedStatement})`
}
}).filter(Boolean)
// join all root statements with AND
.join(" and ")
if (statement) {
return 'where ' + statement
const keys = Object.keys(filters)
const filterTemplates = SQL_FILTER_TEMPLATES[table]
const _resolveTemplateToStatement = (dotKey: string): string | null => {
const template = filterTemplates[dotKey]
const value = get(filters, dotKey)
if (value !== undefined && typeof template === 'function') {
return template(value)
} else if (template === undefined) {
// resolve unknwon filters (possibly from filter overrides)
// no template, set a default
if (typeof value === 'string') {
return `${dotKey} = '${value}'`
} else {
return `${dotKey} = ${value}`
}
} else if (value === undefined && typeof template === 'function') {
return null
} else if (template && value === false) {
// template present, but value is false
return null
} else {
return ''
return template
}
}
const statement = keys
.map((rootKey) => {
if (typeof filters[rootKey] === 'object') {
// join all statements with an OR
const nestedStatements = getDotKeys(filters[rootKey] as Filters, rootKey)
.map(_resolveTemplateToStatement)
.filter(Boolean)
if (nestedStatements.length > 0) {
return `(${nestedStatements.join(' or ')})`
} else {
return null
}
} else {
const nestedStatement = _resolveTemplateToStatement(rootKey)
if (nestedStatement === null) return null
return `(${nestedStatement})`
}
})
.filter(Boolean)
// join all root statements with AND
.join(' and ')
if (statement) {
return 'where ' + statement
} else {
return ''
}
}
export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
const where = _genWhereStatement(table, filters)
const where = _genWhereStatement(table, filters)
switch (table) {
case 'edge_logs':
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
switch (table) {
case 'edge_logs':
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
from ${table}
cross join unnest(metadata) as m
cross join unnest(m.request) as request
@@ -100,27 +119,27 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
${where}
limit 100
`
break
break
case 'postgres_logs':
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
case 'postgres_logs':
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
cross join unnest(metadata) as m
cross join unnest(m.parsed) as metadataparsed
${where}
limit 100
`
break
break
case 'function_logs':
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
case 'function_logs':
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
cross join unnest(metadata) as metadata
${where}
limit 100
`
break
break
case 'function_edge_logs':
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
case 'function_edge_logs':
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
cross join unnest(metadata) as m
cross join unnest(m.response) as response
cross join unnest(m.request) as request
@@ -128,8 +147,8 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
limit 100
`
default:
return ""
break
}
}
default:
return ''
break
}
}
@@ -1,4 +1,4 @@
import React, { useEffect, useReducer, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { Typography, IconAlertCircle, IconRewind, Button, Card, Input } from '@supabase/ui'
@@ -10,15 +10,13 @@ import {
LogsTableName,
QueryType,
LogEventChart,
FilterObject,
Filters,
unixMicroToIsoTimestamp,
} from 'components/interfaces/Settings/Logs'
import dayjs from 'dayjs'
import useLogsPreview from 'hooks/analytics/useLogsPreview'
import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel'
import { LOGS_TABLES } from './Logs.constants'
import { Override } from './Logs.types'
import ShimmerLine from 'components/ui/ShimmerLine'
import LoadingOpacity from 'components/ui/LoadingOpacity'
@@ -47,29 +45,26 @@ export const LogsPreviewer: React.FC<Props> = ({
tableName,
}) => {
const router = useRouter()
const { s, te, ts } = router.query
const { s, ite, its } = router.query
const [showChart, setShowChart] = useState(true)
const table = !tableName ? LOGS_TABLES[queryType] : tableName
const [
{ error, logData, params, newCount, filters, isLoading, oldestTimestamp },
{ loadOlder, setFilters, refresh, setTo, setFrom },
{ error, logData, params, newCount, filters, isLoading },
{ loadOlder, setFilters, refresh, setParams },
] = useLogsPreview(projectRef as string, table, filterOverride)
useEffect(() => {
setFilters((prev) => ({ ...prev, search_query: s as string }))
if (te) {
setTo(te as string)
} else {
setTo('')
if (ite || its) {
setParams((prev) => ({
...prev,
iso_timestamp_start: (its || '') as string,
iso_timestamp_end: (ite || '') as string,
}))
}
if (ts) {
setFrom(ts as string)
} else {
setFrom('')
}
}, [s, te, ts])
}, [s, ite, its])
const onSelectTemplate = (template: LogTemplate) => {
setFilters((prev: any) => ({ ...prev, search_query: template.searchString }))
@@ -81,33 +76,27 @@ export const LogsPreviewer: React.FC<Props> = ({
pathname: router.pathname,
query: {
...router.query,
te: undefined,
ts: undefined,
ite: undefined,
its: undefined,
// ...whereFilters,
},
})
}
const handleSearch: LogSearchCallback = async ({ query, to, from, fromMicro, toMicro }) => {
let toValue, fromValue
if (to || toMicro) {
toValue = toMicro ? toMicro : dayjs(to).valueOf() * 1000
setTo(String(toValue))
}
if (from || fromMicro) {
fromValue = fromMicro ? fromMicro : dayjs(from).valueOf() * 1000
setFrom(String(fromValue))
}
setFilters((prev) => ({ ...prev, search_query: query || '' }))
const handleSearch: LogSearchCallback = async ({ query = '', to, from }) => {
setParams((prev) => ({
...prev,
iso_timestamp_start: from || prev.iso_timestamp_start || '',
iso_timestamp_end: to || prev.iso_timestamp_end || '',
}))
setFilters((prev) => ({ ...prev, search_query: query }))
router.push({
pathname: router.pathname,
query: {
...router.query,
s: query || '',
ts: fromValue,
te: toValue,
its: from || its || '',
ite: to || ite || '',
},
})
}
@@ -123,18 +112,16 @@ export const LogsPreviewer: React.FC<Props> = ({
onRefresh={handleRefresh}
onSearch={handleSearch}
defaultSearchValue={filters.search_query as string}
defaultToValue={
params.timestamp_end ? dayjs(Number(params.timestamp_end) / 1000).toISOString() : ''
}
defaultFromValue={
params.timestamp_start
? dayjs(Number(params.timestamp_start) / 1000).toISOString()
: oldestTimestamp
? dayjs(Number(oldestTimestamp) / 1000).toISOString()
: ''
}
defaultToValue={params.iso_timestamp_end}
defaultFromValue={params.iso_timestamp_start}
onExploreClick={() => {
router.push(`/project/${projectRef}/logs-explorer?q=${encodeURIComponent(params.rawSql)}`)
router.push(
`/project/${projectRef}/logs-explorer?q=${encodeURIComponent(
params.rawSql || ''
)}&its=${encodeURIComponent(params.iso_timestamp_start || '')}&ite=${encodeURIComponent(
params.iso_timestamp_end || ''
)}`
)
}}
onSelectTemplate={onSelectTemplate}
filters={filters}
@@ -157,7 +144,8 @@ export const LogsPreviewer: React.FC<Props> = ({
<LogEventChart
data={!isLoading ? logData : undefined}
onBarClick={(timestampMicro) => {
handleSearch({ query: filters.search_query as string, toMicro: timestampMicro })
const to = unixMicroToIsoTimestamp(timestampMicro)
handleSearch({ query: filters.search_query as string, to, from: null })
}}
/>
)}
@@ -1,6 +1,24 @@
import { Button, Dropdown, Typography, IconChevronDown, IconPlay } from '@supabase/ui'
import {
Button,
Dropdown,
Typography,
IconChevronDown,
IconPlay,
Badge,
Popover,
Alert,
} from '@supabase/ui'
import Flag from 'components/ui/Flag/Flag'
import { LogsTableName, LOGS_SOURCE_DESCRIPTION, LogTemplate } from '.'
import {
EXPLORER_DATEPICKER_HELPERS,
LogsTableName,
LogsWarning,
LOGS_SOURCE_DESCRIPTION,
LogTemplate,
} from '.'
import DatePickers from './Logs.DatePickers'
import Link from 'next/link'
interface Props {
templates?: LogTemplate[]
onSelectTemplate: (template: LogTemplate) => void
@@ -10,6 +28,10 @@ interface Props {
onSave?: () => void
hasEditorValue: boolean
isLoading: boolean
onDateChange: React.ComponentProps<typeof DatePickers>['onChange']
defaultTo: string
defaultFrom: string
warnings: LogsWarning[]
}
const LogsQueryPanel: React.FC<Props> = ({
@@ -21,6 +43,10 @@ const LogsQueryPanel: React.FC<Props> = ({
onSave,
onSelectSource,
isLoading,
defaultFrom,
defaultTo,
onDateChange,
warnings,
}) => (
<div
className="
@@ -63,6 +89,41 @@ bg-panel-header-light dark:bg-panel-header-dark
Templates
</Button>
</Dropdown>
<DatePickers
to={defaultTo}
from={defaultFrom}
onChange={onDateChange}
helpers={EXPLORER_DATEPICKER_HELPERS}
/>
<div className="overflow-hidden">
<div
className={` transition-all duration-300 ${
warnings.length > 0 ? 'opacity-100' : 'invisible w-0 h-0 opacity-0'
}`}
>
<Popover
portalled
overlay={
<Alert variant="warning" title="">
<div className="flex flex-col gap-3">
{warnings.map((warning, index) => (
<p key={index}>
{warning.text}{' '}
{warning.link && (
<Link href={warning.link}>{warning.linkText || 'View'}</Link>
)}
</p>
))}
</div>
</Alert>
}
>
<Badge color="yellow">
{warnings.length} {warnings.length > 1 ? 'warnings' : 'warning'}
</Badge>
</Popover>
</div>
</div>
</div>
<div>
<div className="flex items-center gap-4">
@@ -9,7 +9,7 @@ import {
IconEye,
IconEyeOff,
} from '@supabase/ui'
import { Filters, LogSearchCallback, LogTemplate } from '.'
import { Filters, LogSearchCallback, LogTemplate, PREVIEWER_DATEPICKER_HELPERS } from '.'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { FILTER_OPTIONS, LogsTableName } from './Logs.constants'
@@ -101,9 +101,8 @@ const PreviewFilterPanel: FC<Props> = ({
Refresh
</Button>
)
const handleSearch = (partial: Partial<{ query: string; to: string; from: string }>) =>
onSearch({ query: search, ...partial })
const handleSearch = (partial: Partial<Parameters<LogSearchCallback>[0]>) =>
onSearch({ query: search, to: partial?.to || null, from: partial?.from || null })
return (
<div
@@ -147,11 +146,10 @@ const PreviewFilterPanel: FC<Props> = ({
</form>
<DatePickers
onChange={async (e: { to: string; from: string }) => {
handleSearch(e)
}}
onChange={handleSearch}
to={defaultToValue}
from={defaultFromValue}
helpers={PREVIEWER_DATEPICKER_HELPERS}
/>
<div>
@@ -1,17 +1,18 @@
import React, { FC } from 'react'
import { isNil } from 'lodash'
import { Select } from '@supabase/ui'
import { PostgresType } from '@supabase/postgres-meta'
import InputWithSuggestions from './InputWithSuggestions'
import { POSTGRES_DATA_TYPES } from '../SidePanelEditor.constants'
import { ColumnField, EnumType } from '../SidePanelEditor.types'
import { ColumnField } from '../SidePanelEditor.types'
import { Suggestion } from './ColumnEditor.types'
import { getSelectedEnumValues } from './ColumnEditor.utils'
import { typeExpressionSuggestions } from './ColumnEditor.constants'
interface Props {
columnFields: ColumnField
enumTypes: EnumType[]
enumTypes: PostgresType[]
onUpdateField: (changes: Partial<ColumnField>) => void
}
@@ -27,6 +28,7 @@ const ColumnDefaultValue: FC<Props> = ({
isNil(columnFields.format) && !POSTGRES_DATA_TYPES.includes(columnFields.format)
if (isUserDefinedEnum) {
const enumValues = getSelectedEnumValues(columnFields.format, enumTypes)
return (
<Select
label="Default Value"
@@ -37,7 +39,7 @@ const ColumnDefaultValue: FC<Props> = ({
<Select.Option key="empty-enum" value="">
---
</Select.Option>
{getSelectedEnumValues(columnFields.format, enumTypes).map((value: string) => (
{enumValues.map((value: string) => (
<Select.Option key={value} value={value}>
{value}
</Select.Option>
@@ -2,7 +2,12 @@ import { FC, useEffect, useState } from 'react'
import { isUndefined, isEmpty } from 'lodash'
import { Dictionary } from '@supabase/grid'
import { Checkbox, SidePanel, Space, Input, Divider } from '@supabase/ui'
import { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
import {
PostgresColumn,
PostgresRelationship,
PostgresTable,
PostgresType,
} from '@supabase/postgres-meta'
import ActionBar from '../ActionBar'
import HeaderTitle from './HeaderTitle'
@@ -18,18 +23,13 @@ import {
generateCreateColumnPayload,
generateUpdateColumnPayload,
} from './ColumnEditor.utils'
import {
EnumType,
ColumnField,
CreateColumnPayload,
UpdateColumnPayload,
} from '../SidePanelEditor.types'
import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from '../SidePanelEditor.types'
interface Props {
column?: PostgresColumn
selectedTable: PostgresTable
tables: PostgresTable[]
enumTypes: EnumType[]
enumTypes: PostgresType[]
visible: boolean
closePanel: () => void
saveChanges: (
@@ -1,14 +1,14 @@
import { find, isUndefined, isEqual, isNull } from 'lodash'
import { Dictionary } from '@supabase/grid'
import { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
import {
PostgresColumn,
PostgresRelationship,
PostgresTable,
PostgresType,
} from '@supabase/postgres-meta'
import { uuidv4 } from 'lib/helpers'
import {
ColumnField,
EnumType,
CreateColumnPayload,
UpdateColumnPayload,
} from '../SidePanelEditor.types'
import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from '../SidePanelEditor.types'
const isSQLExpression = (input: string) => {
if (input[0] === '(' && input[input.length - 1] === ')') {
@@ -138,9 +138,9 @@ export const generateUpdateColumnPayload = (
return payload
}
export const getSelectedEnumValues = (type: string, enums: EnumType[]) => {
export const getSelectedEnumValues = (type: string, enums: PostgresType[]) => {
const enumType = find(enums, { name: type })
return enumType?.values ?? []
return (enumType?.enums ?? []) as string[]
}
export const validateFields = (field: ColumnField) => {
@@ -1,11 +1,12 @@
import React, { FC } from 'react'
import { IconCalendar, IconType, IconHash, Listbox, IconToggleRight } from '@supabase/ui'
import { PostgresType } from '@supabase/postgres-meta'
import { POSTGRES_DATA_TYPE_OPTIONS } from '../SidePanelEditor.constants'
import { PostgresDataTypeOption, EnumType } from '../SidePanelEditor.types'
import { PostgresDataTypeOption } from '../SidePanelEditor.types'
interface Props {
value: string
enumTypes: EnumType[]
enumTypes: PostgresType[]
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
className?: string
error?: any
@@ -66,7 +67,7 @@ const ColumnType: FC<Props> = ({
</Listbox.Option>
{enumTypes.length > 0 ? (
enumTypes.map((enumType: EnumType) => (
enumTypes.map((enumType: PostgresType) => (
<Listbox.Option
key={enumType.name}
value={enumType.name}
@@ -1,6 +1,6 @@
import { FC } from 'react'
import { last } from 'lodash'
import { Typography, IconHome, IconChevronRight } from '@supabase/ui'
import { IconHome, IconChevronRight } from '@supabase/ui'
interface Props {
breadcrumbs: string[]
@@ -10,17 +10,15 @@ interface Props {
const DrilldownBreadCrumbs: FC<Props> = ({ breadcrumbs = [], onSelectBreadcrumb = () => {} }) => {
return (
<div className="flex items-center space-x-2">
<Typography>
<IconHome size={16} strokeWidth={2} />
</Typography>
<IconHome size={16} strokeWidth={2} />
{breadcrumbs.length > 0 &&
breadcrumbs.map((crumb) => (
<div className="flex items-center space-x-2" key={crumb}>
<IconChevronRight size={16} strokeWidth={2} />
{crumb === last(breadcrumbs) ? (
<Typography.Text>{crumb}</Typography.Text>
<p className="text-xs font-mono">{crumb}</p>
) : (
<Typography.Text>
<p className="text-xs font-mono">
<div
className="cursor-pointer"
onClick={() =>
@@ -29,7 +27,7 @@ const DrilldownBreadCrumbs: FC<Props> = ({ breadcrumbs = [], onSelectBreadcrumb
>
{crumb}
</div>
</Typography.Text>
</p>
)}
</div>
))}
@@ -1,5 +1,5 @@
import { FC } from 'react'
import { IconChevronRight, Typography } from '@supabase/ui'
import { IconChevronRight } from '@supabase/ui'
import { isNull } from 'lodash'
import { Dictionary } from '@supabase/grid'
@@ -15,9 +15,7 @@ const DrilldownPane: FC<Props> = ({ pane, jsonData, activeKey, onSelectKey = ()
return (
<div className={`flex-1 ${pane === 2 ? 'border-l border-gray-500' : ''}`}>
<div className="py-2 px-5 flex space-x-2">
<Typography.Text small type="danger">
Invalid JSON
</Typography.Text>
<p className="text-sm">Invalid JSON</p>
</div>
</div>
)
@@ -27,9 +25,7 @@ const DrilldownPane: FC<Props> = ({ pane, jsonData, activeKey, onSelectKey = ()
return (
<div className={`flex-1 max-w-[50%] ${pane === 2 ? 'border-l border-gray-500' : ''}`}>
<div className="py-2 px-5 flex space-x-2">
<Typography.Text small className="opacity-50">
No data available
</Typography.Text>
<p className="opacity-50 text-sm">No data available</p>
</div>
</div>
)
@@ -55,25 +51,20 @@ const DrilldownPane: FC<Props> = ({ pane, jsonData, activeKey, onSelectKey = ()
`}
onClick={() => onSelectKey(key, pane)}
>
<Typography.Text small className="font-mono !text-blue-700 dark:!text-blue-300">
{key}
</Typography.Text>
<p className="text-xs font-mono !text-blue-700 dark:!text-blue-1100">{key}</p>
<div className={`${key === activeKey ? 'block' : 'hidden'} group-hover:block`}>
<Typography>
<IconChevronRight strokeWidth={2} size={16} />
</Typography>
<IconChevronRight strokeWidth={2} size={16} />
</div>
</div>
))}
{keysWithoutChildren.map((key: string) => (
<div key={key} className="py-2 px-5 flex space-x-2">
<Typography.Text small className="font-mono !text-blue-700 dark:!text-blue-300">
{key}:
</Typography.Text>
<Typography.Text
small
className={`font-mono break-all ${
typeof jsonData[key] !== 'string' ? '!text-green-700 dark:!text-green-400' : '!text-yellow-700 dark:!text-yellow-500'
<p className="text-xs font-mono !text-blue-700 dark:!text-blue-1100">{key}:</p>
<p
className={`text-xs font-mono break-all ${
typeof jsonData[key] !== 'string'
? '!text-green-700 dark:!text-green-1000'
: '!text-yellow-700 dark:!text-yellow-900'
}`}
>
{isNull(jsonData[key])
@@ -81,7 +72,7 @@ const DrilldownPane: FC<Props> = ({ pane, jsonData, activeKey, onSelectKey = ()
: typeof jsonData[key] === 'string'
? `"${jsonData[key]}"`
: jsonData[key].toString()}
</Typography.Text>
</p>
</div>
))}
</div>
@@ -56,20 +56,22 @@ const JsonEdit: FC<JsonEditProps> = ({ column, jsonString, visible, closePanel,
onCancel={closePanel}
customFooter={<ActionBar closePanel={closePanel} applyFunction={validateJSON} />}
>
<TwoOptionToggle
options={['view', 'edit']}
activeOption={view}
borderOverride="border-gray-500"
onClickOption={onToggleClick}
/>
<SidePanel.Content>
<TwoOptionToggle
options={['view', 'edit']}
activeOption={view}
borderOverride="border-gray-500"
onClickOption={onToggleClick}
/>
<div className="flex-auto flex flex-col mt-4">
{view === 'edit' ? (
<Editor column={column} value={jsonStr} onChange={onInputChange} />
) : (
<Viewer column={column} value={jsonStr} />
)}
</div>
<div className="flex-auto flex flex-col mt-4 space-y-2">
{view === 'edit' ? (
<Editor column={column} value={jsonStr} onChange={onInputChange} />
) : (
<Viewer column={column} value={jsonStr} />
)}
</div>
</SidePanel.Content>
</SidePanel>
)
}
@@ -45,24 +45,11 @@ const SidePanelEditor: FC<Props> = ({
}) => {
const { meta, ui } = useStore()
const [enumTypes, setEnumTypes] = useState<any[]>([])
const [isEdited, setIsEdited] = useState<boolean>(false)
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
const tables = meta.tables.list()
useEffect(() => {
let cancel = false
const fetchEnumTypes = async () => {
const enumTypes = await meta.schemas.getEnums()
if (!cancel) setEnumTypes(enumTypes)
}
fetchEnumTypes()
return () => {
cancel = true
}
}, [])
const enumTypes = meta.types.list()
const saveRow = async (
payload: any,
@@ -195,12 +182,25 @@ const SidePanelEditor: FC<Props> = ({
category: 'loading',
message: `Updating table: ${selectedTableToEdit?.name}...`,
})
const table: any = await meta.updateTable(toastId, selectedTableToEdit, payload, columns)
ui.setNotification({
id: toastId,
category: 'success',
message: `Successfully updated ${table.name}!`,
})
const { table, hasError }: any = await meta.updateTable(
toastId,
selectedTableToEdit,
payload,
columns
)
if (hasError) {
ui.setNotification({
id: toastId,
category: 'info',
message: `Table ${table.name} has been updated, but there were some errors`,
})
} else {
ui.setNotification({
id: toastId,
category: 'success',
message: `Successfully updated ${table.name}!`,
})
}
}
} catch (error: any) {
saveTableError = true
@@ -75,12 +75,6 @@ export interface ColumnField {
// column: PostgresColumn
// }
export interface EnumType {
id: number
name: string
values: any[]
}
export interface PostgresDataTypeOption {
name: string
description: string
@@ -11,8 +11,9 @@ import {
IconSettings,
Button,
} from '@supabase/ui'
import { PostgresType } from '@supabase/postgres-meta'
import { ColumnField, EnumType } from '../SidePanelEditor.types'
import { ColumnField } from '../SidePanelEditor.types'
import ColumnType from '../ColumnEditor/ColumnType'
import InputWithSuggestions from '../ColumnEditor/InputWithSuggestions'
import { Suggestion } from '../ColumnEditor/ColumnEditor.types'
@@ -38,7 +39,7 @@ import { typeExpressionSuggestions } from '../ColumnEditor/ColumnEditor.constant
interface Props {
column: ColumnField
enumTypes: EnumType[]
enumTypes: PostgresType[]
isNewRecord: boolean
hasImportContent: boolean
dragHandleProps?: any
@@ -49,7 +50,7 @@ interface Props {
const Column: FC<Props> = ({
column = {} as ColumnField,
enumTypes = [] as EnumType[],
enumTypes = [] as PostgresType[],
isNewRecord = false,
hasImportContent = false,
dragHandleProps = {},
@@ -9,7 +9,12 @@ import {
IconTrash,
Typography,
} from '@supabase/ui'
import { PostgresTable, PostgresColumn, PostgresRelationship } from '@supabase/postgres-meta'
import {
PostgresTable,
PostgresColumn,
PostgresRelationship,
PostgresType,
} from '@supabase/postgres-meta'
import {
DragDropContext,
Droppable,
@@ -22,7 +27,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
import Column from './Column'
import InformationBox from 'components/ui/InformationBox'
import ForeignKeySelector from '../ForeignKeySelector/ForeignKeySelector'
import { ColumnField, EnumType } from '../SidePanelEditor.types'
import { ColumnField } from '../SidePanelEditor.types'
import { ImportContent } from './TableEditor.types'
import { generateColumnField } from '../ColumnEditor/ColumnEditor.utils'
@@ -30,7 +35,7 @@ interface Props {
table?: Partial<PostgresTable>
tables: PostgresTable[]
columns?: ColumnField[]
enumTypes: EnumType[]
enumTypes: PostgresType[]
importContent?: ImportContent
isNewRecord: boolean
onColumnsUpdated: (columns: ColumnField[]) => void
@@ -1,19 +1,14 @@
import { FC, useEffect, useState } from 'react'
import { isUndefined, isEmpty } from 'lodash'
import { Badge, Checkbox, SidePanel, Input } from '@supabase/ui'
import { PostgresTable } from '@supabase/postgres-meta'
import { PostgresTable, PostgresType } from '@supabase/postgres-meta'
import { useStore } from 'hooks'
import ActionBar from '../ActionBar'
import HeaderTitle from './HeaderTitle'
import ColumnManagement from './ColumnManagement'
import SpreadsheetImport from './SpreadsheetImport/SpreadsheetImport'
import {
ColumnField,
EnumType,
CreateTablePayload,
UpdateTablePayload,
} from '../SidePanelEditor.types'
import { ColumnField, CreateTablePayload, UpdateTablePayload } from '../SidePanelEditor.types'
import { DEFAULT_COLUMNS } from './TableEditor.constants'
import { TableField, ImportContent } from './TableEditor.types'
import {
@@ -22,12 +17,11 @@ import {
generateTableFieldFromPostgresTable,
formatImportedContentToColumnFields,
} from './TableEditor.utils'
import SidePanelEditor from '..'
interface Props {
table?: PostgresTable
tables: PostgresTable[]
enumTypes: EnumType[]
enumTypes: PostgresType[]
selectedSchema: string
isDuplicating: boolean
visible: boolean
@@ -50,7 +44,7 @@ interface Props {
const TableEditor: FC<Props> = ({
table,
tables = [],
enumTypes = [] as EnumType[],
enumTypes = [] as PostgresType[],
selectedSchema,
isDuplicating,
visible = false,
@@ -100,7 +100,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
key: 'ext-guides',
icon: '/img/book-open.svg',
label: 'API Reference',
href: 'https://supabase.com/docs/client/supabase-client',
href: 'https://supabase.com/docs/reference/javascript/supabase-client',
external: true,
},
],
@@ -1,5 +1,6 @@
import { ReactElement, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore } from 'hooks'
import Error from 'components/ui/Error'
@@ -8,9 +9,16 @@ import ProjectLayout from 'components/layouts/ProjectLayout/ProjectLayout'
import { generateDocsMenu } from './DocsLayout.utils'
function DocsLayout({ title, children }: { title: string; children: ReactElement }) {
const router = useRouter()
const { meta, ui } = useStore()
const { data, isLoading, error } = meta.openApi
const getPage = () => {
const { page, resource } = router.query
if (!page && !resource) return 'introduction'
return (page || resource || '') as string
}
useEffect(() => {
if (ui.selectedProject) {
meta.openApi.load()
@@ -34,7 +42,12 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement
title={title || 'API Docs'}
isLoading={isLoading}
product="API Docs"
productMenu={<ProductMenu menu={generateDocsMenu(projectRef, tableNames, functionNames)} />}
productMenu={
<ProductMenu
page={getPage()}
menu={generateDocsMenu(projectRef, tableNames, functionNames)}
/>
}
>
{children}
</ProjectLayout>
@@ -13,13 +13,13 @@ export const generateDocsMenu = (
{ name: 'Introduction', key: 'introduction', url: `/project/${ref}/api`, items: [] },
{
name: 'Authentication',
key: 'authentication',
key: 'auth',
url: `/project/${ref}/api?page=auth`,
items: [],
},
{
name: 'User Management',
key: 'user-management',
key: 'users',
url: `/project/${ref}/api?page=users`,
items: [],
},
@@ -30,7 +30,7 @@ export const generateDocsMenu = (
items: [
{
name: 'Introduction',
key: 'tables-introduction',
key: 'tables-intro',
url: `/project/${ref}/api?page=tables-intro`,
items: [],
},
@@ -49,7 +49,7 @@ export const generateDocsMenu = (
items: [
{
name: 'Introduction',
key: 'sp-introduction',
key: 'rpc-intro',
url: `/project/${ref}/api?page=rpc-intro`,
items: [],
},
@@ -72,7 +72,7 @@ export const generateDocsMenu = (
{
name: 'API Reference',
key: 'api-reference',
url: `https://supabase.com/docs/client/supabase-client`,
url: `https://supabase.com/docs/reference/javascript/supabase-client`,
icon: <IconBookOpen size={14} strokeWidth={2} />,
items: [],
isExternal: true,
@@ -30,20 +30,10 @@ const PageLayout = ({
<BaseLayout>
<div className="h-full flex flex-col flex-grow py-6">
<div
className="
w-full mx-auto
transition-all
flex flex-col
px-5
lg:px-16
xl:px-24
1xl:px-28
2xl:px-32
gap-4
"
className={[
'w-full mx-auto flex flex-col transition-all gap-4 px-5',
'lg:px-16 xl:px-24 1xl:px-28 2xl:px-32',
].join(' ')}
>
<div className="flex items-center gap-3">
<div
@@ -59,20 +49,10 @@ const PageLayout = ({
<LogsNavigation />
</div>
<div
// weird repetitive styling
className="
h-full flex flex-col flex-grow
w-full mx-auto
transition-all
px-5
lg:px-16
xl:px-24
1xl:px-28
2xl:px-32
gap-4
"
className={[
'h-full w-full flex flex-col flex-grow mx-auto gap-4 transition-all',
'px-5 lg:px-16 1xl:px-28 2xl:px-32',
].join(' ')}
>
{children}
</div>
@@ -2,10 +2,6 @@ import Link from 'next/link'
import { FC, useEffect, useRef } from 'react'
import { observer } from 'mobx-react-lite'
import { Badge, IconArrowRight, IconLoader, Button } from '@supabase/ui'
import {
DisplayApiSettings,
DisplayConfigSettings,
} from 'components/to-be-cleaned/DisplayProjectSettings'
import ExampleProject from 'components/interfaces/Home/ExampleProject'
import ClientLibrary from 'components/interfaces/Home/ClientLibrary'
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
@@ -14,6 +10,7 @@ import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { useStore } from 'hooks'
import { get } from 'lib/common/fetch'
import { Project } from 'types'
import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings'
type ProjectBuildingState = { project: Project }
const ProjectBuildingState: FC<ProjectBuildingState> = ({ project }) => {
@@ -27,12 +24,13 @@ const ProjectBuildingState: FC<ProjectBuildingState> = ({ project }) => {
if (projectStatus && !projectStatus.error) {
const { status } = projectStatus
if (status === PROJECT_STATUS.ACTIVE_HEALTHY) {
clearInterval(checkServerInterval.current)
const res = await get(`${API_URL}/props/project/${project.ref}/connection-string`)
if (res && res.connectionString) {
app.onProjectConnectionStringUpdated(project.id, res.connectionString)
}
app.onProjectStatusUpdated(project.id, status)
clearInterval(checkServerInterval.current)
}
}
}
@@ -0,0 +1,82 @@
import Link from 'next/link'
import { FC, useEffect, useRef } from 'react'
import { observer } from 'mobx-react-lite'
import { Badge, IconLoader, IconMonitor, IconServer } from '@supabase/ui'
import { Project } from 'types'
import { headWithTimeout } from 'lib/common/fetch'
import { useStore } from 'hooks'
import ShimmerLine from 'components/ui/ShimmerLine'
interface Props {
project: Project
}
const ProjectRestartingState: FC<Props> = ({ project }) => {
const { app } = useStore()
const checkProjectConnectionIntervalRef = useRef<number>()
useEffect(() => {
if (!project.restUrl || !project.internalApiKey) return
// Check project connection status every 4 seconds
checkProjectConnectionIntervalRef.current = window.setInterval(testProjectConnection, 4000)
return () => {
clearInterval(checkProjectConnectionIntervalRef.current)
}
}, [project])
const testProjectConnection = async () => {
const headers = {
apikey: project.internalApiKey,
Authorization: `Bearer ${project.internalApiKey}`,
}
const { error } = await headWithTimeout(project.restUrl!, [], {
headers,
credentials: 'omit',
timeout: 2000,
})
if (error === undefined) {
clearInterval(checkProjectConnectionIntervalRef.current)
app.onProjectPostgrestStatusUpdated(project.id, 'ONLINE')
}
}
return (
<div className="max-w-7xl mx-auto w-full my-16 space-y-16">
<div className="mx-6 space-y-16">
<div className="flex flex-col space-y-4 lg:space-y-0 lg:flex-row lg:space-x-6 lg:items-center">
<h1 className="text-3xl">{project.name}</h1>
<div>
<Badge color="brand">
<div className="flex items-center gap-2">
<IconLoader className="animate-spin" size={12} />
<span>Connecting to project</span>
</div>
</Badge>
</div>
</div>
<div className="rounded bg-scale-300 border border-scale-400 p-8 flex items-center justify-center h-[500px]">
<div className="w-[420px] space-y-4">
<div className="max-w-[300px] mx-auto flex items-center justify-center space-x-4 lg:space-x-8">
<IconMonitor className="text-scale-1100" size={50} strokeWidth={1.5} />
<ShimmerLine active />
<IconServer className="text-scale-1100" size={50} strokeWidth={1.5} />
</div>
<p className="text-center">Connecting to {project.name}</p>
<p className="text-center text-scale-1100 text-sm">
This may take a few minutes, but if your dashboard hasn't connected within 2 minutes,
you can open a support ticket{' '}
<span className="text-green-1000">
<Link href={`/support/new?ref=${project.ref}`}>here</Link>
</span>
.
</p>
</div>
</div>
</div>
</div>
)
}
export default observer(ProjectRestartingState)
@@ -89,7 +89,7 @@ export const generateOtherRoutes = (ref: string, project?: ProjectBase) => {
...(IS_PLATFORM
? [
{
key: 'logsExplorer',
key: 'logs-explorer',
label: 'Logs Explorer',
icon: <IconList size={18} strokeWidth={2} />,
link: isProjectBuilding ? buildingUrl : `/project/${ref}/logs-explorer`,
@@ -3,12 +3,14 @@ import { FC, ReactNode } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore } from 'hooks'
import { PROJECT_STATUS } from 'lib/constants'
import Connecting from 'components/ui/Loading'
import NavigationBar from './NavigationBar/NavigationBar'
import ProductMenuBar from './ProductMenuBar'
import LayoutHeader from './LayoutHeader'
import TestConnection from './TestConnection'
import ConnectingState from './ConnectingState'
import BuildingState from './BuildingState'
interface Props {
title?: string
@@ -57,7 +59,7 @@ const ProjectLayout: FC<Props> = ({
)
}
export default observer(ProjectLayout)
export default ProjectLayout
interface MenuBarWrapperProps {
isLoading: boolean
@@ -73,20 +75,41 @@ interface ContentWrapperProps {
isLoading: boolean
}
/**
* Check project.status to show building state or error state
*
* TODO: how can we test project connection properly?
* ex: the status is ACTIVE_HEALTHY but the project instance is down.
*
* [Joshen] As of 210422: Current testing connection by pinging postgres
* Ideally we'd have a more specific monitoring of the project such as during restarts
* But that will come later: https://supabase.slack.com/archives/C01D6TWFFFW/p1650427619665549
*
* Just note that this logic does not differentiate between a "restarting" state and
* a "something is wrong and can't connect to project" state.
*
* [TODO] Next iteration should scrape long polling and just listen to the project's status
*/
const ContentWrapper: FC<ContentWrapperProps> = observer(({ isLoading, children }) => {
const { ui } = useStore()
const router = useRouter()
const project = ui.selectedProject
const requiresDbConnection: boolean = router.pathname !== '/project/[ref]/settings/general'
const isProjectBuilding = [PROJECT_STATUS.COMING_UP, PROJECT_STATUS.RESTORING].includes(
ui.selectedProject?.status ?? ''
)
const isProjectOffline = ui.selectedProject?.postgrestStatus === 'OFFLINE'
return (
<>
{isLoading || project === undefined ? (
{isLoading || ui.selectedProject === undefined ? (
<Connecting />
) : requiresDbConnection ? (
<TestConnection project={project!}>
<div className="flex flex-col flex-1 overflow-y-auto">{children}</div>
</TestConnection>
) : // [TODO] Temp fix commenting out ConnectingState as some projects take
// really long to ping postgrest such that the timeout cancels out too early
// requiresDbConnection && isProjectOffline ? (
// <ConnectingState project={ui.selectedProject} />
// ) :
requiresDbConnection && isProjectBuilding ? (
<BuildingState project={ui.selectedProject} />
) : (
<>{children}</>
)}
@@ -1,46 +0,0 @@
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { FC, ReactNode } from 'react'
import { Project } from 'types'
import BuildingState from 'components/to-be-cleaned/BuildingState'
import { PROJECT_STATUS } from 'lib/constants'
interface Props {
project: Project
children: ReactNode
}
/**
* Check project.status to show building state or error state
*
* TODO: how can we test project connection properly?
* ex: the status is ACTIVE_HEALTHY but the project instance is down.
*/
const TestConnection: FC<Props> = ({ project, children }) => {
const router = useRouter()
const isBuilding = [PROJECT_STATUS.COMING_UP, PROJECT_STATUS.RESTORING].includes(project?.status)
const isProjectActive = project.status === PROJECT_STATUS.ACTIVE_HEALTHY
if (isBuilding && router.asPath !== `/project/${project.ref}/building`) {
router.push(`/project/${project.ref}/building`, undefined, { shallow: true })
}
return (
<>
{isProjectActive ? (
children
) : isBuilding ? (
<BuildingState project={project} />
) : (
<div className="w-full h-full m-4 flex flex-col items-center justify-center">
<p className="text-scale-1100">{`Invalid project status: ${project.status}`}</p>
<p className="text-scale-1100">
Try refreshing your browser, or reach out to us at support@supabase.io
</p>
</div>
)}
</>
)
}
export default observer(TestConnection)
@@ -1,12 +1,11 @@
import { FC, ReactNode } from 'react'
import { isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore } from 'hooks'
import { generateSettingsMenu } from './SettingsMenu.utils'
import BaseLayout from '../'
import ProductMenu from 'components/ui/ProductMenu'
import { generateSettingsMenu } from './SettingsMenu.utils'
import { useRouter } from 'next/router'
interface Props {
title?: string
@@ -15,15 +14,18 @@ interface Props {
const SettingsLayout: FC<Props> = ({ title, children }) => {
const { ui } = useStore()
const projectRef = ui.selectedProject?.ref ?? 'default'
const projectRef = ui.selectedProjectRef as string
const projectBaseInfo = ui.selectedProjectBaseInfo
const router = useRouter()
const page = router.pathname.split('/')[4]
const menuRoutes = generateSettingsMenu(projectRef, projectBaseInfo)
return (
<BaseLayout
title={title || 'Settings'}
product="Settings"
productMenu={<ProductMenu page={page} menu={generateSettingsMenu(projectRef)} />}
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
@@ -1,27 +1,45 @@
import { LOG_TYPE_LABEL_MAPPING } from 'components/interfaces/Settings/Logs'
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import { PROJECT_STATUS } from 'lib/constants'
import { ProjectBase } from 'types'
export const generateSettingsMenu = (ref: string): ProductMenuGroup[] => {
export const generateSettingsMenu = (ref: string, project?: ProjectBase): ProductMenuGroup[] => {
const isProjectBuilding = project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY
const buildingUrl = `/project/${ref}/building`
return [
{
title: 'Project settings',
items: [
{ name: 'General', key: 'general', url: `/project/${ref}/settings/general`, items: [] },
{ name: 'Database', key: 'database', url: `/project/${ref}/settings/database`, items: [] },
{ name: 'API', key: 'api', url: `/project/${ref}/settings/api`, items: [] },
{ name: 'Authentication', key: 'auth', url: `/project/${ref}/auth/settings`, items: [] },
{
name: 'Database',
key: 'database',
url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/database`,
items: [],
},
{
name: 'API',
key: 'api',
url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/api`,
items: [],
},
{
name: 'Authentication',
key: 'auth',
url: isProjectBuilding ? buildingUrl : `/project/${ref}/auth/settings`,
items: [],
},
// TODO(thebengeu): Uncomment once all tenants migrated to multitenant storage API.
// {
// name: 'Storage',
// key: 'storage',
// url: `/project/${ref}/storage/settings`,
// url: isProjectBuilding ? buildingUrl : `/project/${ref}/storage/settings`,
// items: [],
// },
{
name: 'Billing & Usage',
key: 'billing',
url: `/project/${ref}/settings/billing`,
url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/billing`,
items: [],
},
],
@@ -36,6 +36,7 @@ const TableEditorLayout: FC<Props> = ({
if (ui.selectedProject) {
meta.schemas.load()
meta.tables.load()
meta.types.load()
}
}, [ui.selectedProject])
@@ -15,7 +15,7 @@ import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import { BarChart, AreaChart } from './ChartRenderer'
import { ChartData } from './ChartHandler.types'
import { TooltipProps } from 'recharts'
import { AreaProps } from 'recharts'
interface Props {
label: string
@@ -34,6 +34,7 @@ interface Props {
format?: string
highlightedValue?: string | number
onBarClick?: (v: any) => void
areaType?: AreaProps['type']
}
/**
@@ -62,6 +63,7 @@ const ChartHandler: FC<Props> = ({
format,
highlightedValue,
onBarClick,
areaType,
}) => {
const router = useRouter()
const { ref } = router.query
@@ -120,6 +122,8 @@ const ChartHandler: FC<Props> = ({
highlightedValue = highlightedValue
? highlightedValue
: provider === 'daily-stats' && !attribute.includes('ingress') && !attribute.includes('egress')
? chartData?.maximum
: provider === 'daily-stats'
? chartData?.total
: provider === 'log-stats'
@@ -136,7 +140,6 @@ const ChartHandler: FC<Props> = ({
}
if (isUndefined(chartData)) {
console.log('i am undefined chart data')
return (
<div className="w-full h-52 flex flex-col space-y-4 items-center justify-center">
<IconAlertCircle className="text-scale-700" />
@@ -1,230 +0,0 @@
import useSWR from 'swr'
import { FC } from 'react'
import { useRouter } from 'next/router'
import { get } from 'lib/common/fetch'
import { Badge, IconAlertCircle, Input, Loading, Typography } from '@supabase/ui'
import { JwtSecretUpdateStatus, ProjectEvents } from '@supabase/shared-types/out/events'
import { useJwtSecretUpdateStatus } from 'hooks'
import { API_URL } from 'lib/constants'
import Panel from './Panel'
const { Text, Title } = Typography
const API_SERVICE_ID = 1
export const DisplayApiSettings = () => {
const router = useRouter()
const { ref } = router.query
const { data, error }: any = useSWR(`${API_URL}/props/project/${ref}/settings`, get)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
if (!data || isJwtSecretUpdateStatusLoading)
return (
<ApiContentWrapper>
<Panel.Content className="py-8">
{error || isJwtSecretUpdateStatusError ? (
<div className="flex items-center space-x-2">
<Typography.Text type="secondary">
<IconAlertCircle strokeWidth={2} />
</Typography.Text>
<Typography.Text type="secondary">Failed to fetch API keys</Typography.Text>
</div>
) : (
<div className="py-4">
{/* @ts-ignore */}
<Loading active={true} />
</div>
)}
</Panel.Content>
</ApiContentWrapper>
)
// Get the API service
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
const apiKeys = apiService?.service_api_keys ?? []
return (
<ApiContentWrapper>
{!data || isJwtSecretUpdateStatusLoading ? (
// @ts-ignore
<Loading active={true} />
) : (
apiKeys.map((x: any, i: number) => (
<Panel.Content
key={x.api_key}
className={
i >= 1 &&
'border-t border-panel-border-interior-light dark:border-panel-border-interior-dark'
}
>
<Input
layout="horizontal"
// @ts-ignore
label={
<>
{x.tags?.split(',').map((x: any, i: number) => (
<code key={`${x}${i}`} className="text-xs text-code">
{x}
</code>
))}
{x.tags === 'service_role' && (
<>
<code className="text-xs bg-red-900 text-white">{'secret'}</code>
</>
)}
{x.tags === 'anon' && <code className="text-xs text-code">{'public'}</code>}
</>
}
readOnly
copy={isNotUpdatingJwtSecret}
className="input-mono"
disabled
reveal={x.tags !== 'anon' && isNotUpdatingJwtSecret}
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed, new API key may have issues'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: x.api_key
}
onChange={() => {}}
descriptionText={
x.tags === 'service_role'
? 'This key has the ability to bypass Row Level Security. Never share it publicly.'
: 'This key is safe to use in a browser if you have enabled Row Level Security for your tables and configured policies.'
}
/>
</Panel.Content>
))
)}
</ApiContentWrapper>
)
}
export const DisplayConfigSettings = () => {
const router = useRouter()
const { ref } = router.query
const { data, error }: any = useSWR(`${API_URL}/props/project/${ref}/settings`, get)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
if (!data || isJwtSecretUpdateStatusLoading)
return (
<ConfigContentWrapper>
<Panel.Content className="py-8">
{error || isJwtSecretUpdateStatusError ? (
<div className="flex items-center space-x-2">
<Typography.Text type="secondary">
<IconAlertCircle strokeWidth={2} />
</Typography.Text>
<Typography.Text type="secondary">
Failed to fetch project configuration
</Typography.Text>
</div>
) : (
<div className="py-4">
{/* @ts-ignore */}
<Loading active={true} />
</div>
)}
</Panel.Content>
</ConfigContentWrapper>
)
// Get the API service
const jwtSecret = data?.project.jwt_secret ?? ''
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
const apiConfig = apiService.app_config
return (
<section>
<Panel
title={
<Typography.Title level={5} className="mb-0">
Project Configuration
</Typography.Title>
}
>
<Panel.Content>
<Input
label="URL"
readOnly
copy
disabled
className="input-mono"
value={`https://${apiConfig.endpoint}`}
descriptionText="A RESTful endpoint for querying and managing your database."
layout="horizontal"
/>
</Panel.Content>
<Panel.Content className="border-t border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Input
label="JWT Secret"
readOnly
copy={isNotUpdatingJwtSecret}
reveal={isNotUpdatingJwtSecret}
disabled
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: jwtSecret
}
className="input-mono"
descriptionText="Used to decode your JWTs. You can also use this to mint your own JWTs."
layout="horizontal"
/>
</Panel.Content>
</Panel>
</section>
)
}
const ApiContentWrapper: FC<any> = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project API keys</h5>
<p className="text-sm text-scale-1000">
Your API is secured behind an API gateway which requires an API Key for every request.
<br />
You can use the keys below to use Supabase client libraries.
</p>
</div>
}
>
{children}
</Panel>
)
}
const ConfigContentWrapper: FC<any> = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project Configuration</h5>
</div>
}
>
{children}
</Panel>
)
}
+1 -1
View File
@@ -22,7 +22,7 @@ function Panel(props: any) {
className={`
border border-panel-border-light dark:border-panel-border-dark
shadow-sm overflow-hidden
rounded rounded-md ${props.noMargin ? '' : 'mb-8'} ${props.className}`}
rounded-md ${props.noMargin ? '' : 'mb-8'} ${props.className}`}
>
{props.title && (
<div className={headerClasses.join(' ')}>
@@ -0,0 +1,6 @@
## What to clean for Storage
- Convert files from javascript to typescript
- Not use mobx store for UI related states, these could simply be done via react's useState or useReducer
- Involves some heavy surgery nonetheless as 90% of the explorer logic is written a mobx store StorageExplorerStore
- If anyone is able to get to it before I do, please feel free to take this on and perhaps even write it more simply 🙏
@@ -12,8 +12,6 @@ const FileExplorer = ({
openedFolders = [],
selectedItems = [],
selectedFilePreview = {},
isSearching = false,
itemSearchString = '',
onCheckItem = () => {},
onSelectItemDelete = () => {},
onSelectItemRename = () => {},
@@ -31,6 +29,7 @@ const FileExplorer = ({
onSelectCreateFolder = () => {},
onChangeView = () => {},
onChangeSortBy = () => {},
onColumnLoadMore = () => {},
}) => {
const fileExplorerRef = useRef(null)
@@ -93,6 +92,7 @@ const FileExplorer = ({
onSelectItemMove={onSelectItemMove}
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
onColumnLoadMore={onColumnLoadMore}
/>
))}
</div>
@@ -106,8 +106,6 @@ const FileExplorer = ({
column={columns[columns.length - 1]}
selectedItems={selectedItems}
selectedFilePreview={selectedFilePreview}
isSearching={isSearching}
itemSearchString={itemSearchString}
onCheckItem={onCheckItem}
onSelectFile={onSelectFile}
onRenameFile={onRenameFile}
@@ -122,6 +120,7 @@ const FileExplorer = ({
onSelectItemMove={onSelectItemMove}
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
onColumnLoadMore={onColumnLoadMore}
/>
)}
</>
@@ -56,8 +56,6 @@ const FileExplorerColumn = ({
openedFolders = [],
selectedItems = [],
selectedFilePreview = {},
isSearching = false,
itemSearchString = '',
onCheckItem = () => {},
onSelectItemDelete = () => {},
onSelectItemRename = () => {},
@@ -72,6 +70,7 @@ const FileExplorerColumn = ({
onCreateFolder = () => {},
onSelectAllItemsInColumn = () => {},
onSelectColumnEmptySpace = () => {},
onColumnLoadMore = () => {},
}) => {
const [isDraggedOver, setIsDraggedOver] = useState(false)
const fileExplorerColumnRef = useRef(null)
@@ -93,12 +92,7 @@ const FileExplorerColumn = ({
(item) => item.type === STORAGE_ROW_TYPES.FILE
)
const columnItems = isSearching
? column.items.filter((item) =>
item.name.toLowerCase().includes(itemSearchString.toLowerCase())
)
: column.items
const columnItems = column.items
const columnItemsSize = sum(columnItems.map((item) => get(item, ['metadata', 'size'], 0)))
const { show } = useContextMenu()
@@ -207,8 +201,7 @@ const FileExplorerColumn = ({
<div
className={`
${fullWidth ? 'w-full' : 'w-64 border-r border-gray-500'}
${view === STORAGE_VIEWS.COLUMNS ? 'my-1' : 'mb-1'}
flex-shrink-0 overflow-auto flex flex-col space-y-1
flex-shrink-0 overflow-auto flex flex-col space-y-1 my-1
`}
>
<ShimmeringLoader />
@@ -239,21 +232,12 @@ const FileExplorerColumn = ({
onSelectItemMove,
}}
ItemComponent={FileExplorerRow}
getItemSize={(index) => {
if (index !== 0 && index === columnItems.length) return 85
return 37
}}
// Scaffold props for infinite loading
hasNextPage={false}
isLoadingNextPage={false}
onLoadNextPage={() => {}}
getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)}
hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems}
isLoadingNextPage={column.isLoadingMoreItems}
onLoadNextPage={() => onColumnLoadMore(index, column)}
/>
{/* Search empty state */}
{isSearching && columnItems.length === 0 && column.items.length > 0 && (
<div className="text-sm mx-3 my-2 opacity-50">No results found based on your search</div>
)}
{/* Drag drop upload CTA for when column is empty */}
{column.items.length === 0 && column.status !== STORAGE_ROW_STATUS.LOADING && (
<div className="h-full w-full flex flex-col items-center justify-center">
@@ -180,7 +180,7 @@ const FileExplorerRow = ({
<div
className={`
storage-row px-4 flex items-center justify-between hover:bg-panel-footer-light dark:hover:bg-panel-footer-dark
${isOpened ? 'bg-bg-secondary-light dark:bg-bg-alt-dark' : ''} ${
${isOpened ? 'bg-scale-400' : ''} ${
isPreviewed ? 'bg-green-500 hover:bg-green-500 dark:hover:bg-green-500' : ''
} ${view === STORAGE_VIEWS.LIST ? 'min-w-min' : ''}
${item.status !== STORAGE_ROW_STATUS.LOADING ? 'cursor-pointer' : ''}
@@ -1,4 +1,4 @@
import { FC, useEffect, useState, useRef } from 'react'
import { useEffect, useState, useRef } from 'react'
import { observer } from 'mobx-react-lite'
import { compact, find, isEmpty, uniqBy, get } from 'lodash'
@@ -48,6 +48,7 @@ const StorageExplorer = observer(({ bucket }) => {
addNewFolderPlaceholder,
addNewFolder,
fetchFolderContents,
fetchMoreFolderContents,
fetchFoldersByPath,
renameFolder,
deleteFolder,
@@ -72,6 +73,22 @@ const StorageExplorer = observer(({ bucket }) => {
// Requires a fixed height to ensure that explorer is constrained to the viewport
const fileExplorerHeight = window.innerHeight - 122
useEffect(async () => {
const currentFolderIdx = openedFolders.length - 1
const currentFolder = openedFolders[currentFolderIdx]
if (itemSearchString) {
await fetchFolderContents(
currentFolder.id,
currentFolder.name,
currentFolderIdx,
itemSearchString
)
} else if (currentFolder) {
await fetchFolderContents(currentFolder.id, currentFolder.name, currentFolderIdx)
}
}, [itemSearchString])
useEffect(() => {
// Load user preferences (view and sort)
loadExplorerPreferences()
@@ -243,7 +260,9 @@ const StorageExplorer = observer(({ bucket }) => {
const onToggleSearch = (bool) => {
setIsSearching(bool)
if (bool === false) setItemSearchString('')
if (bool === false) {
setItemSearchString('')
}
}
return (
@@ -289,8 +308,6 @@ const StorageExplorer = observer(({ bucket }) => {
openedFolders={openedFolders}
selectedItems={selectedItems}
selectedFilePreview={selectedFilePreview}
isSearching={isSearching}
itemSearchString={itemSearchString}
onCheckItem={onCheckItem}
onSelectFile={onSelectFile}
onRenameFile={onRenameFile}
@@ -308,6 +325,9 @@ const StorageExplorer = observer(({ bucket }) => {
onSelectCreateFolder={onSelectCreateFolder}
onChangeView={onChangeView}
onChangeSortBy={onChangeSortBy}
onColumnLoadMore={(index, column) =>
fetchMoreFolderContents(index, column, itemSearchString)
}
/>
<PreviewPane
isOpen={!isEmpty(selectedFilePreview)}
@@ -55,7 +55,7 @@ const PolicyAllowedOperations = ({ allowedOperations = [], onToggleOperation = (
</label>
<p className="text-sm text-scale-900">
Based on the operations you have selected, you can use any of the highlighted functions in
the supabase-js Javascript library
the supabase-js JavaScript library
</p>
</div>
<div className="w-2/3">
+121 -234
View File
@@ -1,9 +1,5 @@
import React, { useState, forwardRef, useEffect } from 'react'
import React, { useState, useEffect } from 'react'
import DatePicker from 'react-datepicker'
// import dayjs from 'dayjs'
import {
Button,
Popover,
@@ -11,40 +7,31 @@ import {
IconChevronRight,
IconArrowRight,
IconCalendar,
IconGlobe,
Input,
Toggle,
Select,
} from '@supabase/ui'
import { format } from 'date-fns'
import TimeSplitInput from './TimeSplitInput'
// import DateSplitInput from './DateSplitInput'
import DividerSlash from './DividerSlash'
import dayjs from 'dayjs'
import { ButtonProps } from '@supabase/ui/dist/cjs/components/Button/Button'
import { DatePickerToFrom } from 'components/interfaces/Settings/Logs'
var utc = require('dayjs/plugin/utc')
var timezone = require('dayjs/plugin/timezone') // dependent on utc plugin
dayjs.extend(utc)
dayjs.extend(timezone)
interface RootProps {
onChange?: ({ to, from }: { to: string; from: string }) => void
export interface DatePickerProps {
onChange?: (args: DatePickerToFrom) => void
to?: string
from?: string
triggerButtonType?: ButtonProps['type']
triggerButtonClassName?: string
renderFooter?: (args: DatePickerToFrom) => React.ReactNode | void
}
const START_DATE_DEFAULT = new Date()
const END_DATE_DEFAULT = new Date()
// console.log('START_DATE_DEFAULT', START_DATE_DEFAULT)
const START_TIME_DEFAULT = { HH: '00', mm: '00', ss: '00' }
const END_TIME_DEFAULT = { HH: '23', mm: '59', ss: '59' }
@@ -54,24 +41,31 @@ function _DatePicker({
onChange,
triggerButtonType = 'default',
triggerButtonClassName = '',
}: RootProps) {
renderFooter = () => null,
}: DatePickerProps) {
const [open, setOpen] = useState<boolean>(false)
const [showTime, setShowTime] = useState<boolean>(true)
const [appliedStartDate, setAppliedStartDate] = useState<any>(null)
const [appliedEndDate, setAppliedEndDate] = useState<any>(null)
const [startDate, setStartDate] = useState<any>(START_DATE_DEFAULT)
const [endDate, setEndDate] = useState<any>(END_DATE_DEFAULT)
const [appliedStartDate, setAppliedStartDate] = useState<null | Date>(null)
const [appliedEndDate, setAppliedEndDate] = useState<null | Date>(null)
const [startDate, setStartDate] = useState<Date | null>(START_DATE_DEFAULT)
const [endDate, setEndDate] = useState<Date | null>(END_DATE_DEFAULT)
const [startTime, setStartTime] = useState<any>(START_TIME_DEFAULT)
const [endTime, setEndTime] = useState<any>(END_TIME_DEFAULT)
useEffect(() => {
if (!to || !from) {
if (!from) {
setAppliedStartDate(null)
} else if (from !== appliedStartDate?.toISOString()) {
const start = dayjs(from).toDate()
setAppliedStartDate(start)
setStartDate(start)
}
if (!to) {
setAppliedEndDate(null)
} else if (to !== appliedEndDate?.toISOString()) {
const end = dayjs(to).toDate()
setAppliedEndDate(end)
setEndDate(end)
}
}, [to, from])
@@ -115,168 +109,77 @@ function _DatePicker({
setAppliedStartDate(null)
setAppliedEndDate(null)
}
const DEFAULT_DATE_FORMAT = 'ddd MMM YYYY HH:mm:ss'
return (
<>
<Popover
open={open}
onOpenChange={(e) => setOpen(e)}
size="small"
align="center"
side="bottom"
header={
<>
<div className="flex justify-between items-stretch py-2">
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="start"
startTime={startTime}
endTime={endTime}
time={startTime}
setTime={setStartTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
/>
</div>
<div
className={`
<Popover
open={open}
onOpenChange={(e) => setOpen(e)}
size="small"
align="center"
side="bottom"
header={
<>
<div className="flex justify-between items-stretch py-2">
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="start"
startTime={startTime}
endTime={endTime}
time={startTime}
setTime={setStartTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
/>
</div>
<div
className={`
w-12
flex
items-center
justify-center
text-scale-900
`}
>
<IconArrowRight strokeWidth={1.5} size={14} />
</div>
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="end"
startTime={startTime}
endTime={endTime}
time={endTime}
setTime={setEndTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
/>
</div>
>
<IconArrowRight strokeWidth={1.5} size={14} />
</div>
</>
}
overlay={
<>
<div className="px-3 py-4">
<DatePicker
selected={startDate}
onChange={(dates) => {
handleDatePickerChange(dates)
}}
dateFormat="MMMM d, yyyy h:mm aa"
<div className="grow flex flex-col gap-1">
<TimeSplitInput
type="end"
startTime={startTime}
endTime={endTime}
time={endTime}
setTime={setEndTime}
setStartTime={setStartTime}
setEndTime={setEndTime}
startDate={startDate}
endDate={endDate}
selectsRange
// showTimeSelect
// nextMonthButtonLabel=">"
// previousMonthButtonLabel="<"
// popperClassName="react-datepicker-left"
// customInput={<ButtonInput />}
// dateFormat={DEFAULT_DATE_FORMAT}
// dateFormat={(locale, date) => dayjs(date).format('MM-YYYY')}
inline
dayClassName={() => 'cursor-pointer'}
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<div className="flex items-center justify-between px-2 py-2">
<div className="flex items-center justify-between w-full">
<button
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
type="button"
className={`
${prevMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
<IconChevronLeft size={16} strokeWidth={2} />
</button>
<span className="text-sm text-scale-1100">{format(date, 'MMMM yyyy')}</span>
<button
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
type="button"
className={`
${nextMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
<IconChevronRight size={16} strokeWidth={2} />
</button>
</div>
</div>
)}
/>
</div>
<Popover.Seperator />
<div className="flex items-center justify-end gap-2 py-2 pb-4 px-3">
<Button type="default" onClick={() => handleClear()}>
Clear
</Button>
<Button onClick={() => handleSubmit()}>Apply</Button>
</div>
</>
}
>
<Button
type={triggerButtonType}
as="span"
icon={<IconCalendar />}
className={triggerButtonClassName}
>
{/* Custom */}
{appliedStartDate && appliedEndDate && appliedStartDate !== appliedEndDate ? (
<>
{format(new Date(appliedStartDate), 'dd MMM')} -{' '}
{format(new Date(appliedEndDate), 'dd MMM')}
</>
) : appliedStartDate || appliedEndDate ? (
format(new Date(appliedStartDate || appliedEndDate), 'dd MMM')
) : (
'Custom'
)}
</Button>
</Popover>
{/* <div className="relative w-40">
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
nextMonthButtonLabel=">"
previousMonthButtonLabel="<"
popperClassName="react-datepicker-right"
customInput={<ButtonInput />}
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<>
</div>
</>
}
overlay={
<>
<div className="px-3 py-4">
<DatePicker
selected={startDate}
onChange={(dates) => {
handleDatePickerChange(dates)
}}
dateFormat="MMMM d, yyyy h:mm aa"
startDate={startDate}
endDate={endDate}
selectsRange
inline
dayClassName={() => 'cursor-pointer'}
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<div className="flex items-center justify-between px-2 py-2">
<div className="flex items-center justify-between w-full">
<button
@@ -284,27 +187,19 @@ function _DatePicker({
disabled={prevMonthButtonDisabled}
type="button"
className={`
${
prevMonthButtonDisabled &&
'cursor-not-allowed opacity-50'
}
${prevMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
<IconChevronLeft size={16} strokeWidth={2} />
</button>
<span className="text-sm text-scale-1100">
{format(date, 'MMMM yyyy')}
</span>
<span className="text-sm text-scale-1100">{format(date, 'MMMM yyyy')}</span>
<button
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
type="button"
className={`
${
nextMonthButtonDisabled &&
'cursor-not-allowed opacity-50'
}
${nextMonthButtonDisabled && 'cursor-not-allowed opacity-50'}
text-scale-1100 hover:text-scale-1200 focus:outline-none
`}
>
@@ -312,51 +207,43 @@ function _DatePicker({
</button>
</div>
</div>
</>
)}
/>
</div>
</div>
<div className="flex items-center justify-center max-w-2xl py-20 mx-auto space-x-4">
<span className="font-medium text-gray-900">Default Components:</span>
<div className="relative w-40">
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
selectsStart
startDate={startDate}
endDate={endDate}
nextMonthButtonLabel=">"
previousMonthButtonLabel="<"
popperClassName="react-datepicker-left"
/>
</div>
<div className="relative w-40">
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
nextMonthButtonLabel=">"
previousMonthButtonLabel="<"
popperClassName="react-datepicker-right"
/>
</div> */}
</>
)}
/>
</div>
{renderFooter({
from: startDate?.toISOString() || null,
to: endDate?.toISOString() || null,
})}
<Popover.Seperator />
<div className="flex items-center justify-end gap-2 py-2 pb-4 px-3">
<Button type="default" onClick={() => handleClear()}>
Clear
</Button>
<Button onClick={() => handleSubmit()}>Apply</Button>
</div>
</>
}
>
<Button
type={triggerButtonType}
as="span"
icon={<IconCalendar />}
className={triggerButtonClassName}
>
{/* Custom */}
{appliedStartDate && appliedEndDate && appliedStartDate !== appliedEndDate ? (
<>
{format(new Date(appliedStartDate), 'dd MMM')} -{' '}
{format(new Date(appliedEndDate), 'dd MMM')}
</>
) : appliedStartDate || appliedEndDate ? (
format(new Date((appliedStartDate || appliedEndDate)!), 'dd MMM')
) : (
'Custom'
)}
</Button>
</Popover>
)
}
const ButtonInput = forwardRef(({ value, onClick }: any, ref): any => (
<button
onClick={onClick}
// @ts-ignore
ref={ref}
type="button"
className="inline-flex justify-start w-full px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-0 focus:ring-blue-500"
>
{value}
</button>
))
export default _DatePicker
@@ -1 +1,2 @@
export { default as DatePicker } from './DatePicker'
export type {DatePickerProps} from "./DatePicker"
+5 -3
View File
@@ -23,7 +23,7 @@ interface ItemProps {
const Item: FC<ItemProps> = memo(({ data, index, style }) => {
const { items, itemProps, ItemComponent } = data
const item = items[index]
const item = index < items.length ? items[index] : undefined
return item ? (
<div style={style}>
@@ -52,7 +52,9 @@ const InfiniteList = ({
const loadMoreItems = isLoadingNextPage ? () => {} : onLoadNextPage
// Every row is loaded except for our loading indicator row
const isItemLoaded = (index: number) => !hasNextPage || !!items[index]
const isItemLoaded = (index: number) => {
return !hasNextPage || index < items.length
}
const itemCount = hasNextPage ? items.length + 1 : items.length
const itemData = createItemData(items, { itemProps, ItemComponent })
@@ -63,8 +65,8 @@ const InfiniteList = ({
<AutoSizer>
{({ height, width }) => (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
isItemLoaded={isItemLoaded}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
@@ -1,6 +1,6 @@
import { FC, ReactNode } from 'react'
import Link from 'next/link'
import { Menu, Typography } from '@supabase/ui'
import { Menu } from '@supabase/ui'
interface Props {
name: string | ReactNode
@@ -25,10 +25,8 @@ const ProductMenuItem: FC<Props> = ({
}) => {
const menuItem = (
<Menu.Item icon={icon} rounded active={isActive} onClick={onClick}>
<div className="flex w-full justify-between">
<span className={'truncate flex items-center ' + textClassName}>
{name}
</span>
<div className="flex w-full justify-between items-center">
<span className={'truncate flex items-center ' + textClassName}>{name}</span>
{action}
</div>
</Menu.Item>
@@ -0,0 +1,110 @@
import { FC } from 'react'
import { useRouter } from 'next/router'
import { Input } from '@supabase/ui'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { useJwtSecretUpdateStatus, useProjectSettings } from 'hooks'
import { SettingsLoadingState } from './SettingsLoadingState'
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
import Panel from 'components/to-be-cleaned/Panel'
const DisplayApiSettings = () => {
const router = useRouter()
const { ref } = router.query
const {
services,
isLoading: isProjectSettingsLoading,
isError: isProjectSettingsError,
} = useProjectSettings(ref as string | undefined)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
// Get the API service
const apiService = (services ?? []).find((x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID)
const apiKeys = apiService?.service_api_keys ?? []
// api keys should not be empty. However it can be populated with a delay on project creation
const isApikeysEmpty = apiKeys.length === 0
return (
<ApiContentWrapper>
{isProjectSettingsLoading || isJwtSecretUpdateStatusLoading || isApikeysEmpty ? (
<SettingsLoadingState
isError={isProjectSettingsError || isJwtSecretUpdateStatusError}
errorMessage="Failed to fetch API keys"
/>
) : (
apiKeys.map((x: any, i: number) => (
<Panel.Content
key={x.api_key}
className={
i >= 1 &&
'border-t border-panel-border-interior-light dark:border-panel-border-interior-dark'
}
>
<Input
layout="horizontal"
// @ts-ignore
label={
<>
{x.tags?.split(',').map((x: any, i: number) => (
<code key={`${x}${i}`} className="text-xs text-code">
{x}
</code>
))}
{x.tags === 'service_role' && (
<>
<code className="text-xs bg-red-900 text-white">{'secret'}</code>
</>
)}
{x.tags === 'anon' && <code className="text-xs text-code">{'public'}</code>}
</>
}
readOnly
copy={isNotUpdatingJwtSecret}
className="input-mono"
disabled
reveal={x.tags !== 'anon' && isNotUpdatingJwtSecret}
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed, new API key may have issues'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: x.api_key
}
onChange={() => {}}
descriptionText={
x.tags === 'service_role'
? 'This key has the ability to bypass Row Level Security. Never share it publicly.'
: 'This key is safe to use in a browser if you have enabled Row Level Security for your tables and configured policies.'
}
/>
</Panel.Content>
))
)}
</ApiContentWrapper>
)
}
export default DisplayApiSettings
const ApiContentWrapper: FC = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project API keys</h5>
<p className="text-sm text-scale-1000">
Your API is secured behind an API gateway which requires an API Key for every request.
<br />
You can use the keys below to use Supabase client libraries.
</p>
</div>
}
>
{children}
</Panel>
)
}
@@ -0,0 +1,92 @@
import { FC } from 'react'
import { useRouter } from 'next/router'
import { Input } from '@supabase/ui'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { useJwtSecretUpdateStatus, useProjectSettings } from 'hooks'
import { SettingsLoadingState } from './SettingsLoadingState'
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
import Panel from 'components/to-be-cleaned/Panel'
const DisplayConfigSettings = () => {
const router = useRouter()
const { ref } = router.query
const {
project,
services,
isLoading: isProjectSettingsLoading,
isError: isProjectSettingsError,
} = useProjectSettings(ref as string | undefined)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
// Get the API service
const jwtSecret = project?.jwt_secret ?? ''
const apiService = (services ?? []).find((x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID)
const apiConfig = apiService?.app_config ?? {}
return (
<ConfigContentWrapper>
{isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
<SettingsLoadingState
isError={isProjectSettingsError || isJwtSecretUpdateStatusError}
errorMessage="Failed to fetch project configuration"
/>
) : (
<>
<Panel.Content>
<Input
label="URL"
readOnly
copy
disabled
className="input-mono"
value={`https://${apiConfig.endpoint}`}
descriptionText="A RESTful endpoint for querying and managing your database."
layout="horizontal"
/>
</Panel.Content>
<Panel.Content className="border-t border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Input
label="JWT Secret"
readOnly
copy={isNotUpdatingJwtSecret}
reveal={isNotUpdatingJwtSecret}
disabled
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: jwtSecret
}
className="input-mono"
descriptionText="Used to decode your JWTs. You can also use this to mint your own JWTs."
layout="horizontal"
/>
</Panel.Content>
</>
)}
</ConfigContentWrapper>
)
}
export default DisplayConfigSettings
const ConfigContentWrapper: FC = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project Configuration</h5>
</div>
}
>
{children}
</Panel>
)
}
@@ -0,0 +1,24 @@
import { FC } from 'react'
import { IconAlertCircle, Loading, Typography } from '@supabase/ui'
import { SettingsLoadingStateProps } from './SettingsLoadingState.types'
import Panel from 'components/to-be-cleaned/Panel'
export const SettingsLoadingState: FC<SettingsLoadingStateProps> = ({ isError, errorMessage }) => {
return (
<Panel.Content className="py-8">
{isError ? (
<div className="flex items-center space-x-2">
<Typography.Text type="secondary">
<IconAlertCircle strokeWidth={2} />
</Typography.Text>
<Typography.Text type="secondary">{errorMessage}</Typography.Text>
</div>
) : (
<div className="py-4">
{/* @ts-ignore */}
<Loading active={true} />
</div>
)}
</Panel.Content>
)
}
@@ -0,0 +1,4 @@
export interface SettingsLoadingStateProps {
isError: boolean
errorMessage: string
}
@@ -0,0 +1,4 @@
import DisplayApiSettings from './DisplayApiSettings'
import DisplayConfigSettings from './DisplayConfigSettings'
export { DisplayApiSettings, DisplayConfigSettings }
+13 -10
View File
@@ -5,17 +5,22 @@ import {
genCountQuery,
genDefaultQuery,
genQueryParams,
getDefaultHelper,
LogData,
Logs,
LogsEndpointParams,
LogsTableName,
PREVIEWER_DATEPICKER_HELPERS,
} from 'components/interfaces/Settings/Logs'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import useSWR from 'swr'
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
interface Data {
logData: LogData[]
error: string | Object | null
@@ -30,14 +35,14 @@ interface Handlers {
loadOlder: () => void
refresh: () => void
setFilters: (filters: Filters | ((previous: Filters) => Filters)) => void
setFrom: (value: string) => void
setTo: (value: string) => void
setParams: Dispatch<SetStateAction<LogsEndpointParams>>
}
function useLogsPreview(
projectRef: string,
table: LogsTableName,
filterOverride?: Filters
): [Data, Handlers] {
const defaultHelper = getDefaultHelper(PREVIEWER_DATEPICKER_HELPERS)
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
const [filters, setFilters] = useState<Filters>({ ...filterOverride })
@@ -46,10 +51,8 @@ function useLogsPreview(
project: projectRef,
sql: '',
rawSql: '',
period_start: '',
period_end: '',
timestamp_start: '',
timestamp_end: '',
iso_timestamp_start: defaultHelper.calcFrom(),
iso_timestamp_end: defaultHelper.calcTo(),
})
useEffect(() => {
@@ -79,8 +82,9 @@ function useLogsPreview(
} else {
const len = prevPageData.result.length
const { timestamp: tsLimit }: LogData = prevPageData.result[len - 1]
const isoTsLimit = dayjs.utc(Number(tsLimit / 1000)).toISOString()
// create new key from params
queryParams = genQueryParams({ ...params, timestamp_end: String(tsLimit) } as any)
queryParams = genQueryParams({ ...params, iso_timestamp_end: isoTsLimit } as any)
}
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${queryParams}`
}
@@ -105,7 +109,7 @@ function useLogsPreview(
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams({
...params,
sql: genCountQuery(table),
period_start: String(latestRefresh),
iso_timestamp_start: latestRefresh,
} as any)}`
}
@@ -157,11 +161,10 @@ function useLogsPreview(
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
},
{
setFrom: (value) => setParams((prev) => ({ ...prev, timestamp_start: value })),
setTo: (value) => setParams((prev) => ({ ...prev, timestamp_end: value })),
setFilters: handleSetFilters,
refresh,
loadOlder: () => setSize((prev) => prev + 1),
setParams,
},
]
}
+15 -7
View File
@@ -1,9 +1,13 @@
import { cleanQuery, genQueryParams } from 'components/interfaces/Settings/Logs'
import {
cleanQuery,
EXPLORER_DATEPICKER_HELPERS,
genQueryParams,
getDefaultHelper,
} from 'components/interfaces/Settings/Logs'
import { Dispatch, SetStateAction, useState } from 'react'
import { LogsEndpointParams, Logs, LogData } from 'components/interfaces/Settings/Logs/Logs.types'
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite/dist/infinite'
import { API_URL } from 'lib/constants'
import useSWR, { mutate } from 'swr'
import useSWR from 'swr'
import { get } from 'lib/common/fetch'
interface Data {
params: LogsEndpointParams
@@ -14,18 +18,20 @@ interface Data {
interface Handlers {
changeQuery: (newQuery?: string) => void
runQuery: () => void
setParams: Dispatch<SetStateAction<LogsEndpointParams>>
}
const useLogsQuery = (
projectRef: string,
initialParams: Partial<LogsEndpointParams> = {}
): [Data, Handlers] => {
const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS)
const [params, setParams] = useState<LogsEndpointParams>({
project: projectRef,
sql: '',
rawSql: '',
// timestamp_start: '',
// timestamp_end: '',
iso_timestamp_start: defaultHelper.calcFrom(),
iso_timestamp_end: defaultHelper.calcTo(),
...initialParams,
})
@@ -36,7 +42,9 @@ const useLogsQuery = (
isValidating: isLoading,
mutate,
} = useSWR<Logs>(
params.sql ? `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${queryParams}` : null,
params.sql
? `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${queryParams}`
: null,
get,
{ revalidateOnFocus: false }
)
@@ -51,7 +59,7 @@ const useLogsQuery = (
return [
{ params, isLoading, logData: data?.result ? data?.result : [], error },
{ changeQuery, runQuery: () => mutate() },
{ changeQuery, runQuery: () => mutate(), setParams },
]
}
export default useLogsQuery
+1 -3
View File
@@ -1,10 +1,8 @@
import { useProfile, useStore } from 'hooks'
import { ComponentType, useEffect, useState } from 'react'
import { flatten, isUndefined } from 'lodash'
import { NextRouter, useRouter } from 'next/router'
import { IS_PLATFORM } from 'lib/constants'
import Connecting from 'components/ui/Loading'
import { Project } from 'types'
const PLATFORM_ONLY_PAGES = ['storage', 'reports', 'settings']
@@ -79,7 +77,7 @@ export function withAuth(
}
function defaultRedirectTo(ref: string | string[] | undefined) {
return IS_PLATFORM ? '/' : !isUndefined(ref) ? `/project/${ref}` : '/'
return IS_PLATFORM ? '/' : ref !== undefined ? `/project/${ref}` : '/'
}
function checkRedirectTo(
@@ -4,7 +4,7 @@ import { get as _get, maxBy } from 'lodash'
import { get } from 'lib/common/fetch'
import { API_URL, DATE_FORMAT, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { chargeableProducts } from 'components/interfaces/Billing'
import { Dictionary } from '@supabase/grid'
import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types'
/**
* Get project payg statistics
@@ -16,7 +16,7 @@ export function useProjectPaygStatistics(ref?: string, tierProdId?: string) {
const startDate = dayjs().utc().startOf('month').format(DATE_FORMAT)
const endDate = dayjs().utc().endOf('month').format(DATE_FORMAT)
const attributes =
'total_db_size_bytes,total_db_egress_bytes,total_storage_size_bytes,total_storage_egress'
'total_egress,total_db_size_bytes,total_db_egress_bytes,total_storage_size_bytes,total_storage_egress'
const url = `${API_URL}/projects/${ref}/daily-stats?attribute=${attributes}&startDate=${encodeURIComponent(
startDate
)}&endDate=${encodeURIComponent(endDate)}&interval='1d'`
@@ -44,13 +44,29 @@ export function useProjectPaygStatistics(ref?: string, tierProdId?: string) {
}
}
function getPaygStats(data: any): Dictionary<number> {
function getPaygStats(data: any): PaygStats {
const paygStats: any = {}
if (!data) return paygStats
chargeableProducts.forEach((product: any) => {
product.features.forEach((feature: any) => {
paygStats[feature.attribute] = _get(maxBy(data, feature.attribute), feature.attribute)
let dataModified: any = []
data.map((x: any) => {
dataModified.push({ ...x, total_egress_modified: x.total_egress - x.total_storage_egress })
})
chargeableProducts.forEach((product) => {
product.features.forEach((feature) => {
// calculate max
const max = _get(maxBy(dataModified, feature.attribute), feature.attribute)
// calculate sum
let sum = 0
dataModified.map((x: any) => {
sum = sum + x[feature.attribute]
})
// set sum and max under attribute name
paygStats[feature.attribute] = {
sum,
max,
}
})
})
return paygStats
+15 -2
View File
@@ -1,10 +1,23 @@
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { API_URL, DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
import useSWR from 'swr'
export function useProjectSettings(ref?: string) {
const url = `${API_URL}/props/project/${ref}/settings`
const { data, error } = useSWR<any>(ref ? url : null, get)
const { data, error } = useSWR<any>(ref ? url : null, get, {
/**
* on project creation, the service_api_keys will be populated with a delay
* check for data.services.service_api_keys, return a valid refresh interval if it's empty
*/
refreshInterval: function (data: any) {
const apiService = (data?.services ?? []).find(
(x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID
)
const apiKeys = apiService?.service_api_keys ?? []
const interval = apiKeys.length === 0 ? 2000 : 0
return interval
},
})
const anyError = data?.error || error
return {
+29
View File
@@ -24,3 +24,32 @@ export async function head<T = any>(
return handleError(error, requestId)
}
}
export async function headWithTimeout<T = any>(
url: string,
headersToRetrieve: string[],
options?: { [prop: string]: any }
): Promise<SupaResponse<T>> {
const requestId = uuidv4()
try {
const timeout = options?.timeout ?? 60000
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), timeout)
const { headers: optionHeaders, ...otherOptions } = options ?? {}
const headers = constructHeaders(requestId, optionHeaders)
const response = await fetch(url, {
method: 'HEAD',
credentials: 'include',
referrerPolicy: 'no-referrer-when-downgrade',
headers,
...otherOptions,
signal: controller.signal,
})
clearTimeout(id)
if (!response.ok) return handleResponseError(response, requestId)
return handleHeadResponse(response, requestId, headersToRetrieve)
} catch (error) {
return handleError(error, requestId)
}
}
+2
View File
@@ -140,3 +140,5 @@ export const PASSWORD_STRENGTH_PERCENTAGE = {
export const DEFAULT_FREE_PROJECTS_LIMIT = 2
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES = 5 * 1024 * 1024 * 1024 // 5 GB
export const DEFAULT_PROJECT_API_SERVICE_ID = 1
@@ -0,0 +1,30 @@
// [Joshen] This file isn't being used at the moment but its to prepare type files when we refactor
// storage dashboard and use typescript for it
export interface ExplorerColumn {
id: string
name: string
items: ExplorerItem[]
status?: 'READY' | 'LOADING'
// New variables to support infinite scrolling
hasMoreItems: boolean
isLoadingMoreItems: boolean
}
export interface ExplorerItem {
id?: string
name: string
type: 'FILE' | 'FOLDER'
status: 'READY' | 'LOADING' | 'EDITING'
metadata?: ItemMetadata
updated_at?: string
last_accessed_at?: string
created_at?: string
}
export interface ItemMetadata {
cacheControl: string
mimetype: string
size: number
}
@@ -42,7 +42,7 @@ export const useStorageExplorerStore = () => {
return useContext(StorageExplorerContext)
}
const LIMIT = 100000
const LIMIT = 200
const OFFSET = 0
const DEFAULT_EXPIRY = 10 * 365 * 24 * 60 * 60 // in seconds, default to 1 year
const PREVIEW_SIZE_LIMIT = 10000000 // 10MB
@@ -225,6 +225,12 @@ class StorageExplorerStore {
this.columns = this.columns.slice(0, index + 1)
}
setColumnIsLoadingMore = (index, isLoadingMoreItems = true) => {
this.columns = this.columns.map((col, idx) => {
return idx === index ? { ...col, isLoadingMoreItems } : col
})
}
pushOpenedFolderAtIndex = (folder, index) => {
this.openedFolders = this.openedFolders.slice(0, index).concat(folder)
}
@@ -266,10 +272,11 @@ class StorageExplorerStore {
}
addNewFolderPlaceholder = (columnIndex) => {
const isPrepend = true
const folderName = 'Untitled folder'
const folderType = STORAGE_ROW_TYPES.FOLDER
const columnIdx = columnIndex === -1 ? this.getLatestColumnIndex() : columnIndex
this.addTempRow(folderType, folderName, STORAGE_ROW_STATUS.EDITING, columnIdx)
this.addTempRow(folderType, folderName, STORAGE_ROW_STATUS.EDITING, columnIdx, {}, isPrepend)
}
addNewFolder = async (folderName, columnIndex) => {
@@ -917,38 +924,78 @@ class StorageExplorerStore {
/* Folders CRUD */
fetchFolderContents = async (folderId, folderName, index, showLoading = true) => {
fetchFolderContents = async (folderId, folderName, index, searchString = '') => {
this.abortApiCalls()
if (showLoading) {
this.updateRowStatus(folderName, STORAGE_ROW_STATUS.LOADING, index)
this.pushColumnAtIndex(
{ id: folderId, name: folderName, status: STORAGE_ROW_STATUS.LOADING, items: [] },
index
)
}
this.updateRowStatus(folderName, STORAGE_ROW_STATUS.LOADING, index)
this.pushColumnAtIndex(
{ id: folderId, name: folderName, status: STORAGE_ROW_STATUS.LOADING, items: [] },
index
)
const prefix = this.openedFolders.map((folder) => folder.name).join('/')
const options = { limit: LIMIT, offset: OFFSET, sortBy: { column: this.sortBy, order: 'asc' } }
const options = {
limit: LIMIT,
offset: OFFSET,
search: searchString,
sortBy: { column: this.sortBy, order: 'asc' },
}
const parameters = { signal: this.abortController.signal }
const { data: items, error } = await this.supabaseClient.storage
.from(this.selectedBucket.name)
.list(prefix, options, parameters)
if (showLoading) {
this.updateRowStatus(folderName, STORAGE_ROW_STATUS.READY, index)
}
this.updateRowStatus(folderName, STORAGE_ROW_STATUS.READY, index)
if (!error) {
const formattedItems = this.formatFolderItems(items)
this.pushColumnAtIndex(
{ id: folderId || folderName, name: folderName, items: formattedItems },
{
id: folderId || folderName,
name: folderName,
items: formattedItems,
hasMoreItems: formattedItems.length === LIMIT,
isLoadingMoreItems: false,
},
index
)
}
}
fetchMoreFolderContents = async (index, column, searchString = '') => {
this.setColumnIsLoadingMore(index)
const prefix = this.openedFolders.map((folder) => folder.name).join('/')
const options = {
limit: LIMIT,
offset: column.items.length,
search: searchString,
sortBy: { column: this.sortBy, order: 'asc' },
}
const parameters = { signal: this.abortController.signal }
const { data: items, error } = await this.supabaseClient.storage
.from(this.selectedBucket.name)
.list(prefix, options, parameters)
if (!error) {
// Add items to column
const formattedItems = this.formatFolderItems(items)
this.columns = this.columns.map((col, idx) => {
if (idx === index) {
return {
...col,
items: col.items.concat(formattedItems),
isLoadingMoreItems: false,
hasMoreItems: items.length === LIMIT,
}
}
return col
})
}
}
refetchAllOpenedFolders = async () => {
const paths = this.openedFolders.map((folder) => folder.name)
await this.fetchFoldersByPath(paths)
@@ -984,6 +1031,8 @@ class StorageExplorerStore {
id: null,
name: idx === 0 ? this.selectedBucket.name : pathsWithEmptyPrefix[idx],
items: formattedItems,
hasMoreItems: formattedItems.length === LIMIT,
isLoadingMoreItems: false,
}
})
@@ -1182,11 +1231,13 @@ class StorageExplorerStore {
return formattedItems
}
addTempRow = (type, name, status, columnIndex, metadata = {}) => {
addTempRow = (type, name, status, columnIndex, metadata = {}, isPrepend = false) => {
const updatedColumns = this.columns.map((column, idx) => {
if (idx === columnIndex) {
const tempRow = { type, name, status, metadata }
const updatedItems = column.items.concat([tempRow])
const updatedItems = isPrepend
? [tempRow].concat(column.items)
: column.items.concat([tempRow])
return { ...column, items: updatedItems }
}
return column
+1
View File
@@ -38,6 +38,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
MAILER_URLPATHS_CONFIRMATION: '',
MAILER_URLPATHS_RECOVERY: '',
MAILER_URLPATHS_EMAIL_CHANGE: '',
SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION: false,
SMTP_ADMIN_EMAIL: '',
SMTP_HOST: null,
SMTP_PORT: null,
+38
View File
@@ -0,0 +1,38 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { PG_META_URL } from 'lib/constants'
import apiWrapper from 'lib/api/apiWrapper'
import { constructHeaders } from 'lib/api/apiHelpers'
import { get } from 'lib/common/fetch'
export default (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
switch (method) {
case 'GET':
return handleGetAll(req, res)
default:
res.setHeader('Allow', ['GET'])
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } })
}
}
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
const headers = constructHeaders(req.headers)
const response = await get(`${PG_META_URL}/types`, { headers })
const includeSystemSchemas = req.query['include_system_schemas'] === 'true'
if (response.error) {
return res.status(400).json({ error: response.error })
}
if (!includeSystemSchemas) {
const types = response?.filter((x: any) => x.schema === 'public')
return res.status(200).json(types)
}
return res.status(200).json(response)
}
+3
View File
@@ -33,6 +33,9 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
db_name: 'postgres',
db_ssl: false,
}),
kpsVersion: 'kps-v1.0.0',
restUrl: process.env.SUPABASE_REST_URL || 'http://localhost:8000/rest/v1/',
internalApiKey: process.env.SUPABASE_ANON_KEY,
}
return res.status(200).json(response)
+1 -1
View File
@@ -60,7 +60,7 @@ const Home: NextPage = () => {
const onDeleteProject = async (project?: Project) => {
if (!project) return
setIsDeletingProject(true)
const response = await delete_(`${API_URL}/projects/${project.ref}/remove`)
const response = await delete_(`${API_URL}/projects/${project.ref}`)
if (response.error) {
return ui.setNotification({ category: 'error', message: response.error.message })
+2 -3
View File
@@ -188,9 +188,8 @@ export const Wizard = observer(() => {
message: `Failed to create new project: ${response.error.message}`,
})
} else {
// Use redirect to reload store data properly
// after creating a new project
window.location.replace(`/project/${response.ref}/building`)
app.onProjectCreated(response)
router.push(`/project/${response.ref}/building`)
}
}
@@ -121,6 +121,7 @@ const Settings = () => {
'JWT_EXP',
'DISABLE_SIGNUP',
'PASSWORD_MIN_LENGTH',
'SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION',
])}
model={{
SITE_URL: model.SITE_URL || undefined,
@@ -128,6 +129,7 @@ const Settings = () => {
DISABLE_SIGNUP: model.DISABLE_SIGNUP,
JWT_EXP: model.JWT_EXP || undefined,
PASSWORD_MIN_LENGTH: model.PASSWORD_MIN_LENGTH || undefined,
SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION: model.SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION || false,
}}
onSubmit={(model: any) => onFormSubmit(model)}
>
@@ -143,6 +145,7 @@ const Settings = () => {
/>
<NumField name="JWT_EXP" step="1" />
<NumField name="PASSWORD_MIN_LENGTH" step="1" />
<ToggleField name="SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION" />
<ToggleField name="DISABLE_SIGNUP" />
</SchemaFormPanel>
</div>
+9 -16
View File
@@ -1,33 +1,26 @@
import * as React from 'react'
import { FC, useEffect } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { useStore, withAuth } from 'hooks'
import { withAuth } from 'hooks'
import ProjectLayout from 'components/layouts'
type ProjectBuildingPageState = {} & any
const ProjectBuildingPage: React.FC<ProjectBuildingPageState> = () => {
const { ui } = useStore()
const project: any = ui.selectedProject
const ProjectBuildingPage: FC = () => {
return (
<ProjectLayout title="Project Building">
<RedirectToDashboard projectRef={project?.ref ?? ''} />
<RedirectToDashboard />
</ProjectLayout>
)
}
export default withAuth(observer(ProjectBuildingPage))
export default withAuth(ProjectBuildingPage)
type RedirectToDashboardState = {
projectRef: string
}
const RedirectToDashboard: React.FC<RedirectToDashboardState> = ({ projectRef }) => {
const RedirectToDashboard: FC = () => {
const router = useRouter()
const { ref } = router.query
React.useEffect(() => {
useEffect(() => {
// Use redirect to reload store data properly after project has been
// been created or unpaused, this is necessarily especially for unpausing
// so that the dashboard fetches the updated connection strings
window.location.replace(`/project/${projectRef}`)
window.location.replace(`/project/${ref}`)
}, [])
return null
}
@@ -5,9 +5,7 @@ import FunctionsLayout from 'components/interfaces/Functions/FunctionsLayout'
import CommandRender from 'components/interfaces/Functions/CommandRender'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toJS } from 'mobx'
import { IconExternalLink, IconGlobe, IconTerminal, Loading } from '@supabase/ui'
import Link from 'next/link'
import { IconGlobe, IconTerminal } from '@supabase/ui'
import dayjs from 'dayjs'
import { NextPageWithLayout } from 'types'
@@ -23,10 +21,6 @@ const PageLayout: NextPageWithLayout = () => {
setSelectedFunction(functions.byId(id))
}, [functions.isLoaded, ui.selectedProject])
// if (!functions.isLoading) return <></>
console.log('selected', functions.byId(id))
const managementCommands: any = [
{
command: `supabase functions deploy ${selectedFunction?.slug}`,
+3 -3
View File
@@ -5,7 +5,7 @@ import { useStore, withAuth } from 'hooks'
import { ExampleProject, ClientLibrary } from 'components/interfaces/Home'
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
import { IS_PLATFORM } from 'lib/constants'
import BaseLayout from 'components/layouts'
import ProjectLayout from 'components/layouts'
import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection'
const Home: NextPage = () => {
@@ -18,7 +18,7 @@ const Home: NextPage = () => {
: 'Welcome to your project'
return (
<BaseLayout>
<ProjectLayout>
<div className="max-w-7xl mx-auto w-full my-16 space-y-16">
<div className="mx-6 flex space-x-6 items-center">
<h1 className="text-3xl">{projectName}</h1>
@@ -45,7 +45,7 @@ const Home: NextPage = () => {
</div>
</div>
</div>
</BaseLayout>
</ProjectLayout>
)
}
@@ -2,12 +2,15 @@ import React, { useEffect, useState } from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Typography, IconAlertCircle, Card, Input, Alert, Modal, Form, Button } from '@supabase/ui'
import { Input, Modal, Form, Button } from '@supabase/ui'
import { useStore, withAuth } from 'hooks'
import CodeEditor from 'components/ui/CodeEditor'
import {
DatePickerToFrom,
LogsQueryPanel,
LogsTableName,
LogsWarning,
LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD,
LogTable,
LogTemplate,
TEMPLATES,
@@ -17,37 +20,58 @@ import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { LogsExplorerLayout } from 'components/layouts'
import ShimmerLine from 'components/ui/ShimmerLine'
import LoadingOpacity from 'components/ui/LoadingOpacity'
import { LogSqlSnippets, UserContent } from 'types'
import { UserContent } from 'types'
import toast from 'react-hot-toast'
import dayjs from 'dayjs'
export const LogsExplorerPage: NextPage = () => {
const router = useRouter()
const { ref, q } = router.query
const { ref, q, ite, its } = router.query
const [editorId, setEditorId] = useState<string>(uuidv4())
const [editorValue, setEditorValue] = useState<string>('')
const [saveModalOpen, setSaveModalOpen] = useState<boolean>(false)
const [warnings, setWarnings] = useState<LogsWarning[]>([])
const { content } = useStore()
const [{ logData, error, isLoading }, { changeQuery, runQuery }] = useLogsQuery(ref as string)
const [{ params, logData, error, isLoading }, { changeQuery, runQuery, setParams }] =
useLogsQuery(ref as string, {
iso_timestamp_start: (its || '') as string,
iso_timestamp_end: (ite || '') as string,
})
useEffect(() => {
// on mount, set initial values
if (q !== undefined && q !== '') {
changeQuery(q as string)
runQuery()
if (q) {
onSelectTemplate({
mode: 'custom',
searchString: q as string,
})
}
}, [q])
}, [])
useEffect(() => {
let newWarnings = []
const start = params.iso_timestamp_start ? dayjs(params.iso_timestamp_start) : dayjs()
const end = params.iso_timestamp_end ? dayjs(params.iso_timestamp_end) : dayjs()
const daysDiff = Math.abs(start.diff(end, 'days'))
if (
editorValue &&
!editorValue.includes('limit') &&
daysDiff > LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD
) {
newWarnings.push({ text: 'When querying large date ranges, include a LIMIT clause.' })
}
setWarnings(newWarnings)
}, [editorValue, params.iso_timestamp_start, params.iso_timestamp_end])
const onSelectTemplate = (template: LogTemplate) => {
setEditorValue(template.searchString)
changeQuery(template.searchString)
setEditorId(uuidv4())
runQuery()
router.push({
pathname: router.pathname,
query: { ...router.query, q: template.searchString },
})
}
const handleRun = () => {
@@ -74,11 +98,26 @@ export const LogsExplorerPage: NextPage = () => {
setSaveModalOpen(!saveModalOpen)
}
const handleDateChange = ({ to, from }: DatePickerToFrom) => {
setParams((prev) => ({
...prev,
iso_timestamp_start: from || '',
iso_timestamp_end: to || '',
}))
router.push({
pathname: router.pathname,
query: { ...router.query, its: from || '', ite: to || '' },
})
}
return (
<LogsExplorerLayout>
<div className="h-full flex flex-col flex-grow gap-4">
<div className="border rounded">
<LogsQueryPanel
defaultFrom={params.iso_timestamp_start || ''}
defaultTo={params.iso_timestamp_end || ''}
onDateChange={handleDateChange}
onSelectSource={handleInsertSource}
onClear={handleClear}
onRun={handleRun}
@@ -87,6 +126,7 @@ export const LogsExplorerPage: NextPage = () => {
onSelectTemplate={onSelectTemplate}
onSave={handleOnSave}
isLoading={isLoading}
warnings={warnings}
/>
<div className="min-h-[7rem] h-48">
@@ -28,7 +28,6 @@ export const LogsExplorerPage: NextPage = () => {
const saved = content.list()
console.log('saved in parent', saved.length)
return (
<LogsExplorerLayout>
<div className="flex flex-col gap-3">
+1 -1
View File
@@ -45,7 +45,7 @@ import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
import Panel from 'components/to-be-cleaned/Panel'
import MultiSelectUI from 'components/to-be-cleaned/MultiSelect'
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
import { DisplayApiSettings } from 'components/to-be-cleaned/DisplayProjectSettings'
import { DisplayApiSettings } from 'components/ui/ProjectSettings'
const JWT_SECRET_UPDATE_ERROR_MESSAGES = {
[JwtSecretUpdateError.APIServicesConfigurationUpdateFailed]:
@@ -11,6 +11,7 @@ import { PAYGUsage, Subscription, Invoices } from 'components/interfaces/Billing
import ProjectUsage from 'components/to-be-cleaned/Usage'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types'
type ProjectBillingProps = {} & any
const ProjectBilling: FC<ProjectBillingProps> = ({ store }) => {
@@ -35,15 +36,18 @@ type SettingsProps = {
}
const Settings: FC<SettingsProps> = ({ project }) => {
const { ui } = useStore()
const {
subscription,
isLoading: loading,
error,
} = useProjectSubscription(ui.selectedProject?.ref)
const { paygStats } = useProjectPaygStatistics(
const { paygStats }: { paygStats: PaygStats | undefined } = useProjectPaygStatistics(
ui.selectedProject?.ref,
subscription?.tier?.supabase_prod_id
)
const [dateRange, setDateRange] = useState<any>()
useEffect(() => {
+22 -21
View File
@@ -1,10 +1,10 @@
import React, { FC, useState } from 'react'
import { FC, useState } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { toJS } from 'mobx'
import { projects } from 'stores/jsonSchema'
import { AutoField } from 'uniforms-bootstrap4'
import { Alert, Button, Input, Typography, IconRefreshCcw } from '@supabase/ui'
import { Alert, Button, Input, IconRefreshCcw } from '@supabase/ui'
import { API_URL } from 'lib/constants'
import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers'
@@ -30,8 +30,13 @@ const ProjectSettings = () => {
export default withAuth(observer(ProjectSettings))
const RestartServerButton: FC<any> = ({ projectRef }: any) => {
const { ui } = useStore()
interface RestartServerButtonProps {
projectId: number
projectRef: string
}
const RestartServerButton: FC<RestartServerButtonProps> = observer(({ projectRef, projectId }) => {
const { ui, app } = useStore()
const router = useRouter()
const [loading, setLoading] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
@@ -42,8 +47,9 @@ const RestartServerButton: FC<any> = ({ projectRef }: any) => {
setLoading(true)
try {
await post(`${API_URL}/projects/${projectRef}/restart`, {})
ui.setNotification({ category: 'info', message: 'Requested server restart' })
setLoading(false)
app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE')
ui.setNotification({ category: 'success', message: 'Restarting server' })
router.push(`/project/${projectRef}`)
} catch (error) {
ui.setNotification({ error, category: 'error', message: 'Unable to restart server' })
setLoading(false)
@@ -68,7 +74,7 @@ const RestartServerButton: FC<any> = ({ projectRef }: any) => {
</Button>
</>
)
}
})
const GeneralSettings = observer(() => {
const { app, ui } = useStore()
@@ -106,9 +112,9 @@ const GeneralSettings = observer(() => {
<section>
<Panel
title={
<Typography.Title key="panel-title" level={5} className="mb-0">
<h5 key="panel-title" className="text-base mb-0">
Infrastructure
</Typography.Title>
</h5>
}
>
<Panel.Content>
@@ -126,36 +132,31 @@ const GeneralSettings = observer(() => {
<Panel.Content className="border-t border-panel-border-interior-light dark:border-panel-border-interior-dark">
<div className="w-full flex items-center justify-between">
<div>
<Typography.Text className="block">Restart Server</Typography.Text>
<p>Restart server</p>
<div style={{ maxWidth: '420px' }}>
<p className="opacity-50 text-sm">
Your project will not be available for a few minutes.
</p>
</div>
</div>
<RestartServerButton projectRef={project?.ref} />
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
</div>
</Panel.Content>
</Panel>
</section>
<section>
<Panel title={<Typography.Text className="uppercase">Danger Zone</Typography.Text>}>
<Panel title={<p className="uppercase">Danger Zone</p>}>
<Panel.Content>
<Alert
variant="danger"
withIcon
// @ts-ignore
title={
<Typography.Text>
Deleting this project will also remove your database.
</Typography.Text>
}
title="Deleting this project will also remove your database."
>
<div className="flex flex-col">
<Typography.Text className="block mb-4">
<p className="block mb-4">
Make sure you have made a backup if you want to keep your data.
</Typography.Text>
</p>
<ProjectDeleteModal project={project} />
</div>
</Alert>
@@ -181,7 +182,7 @@ const ProjectDeleteModal = ({ project }: any) => {
async function handleDeleteProject() {
setLoading(true)
try {
const response = await delete_(`${API_URL}/projects/${project.ref}/remove`)
const response = await delete_(`${API_URL}/projects/${project.ref}`)
if (response.error) throw response.error
app.onProjectDeleted(response)
ui.setNotification({ category: 'success', message: `Successfully deleted ${project.name}` })
+25
View File
@@ -12,10 +12,12 @@ export interface IAppStore {
projects: IProjectStore
organizations: OrganizationStore
database: IDatabaseStore
onProjectCreated: (project: any) => void
onProjectUpdated: (project: any) => void
onProjectDeleted: (project: any) => void
onProjectConnectionStringUpdated: (projectId: number, value: string) => void
onProjectStatusUpdated: (projectId: number, value: string) => void
onProjectPostgrestStatusUpdated: (projectId: number, value: 'OFFLINE' | 'ONLINE') => void
onOrgAdded: (org: any) => void
onOrgUpdated: (org: any) => void
onOrgDeleted: (org: any) => void
@@ -39,6 +41,23 @@ export default class AppStore implements IAppStore {
this.database = new DatabaseStore(rootStore, `${this.baseUrl}/database`, headers)
}
onProjectCreated(project: any) {
if (project && project.id) {
const temp: Project = {
id: project.id,
ref: project.ref,
name: project.name,
status: project.status,
organization_id: project.organization_id,
cloud_provider: project.cloud_provider,
region: project.region,
inserted_at: project.inserted_at,
subscription_id: project.subscription_id,
}
this.projects.data[project.id] = temp
}
}
onProjectUpdated(project: any) {
if (project && project.id) {
const clone = cloneDeep(this.projects.data[project.id])
@@ -69,6 +88,12 @@ export default class AppStore implements IAppStore {
this.projects.data[projectId] = clone
}
onProjectPostgrestStatusUpdated(projectId: number, value: 'OFFLINE' | 'ONLINE') {
const clone = cloneDeep(this.projects.data[projectId])
clone.postgrestStatus = value
this.projects.data[projectId] = clone
}
onOrgUpdated(updatedOrg: Organization) {
if (updatedOrg && updatedOrg.id) {
const originalOrg = this.organizations.data[updatedOrg.id]
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more