mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge branch 'master' into chore/mono
This commit is contained in:
commit
81a55e69c8
134 files changed
+2109
-1320
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||

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

|
||||
|
||||
|
||||
+1
-1
@@ -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) করতে পারেন।
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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.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://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://github.com/sponsors/supabase)
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
+5
-3
@@ -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>
|
||||
|
||||
+8
-8
@@ -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: (
|
||||
|
||||
+9
-9
@@ -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) => {
|
||||
|
||||
+4
-3
@@ -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}
|
||||
|
||||
+5
-7
@@ -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>
|
||||
))}
|
||||
|
||||
+12
-21
@@ -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>
|
||||
|
||||
+15
-13
@@ -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 = {},
|
||||
|
||||
+8
-3
@@ -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
|
||||
|
||||
+4
-10
@@ -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>
|
||||
|
||||
+3
-5
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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}` })
|
||||
|
||||
@@ -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
Reference in new issue
Block a user