mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 02:45:07 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
bcce8bf453
184 files changed
+3581
-1681
No files matched your search
@@ -123,7 +123,7 @@ const ClippyModal: FC<Props> = ({ onClose }) => {
|
||||
return (
|
||||
<Modal size="xlarge" visible={true} onCancel={onClose} closable={false} hideFooter>
|
||||
<div
|
||||
className={`mx-auto max-h-[75vh] flex flex-col gap-4 rounded-lg p-4 md:pt-6 md:px-6 pb-2 w-full shadow-2xl overflow-hidden border text-left border-scale-500 bg-scale-100 dark:bg-scale-300 cursor-auto relative min-w-[340px]`}
|
||||
className={`mx-auto max-h-[50vh] lg:max-h-[75vh] flex flex-col gap-4 rounded-lg p-4 md:pt-6 md:px-6 pb-2 w-full shadow-2xl overflow-hidden border text-left border-scale-500 bg-scale-100 dark:bg-scale-300 cursor-auto relative min-w-[340px]`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="relative">
|
||||
|
||||
@@ -11,12 +11,14 @@ import clippyImage from '../../../public/img/clippy.png'
|
||||
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
import { useClippy } from '~/components/Clippy/ClippyProvider'
|
||||
import { useFlag } from '~/hooks/useFlag'
|
||||
|
||||
const TopNavBarRef: FC = () => {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const { open: openClippy } = useClippy()
|
||||
const enableClippy = useFlag('enableClippy')
|
||||
|
||||
const { asPath, push } = useRouter()
|
||||
const pathSegments = asPath.split('/')
|
||||
@@ -89,15 +91,17 @@ const TopNavBarRef: FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex md:hidden cursor-pointer">
|
||||
<Image
|
||||
onClick={openClippy}
|
||||
width={26}
|
||||
height={29}
|
||||
src={isDarkMode ? clippyImageDark : clippyImage}
|
||||
alt="Clippy"
|
||||
/>
|
||||
</div>
|
||||
{enableClippy && (
|
||||
<div className="flex md:hidden cursor-pointer">
|
||||
<Image
|
||||
onClick={openClippy}
|
||||
width={26}
|
||||
height={29}
|
||||
src={isDarkMode ? clippyImageDark : clippyImage}
|
||||
alt="Clippy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<SearchButton className="md:w-full lg:w-96">
|
||||
<div
|
||||
className="
|
||||
|
||||
@@ -92,12 +92,12 @@ This is what your `schema.prisma` file should look like:
|
||||
|
||||
```go
|
||||
datasource db {
|
||||
provider = “postgresql”
|
||||
url = env(“DATABASE_URL”)
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
generator client {
|
||||
provider = “prisma-client-js”
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
@@ -148,7 +148,7 @@ export const postgres = [
|
||||
title: 'Select first row per group',
|
||||
hasLightIcon: true,
|
||||
href: '/guides/resources/postgres/first-row-in-group',
|
||||
description: 'Retreive the first row in each distinct group.',
|
||||
description: 'Retrieve the first row in each distinct group.',
|
||||
},
|
||||
{
|
||||
title: 'Print PostgreSQL version',
|
||||
|
||||
+140
-18
@@ -9,13 +9,16 @@
|
||||
|
||||
[Supabase](https://supabase.com) 是 Firebase 的開放原始碼替代方案。我們以企業等級的開放原始碼工具,打造 Firebase 的功能。
|
||||
|
||||
- [x] Postgres 資料庫託管
|
||||
- [x] 即時訂閱
|
||||
- [x] 認證與授權
|
||||
- [x] Postgres 資料庫託管 [Docs](https://supabase.com/docs/guides/database)
|
||||
- [x] 認證與授權 [Docs](https://supabase.com/docs/guides/auth)
|
||||
- [x] 自動產生的 API
|
||||
- [x] 儀表版
|
||||
- [x] Storage
|
||||
- [x] REST. [Docs](https://supabase.com/docs/guides/api#rest-api)
|
||||
- [x] 實時訂閱. [Docs](https://supabase.com/docs/guides/api#realtime-api)
|
||||
- [x] GraphQL (Beta). [Docs](https://supabase.com/docs/guides/api#graphql-api)
|
||||
- [x] Functions
|
||||
- [x] 數據庫 Functions. [Docs](https://supabase.com/docs/guides/database/functions)
|
||||
- [x] 邊緣 Functions [Docs](https://supabase.com/docs/guides/functions)
|
||||
- [x] 儀表版
|
||||
|
||||

|
||||
|
||||
@@ -23,11 +26,14 @@
|
||||
|
||||
完整的說明文件請見:[supabase.com/docs](https://supabase.com/docs)
|
||||
|
||||
如何貢獻請見:[Getting Started](./DEVELOPERS.md)
|
||||
|
||||
## 社群與支援
|
||||
|
||||
- [社群論壇](https://github.com/supabase/supabase/discussions)。適合:打造、討論資料庫最佳作法的協助。
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues)。適合:在使用 Supabase 的時候遇到臭蟲和錯誤。
|
||||
- [電子郵件支援](https://supabase.com/docs/support#business-support)。適合:資料庫或基礎建設遇到的問題。
|
||||
- [Discord](https://discord.supabase.com)。適合:分享你的應用程式並與社群互動。
|
||||
|
||||
## 狀態
|
||||
|
||||
@@ -48,7 +54,7 @@ Supabase 是一套開放原始碼工具的組合包。我們以企業等級的
|
||||
|
||||
**目前架構**
|
||||
|
||||
Supabase 是[託管平台](https://app.supabase.com)。只要註冊,不必安裝任何東西,就可以開始使用 Supabase。目前還在打造本地端開發體驗:連同平台的穩定度,這是我們專注的核心。
|
||||
Supabase 是[託管平台](https://app.supabase.com)。只要註冊,不必安裝任何東西,就可以開始使用 Supabase。你也可以[自託管](https://supabase.com/docs/guides/hosting/overview)和[本地端開發](https://supabase.com/docs/guides/local-development)。
|
||||
|
||||

|
||||
|
||||
@@ -64,18 +70,134 @@ Supabase 是[託管平台](https://app.supabase.com)。只要註冊,不必安
|
||||
|
||||
客戶端 Library 是模組化的。每個子 Library 都是單一外部系統的獨立實作。這是支援既有工具的方法之一。
|
||||
|
||||
- **`supabase-{lang}`**: Combines libraries and adds enrichments.
|
||||
- `postgrest-{lang}`: 銜接 [PostgREST](https://github.com/postgrest/postgrest) 的客戶端 Library
|
||||
- `realtime-{lang}`: 銜接 [Realtime](https://github.com/supabase/realtime) 的客戶端 Library
|
||||
- `gotrue-{lang}`: 銜接 [GoTrue](https://github.com/netlify/gotrue) 的客戶端 Library
|
||||
|
||||
| Repo | 官方 | 社群 |
|
||||
| --------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`supabase-{lang}`** | [`JS`](https://github.com/supabase/supabase-js) | [`C#`](https://github.com/supabase/supabase-csharp) \| [`Flutter`](https://github.com/supabase/supabase-flutter) \| [`Python`](https://github.com/supabase/supabase-py) \| `Rust` \| [`Ruby`](https://github.com/supabase/supabase-rb) \| `Go` |
|
||||
| `postgrest-{lang}` | [`JS`](https://github.com/supabase/postgrest-js) | [`C#`](https://github.com/supabase/postgrest-csharp) \| [`Dart`](https://github.com/supabase/postgrest-dart) \| [`Python`](https://github.com/supabase/postgrest-py) \| [`Rust`](https://github.com/supabase/postgrest-rs) \| [`Ruby`](https://github.com/supabase/postgrest-rb) \| [`Go`](https://github.com/supabase/postgrest-go) |
|
||||
| `realtime-{lang}` | [`JS`](https://github.com/supabase/realtime-js) | [`C#`](https://github.com/supabase/realtime-csharp) \| [`Dart`](https://github.com/supabase/realtime-dart) \| [`Python`](https://github.com/supabase/realtime-py) \| `Rust` \| `Ruby` \| `Go` |
|
||||
| `gotrue-{lang}` | [`JS`](https://github.com/supabase/gotrue-js) | [`C#`](https://github.com/supabase/gotrue-csharp) \| [`Dart`](https://github.com/supabase/gotrue-dart) \| [`Python`](https://github.com/supabase/gotrue-py) \| `Rust` \| `Ruby` \| `Go` |
|
||||
|
||||
<table style="table-layout:fixed; white-space: nowrap;">
|
||||
<tr>
|
||||
<th>語言</th>
|
||||
<th>客戶端</th>
|
||||
<th colspan="5">精選客戶端(包含在 Supabase 客戶端)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Supabase</th>
|
||||
<th><a href="https://github.com/postgrest/postgrest" target="_blank" rel="noopener noreferrer">PostgREST</a></th>
|
||||
<th><a href="https://github.com/supabase/gotrue" target="_blank" rel="noopener noreferrer">GoTrue</a></th>
|
||||
<th><a href="https://github.com/supabase/realtime" target="_blank" rel="noopener noreferrer">Realtime</a></th>
|
||||
<th><a href="https://github.com/supabase/storage-api" target="_blank" rel="noopener noreferrer">Storage</a></th>
|
||||
<th>Functions</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="7">⚡️ 官方 ⚡️</th>
|
||||
<tr>
|
||||
<td>JavaScript (TypeScript)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-js" target="_blank" rel="noopener noreferrer">postgrest-js</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">gotrue-js</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-flutter</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
|
||||
</tr>
|
||||
<th colspan="7">💚 社群 💚</th>
|
||||
<tr>
|
||||
<td>C#</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-csharp" target="_blank" rel="noopener noreferrer">supabase-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-csharp" target="_blank" rel="noopener noreferrer">postgrest-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-csharp" target="_blank" rel="noopener noreferrer">gotrue-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-csharp" target="_blank" rel="noopener noreferrer">realtime-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-csharp" target="_blank" rel="noopener noreferrer">storage-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-csharp" target="_blank" rel="noopener noreferrer">functions-csharp</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-go" target="_blank" rel="noopener noreferrer">postgrest-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-go" target="_blank" rel="noopener noreferrer">gotrue-go</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-go" target="_blank" rel="noopener noreferrer">storage-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-go" target="_blank" rel="noopener noreferrer">functions-go</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Java</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-java" target="_blank" rel="noopener noreferrer">gotrue-java</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-java" target="_blank" rel="noopener noreferrer">storage-java</a></td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kotlin</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt" target="_blank" rel="noopener noreferrer">supabase-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-kt" target="_blank" rel="noopener noreferrer">postgrest-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-kt" target="_blank" rel="noopener noreferrer">gotrue-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Realtime" target="_blank" rel="noopener noreferrer">realtime-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Storage" target="_blank" rel="noopener noreferrer">storage-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Functions" target="_blank" rel="noopener noreferrer">functions-kt</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Python</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-py" target="_blank" rel="noopener noreferrer">supabase-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ruby</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-rb" target="_blank" rel="noopener noreferrer">supabase-rb</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-rb" target="_blank" rel="noopener noreferrer">postgrest-rb</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rust</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-rs" target="_blank" rel="noopener noreferrer">postgrest-rs</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-gdscript" target="_blank" rel="noopener noreferrer">postgrest-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-gdscript" target="_blank" rel="noopener noreferrer">gotrue-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-gdscript" target="_blank" rel="noopener noreferrer">realtime-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--- Remove this list if you're traslating to another language, it's hard to keep updated across multiple files-->
|
||||
<!--- Keep only the link to the list of translation files-->
|
||||
|
||||
|
||||
Generated
-16
@@ -38976,7 +38976,6 @@
|
||||
"semver": "^6.3.0",
|
||||
"shared-data": "*",
|
||||
"sqlstring": "^2.3.2",
|
||||
"swr": "^1.2.1",
|
||||
"ui": "*",
|
||||
"uniforms-bootstrap4": "^3.6.2",
|
||||
"uniforms-bridge-json-schema": "^3.6.2",
|
||||
@@ -39318,14 +39317,6 @@
|
||||
"react-dom": "^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/swr": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz",
|
||||
"integrity": "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/typescript": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
|
||||
@@ -65856,7 +65847,6 @@
|
||||
"shared-data": "*",
|
||||
"sqlstring": "^2.3.2",
|
||||
"storybook-dark-mode": "^1.0.4",
|
||||
"swr": "^1.2.1",
|
||||
"tailwindcss": "^3.0.22",
|
||||
"ts-jest": "^27.0.7",
|
||||
"typescript": "4.8.4",
|
||||
@@ -66083,12 +66073,6 @@
|
||||
"reduce-css-calc": "^2.1.8"
|
||||
}
|
||||
},
|
||||
"swr": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz",
|
||||
"integrity": "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==",
|
||||
"requires": {}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
|
||||
|
||||
@@ -80,6 +80,7 @@ export function RightSlot({ children }: any) {
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
disabled?: boolean
|
||||
@@ -87,11 +88,15 @@ interface ItemProps {
|
||||
rightSlot?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Item({ children, icon, disabled, onClick, rightSlot }: ItemProps) {
|
||||
export function Item({ className, children, icon, disabled, onClick, rightSlot }: ItemProps) {
|
||||
let __styles = styleHandler('dropdown')
|
||||
|
||||
return (
|
||||
<RadixDropdown.Item className={__styles.item} disabled={disabled} onSelect={onClick}>
|
||||
<RadixDropdown.Item
|
||||
className={`${__styles.item} ${className}`}
|
||||
disabled={disabled}
|
||||
onSelect={onClick}
|
||||
>
|
||||
{icon && icon}
|
||||
<span>{children}</span>
|
||||
</RadixDropdown.Item>
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Toaster as HotToaster,
|
||||
toast as hotToast,
|
||||
resolveValue,
|
||||
} from 'react-hot-toast'
|
||||
import {
|
||||
Toast as HotToastProps,
|
||||
ToastType,
|
||||
} from 'react-hot-toast/dist/core/types'
|
||||
import { Toaster as HotToaster, toast as hotToast, resolveValue } from 'react-hot-toast'
|
||||
import { Toast as HotToastProps, ToastType } from 'react-hot-toast/dist/core/types'
|
||||
import * as Portal from '@radix-ui/react-portal'
|
||||
import { IconCheck } from '../Icon/icons/IconCheck'
|
||||
import { IconLoader } from '../Icon/icons/IconLoader'
|
||||
@@ -30,10 +23,7 @@ export interface ToastProps extends Partial<HotToastProps> {
|
||||
width?: 'xs' | 'sm' | 'md'
|
||||
}
|
||||
|
||||
function Message({
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Typography.Text>) {
|
||||
function Message({ children, ...props }: ComponentProps<typeof Typography.Text>) {
|
||||
return (
|
||||
<Typography.Text className={ToastStyles['sbui-toast-message']} {...props}>
|
||||
{children}
|
||||
@@ -41,15 +31,9 @@ function Message({
|
||||
)
|
||||
}
|
||||
|
||||
function Description({
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Typography.Text>) {
|
||||
function Description({ children, ...props }: ComponentProps<typeof Typography.Text>) {
|
||||
return (
|
||||
<Typography.Text
|
||||
className={ToastStyles['sbui-toast-description']}
|
||||
{...props}
|
||||
>
|
||||
<Typography.Text className={ToastStyles['sbui-toast-description']} {...props}>
|
||||
{children}
|
||||
</Typography.Text>
|
||||
)
|
||||
@@ -100,18 +84,10 @@ function Toast({
|
||||
}
|
||||
|
||||
const _message =
|
||||
typeof message === 'string' ? (
|
||||
<Message>{message}</Message>
|
||||
) : (
|
||||
resolveValue(message, rest)
|
||||
)
|
||||
typeof message === 'string' ? <Message>{message}</Message> : resolveValue(message, rest)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${containerClasses.join(' ')} ${
|
||||
visible ? 'animate-enter' : 'animate-leave'
|
||||
}`}
|
||||
>
|
||||
<div className={`${containerClasses.join(' ')} ${visible ? 'animate-enter' : 'animate-leave'}`}>
|
||||
<div>
|
||||
<Typography.Text className={ToastStyles['sbui-toast-icon-container']}>
|
||||
{type === 'loading' ? (
|
||||
@@ -129,28 +105,21 @@ function Toast({
|
||||
{_message}
|
||||
{description && <Description>{description}</Description>}
|
||||
</div>
|
||||
{actions && (
|
||||
<div className={ToastStyles['sbui-toast-details__actions']}>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{actions && <div className={ToastStyles['sbui-toast-details__actions']}>{actions}</div>}
|
||||
</div>
|
||||
{closable && (
|
||||
<div className={ToastStyles['sbui-toast-close-container']}>
|
||||
<button
|
||||
aria-label="Close alert"
|
||||
className={closeButtonClasses.join(' ')}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
hotToast.dismiss(id)
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<IconX
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
size="small"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<IconX className="h-5 w-5" aria-hidden="true" size="small" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -200,8 +169,7 @@ type ToastOptions = Partial<
|
||||
>
|
||||
|
||||
export function toast(message: string, opts?: ToastOptions) {
|
||||
const { description, closable, actions, actionsPosition, type, ...rest } =
|
||||
opts || {}
|
||||
const { description, closable, actions, actionsPosition, type, ...rest } = opts || {}
|
||||
|
||||
return hotToast(
|
||||
({ message: _m, type: _t, ...t }) => (
|
||||
@@ -219,16 +187,13 @@ export function toast(message: string, opts?: ToastOptions) {
|
||||
)
|
||||
}
|
||||
|
||||
const createToastType = (type: ToastType) => (
|
||||
message: string,
|
||||
opts?: Omit<ToastOptions, 'type'>
|
||||
) => toast(message, { ...opts, type })
|
||||
const createToastType = (type: ToastType) => (message: string, opts?: Omit<ToastOptions, 'type'>) =>
|
||||
toast(message, { ...opts, type })
|
||||
|
||||
toast.success = createToastType('success')
|
||||
toast.error = createToastType('error')
|
||||
toast.loading = createToastType('loading')
|
||||
toast.promise = (...args: Parameters<typeof hotToast.promise>) =>
|
||||
hotToast.promise(...args)
|
||||
toast.promise = (...args: Parameters<typeof hotToast.promise>) => hotToast.promise(...args)
|
||||
|
||||
Toast.Toaster = Toaster
|
||||
Toast.toast = toast
|
||||
|
||||
@@ -4440,7 +4440,8 @@ functions:
|
||||
.from('avatars')
|
||||
.update('public/avatar1.png', avatarFile, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
// Overwrite file if it exists
|
||||
upsert: true
|
||||
})
|
||||
```
|
||||
- id: update-file-using-arraybuffer-from-base64-file-data
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const useProjectSubscriptionQuery = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
tier: {
|
||||
supabase_prod_id: 'tier_free',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,20 +1,15 @@
|
||||
export const useFlag = jest.fn().mockReturnValue(true)
|
||||
export const checkPermissions = jest.fn().mockReturnValue(true)
|
||||
export const useProjectSubscription = jest.fn().mockReturnValue({
|
||||
subscription: {
|
||||
tier: {
|
||||
supabase_prod_id: 'tier_free',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { useStore, StoreProvider } = jest.requireActual('hooks')
|
||||
export const useStore = useStore
|
||||
|
||||
export const useParams = jest.fn().mockImplementation(() => ({ ref: '123' }))
|
||||
|
||||
// mocks browser event listener adding/removing
|
||||
window.matchMedia = jest.fn().mockReturnValue({
|
||||
removeEventListener: jest.fn(),
|
||||
})
|
||||
export const StoreProvider = StoreProvider
|
||||
|
||||
export const withAuth = jest.fn()
|
||||
export const withAuth = jest.fn()
|
||||
@@ -184,6 +184,7 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
return (
|
||||
<div className="sb-grid">
|
||||
<Header
|
||||
table={table}
|
||||
sorts={sorts}
|
||||
filters={filters}
|
||||
onAddRow={editable ? props.onAddRow : undefined}
|
||||
|
||||
@@ -99,6 +99,7 @@ export async function initTable(
|
||||
editable: props.editable,
|
||||
defaultWidth: props.gridProps?.defaultColumnWidth,
|
||||
onAddColumn: props.editable ? props.onAddColumn : undefined,
|
||||
onExpandJSONEditor: props.editable ? props.onExpandJSONEditor : () => {},
|
||||
})
|
||||
|
||||
dispatch({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, useCallback, KeyboardEvent, ReactNode } from 'react'
|
||||
import { useClickedOutside } from 'hooks'
|
||||
import { FC, useRef, useEffect, useCallback, KeyboardEvent, ReactNode } from 'react'
|
||||
|
||||
interface BlockKeysProps {
|
||||
value: string | null
|
||||
@@ -13,6 +14,9 @@ interface BlockKeysProps {
|
||||
* Example: press enter to add newline on textEditor
|
||||
*/
|
||||
export const BlockKeys: FC<BlockKeysProps> = ({ value, children, onEscape, onEnter }) => {
|
||||
const ref = useRef(null)
|
||||
const isClickedOutside = useClickedOutside(ref)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(ev: KeyboardEvent<HTMLDivElement>) => {
|
||||
switch (ev.key) {
|
||||
@@ -32,12 +36,12 @@ export const BlockKeys: FC<BlockKeysProps> = ({ value, children, onEscape, onEnt
|
||||
[value]
|
||||
)
|
||||
|
||||
function onBlur() {
|
||||
if (onEnter) onEnter(value)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isClickedOutside && onEnter !== undefined) onEnter(value)
|
||||
}, [isClickedOutside])
|
||||
|
||||
return (
|
||||
<div onKeyDown={handleKeyDown} onBlur={onBlur}>
|
||||
<div ref={ref} onKeyDown={handleKeyDown}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Popover } from 'ui'
|
||||
import { IconMaximize, Popover } from 'ui'
|
||||
import { useState, useCallback } from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { EditorProps } from '@supabase/react-data-grid'
|
||||
|
||||
import { useTrackedState } from 'components/grid/store'
|
||||
import { BlockKeys, MonacoEditor, NullValue } from 'components/grid/components/common'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
|
||||
interface JsonEditorProps<TRow, TSummaryRow = unknown> extends EditorProps<TRow, TSummaryRow> {
|
||||
onExpandEditor: (column: string, row: TRow) => void
|
||||
}
|
||||
|
||||
export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
row,
|
||||
column,
|
||||
onRowChange,
|
||||
}: EditorProps<TRow, TSummaryRow>) {
|
||||
onExpandEditor,
|
||||
}: JsonEditorProps<TRow, TSummaryRow>) {
|
||||
const state = useTrackedState()
|
||||
|
||||
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
|
||||
@@ -28,12 +35,20 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
commitChange(newValue)
|
||||
}, [])
|
||||
|
||||
function onChange(_value: string | undefined) {
|
||||
const onChange = (_value: string | undefined) => {
|
||||
if (!_value || _value == '') setValue(null)
|
||||
else setValue(_value)
|
||||
}
|
||||
|
||||
function commitChange(newValue: string | null) {
|
||||
const onSelectExpand = () => {
|
||||
cancelChanges()
|
||||
onExpandEditor(column.key, {
|
||||
...row,
|
||||
[column.key]: tryParseJson(value) || (row as any)[column.key],
|
||||
})
|
||||
}
|
||||
|
||||
const commitChange = (newValue: string | null) => {
|
||||
if (!newValue) {
|
||||
onRowChange({ ...row, [column.key]: null }, true)
|
||||
setIsPopoverOpen(false)
|
||||
@@ -62,9 +77,45 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
language="json"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="flex items-center justify-end p-2 bg-scale-400 space-x-2">
|
||||
<p className="text-xs text-scale-1100">Save changes</p>
|
||||
<code className="text-xs">⏎</code>
|
||||
<div className="flex items-start justify-between p-2 bg-scale-400 space-x-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="px-1.5 py-[2.5px] rounded bg-scale-600 border border-scale-700 flex items-center justify-center">
|
||||
<span className="text-[10px]">⏎</span>
|
||||
</div>
|
||||
<p className="text-xs text-scale-1100">Save changes</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="px-1 py-[2.5px] rounded bg-scale-600 border border-scale-700 flex items-center justify-center">
|
||||
<span className="text-[10px]">Esc</span>
|
||||
</div>
|
||||
<p className="text-xs text-scale-1100">Cancel changes</p>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div
|
||||
className={[
|
||||
'border border-scale-700 rounded p-1 flex items-center justify-center',
|
||||
'transition cursor-pointer bg-scale-600 hover:bg-scale-700',
|
||||
].join(' ')}
|
||||
onClick={() => onSelectExpand()}
|
||||
>
|
||||
<IconMaximize size={12} strokeWidth={2} />
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">Expand editor</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</BlockKeys>
|
||||
}
|
||||
|
||||
@@ -44,9 +44,21 @@ export function TextEditor<TRow, TSummaryRow = unknown>({
|
||||
value={value ?? ''}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="flex items-center justify-end p-2 bg-scale-400 space-x-2">
|
||||
<p className="text-xs text-scale-1100">Save changes</p>
|
||||
<code className="text-xs">⏎</code>
|
||||
<div className="flex items-start justify-between p-2 bg-scale-400 space-x-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="px-1.5 py-[2.5px] rounded bg-scale-600 border border-scale-700 flex items-center justify-center">
|
||||
<span className="text-[10px]">⏎</span>
|
||||
</div>
|
||||
<p className="text-xs text-scale-1100">Save changes</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="px-1 py-[2.5px] rounded bg-scale-600 border border-scale-700 flex items-center justify-center">
|
||||
<span className="text-[10px]">Esc</span>
|
||||
</div>
|
||||
<p className="text-xs text-scale-1100">Cancel changes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlockKeys>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { FC, useState, ReactNode } from 'react'
|
||||
import { Button, IconDownload, IconPlus, IconX, IconTrash } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
IconDownload,
|
||||
IconPlus,
|
||||
IconX,
|
||||
IconTrash,
|
||||
Dropdown,
|
||||
IconColumns,
|
||||
IconChevronDown,
|
||||
} from 'ui'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
@@ -7,7 +16,7 @@ import FilterDropdown from './filter'
|
||||
import SortPopover from './sort'
|
||||
import RefreshButton from './RefreshButton'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
import { Sort, Filter } from 'components/grid/types'
|
||||
import { Sort, Filter, SupaTable } from 'components/grid/types'
|
||||
import { exportRowsToCsv } from 'components/grid/utils'
|
||||
import { useDispatch, useTrackedState } from 'components/grid/store'
|
||||
|
||||
@@ -17,6 +26,7 @@ import { useDispatch, useTrackedState } from 'components/grid/store'
|
||||
const MAX_EXPORT_ROW_COUNT = 500000
|
||||
|
||||
interface HeaderProps {
|
||||
table: Partial<SupaTable>
|
||||
sorts: Sort[]
|
||||
filters: Filter[]
|
||||
onAddColumn?: () => void
|
||||
@@ -24,7 +34,14 @@ interface HeaderProps {
|
||||
headerActions?: ReactNode
|
||||
}
|
||||
|
||||
const Header: FC<HeaderProps> = ({ sorts, filters, onAddColumn, onAddRow, headerActions }) => {
|
||||
const Header: FC<HeaderProps> = ({
|
||||
table,
|
||||
sorts,
|
||||
filters,
|
||||
onAddColumn,
|
||||
onAddRow,
|
||||
headerActions,
|
||||
}) => {
|
||||
const state = useTrackedState()
|
||||
const { selectedRows } = state
|
||||
|
||||
@@ -34,6 +51,7 @@ const Header: FC<HeaderProps> = ({ sorts, filters, onAddColumn, onAddRow, header
|
||||
<RowHeader sorts={sorts} filters={filters} />
|
||||
) : (
|
||||
<DefaultHeader
|
||||
table={table}
|
||||
sorts={sorts}
|
||||
filters={filters}
|
||||
onAddColumn={onAddColumn}
|
||||
@@ -47,29 +65,20 @@ const Header: FC<HeaderProps> = ({ sorts, filters, onAddColumn, onAddRow, header
|
||||
export default Header
|
||||
|
||||
interface DefaultHeaderProps {
|
||||
table: Partial<SupaTable>
|
||||
sorts: Sort[]
|
||||
filters: Filter[]
|
||||
onAddColumn?: () => void
|
||||
onAddRow?: () => void
|
||||
}
|
||||
const DefaultHeader: FC<DefaultHeaderProps> = ({ sorts, filters, onAddColumn, onAddRow }) => {
|
||||
const renderNewColumn = (onAddColumn?: () => void) => {
|
||||
if (!onAddColumn) return null
|
||||
return (
|
||||
<Button type="text" onClick={onAddColumn}>
|
||||
New Column
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const renderAddRow = (onAddRow?: () => void) => {
|
||||
if (!onAddRow) return null
|
||||
return (
|
||||
<Button size="tiny" icon={<IconPlus size={14} strokeWidth={2} />} onClick={onAddRow}>
|
||||
Insert row
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const DefaultHeader: FC<DefaultHeaderProps> = ({
|
||||
table,
|
||||
sorts,
|
||||
filters,
|
||||
onAddColumn,
|
||||
onAddRow,
|
||||
}) => {
|
||||
const canAddNew = onAddRow !== undefined || onAddColumn !== undefined
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -78,11 +87,78 @@ const DefaultHeader: FC<DefaultHeaderProps> = ({ sorts, filters, onAddColumn, on
|
||||
<FilterDropdown />
|
||||
<SortPopover />
|
||||
</div>
|
||||
<div className="h-[50%] w-px bg-scale-600"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderNewColumn(onAddColumn)}
|
||||
{renderAddRow(onAddRow)}
|
||||
</div>
|
||||
{canAddNew && (
|
||||
<>
|
||||
<div className="h-[20px] w-px border-r border-scale-600"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
size="medium"
|
||||
overlay={[
|
||||
...(onAddRow !== undefined
|
||||
? [
|
||||
<Dropdown.Item
|
||||
key="add-row"
|
||||
className="group"
|
||||
onClick={onAddRow}
|
||||
disabled={onAddRow === undefined}
|
||||
icon={
|
||||
<div className="-mt-2 pr-1.5">
|
||||
<div className="border border-scale-1000 w-[15px] h-[4px]" />
|
||||
<div className="border border-scale-1000 w-[15px] h-[4px] my-[2px]" />
|
||||
<div
|
||||
className={[
|
||||
'border border-scale-1100 w-[15px] h-[4px] translate-x-0.5',
|
||||
'transition duration-200 group-hover:border-brand-900 group-hover:translate-x-0',
|
||||
].join(' ')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="">
|
||||
<p>Insert row</p>
|
||||
<p className="text-scale-1000">Insert a new row into {table.name}</p>
|
||||
</div>
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
: []),
|
||||
...(onAddColumn !== undefined
|
||||
? [
|
||||
<Dropdown.Item
|
||||
key="add-column"
|
||||
className="group"
|
||||
onClick={onAddColumn}
|
||||
disabled={onAddColumn === undefined}
|
||||
icon={
|
||||
<div className="flex -mt-2 pr-1.5">
|
||||
<div className="border border-scale-1000 w-[4px] h-[15px]" />
|
||||
<div className="border border-scale-1000 w-[4px] h-[15px] mx-[2px]" />
|
||||
<div
|
||||
className={[
|
||||
'border border-scale-1100 w-[4px] h-[15px] -translate-y-0.5',
|
||||
'transition duration-200 group-hover:border-brand-900 group-hover:translate-y-0',
|
||||
].join(' ')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="">
|
||||
<p>Insert column</p>
|
||||
<p className="text-scale-1000">Insert a new column into {table.name}</p>
|
||||
</div>
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
<Button size="tiny" icon={<IconChevronDown size={14} strokeWidth={1.5} />}>
|
||||
Insert
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,18 +70,22 @@ const RowContextMenu: React.FC<RowContextMenuProps> = ({}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu id={ROW_CONTEXT_MENU_ID} animation={false}>
|
||||
<Menu
|
||||
id={ROW_CONTEXT_MENU_ID}
|
||||
animation={false}
|
||||
className="!bg-scale-300 border border-scale-500"
|
||||
>
|
||||
<Item onClick={onCopyCellContent}>
|
||||
<IconClipboard size="tiny" />
|
||||
<span className="sb-grid-context-menu__label">Copy cell content</span>
|
||||
<span className="ml-2 text-xs">Copy cell content</span>
|
||||
</Item>
|
||||
<Item onClick={onEditRowClick} hidden={isItemHidden} data="edit">
|
||||
<IconEdit size="tiny" />
|
||||
<span className="sb-grid-context-menu__label">Edit row</span>
|
||||
<span className="ml-2 text-xs">Edit row</span>
|
||||
</Item>
|
||||
<Item onClick={onDeleteRow} hidden={isItemHidden} data="delete">
|
||||
<IconTrash size="tiny" stroke="red" />
|
||||
<span className="sb-grid-context-menu__label">Delete row</span>
|
||||
<span className="ml-2 text-xs">Delete row</span>
|
||||
</Item>
|
||||
</Menu>
|
||||
</>
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface SupabaseGridProps {
|
||||
*/
|
||||
table: SupaTable | string
|
||||
/**
|
||||
*
|
||||
* run sql query
|
||||
*/
|
||||
onSqlQuery: (query: string) => Promise<{ data?: any; error?: any }>
|
||||
@@ -25,9 +26,6 @@ export interface SupabaseGridProps {
|
||||
* Optional react node to display in grid header
|
||||
*/
|
||||
headerActions?: React.ReactNode
|
||||
/**
|
||||
* enable table editor
|
||||
*/
|
||||
editable?: boolean
|
||||
/**
|
||||
* props to config grid view
|
||||
@@ -65,10 +63,8 @@ export interface SupabaseGridProps {
|
||||
* show edit row button if available
|
||||
*/
|
||||
onEditRow?: (row: SupaRow) => void
|
||||
/**
|
||||
* error handler
|
||||
*/
|
||||
onError?: (error: any) => void
|
||||
onExpandJSONEditor: (column: string, row: SupaRow) => void
|
||||
}
|
||||
|
||||
export interface SupabaseGridRef {
|
||||
|
||||
@@ -42,6 +42,7 @@ export function getGridColumns(
|
||||
editable?: boolean
|
||||
defaultWidth?: string | number
|
||||
onAddColumn?: () => void
|
||||
onExpandJSONEditor: (column: string, row: SupaRow) => void
|
||||
}
|
||||
): any[] {
|
||||
const columns = table.columns.map((x, idx) => {
|
||||
@@ -76,7 +77,9 @@ export function getGridColumns(
|
||||
format={x.format}
|
||||
/>
|
||||
),
|
||||
editor: options?.editable ? getColumnEditor(x, columnType) : undefined,
|
||||
editor: options?.editable
|
||||
? getColumnEditor(x, columnType, options.onExpandJSONEditor)
|
||||
: undefined,
|
||||
formatter: getColumnFormatter(x, columnType),
|
||||
}
|
||||
|
||||
@@ -91,7 +94,11 @@ export function getGridColumns(
|
||||
return gridColumns
|
||||
}
|
||||
|
||||
function getColumnEditor(columnDefinition: SupaColumn, columnType: ColumnType) {
|
||||
function getColumnEditor(
|
||||
columnDefinition: SupaColumn,
|
||||
columnType: ColumnType,
|
||||
onExpandJSONEditor: (column: string, row: any) => void
|
||||
) {
|
||||
if (columnDefinition.isPrimaryKey || !columnDefinition.isUpdatable) {
|
||||
return
|
||||
}
|
||||
@@ -117,7 +124,7 @@ function getColumnEditor(columnDefinition: SupaColumn, columnType: ColumnType) {
|
||||
}
|
||||
case 'array':
|
||||
case 'json': {
|
||||
return JsonEditor
|
||||
return (p: any) => <JsonEditor {...p} onExpandEditor={onExpandJSONEditor} />
|
||||
}
|
||||
case 'number': {
|
||||
return NumberEditor
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Modal, IconTrash } from 'ui'
|
||||
import { useStore } from 'hooks'
|
||||
import { delete_ } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { AccessToken, useAccessTokens } from 'hooks/queries/useAccessTokens'
|
||||
import { AccessToken, useAccessTokensQuery } from 'data/access-tokens/access-tokens-query'
|
||||
import { useAccessTokenDeleteMutation } from 'data/access-tokens/access-tokens-delete-mutation'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
@@ -11,21 +10,21 @@ import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
|
||||
const AccessTokenList = observer(() => {
|
||||
const { ui } = useStore()
|
||||
const { mutateDeleteToken } = useAccessTokens()
|
||||
const { tokens, isLoading } = useAccessTokens()
|
||||
const { data: tokens, isLoading } = useAccessTokensQuery()
|
||||
const { mutateAsync: deleteToken } = useAccessTokenDeleteMutation()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [token, setToken] = useState<AccessToken | undefined>(undefined)
|
||||
|
||||
async function onDeleteToken(tokenId: number) {
|
||||
const response = await delete_(`${API_URL}/profile/access-tokens/${tokenId}`)
|
||||
if (response.error) {
|
||||
try {
|
||||
await deleteToken({ id: tokenId })
|
||||
setIsOpen(false)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete token: ${response.error.message}`,
|
||||
message: `Failed to delete token: ${error.message}`,
|
||||
})
|
||||
} else {
|
||||
mutateDeleteToken(tokenId)
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ const AccessTokenList = observer(() => {
|
||||
</Table.tr>
|
||||
) : (
|
||||
<>
|
||||
{tokens?.map((x: AccessToken) => {
|
||||
{tokens?.map((x) => {
|
||||
return (
|
||||
<Table.tr key={x.token_alias}>
|
||||
<Table.td>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Input, Button, Modal, Form, Alert } from 'ui'
|
||||
import { useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { NewAccessToken, useAccessTokens } from 'hooks/queries/useAccessTokens'
|
||||
import {
|
||||
NewAccessToken,
|
||||
useAccessTokenCreateMutation,
|
||||
} from 'data/access-tokens/access-tokens-create-mutation'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
const NewAccessTokenButton = observer(() => {
|
||||
const { ui } = useStore()
|
||||
const { mutateNewToken } = useAccessTokens()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [newToken, setNewToken] = useState<NewAccessToken | undefined>(undefined)
|
||||
|
||||
@@ -20,21 +20,24 @@ const NewAccessTokenButton = observer(() => {
|
||||
return errors
|
||||
}
|
||||
|
||||
const { mutateAsync: createAccessToken } = useAccessTokenCreateMutation()
|
||||
|
||||
async function onFormSubmit(values: any, { setSubmitting }: any) {
|
||||
setSubmitting(true)
|
||||
const response = await post(`${API_URL}/profile/access-tokens`, { name: values.tokenName })
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create token: ${response.error.message}`,
|
||||
})
|
||||
setSubmitting(false)
|
||||
} else {
|
||||
mutateNewToken(response)
|
||||
|
||||
try {
|
||||
const response = await createAccessToken({ name: values.tokenName })
|
||||
setNewToken(response)
|
||||
|
||||
setSubmitting(false)
|
||||
setIsOpen(false)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create token: ${error.message}`,
|
||||
})
|
||||
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +71,7 @@ const NewAccessTokenButton = observer(() => {
|
||||
validate={validate}
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<div className="space-y-4 py-3">
|
||||
<div className="py-3 space-y-4">
|
||||
<Modal.Content>
|
||||
<Input
|
||||
id="tokenName"
|
||||
@@ -109,7 +112,7 @@ const NewTokenItem: FC<NewTokenItemProps> = observer(({ data }) => {
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
className="input-mono max-w-xl"
|
||||
className="max-w-xl input-mono"
|
||||
value={data.token}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, Loading } from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useStore, useFlag, useProjectUsage } from 'hooks'
|
||||
import { checkPermissions, useFlag, useParams } from 'hooks'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
|
||||
@@ -14,6 +14,7 @@ import CostBreakdownRow from './CostBreakdownRow'
|
||||
import { StripeSubscription } from './Subscription.types'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
|
||||
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
|
||||
interface Props {
|
||||
project: any
|
||||
@@ -32,10 +33,9 @@ const Subscription: FC<Props> = ({
|
||||
currentPeriodStart,
|
||||
currentPeriodEnd,
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
|
||||
const canReadSubscription = checkPermissions(PermissionAction.READ, 'subscriptions')
|
||||
@@ -44,7 +44,7 @@ const Subscription: FC<Props> = ({
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
|
||||
const { usage, isLoading: loadingUsage } = useProjectUsage(ref as string)
|
||||
const { data: usage, isLoading: loadingUsage } = useProjectUsageQuery({ projectRef })
|
||||
|
||||
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
|
||||
const isEnterprise = subscription.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
|
||||
@@ -62,7 +62,7 @@ const Subscription: FC<Props> = ({
|
||||
? 0
|
||||
: Object.keys(usage)
|
||||
.map((productKey) => {
|
||||
return usage[productKey].cost ?? 0
|
||||
return usage[productKey as keyof ProjectUsageResponse].cost ?? 0
|
||||
})
|
||||
.reduce((prev, current) => prev + current, 0)
|
||||
|
||||
@@ -71,7 +71,7 @@ const Subscription: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Loading active={loading || loadingUsage}>
|
||||
<div className="mb-8 w-full overflow-hidden rounded border border-panel-border-light dark:border-panel-border-dark">
|
||||
<div className="w-full mb-8 overflow-hidden border rounded border-panel-border-light dark:border-panel-border-dark">
|
||||
<div className="bg-panel-body-light dark:bg-panel-body-dark">
|
||||
<div className="flex items-center justify-between px-6 pt-4">
|
||||
<div className="flex flex-col">
|
||||
@@ -134,7 +134,7 @@ const Subscription: FC<Props> = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 px-6 pb-4">
|
||||
<div className="px-6 pb-4 mt-2">
|
||||
<p className="text-sm text-scale-1100">
|
||||
See our{' '}
|
||||
<a href="https://supabase.com/pricing" target="_blank" className="underline">
|
||||
@@ -151,7 +151,7 @@ const Subscription: FC<Props> = ({
|
||||
) : !loading && subscription ? (
|
||||
// Cost breakdown
|
||||
<>
|
||||
<div className="relative flex items-center border-t border-panel-border-light px-6 py-3 dark:border-panel-border-dark">
|
||||
<div className="relative flex items-center px-6 py-3 border-t border-panel-border-light dark:border-panel-border-dark">
|
||||
<div className="w-[40%]">
|
||||
<p className="text-xs uppercase text-scale-900">Item</p>
|
||||
</div>
|
||||
@@ -183,9 +183,11 @@ const Subscription: FC<Props> = ({
|
||||
{isPayg &&
|
||||
USAGE_BASED_PRODUCTS.map((product) => {
|
||||
return product.features.map((feature) => {
|
||||
const amount = usage?.[feature.key]?.usage ?? 0
|
||||
const limit = usage?.[feature.key]?.limit ?? 0
|
||||
const cost = (usage?.[feature.key]?.cost ?? 0).toFixed(2)
|
||||
const amount = usage?.[feature.key as keyof ProjectUsageResponse]?.usage ?? 0
|
||||
const limit = usage?.[feature.key as keyof ProjectUsageResponse]?.limit ?? 0
|
||||
const cost = (
|
||||
usage?.[feature.key as keyof ProjectUsageResponse]?.cost ?? 0
|
||||
).toFixed(2)
|
||||
|
||||
return (
|
||||
<CostBreakdownRow
|
||||
|
||||
@@ -14,5 +14,5 @@ export interface StripeProduct {
|
||||
price_id?: string
|
||||
unit_amount: number
|
||||
supabase_prod_id: string
|
||||
key: string
|
||||
key: 'FREE' | 'PRO' | 'TEAM' | 'ENTERPRISE'
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import Link from 'next/link'
|
||||
import { Button, IconCalendar } from 'ui'
|
||||
import { FormPanel } from 'components/ui/Forms'
|
||||
import { useParams, useProjectSubscription } from 'hooks'
|
||||
import { useParams } from 'hooks'
|
||||
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
|
||||
import { getPITRRetentionDuration } from './PITR.utils'
|
||||
|
||||
const PITRNotice = ({}) => {
|
||||
const { ref } = useParams()
|
||||
const { subscription } = useProjectSubscription(ref)
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
|
||||
const retentionPeriod = getPITRRetentionDuration(subscription?.addons ?? [])
|
||||
|
||||
return (
|
||||
@@ -14,10 +15,10 @@ const PITRNotice = ({}) => {
|
||||
disabled={true}
|
||||
footer={
|
||||
<div className="flex items-center justify-between p-6">
|
||||
<span className="text-scale-1000 text-sm">
|
||||
<span className="text-sm text-scale-1000">
|
||||
You can also increase your recovery retention period updating your PITR add-on
|
||||
</span>
|
||||
<Link href={`/project/${ref}/settings/billing/update/pro`}>
|
||||
<Link href={`/project/${projectRef}/settings/billing/update/pro`}>
|
||||
<a>
|
||||
<Button as="span" type="default">
|
||||
Increase retention period
|
||||
@@ -27,8 +28,8 @@ const PITRNotice = ({}) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-6 flex space-x-6">
|
||||
<div className="h-10 w-10 rounded flex items-center justify-center bg-scale-700">
|
||||
<div className="flex p-6 space-x-6">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded bg-scale-700">
|
||||
<IconCalendar strokeWidth={2} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button, IconTerminal, IconMaximize2, IconMinimize2, IconBookOpen, IconCode } from 'ui'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useAccessTokens } from 'hooks/queries/useAccessTokens'
|
||||
import { Commands } from './Functions.types'
|
||||
import CommandRender from 'components/interfaces/Functions/CommandRender'
|
||||
import { FC, useState } from 'react'
|
||||
import { useStore } from 'hooks'
|
||||
import { useAccessTokensQuery } from 'data/access-tokens/access-tokens-query'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useParams } from 'hooks'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useState } from 'react'
|
||||
import { Button, IconBookOpen, IconCode, IconMaximize2, IconMinimize2, IconTerminal } from 'ui'
|
||||
import { Commands } from './Functions.types'
|
||||
|
||||
interface Props {
|
||||
closable?: boolean
|
||||
@@ -14,21 +14,21 @@ interface Props {
|
||||
|
||||
const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ui } = useStore()
|
||||
|
||||
const { tokens } = useAccessTokens()
|
||||
const { data: settings } = useProjectSettingsQuery({ projectRef: ref as string })
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const [showInstructions, setShowInstructions] = useState(!closable)
|
||||
|
||||
// Get the API service
|
||||
const { data: tokens } = useAccessTokensQuery()
|
||||
|
||||
const { data: settings } = useProjectApiQuery({
|
||||
projectRef,
|
||||
})
|
||||
const apiService = settings?.autoApiService
|
||||
const anonKey = apiService?.service_api_keys.find((x: any) => x.name === 'anon key')
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
const endpoint = settings?.autoApiService.app_config.endpoint ?? ''
|
||||
|
||||
const endpoint = apiService?.endpoint ?? ''
|
||||
const endpointSections = endpoint.split('.')
|
||||
const functionsEndpoint = [
|
||||
...endpointSections.slice(0, 1),
|
||||
@@ -37,8 +37,8 @@ const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
].join('.')
|
||||
|
||||
// get the .co or .net TLD from the restUrl
|
||||
const restUrl = ui.selectedProject?.restUrl
|
||||
const restUrlTld = new URL(restUrl as string).hostname.split('.').pop()
|
||||
const restUrl = settings?.autoApiService.restUrl
|
||||
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : ''
|
||||
|
||||
const commands: Commands[] = [
|
||||
{
|
||||
@@ -54,20 +54,20 @@ const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
comment: 'Create a function',
|
||||
},
|
||||
{
|
||||
command: `supabase functions deploy hello-world --project-ref ${ref}`,
|
||||
command: `supabase functions deploy hello-world --project-ref ${projectRef}`,
|
||||
description: 'Deploys function at ./functions/hello-world/index.ts',
|
||||
jsx: () => {
|
||||
return (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> functions deploy hello-world
|
||||
--project-ref {ref}
|
||||
--project-ref {projectRef}
|
||||
</>
|
||||
)
|
||||
},
|
||||
comment: 'Deploy your function',
|
||||
},
|
||||
{
|
||||
command: `curl -L -X POST 'https://${ref}.functions.supabase.${restUrlTld}/hello-world' -H 'Authorization: Bearer ${
|
||||
command: `curl -L -X POST 'https://${projectRef}.functions.supabase.${restUrlTld}/hello-world' -H 'Authorization: Bearer ${
|
||||
anonKey ?? '[YOUR ANON KEY]'
|
||||
}' --data '{"name":"Functions"}'`,
|
||||
description: 'Invokes the hello-world function',
|
||||
@@ -86,13 +86,13 @@ const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="col-span-7 overflow-hidden rounded border bg-scale-100 shadow transition-all dark:bg-scale-300"
|
||||
className="col-span-7 overflow-hidden transition-all border rounded shadow bg-scale-100 dark:bg-scale-300"
|
||||
style={{ maxHeight: showInstructions ? 500 : 80 }}
|
||||
>
|
||||
<div className="space-y-6 px-8 py-6">
|
||||
<div className="px-8 py-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded border bg-scale-100 p-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 p-2 border rounded bg-scale-100">
|
||||
<IconTerminal strokeWidth={2} />
|
||||
</div>
|
||||
<h4>Terminal instructions</h4>
|
||||
@@ -112,7 +112,7 @@ const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
</div>
|
||||
</div>
|
||||
{tokens && tokens.length === 0 ? (
|
||||
<div className="space-y-3 border-t px-8 py-6">
|
||||
<div className="px-8 py-6 space-y-3 border-t">
|
||||
<div>
|
||||
<h3 className="text-base text-scale-1200">You may need to create an access token</h3>
|
||||
<p className="text-sm text-scale-1100">
|
||||
@@ -124,7 +124,7 @@ const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 border-t px-8 py-6">
|
||||
<div className="px-8 py-6 space-y-3 border-t">
|
||||
<div>
|
||||
<h3 className="text-base text-scale-1200">Need help?</h3>
|
||||
<p className="text-sm text-scale-1100">
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Input, IconLoader, IconAlertCircle } from 'ui'
|
||||
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { IconAlertCircle, IconLoader, Input } from 'ui'
|
||||
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { checkPermissions, useJwtSecretUpdateStatus } from 'hooks'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
|
||||
import { checkPermissions, useParams } from 'hooks'
|
||||
import Snippets from 'components/to-be-cleaned/Docs/Snippets'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
|
||||
const APIKeys = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const availableLanguages = [
|
||||
{ name: 'Javascript', key: 'js' },
|
||||
@@ -25,13 +24,16 @@ const APIKeys = () => {
|
||||
data: settings,
|
||||
isError: isProjectSettingsError,
|
||||
isLoading: isProjectSettingsLoading,
|
||||
} = useProjectSettingsQuery({ projectRef: ref as string })
|
||||
} = useProjectApiQuery({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
const {
|
||||
jwtSecretUpdateStatus,
|
||||
data,
|
||||
isError: isJwtSecretUpdateStatusError,
|
||||
isLoading: isJwtSecretUpdateStatusLoading,
|
||||
}: any = useJwtSecretUpdateStatus(ref)
|
||||
} = useJwtSecretUpdatingStatusQuery({ projectRef })
|
||||
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
|
||||
|
||||
const canReadAPIKeys = checkPermissions(PermissionAction.READ, 'service_api_keys')
|
||||
|
||||
@@ -45,7 +47,7 @@ const APIKeys = () => {
|
||||
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
|
||||
|
||||
const apiUrl = `${apiService?.protocol ?? 'https'}://${apiService?.endpoint ?? '-'}`
|
||||
const anonKey = apiKeys.find((key: any) => key.tags === 'anon')
|
||||
const anonKey = apiKeys.find((key) => key.tags === 'anon')
|
||||
|
||||
const clientInitSnippet: any = Snippets.init(apiUrl)
|
||||
const selectedLanguageSnippet =
|
||||
@@ -65,14 +67,14 @@ const APIKeys = () => {
|
||||
}
|
||||
>
|
||||
{isProjectSettingsError || isJwtSecretUpdateStatusError ? (
|
||||
<div className="py-8 flex items-center justify-center space-x-2">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">
|
||||
{isProjectSettingsError ? 'Failed to retrieve API keys' : 'Failed to update JWT secret'}
|
||||
</p>
|
||||
</div>
|
||||
) : isApiKeysEmpty || isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
|
||||
<div className="py-8 flex items-center justify-center space-x-2">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">
|
||||
{isProjectSettingsLoading || isApiKeysEmpty
|
||||
@@ -135,8 +137,8 @@ const APIKeys = () => {
|
||||
This key is safe to use in a browser if you have enabled Row Level Security (RLS)
|
||||
for your tables and configured policies. You may also use the service key which
|
||||
can be found{' '}
|
||||
<Link href={`/project/${ref}/settings/api`}>
|
||||
<a className="text-brand-800 hover:text-brand-900 transition">here</a>
|
||||
<Link href={`/project/${projectRef}/settings/api`}>
|
||||
<a className="transition text-brand-800 hover:text-brand-900">here</a>
|
||||
</Link>{' '}
|
||||
to bypass RLS.
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import useSWR from 'swr'
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import { FC, useState } from 'react'
|
||||
@@ -6,9 +5,10 @@ import { useRouter } from 'next/router'
|
||||
import { Button, Dropdown, IconArchive, IconChevronDown, IconDatabase, IconKey, IconZap } from 'ui'
|
||||
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL, DATE_FORMAT, METRICS } from 'lib/constants'
|
||||
import { DATE_FORMAT, METRICS } from 'lib/constants'
|
||||
import { ChartIntervals } from 'types'
|
||||
import { useParams } from 'hooks'
|
||||
import { useProjectLogStatsQuery } from 'data/logs/project-log-stats-query'
|
||||
|
||||
const CHART_INTERVALS: ChartIntervals[] = [
|
||||
{
|
||||
@@ -25,14 +25,11 @@ interface Props {}
|
||||
|
||||
const ProjectUsage: FC<Props> = ({}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const [interval, setInterval] = useState<string>('hourly')
|
||||
|
||||
const { data, error }: any = useSWR(
|
||||
`${API_URL}/projects/${ref}/log-stats?interval=${interval}`,
|
||||
get
|
||||
)
|
||||
const { data, error } = useProjectLogStatsQuery({ projectRef, interval })
|
||||
|
||||
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
|
||||
const startDate = dayjs()
|
||||
@@ -51,9 +48,9 @@ const ProjectUsage: FC<Props> = ({}) => {
|
||||
if (timestampDigits < 16) {
|
||||
// pad unix timestamp with additional 0 and then forward
|
||||
const paddedTimestamp = String(timestamp) + '0'.repeat(16 - timestampDigits)
|
||||
router.push(`/project/${ref}/logs/edge-logs?te=${paddedTimestamp}`)
|
||||
router.push(`/project/${projectRef}/logs/edge-logs?te=${paddedTimestamp}`)
|
||||
} else {
|
||||
router.push(`/project/${ref}/logs/edge-logs?te=${timestamp}`)
|
||||
router.push(`/project/${projectRef}/logs/edge-logs?te=${timestamp}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +91,7 @@ const ProjectUsage: FC<Props> = ({}) => {
|
||||
</div>
|
||||
}
|
||||
title="Database"
|
||||
href={`/project/${ref}/editor`}
|
||||
href={`/project/${projectRef}/editor`}
|
||||
/>
|
||||
<ChartHandler
|
||||
startDate={startDate}
|
||||
@@ -120,7 +117,7 @@ const ProjectUsage: FC<Props> = ({}) => {
|
||||
</div>
|
||||
}
|
||||
title="Auth"
|
||||
href={`/project/${ref}/auth/users`}
|
||||
href={`/project/${projectRef}/auth/users`}
|
||||
/>
|
||||
<ChartHandler
|
||||
startDate={startDate}
|
||||
@@ -146,7 +143,7 @@ const ProjectUsage: FC<Props> = ({}) => {
|
||||
</div>
|
||||
}
|
||||
title="Storage"
|
||||
href={`/project/${ref}/storage/buckets`}
|
||||
href={`/project/${projectRef}/storage/buckets`}
|
||||
/>
|
||||
<ChartHandler
|
||||
startDate={startDate}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { FC } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { IconLoader, IconAlertCircle } from 'ui'
|
||||
|
||||
import { useProjectUsage } from 'hooks'
|
||||
import { useParams } from 'hooks'
|
||||
import { ProjectUsage, NewProjectPanel } from 'components/interfaces/Home'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
|
||||
const ProjectUsageSection: FC = observer(({}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { usage, error: usageError, isLoading } = useProjectUsage(ref as string)
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: usage, error: usageError, isLoading } = useProjectUsageQuery({ projectRef })
|
||||
|
||||
if (usageError) {
|
||||
return (
|
||||
@@ -25,8 +24,8 @@ const ProjectUsageSection: FC = observer(({}) => {
|
||||
|
||||
const hasProjectData = usage
|
||||
? Object.keys(usage)
|
||||
.map((key) => usage[key].usage)
|
||||
.some((usage) => usage > 0)
|
||||
.map((key) => usage[key as keyof ProjectUsageResponse].usage)
|
||||
.some((usage) => (usage ?? 0) > 0)
|
||||
: false
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,8 @@ import dayjs from 'dayjs'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { IconChevronRight, IconLoader } from 'ui'
|
||||
|
||||
import { useProjectSubscription, useStore } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
|
||||
import Panel from 'components/ui/Panel'
|
||||
|
||||
interface ProjectSummaryProps {
|
||||
@@ -12,7 +13,11 @@ interface ProjectSummaryProps {
|
||||
|
||||
const ProjectSummary: FC<ProjectSummaryProps> = ({ project }) => {
|
||||
const { ui } = useStore()
|
||||
const { subscription, isLoading: loading, error } = useProjectSubscription(project.ref)
|
||||
const {
|
||||
data: subscription,
|
||||
isLoading: loading,
|
||||
error,
|
||||
} = useProjectSubscriptionQuery({ projectRef: project.ref })
|
||||
|
||||
const currentPeriodStart = subscription?.billing?.current_period_start ?? 0
|
||||
const currentPeriodEnd = subscription?.billing?.current_period_end ?? 0
|
||||
@@ -21,13 +26,13 @@ const ProjectSummary: FC<ProjectSummaryProps> = ({ project }) => {
|
||||
if (error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to get project subscription: ${error?.message ?? 'unknown'}`,
|
||||
message: `Failed to get project subscription: ${(error as any)?.message ?? 'unknown'}`,
|
||||
})
|
||||
}
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center px-6 py-3">
|
||||
<div className="flex items-center w-full px-6 py-3">
|
||||
<div className="w-[25%]">
|
||||
<p className="text-sm">{project.name}</p>
|
||||
</div>
|
||||
@@ -47,8 +52,8 @@ const ProjectSummary: FC<ProjectSummaryProps> = ({ project }) => {
|
||||
</div>
|
||||
<div className="flex w-[15%] items-center justify-end">
|
||||
<Link href={`/project/${project.ref}/settings/billing/subscription`}>
|
||||
<a className="group flex items-center space-x-2">
|
||||
<p className="text-xs opacity-0 transition group-hover:opacity-100">View details</p>
|
||||
<a className="flex items-center space-x-2 group">
|
||||
<p className="text-xs transition opacity-0 group-hover:opacity-100">View details</p>
|
||||
<IconChevronRight strokeWidth={1.5} />
|
||||
</a>
|
||||
</Link>
|
||||
@@ -69,7 +74,7 @@ const ProjectsSummary: FC<ProjectsSummaryProps> = ({ projects }) => {
|
||||
<h4>Projects at a glance</h4>
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[25%]">
|
||||
<p className="text-sm opacity-50">Name</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react'
|
||||
import { IconAlertCircle } from 'ui'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { MemberWithFreeProjectLimit } from 'hooks'
|
||||
import { MemberWithFreeProjectLimit } from 'data/organizations/free-project-limit-check-query'
|
||||
|
||||
interface Props {
|
||||
membersExceededLimit: MemberWithFreeProjectLimit[]
|
||||
@@ -21,7 +21,7 @@ const FreeProjectLimitWarning: FC<Props> = ({ membersExceededLimit }) => {
|
||||
The following members have reached their maximum limits for the number of active free
|
||||
tier projects within organizations where they are an administrator or owner:
|
||||
</p>
|
||||
<ul className="list-disc pl-5">
|
||||
<ul className="pl-5 list-disc">
|
||||
{membersExceededLimit.map((member, idx: number) => (
|
||||
<li key={`member-${idx}`}>
|
||||
{member.username || member.primary_email} (Limit: {member.free_project_limit} free
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { isNil } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { object, string } from 'yup'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Form, IconMail, Input, Modal, Select } from 'ui'
|
||||
|
||||
import { Member, User, Role } from 'types'
|
||||
import { checkPermissions, useOrganizationDetail, useStore } from 'hooks'
|
||||
import { checkPermissions, useParams, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useOrganizationMemberInviteCreateMutation } from 'data/organizations/organization-member-invite-create-mutation'
|
||||
|
||||
interface Props {
|
||||
user: User
|
||||
@@ -20,11 +20,9 @@ interface Props {
|
||||
|
||||
const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAddable = [] }) => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const { slug } = router.query
|
||||
const { slug } = useParams()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { mutateOrgMembers } = useOrganizationDetail((slug as string) || '')
|
||||
|
||||
const canInviteMembers = roles.some(({ id: role_id }) =>
|
||||
checkPermissions(PermissionAction.CREATE, 'user_invites', { resource: { role_id } })
|
||||
@@ -37,7 +35,13 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
role: string().required('Role is required'),
|
||||
})
|
||||
|
||||
const { mutateAsync } = useOrganizationMemberInviteCreateMutation()
|
||||
|
||||
const onInviteMember = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
if (!slug) {
|
||||
throw new Error('slug is required')
|
||||
}
|
||||
|
||||
const existingMember = members.find(
|
||||
(member) => member.primary_email === values.email.toLowerCase()
|
||||
)
|
||||
@@ -59,33 +63,27 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
const response = await post(`${API_URL}/organizations/${slug}/members/invite`, {
|
||||
invited_email: values.email.toLowerCase(),
|
||||
owner_id: user.id,
|
||||
role_id: roleId,
|
||||
})
|
||||
try {
|
||||
const response = await mutateAsync({
|
||||
slug,
|
||||
invitedEmail: values.email.toLowerCase(),
|
||||
ownerId: user.id,
|
||||
roleId,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
if (isNil(response)) {
|
||||
ui.setNotification({ category: 'error', message: 'Failed to add member' })
|
||||
} else {
|
||||
ui.setNotification({ category: 'success', message: 'Successfully added new member.' })
|
||||
|
||||
setIsOpen(!isOpen)
|
||||
resetForm({ initialValues: { ...initialValues, role: roleId } })
|
||||
}
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to add member: ${response.error.message}`,
|
||||
message: `Failed to add member: ${error.message}`,
|
||||
})
|
||||
} else if (isNil(response)) {
|
||||
ui.setNotification({ category: 'error', message: 'Failed to add member' })
|
||||
} else {
|
||||
const newMember: Member = {
|
||||
id: 0,
|
||||
invited_id: response.invited_id,
|
||||
invited_at: response.invited_at,
|
||||
primary_email: response.invited_email,
|
||||
username: response.invited_email[0],
|
||||
role_ids: [response.role_id],
|
||||
}
|
||||
mutateOrgMembers([...members, newMember])
|
||||
ui.setNotification({ category: 'success', message: 'Successfully added new member.' })
|
||||
|
||||
setIsOpen(!isOpen)
|
||||
resetForm({ initialValues: { ...initialValues, role: roleId } })
|
||||
}
|
||||
|
||||
setSubmitting(false)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FC, useState } from 'react'
|
||||
import { FC } from 'react'
|
||||
import { Button, Dropdown, IconMoreHorizontal, IconTrash } from 'ui'
|
||||
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
import { checkPermissions, useOrganizationDetail, useParams, useStore } from 'hooks'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { checkPermissions, useParams, useStore } from 'hooks'
|
||||
import { Member, Role } from 'types'
|
||||
|
||||
import { useOrganizationMemberDeleteMutation } from 'data/organizations/organization-member-delete-mutation'
|
||||
import { useOrganizationMemberInviteCreateMutation } from 'data/organizations/organization-member-invite-create-mutation'
|
||||
import { useOrganizationMemberInviteDeleteMutation } from 'data/organizations/organization-member-invite-delete-mutation'
|
||||
import { isInviteExpired } from '../Organization.utils'
|
||||
import { getRolesManagementPermissions } from './TeamSettings.utils'
|
||||
|
||||
@@ -23,9 +24,6 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
const { ui } = useStore()
|
||||
const { slug } = useParams()
|
||||
const { rolesRemovable } = getRolesManagementPermissions(roles)
|
||||
const { mutateOrgMembers } = useOrganizationDetail(slug || '')
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const isExpired = isInviteExpired(member?.invited_at ?? '')
|
||||
const isPendingInviteAcceptance = member.invited_id
|
||||
@@ -39,81 +37,94 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
resource: { role_id: roleId },
|
||||
})
|
||||
|
||||
const {
|
||||
mutateAsync: deleteOrganizationMemberAsync,
|
||||
isLoading: isOrganizationMemberDeleteLoading,
|
||||
} = useOrganizationMemberDeleteMutation({
|
||||
onSuccess() {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully removed ${member.primary_email}`,
|
||||
})
|
||||
},
|
||||
onError(error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete user: ${error.message}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleMemberDelete = async () => {
|
||||
confirmAlert({
|
||||
title: 'Confirm to remove',
|
||||
message: `This is permanent! Are you sure you want to remove ${member.primary_email}`,
|
||||
onAsyncConfirm: async () => {
|
||||
setLoading(true)
|
||||
|
||||
const response = await delete_(
|
||||
`${API_URL}/organizations/${slug}/members/${member.gotrue_id}`
|
||||
)
|
||||
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete user: ${response.error.message}`,
|
||||
})
|
||||
setLoading(false)
|
||||
} else {
|
||||
const updatedMembers = members.filter((m) => m.gotrue_id !== member.gotrue_id)
|
||||
|
||||
mutateOrgMembers(updatedMembers)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully removed ${member.primary_email}`,
|
||||
})
|
||||
if (!slug) {
|
||||
throw new Error('slug is required')
|
||||
}
|
||||
if (!member.gotrue_id) {
|
||||
throw new Error('gotrue_id is required')
|
||||
}
|
||||
|
||||
await deleteOrganizationMemberAsync({
|
||||
slug,
|
||||
gotrueId: member.gotrue_id,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
mutate: createOrganizationMemberInvite,
|
||||
isLoading: isOrganizationMemberInviteCreateLoading,
|
||||
} = useOrganizationMemberInviteCreateMutation({
|
||||
onSuccess() {
|
||||
ui.setNotification({ category: 'success', message: 'Resent the invitation.' })
|
||||
},
|
||||
onError(error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to resend invitation: ${error.message}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
async function handleResendInvite(member: Member) {
|
||||
setLoading(true)
|
||||
if (!member.invited_id || !slug) return
|
||||
|
||||
const roleId = (member?.role_ids ?? [])[0]
|
||||
const response = await post(`${API_URL}/organizations/${slug}/members/invite`, {
|
||||
invited_email: member.primary_email,
|
||||
owner_id: member.invited_id,
|
||||
role_id: roleId,
|
||||
createOrganizationMemberInvite({
|
||||
slug,
|
||||
invitedEmail: member.primary_email,
|
||||
ownerId: member.invited_id,
|
||||
roleId: roleId,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to resend invitation: ${response.error.message}`,
|
||||
})
|
||||
} else {
|
||||
const updatedMembers = [...members]
|
||||
mutateOrgMembers(updatedMembers)
|
||||
ui.setNotification({ category: 'success', message: 'Resent the invitation.' })
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
async function handleRevokeInvitation(member: Member) {
|
||||
setLoading(true)
|
||||
|
||||
const invitedId = member.invited_id
|
||||
if (!invitedId) return
|
||||
|
||||
const response = await delete_(
|
||||
`${API_URL}/organizations/${slug}/members/invite?invited_id=${invitedId}`,
|
||||
{}
|
||||
)
|
||||
|
||||
if (response.error) {
|
||||
const {
|
||||
mutate: deleteOrganizationMemberInvite,
|
||||
isLoading: isOrganizationMemberInviteDeleteLoading,
|
||||
} = useOrganizationMemberInviteDeleteMutation({
|
||||
onSuccess() {
|
||||
ui.setNotification({ category: 'success', message: 'Successfully revoked the invitation.' })
|
||||
},
|
||||
onError(error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to revoke invitation: ${response.error.message}`,
|
||||
message: `Failed to revoke invitation: ${error.message}`,
|
||||
})
|
||||
} else {
|
||||
const updatedMembers = [...members]
|
||||
mutateOrgMembers(updatedMembers)
|
||||
ui.setNotification({ category: 'success', message: 'Successfully revoked the invitation.' })
|
||||
}
|
||||
setLoading(false)
|
||||
},
|
||||
})
|
||||
|
||||
function handleRevokeInvitation(member: Member) {
|
||||
const invitedId = member.invited_id
|
||||
if (!invitedId || !slug) return
|
||||
|
||||
deleteOrganizationMemberInvite({
|
||||
slug,
|
||||
invitedId,
|
||||
})
|
||||
}
|
||||
|
||||
if (!canRemoveMember || (isPendingInviteAcceptance && !canResendInvite && !canRevokeInvite)) {
|
||||
@@ -141,6 +152,11 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const isLoading =
|
||||
isOrganizationMemberDeleteLoading ||
|
||||
isOrganizationMemberInviteDeleteLoading ||
|
||||
isOrganizationMemberInviteCreateLoading
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Dropdown
|
||||
@@ -182,8 +198,8 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
<Button
|
||||
as="span"
|
||||
type="text"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
icon={<IconMoreHorizontal />}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
@@ -5,15 +5,14 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Badge, Button, Loading, Listbox, IconUser, Modal, IconAlertCircle } from 'ui'
|
||||
|
||||
import { Member, Role } from 'types'
|
||||
import { useStore, useOrganizationDetail, useParams } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore, useParams } from 'hooks'
|
||||
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
|
||||
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import MemberActions from './MemberActions'
|
||||
import RolesHelperModal from './RolesHelperModal/RolesHelperModal'
|
||||
import { getRolesManagementPermissions } from './TeamSettings.utils'
|
||||
import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
|
||||
|
||||
interface SelectedMember extends Member {
|
||||
oldRoleId: number
|
||||
@@ -32,10 +31,8 @@ const MembersView: FC<Props> = ({ searchString, roles, members }) => {
|
||||
|
||||
const user = ui.profile
|
||||
|
||||
const { mutateOrgMembers } = useOrganizationDetail(slug ?? '')
|
||||
const { rolesAddable, rolesRemovable } = getRolesManagementPermissions(roles)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedMember, setSelectedMember] = useState<SelectedMember>()
|
||||
const [userRoleChangeModalVisible, setUserRoleChangeModalVisible] = useState(false)
|
||||
|
||||
@@ -61,36 +58,35 @@ const MembersView: FC<Props> = ({ searchString, roles, members }) => {
|
||||
return roles.find((x: any) => x.id === id)?.name
|
||||
}
|
||||
|
||||
const handleRoleChange = async () => {
|
||||
if (!selectedMember) return
|
||||
|
||||
setLoading(true)
|
||||
const { gotrue_id, newRoleId } = selectedMember
|
||||
const response = await patch(`${API_URL}/organizations/${slug}/members/${gotrue_id}`, {
|
||||
role_id: newRoleId,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update role for ${getUserDisplayName(selectedMember)}`,
|
||||
})
|
||||
} else {
|
||||
const updatedMembers = members.map((member: Member) => {
|
||||
if (member.gotrue_id === selectedMember.gotrue_id) {
|
||||
return { ...member, role_ids: [newRoleId] }
|
||||
} else {
|
||||
return member
|
||||
}
|
||||
})
|
||||
mutateOrgMembers(updatedMembers)
|
||||
const { isLoading, mutate } = useOrganizationMemberUpdateMutation({
|
||||
onSuccess() {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully updated role for ${getUserDisplayName(selectedMember)}`,
|
||||
})
|
||||
},
|
||||
onError() {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update role for ${getUserDisplayName(selectedMember)}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleRoleChange = async () => {
|
||||
if (!selectedMember) return
|
||||
|
||||
const { gotrue_id, newRoleId } = selectedMember
|
||||
|
||||
if (!slug) {
|
||||
throw new Error('slug is required')
|
||||
}
|
||||
if (!gotrue_id) {
|
||||
throw new Error('gotrue_id is required')
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
mutate({ slug, gotrueId: gotrue_id, roleId: newRoleId })
|
||||
|
||||
setUserRoleChangeModalVisible(false)
|
||||
}
|
||||
|
||||
@@ -145,7 +141,7 @@ const MembersView: FC<Props> = ({ searchString, roles, members }) => {
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
{x.invited_id ? (
|
||||
<span className="flex rounded-full border-2 border-border-secondary-light p-2 dark:border-border-secondary-dark">
|
||||
<span className="flex p-2 border-2 rounded-full border-border-secondary-light dark:border-border-secondary-dark">
|
||||
<IconUser size={20} strokeWidth={2} />
|
||||
</span>
|
||||
) : isEmailUser ? (
|
||||
@@ -157,7 +153,7 @@ const MembersView: FC<Props> = ({ searchString, roles, members }) => {
|
||||
src={`https://github.com/${x.username}.png?size=80`}
|
||||
width="40"
|
||||
height="40"
|
||||
className="rounded-full border"
|
||||
className="border rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -312,8 +308,8 @@ const MembersView: FC<Props> = ({ searchString, roles, members }) => {
|
||||
block
|
||||
type="warning"
|
||||
size="medium"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
onClick={() => handleRoleChange()}
|
||||
>
|
||||
Confirm
|
||||
|
||||
@@ -3,13 +3,15 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Button, Input, IconSearch } from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
import { useStore, useFlag, useOrganizationRoles, useOrganizationDetail, useParams } from 'hooks'
|
||||
import { useStore, useParams } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import InviteMemberButton from './InviteMemberButton'
|
||||
import MembersView from './MembersView'
|
||||
import { getRolesManagementPermissions, hasMultipleOwners } from './TeamSettings.utils'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query'
|
||||
import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query'
|
||||
|
||||
const TeamSettings = () => {
|
||||
const { ui } = useStore()
|
||||
@@ -18,8 +20,11 @@ const TeamSettings = () => {
|
||||
const user = ui.profile
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const { members } = useOrganizationDetail(slug || '')
|
||||
const { roles } = useOrganizationRoles(slug)
|
||||
const { data: detailData } = useOrganizationDetailQuery({ slug })
|
||||
const { data: rolesData } = useOrganizationRolesQuery({ slug })
|
||||
|
||||
const members = detailData?.members ?? []
|
||||
const roles = rolesData?.roles ?? []
|
||||
|
||||
const { rolesAddable } = getRolesManagementPermissions(roles)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export const usePresetReport = (hooks: PresetHookResult[]) => {
|
||||
const isLoading = data.map((datum) => datum.isLoading).some((v) => v)
|
||||
|
||||
const Layout: React.FC<{ title: string }> = ({ title, children }) => (
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<div className="flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<h1 className="text-2xl text-scale-1200">{title}</h1>
|
||||
<div className="flex flex-row justify-between">
|
||||
<DatePickers
|
||||
|
||||
@@ -44,7 +44,10 @@ const FavouriteButton = () => {
|
||||
*/
|
||||
const { query, name, desc } = sqlEditorStore.activeTab || {}
|
||||
sqlEditorStore.addToFavorite(id, query, name, desc)
|
||||
Telemetry.sendEvent('sql_editor', 'sql_favourited', name)
|
||||
Telemetry.sendEvent(
|
||||
{ category: 'sql_editor', action: 'sql_favourited', label: name },
|
||||
ui.googleAnalyticsProps
|
||||
)
|
||||
|
||||
/*
|
||||
* reload sql data in store and re-select tab
|
||||
@@ -82,7 +85,10 @@ const FavouriteButton = () => {
|
||||
*/
|
||||
const { name } = sqlEditorStore.activeTab || {}
|
||||
sqlEditorStore.unFavorite(id)
|
||||
Telemetry.sendEvent('sql_editor', 'sql_unfavourited', name)
|
||||
Telemetry.sendEvent(
|
||||
{ category: 'sql_editor', action: 'sql_unfavourited', label: name },
|
||||
ui.googleAnalyticsProps
|
||||
)
|
||||
|
||||
/*
|
||||
* reload sql data in store and re-select tab
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FC, useState, Dispatch, SetStateAction } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import {
|
||||
Alert,
|
||||
@@ -24,27 +23,22 @@ import {
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import {
|
||||
useStore,
|
||||
useJwtSecretUpdateStatus,
|
||||
useProjectPostgrestConfig,
|
||||
checkPermissions,
|
||||
} from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore, checkPermissions, useParams } from 'hooks'
|
||||
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
|
||||
import { useJwtSecretUpdateMutation } from 'data/config/jwt-secret-update-mutation'
|
||||
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
|
||||
import {
|
||||
JWT_SECRET_UPDATE_ERROR_MESSAGES,
|
||||
JWT_SECRET_UPDATE_PROGRESS_MESSAGES,
|
||||
} from './API.constants'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const JWTSettings: FC<Props> = ({}) => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const [customToken, setCustomToken] = useState<string>('')
|
||||
const [showCustomTokenInput, setShowCustomTokenInput] = useState(false)
|
||||
@@ -59,41 +53,39 @@ const JWTSettings: FC<Props> = ({}) => {
|
||||
'queue_job.projects.update_jwt'
|
||||
)
|
||||
|
||||
const {
|
||||
changeTrackingId,
|
||||
jwtSecretUpdateError,
|
||||
jwtSecretUpdateProgress,
|
||||
jwtSecretUpdateStatus,
|
||||
mutateJwtSecretUpdateStatus,
|
||||
}: any = useJwtSecretUpdateStatus(ref)
|
||||
|
||||
const { config, isError } = useProjectPostgrestConfig(ref as string | undefined)
|
||||
const { data } = useJwtSecretUpdatingStatusQuery({ projectRef })
|
||||
const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
|
||||
|
||||
const { Failed, Updated, Updating } = JwtSecretUpdateStatus
|
||||
|
||||
const isJwtSecretUpdateFailed = jwtSecretUpdateStatus === Failed
|
||||
const isJwtSecretUpdateFailed = data?.jwtSecretUpdateStatus === Failed
|
||||
const isNotUpdatingJwtSecret =
|
||||
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === Updated
|
||||
const isUpdatingJwtSecret = jwtSecretUpdateStatus === Updating
|
||||
data?.jwtSecretUpdateStatus === undefined || data?.jwtSecretUpdateStatus === Updated
|
||||
const isUpdatingJwtSecret = data?.jwtSecretUpdateStatus === Updating
|
||||
const jwtSecretUpdateErrorMessage =
|
||||
JWT_SECRET_UPDATE_ERROR_MESSAGES[jwtSecretUpdateError as JwtSecretUpdateError]
|
||||
JWT_SECRET_UPDATE_ERROR_MESSAGES[data?.jwtSecretUpdateError as JwtSecretUpdateError]
|
||||
const jwtSecretUpdateProgressMessage =
|
||||
JWT_SECRET_UPDATE_PROGRESS_MESSAGES[jwtSecretUpdateProgress as JwtSecretUpdateProgress]
|
||||
JWT_SECRET_UPDATE_PROGRESS_MESSAGES[data?.jwtSecretUpdateProgress as JwtSecretUpdateProgress]
|
||||
|
||||
const { mutateAsync: updateJwt } = useJwtSecretUpdateMutation()
|
||||
|
||||
async function handleJwtSecretUpdate(
|
||||
jwt_secret: string,
|
||||
setModalVisibility: Dispatch<SetStateAction<boolean>>
|
||||
) {
|
||||
if (!projectRef) return
|
||||
|
||||
setIsSubmittingJwtSecretUpdateRequest(true)
|
||||
try {
|
||||
const trackingId = uuidv4()
|
||||
const res = await patch(`${API_URL}/projects/${ref}/config/secrets`, {
|
||||
jwt_secret,
|
||||
change_tracking_id: trackingId,
|
||||
await updateJwt({
|
||||
projectRef,
|
||||
jwtSecret: jwt_secret,
|
||||
changeTrackingId: trackingId,
|
||||
})
|
||||
if (res.error) throw res.error
|
||||
|
||||
setModalVisibility(false)
|
||||
mutateJwtSecretUpdateStatus()
|
||||
|
||||
ui.setNotification({
|
||||
category: 'info',
|
||||
message:
|
||||
@@ -111,7 +103,7 @@ const JWTSettings: FC<Props> = ({}) => {
|
||||
<Panel title={<h5 className="mb-0">JWT Settings</h5>}>
|
||||
<Panel.Content className="space-y-6 border-t border-panel-border-interior-light dark:border-panel-border-interior-dark">
|
||||
{isError ? (
|
||||
<div className="flex items-center justify-center space-x-2 py-8">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">Failed to retrieve JWT settings</p>
|
||||
</div>
|
||||
@@ -139,7 +131,7 @@ const JWTSettings: FC<Props> = ({}) => {
|
||||
layout="horizontal"
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-md border bg-bg-alt-light p-3 px-6 shadow-sm dark:border-dark dark:bg-bg-alt-dark">
|
||||
<div className="p-3 px-6 border rounded-md shadow-sm bg-bg-alt-light dark:border-dark dark:bg-bg-alt-dark">
|
||||
{isUpdatingJwtSecret ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
@@ -149,7 +141,7 @@ const JWTSettings: FC<Props> = ({}) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm">Generate a new JWT secret</p>
|
||||
<p className="text-sm opacity-50">
|
||||
@@ -223,7 +215,7 @@ const JWTSettings: FC<Props> = ({}) => {
|
||||
<Alert withIcon variant="warning" title="Failed to update JWT secret">
|
||||
Please try again. If the failures persist, please contact Supabase support with
|
||||
the following details: <br />
|
||||
Change tracking ID: {changeTrackingId} <br />
|
||||
Change tracking ID: {data?.changeTrackingId} <br />
|
||||
Error message: {jwtSecretUpdateErrorMessage}
|
||||
</Alert>
|
||||
) : canGenerateNewJWTSecret ? (
|
||||
@@ -283,7 +275,7 @@ const JWTSettings: FC<Props> = ({}) => {
|
||||
}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="space-y-2 py-4">
|
||||
<div className="py-4 space-y-2">
|
||||
<p className="text-sm text-scale-1100">
|
||||
Create a custom JWT secret. Make sure it is a strong combination of characters that
|
||||
cannot be guessed easily.
|
||||
@@ -302,7 +294,7 @@ const JWTSettings: FC<Props> = ({}) => {
|
||||
label="Custom JWT secret"
|
||||
descriptionText="Minimally 32 characters long, '@' and '$' are not allowed."
|
||||
actions={
|
||||
<div className="mr-1 flex items-center justify-center">
|
||||
<div className="flex items-center justify-center mr-1">
|
||||
<Button
|
||||
type="default"
|
||||
icon={showCustomTokenInput ? <IconEye /> : <IconEyeOff />}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useContext, FC, useEffect } from 'react'
|
||||
import { indexOf } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Input, Form, IconAlertCircle, InputNumber } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useStore, useProjectPostgrestConfig } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { checkPermissions, useStore, useParams } from 'hooks'
|
||||
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
|
||||
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
|
||||
import MultiSelect from 'components/ui/MultiSelect'
|
||||
import { PageContext } from 'pages/project/[ref]/settings/api'
|
||||
|
||||
@@ -26,11 +25,10 @@ const PostgrestConfig: FC<Props> = ({}) => {
|
||||
const { ui } = useStore()
|
||||
const { meta } = PageState
|
||||
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const formId = 'project-postgres-config'
|
||||
const { config, isError, isLoading } = useProjectPostgrestConfig(ref as string | undefined)
|
||||
const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
|
||||
|
||||
const initialValues = {
|
||||
db_schema: '',
|
||||
@@ -43,14 +41,20 @@ const PostgrestConfig: FC<Props> = ({}) => {
|
||||
'custom_config_postgrest'
|
||||
)
|
||||
|
||||
const updateConfig = async (updatedConfig: any) => {
|
||||
const { mutateAsync: updatePostgrestConfig } = useProjectPostgrestConfigUpdateMutation()
|
||||
|
||||
const updateConfig = async (updatedConfig: typeof initialValues) => {
|
||||
if (!projectRef) return
|
||||
|
||||
try {
|
||||
const response = await patch(`${API_URL}/projects/${ref}/config/postgrest`, updatedConfig)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
} else {
|
||||
ui.setNotification({ category: 'success', message: 'Successfully saved settings' })
|
||||
}
|
||||
await updatePostgrestConfig({
|
||||
projectRef,
|
||||
dbSchema: updatedConfig.db_schema,
|
||||
maxRows: updatedConfig.max_rows,
|
||||
dbExtraSearchPath: updatedConfig.db_extra_search_path,
|
||||
})
|
||||
|
||||
ui.setNotification({ category: 'success', message: 'Successfully saved settings' })
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
@@ -86,7 +90,7 @@ const PostgrestConfig: FC<Props> = ({}) => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && config) {
|
||||
if (config) {
|
||||
const values = {
|
||||
db_schema: config.db_schema,
|
||||
max_rows: config.max_rows,
|
||||
@@ -94,7 +98,7 @@ const PostgrestConfig: FC<Props> = ({}) => {
|
||||
}
|
||||
resetForm({ values, initialValues: values })
|
||||
}
|
||||
}, [isLoading])
|
||||
}, [config])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -102,7 +106,7 @@ const PostgrestConfig: FC<Props> = ({}) => {
|
||||
disabled={true}
|
||||
header={<p>API settings</p>}
|
||||
footer={
|
||||
<div className="flex py-4 px-8">
|
||||
<div className="flex px-8 py-4">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
@@ -119,7 +123,7 @@ const PostgrestConfig: FC<Props> = ({}) => {
|
||||
}
|
||||
>
|
||||
{isError ? (
|
||||
<div className="flex items-center justify-center space-x-2 py-8">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">Failed to retrieve API settings</p>
|
||||
</div>
|
||||
@@ -142,8 +146,8 @@ const PostgrestConfig: FC<Props> = ({}) => {
|
||||
emptyMessage={
|
||||
<>
|
||||
<IconAlertCircle strokeWidth={2} />
|
||||
<div className="mt-2 flex flex-col text-center">
|
||||
<p className="align-center text-sm">
|
||||
<div className="flex flex-col mt-2 text-center">
|
||||
<p className="text-sm align-center">
|
||||
No schema available to choose
|
||||
</p>
|
||||
<p className="text-xs opacity-50">
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
import { FC, useEffect, useRef } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { JwtSecretUpdateError, JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { IconAlertCircle, Input } from 'ui'
|
||||
|
||||
import { configKeys } from 'data/config/keys'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useStore, useProjectPostgrestConfig, useJwtSecretUpdateStatus } from 'hooks'
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
|
||||
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { configKeys } from 'data/config/keys'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import PostgrestConfig from './PostgrestConfig'
|
||||
import { DisplayApiSettings } from 'components/ui/ProjectSettings'
|
||||
import { JWT_SECRET_UPDATE_ERROR_MESSAGES } from './API.constants'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import JWTSettings from './JWTSettings'
|
||||
import PostgrestConfig from './PostgrestConfig'
|
||||
|
||||
interface Props {
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
const ServiceList: FC<Props> = ({ projectRef }) => {
|
||||
const ServiceList = () => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const queryClient = useQueryClient()
|
||||
const client = useQueryClient()
|
||||
|
||||
const { data: settings, isError } = useProjectSettingsQuery({ projectRef: ref as string })
|
||||
const { mutateConfig } = useProjectPostgrestConfig(ref as string | undefined)
|
||||
const { jwtSecretUpdateError, jwtSecretUpdateStatus }: any = useJwtSecretUpdateStatus(ref)
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: settings, isError } = useProjectApiQuery({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
const previousJwtSecretUpdateStatus = useRef()
|
||||
const { data } = useJwtSecretUpdatingStatusQuery({ projectRef })
|
||||
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
|
||||
const jwtSecretUpdateError = data?.jwtSecretUpdateError
|
||||
|
||||
const previousJwtSecretUpdateStatus = useRef<JwtSecretUpdateStatus>()
|
||||
const { Failed, Updated, Updating } = JwtSecretUpdateStatus
|
||||
const jwtSecretUpdateErrorMessage =
|
||||
JWT_SECRET_UPDATE_ERROR_MESSAGES[jwtSecretUpdateError as JwtSecretUpdateError]
|
||||
@@ -38,8 +36,10 @@ const ServiceList: FC<Props> = ({ projectRef }) => {
|
||||
if (previousJwtSecretUpdateStatus.current === Updating) {
|
||||
switch (jwtSecretUpdateStatus) {
|
||||
case Updated:
|
||||
mutateConfig()
|
||||
queryClient.invalidateQueries({ queryKey: configKeys.settings(projectRef) })
|
||||
client.invalidateQueries(configKeys.api(projectRef))
|
||||
client.invalidateQueries(configKeys.settings(projectRef))
|
||||
client.invalidateQueries(configKeys.postgrest(projectRef))
|
||||
|
||||
ui.setNotification({ category: 'success', message: 'Successfully updated JWT secret' })
|
||||
break
|
||||
case Failed:
|
||||
@@ -60,12 +60,12 @@ const ServiceList: FC<Props> = ({ projectRef }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-scale-1200 mb-6 text-xl">API Settings</h3>
|
||||
<h3 className="mb-6 text-xl text-scale-1200">API Settings</h3>
|
||||
<section>
|
||||
<Panel title={<h5 className="mb-0">Project URL</h5>}>
|
||||
<Panel.Content>
|
||||
{isError ? (
|
||||
<div className="flex items-center justify-center space-x-2 py-4">
|
||||
<div className="flex items-center justify-center py-4 space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">Failed to retrieve project URL</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { FC, useState, useEffect, Fragment } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStore } from 'hooks'
|
||||
import { pluckObjectFields } from 'lib/helpers'
|
||||
@@ -11,6 +11,7 @@ import { patch } from 'lib/common/fetch'
|
||||
import ToggleField from 'components/to-be-cleaned/forms/ToggleField'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import { Input } from 'ui'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
|
||||
interface Props {}
|
||||
|
||||
@@ -32,7 +33,32 @@ const ConnectionPooling: FC<Props> = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection Pooling
|
||||
</h5>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="space-y-8">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Fragment key={i}>
|
||||
<div className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full">
|
||||
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
|
||||
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
|
||||
</div>
|
||||
<Divider light />
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
if (poolingConfiguration.error) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { IconAlertCircle } from 'ui'
|
||||
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
@@ -20,7 +20,7 @@ const CustomDomainConfig = () => {
|
||||
const { ref } = useParams()
|
||||
|
||||
const tier = ui.selectedProject?.subscription_tier
|
||||
const { isLoading: isSettingsLoading, data: settings } = useProjectSettingsQuery({
|
||||
const { isLoading: isSettingsLoading, data: settings } = useProjectApiQuery({
|
||||
projectRef: ref,
|
||||
})
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Alert, Button, IconAlertCircle, IconExternalLink, IconHelpCircle, IconRefreshCw } from 'ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { ProjectSettingsResponse } from 'data/config/project-settings-query'
|
||||
import { ProjectApiResponse } from 'data/config/project-api-query'
|
||||
import { CustomDomainResponse } from 'data/custom-domains/custom-domains-query'
|
||||
import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation'
|
||||
import { useCustomDomainReverifyMutation } from 'data/custom-domains/custom-domains-reverify-mutation'
|
||||
@@ -15,7 +15,7 @@ import InformationBox from 'components/ui/InformationBox'
|
||||
export type CustomDomainVerifyProps = {
|
||||
projectRef?: string
|
||||
customDomain: CustomDomainResponse
|
||||
settings?: ProjectSettingsResponse
|
||||
settings?: ProjectApiResponse
|
||||
}
|
||||
|
||||
const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomainVerifyProps) => {
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import {
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from 'components/ui/Forms'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useCustomDomainCreateMutation } from 'data/custom-domains/custom-domains-create-mutation'
|
||||
|
||||
const schema = yup.object({
|
||||
@@ -23,7 +23,7 @@ const CustomDomainsConfigureHostname = () => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const { mutateAsync: createCustomDomain } = useCustomDomainCreateMutation()
|
||||
const { data: settings } = useProjectSettingsQuery({ projectRef: ref })
|
||||
const { data: settings } = useProjectApiQuery({ projectRef: ref })
|
||||
|
||||
const FORM_ID = 'custom-domains-form'
|
||||
const endpoint = settings?.autoApiService.endpoint
|
||||
|
||||
@@ -20,10 +20,10 @@ import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilte
|
||||
import { LOGS_TABLES } from './Logs.constants'
|
||||
import ShimmerLine from 'components/ui/ShimmerLine'
|
||||
import LoadingOpacity from 'components/ui/LoadingOpacity'
|
||||
import { useProjectSubscription } from 'hooks'
|
||||
import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt'
|
||||
import { StripeProduct } from 'components/interfaces/Billing'
|
||||
import UpgradePrompt from './UpgradePrompt'
|
||||
import { useParams } from 'hooks'
|
||||
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
|
||||
|
||||
/**
|
||||
* Acts as a container component for the entire log display
|
||||
@@ -50,15 +50,15 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
tableName,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { s, ite, its, ref } = router.query
|
||||
const { s, ite, its } = useParams()
|
||||
const [showChart, setShowChart] = useState(true)
|
||||
const { subscription } = useProjectSubscription(ref as string)
|
||||
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
|
||||
const tier = subscription?.tier
|
||||
|
||||
const table = !tableName ? LOGS_TABLES[queryType] : tableName
|
||||
|
||||
const [
|
||||
{ error, logData, params, newCount, filters, isLoading, eventChartData },
|
||||
{ error, logData, params, newCount, filters, isLoading, eventChartData, isLoadingOlder },
|
||||
{ loadOlder, setFilters, refresh, setParams },
|
||||
] = useLogsPreview(projectRef as string, table, filterOverride)
|
||||
|
||||
@@ -152,7 +152,7 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-grow flex-col">
|
||||
<div className="flex flex-col flex-grow h-full">
|
||||
<PreviewFilterPanel
|
||||
csvData={logData}
|
||||
isLoading={isLoading}
|
||||
@@ -205,7 +205,7 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex flex-grow flex-col pt-4">
|
||||
<div className="relative flex flex-col flex-grow pt-4">
|
||||
<ShimmerLine active={isLoading} />
|
||||
<LoadingOpacity active={isLoading}>
|
||||
<LogTable
|
||||
@@ -221,10 +221,16 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
</LoadingOpacity>
|
||||
{!error && (
|
||||
<div className="flex flex-row justify-between p-2">
|
||||
<Button onClick={loadOlder} icon={<IconRewind />} type="default">
|
||||
<Button
|
||||
onClick={loadOlder}
|
||||
icon={<IconRewind />}
|
||||
type="default"
|
||||
loading={isLoadingOlder}
|
||||
disabled={isLoadingOlder}
|
||||
>
|
||||
Load older
|
||||
</Button>
|
||||
<div className="mt-2 flex flex-row justify-end">
|
||||
<div className="flex flex-row justify-end mt-2">
|
||||
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { FC, useEffect } from 'react'
|
||||
import { Badge, Button, IconAlertCircle, IconInfo, Loading } from 'ui'
|
||||
|
||||
import { useStore, useProjectUsage } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
|
||||
import SparkBar from 'components/ui/SparkBar'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
|
||||
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
@@ -17,7 +18,7 @@ interface Props {
|
||||
|
||||
const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
||||
const { ui } = useStore()
|
||||
const { usage, error, isLoading } = useProjectUsage(projectRef)
|
||||
const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef })
|
||||
const router = useRouter()
|
||||
|
||||
const subscriptionTier = ui.selectedProject?.subscription_tier
|
||||
@@ -43,7 +44,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
||||
if (error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to get project's usage data: ${error?.message ?? 'unknown'}`,
|
||||
message: `Failed to get project's usage data: ${(error as any)?.message ?? 'unknown'}`,
|
||||
})
|
||||
}
|
||||
}, [error])
|
||||
@@ -68,8 +69,8 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
||||
showUsageExceedMessage &&
|
||||
product.features
|
||||
.map((feature) => {
|
||||
const featureUsage = usage[feature.key]
|
||||
return featureUsage.usage / featureUsage.limit > 1
|
||||
const featureUsage = usage[feature.key as keyof ProjectUsageResponse]
|
||||
return (featureUsage.usage ?? 0) / featureUsage.limit > 1
|
||||
})
|
||||
.some((x) => x === true)
|
||||
|
||||
@@ -99,23 +100,23 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
||||
</div>
|
||||
</th>
|
||||
{/* Plan Limits */}
|
||||
<th className="hidden p-3 text-left text-xs font-medium leading-4 text-gray-400 lg:table-cell">
|
||||
<th className="hidden p-3 text-xs font-medium leading-4 text-left text-gray-400 lg:table-cell">
|
||||
{isExceededUsage && <Badge color="red">Exceeded usage</Badge>}
|
||||
</th>
|
||||
{/* Usage */}
|
||||
<th className="p-3 text-left text-xs font-medium leading-4 text-gray-400" />
|
||||
<th className="p-3 text-xs font-medium leading-4 text-left text-gray-400" />
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Line items */}
|
||||
{usage === undefined ? (
|
||||
<div className="w-96 px-4 pt-1 pb-4">
|
||||
<div className="px-4 pt-1 pb-4 w-96">
|
||||
<ShimmeringLoader />
|
||||
</div>
|
||||
) : (
|
||||
<tbody>
|
||||
{product.features.map((feature) => {
|
||||
const featureUsage = usage[feature.key]
|
||||
const featureUsage = usage[feature.key as keyof ProjectUsageResponse]
|
||||
const usageValue = featureUsage.usage || 0
|
||||
const usageRatio = usageValue / featureUsage.limit
|
||||
const isApproaching = usageRatio >= USAGE_APPROACHING_THRESHOLD
|
||||
@@ -125,7 +126,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
||||
let usageElement
|
||||
if (!isAvailableInPlan) {
|
||||
usageElement = (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Not included in {planName} tier</span>
|
||||
<Button
|
||||
size="tiny"
|
||||
@@ -175,7 +176,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
||||
key={feature.title}
|
||||
className="border-t border-panel-border-light dark:border-panel-border-dark"
|
||||
>
|
||||
<td className="whitespace-nowrap px-6 py-3 text-sm text-scale-1200">
|
||||
<td className="px-6 py-3 text-sm whitespace-nowrap text-scale-1200">
|
||||
{feature.title}
|
||||
{feature.tooltip && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
@@ -203,7 +204,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
||||
{ui.selectedProject?.subscription_tier !== undefined && (
|
||||
<>
|
||||
{showUsageExceedMessage && (
|
||||
<td className="hidden w-1/5 whitespace-nowrap p-3 text-sm text-scale-1200 lg:table-cell">
|
||||
<td className="hidden w-1/5 p-3 text-sm whitespace-nowrap text-scale-1200 lg:table-cell">
|
||||
{isAvailableInPlan ? (
|
||||
<>{(usageRatio * 100).toFixed(2)} %</>
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FC } from 'react'
|
||||
import { Loading } from 'ui'
|
||||
|
||||
import { useProjectSubscription, useProjectUsage } from 'hooks'
|
||||
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
|
||||
import SparkBar from 'components/ui/SparkBar'
|
||||
@@ -15,8 +16,10 @@ interface ProjectUsageMinimalProps {
|
||||
// [Joshen] This is currently not being used anywhere as of 011122
|
||||
|
||||
const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter }) => {
|
||||
const { usage, error: usageError, isLoading } = useProjectUsage(projectRef)
|
||||
const { subscription, error: subscriptionError } = useProjectSubscription(projectRef)
|
||||
const { data: usage, error: usageError, isLoading } = useProjectUsageQuery({ projectRef })
|
||||
const { data: subscription, error: subscriptionError } = useProjectSubscriptionQuery({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
if (
|
||||
subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PAYG ||
|
||||
@@ -38,8 +41,8 @@ const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter
|
||||
{usage && (
|
||||
<div className="space-y-8">
|
||||
{product.features.map((feature) => {
|
||||
const featureUsage = usage[feature.key]
|
||||
const usageRatio = featureUsage.usage / featureUsage.limit
|
||||
const featureUsage = usage[feature.key as keyof ProjectUsageResponse]
|
||||
const usageRatio = (featureUsage.usage ?? 0) / featureUsage.limit
|
||||
const isApproaching = usageRatio >= USAGE_APPROACHING_THRESHOLD
|
||||
const isExceeded = usageRatio >= 1
|
||||
|
||||
@@ -51,7 +54,7 @@ const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter
|
||||
barClass={`${
|
||||
isExceeded ? 'bg-red-900' : isApproaching ? 'bg-yellow-900' : 'bg-brand-900'
|
||||
}`}
|
||||
value={featureUsage.usage}
|
||||
value={featureUsage.usage ?? 0}
|
||||
max={featureUsage.limit}
|
||||
labelBottom={formatBytes(featureUsage.usage)}
|
||||
labelTop={formatBytes(featureUsage.limit)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { usePushNext } from 'hooks/misc/useAutoAuthRedirect'
|
||||
import { auth } from 'lib/gotrue'
|
||||
import Link from 'next/link'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { Button, Form, Input } from 'ui'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
@@ -16,7 +16,7 @@ const signInSchema = object({
|
||||
const SignInForm = () => {
|
||||
const { ui } = useStore()
|
||||
const pushNext = usePushNext()
|
||||
const { cache } = useSWRConfig()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
@@ -46,9 +46,7 @@ const SignInForm = () => {
|
||||
message: `Signed in successfully!`,
|
||||
})
|
||||
|
||||
// .clear() does actually exist on the cache object, but it's not in the types 🤦🏻
|
||||
// @ts-ignore
|
||||
cache.clear()
|
||||
await queryClient.resetQueries()
|
||||
|
||||
await pushNext()
|
||||
} else {
|
||||
@@ -105,7 +103,7 @@ const SignInForm = () => {
|
||||
|
||||
{/* positioned using absolute instead of labelOptional prop so tabbing between inputs works smoothly */}
|
||||
<Link href="/forgot-password">
|
||||
<a className="text-scale-900 text-sm absolute top-0 right-0">Forgot Password?</a>
|
||||
<a className="absolute top-0 right-0 text-sm text-scale-900">Forgot Password?</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { auth, getReturnToPath } from 'lib/gotrue'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { Button, Form, Input } from 'ui'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
@@ -12,12 +12,12 @@ const signInSchema = object({
|
||||
|
||||
const SignInSSOForm = () => {
|
||||
const { ui } = useStore()
|
||||
const { cache } = useSWRConfig()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
|
||||
const onSignIn = async ({ email, password }: { email: string; password: string }) => {
|
||||
const onSignIn = async ({ email }: { email: string }) => {
|
||||
const toastId = ui.setNotification({
|
||||
category: 'loading',
|
||||
message: `Signing in...`,
|
||||
@@ -42,9 +42,7 @@ const SignInSSOForm = () => {
|
||||
})
|
||||
|
||||
if (!error) {
|
||||
// .clear() does actually exist on the cache object, but it's not in the types 🤦🏻
|
||||
// @ts-ignore
|
||||
cache.clear()
|
||||
await queryClient.resetQueries()
|
||||
|
||||
if (data) {
|
||||
// redirect to SSO identity provider page
|
||||
|
||||
@@ -1,42 +1,48 @@
|
||||
export const CATEGORY_OPTIONS = [
|
||||
{
|
||||
value: 'Problem',
|
||||
value: 'problem',
|
||||
label: 'Issues with APIs / client libraries',
|
||||
description: "Issues with your project's API and client libraries",
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'Database_unresponsive',
|
||||
value: 'dashboard_bug',
|
||||
label: 'Dashboard bug',
|
||||
description: 'Issues with the Supabase dashboard',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'database_unresponsive',
|
||||
label: 'Database unresponsive',
|
||||
description: 'Issues with connecting to your database',
|
||||
query: 'Unable to connect',
|
||||
},
|
||||
{
|
||||
value: 'Performance',
|
||||
value: 'performance',
|
||||
label: 'Performance issues',
|
||||
description: 'Reporting of performance issues is only available on the Pro tier',
|
||||
query: 'Performance',
|
||||
},
|
||||
{
|
||||
value: 'Sales',
|
||||
value: 'sales',
|
||||
label: 'Sales enquiry',
|
||||
description: 'Questions about pricing, paid plans and Enterprise plans',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'Billing',
|
||||
value: 'billing',
|
||||
label: 'Billing',
|
||||
description: 'Issues with credit card charges | invoices | overcharging',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'Abuse',
|
||||
value: 'problem',
|
||||
label: 'Abuse report',
|
||||
description: 'Report abuse of a Supabase project or Supabase brand',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'Refund',
|
||||
value: 'refund',
|
||||
label: 'Refund enquiry',
|
||||
description: 'Formal enquiry form for requesting refunds',
|
||||
query: undefined,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState, FC, ChangeEvent, useRef } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -16,19 +17,19 @@ import {
|
||||
} from 'ui'
|
||||
import { CLIENT_LIBRARIES } from 'common/constants'
|
||||
|
||||
import { Organization, Project } from 'types'
|
||||
import { Project } from 'types'
|
||||
import { useStore, useFlag } from 'hooks'
|
||||
import { post, get } from 'lib/common/fetch'
|
||||
import { detectBrowser } from 'lib/helpers'
|
||||
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
|
||||
import Divider from 'components/ui/Divider'
|
||||
import Connecting from 'components/ui/Loading'
|
||||
import MultiSelect from 'components/ui/MultiSelect'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { formatMessage, uploadAttachments } from './SupportForm.utils'
|
||||
import { CATEGORY_OPTIONS, SEVERITY_OPTIONS, SERVICE_OPTIONS } from './Support.constants'
|
||||
import DisabledStateForFreeTier from './DisabledStateForFreeTier'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import Link from 'next/link'
|
||||
|
||||
const MAX_ATTACHMENTS = 5
|
||||
|
||||
@@ -165,6 +166,7 @@ const SupportForm: FC<Props> = ({ setSentCategory }) => {
|
||||
affectedServices: selectedServices
|
||||
.map((service) => service.replace(/ /g, '_').toLowerCase())
|
||||
.join(';'),
|
||||
browserInformation: detectBrowser(),
|
||||
}
|
||||
|
||||
if (values.projectRef !== 'no-project') {
|
||||
|
||||
@@ -36,7 +36,13 @@ const ActionBar: FC<ActionBarProps> = ({
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-end space-x-3 border-t border-scale-500 px-3 py-4">
|
||||
<Button size="small" type="default" htmlType="button" onClick={closePanel}>
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={closePanel}
|
||||
disabled={isRunning || loading}
|
||||
>
|
||||
{backButtonLabel}
|
||||
</Button>
|
||||
|
||||
@@ -47,8 +53,8 @@ const ActionBar: FC<ActionBarProps> = ({
|
||||
<Button
|
||||
size="small"
|
||||
onClick={onSelectApply}
|
||||
disabled={disableApply || isRunning}
|
||||
loading={isRunning}
|
||||
disabled={disableApply || isRunning || loading}
|
||||
loading={isRunning || loading}
|
||||
>
|
||||
{applyButtonLabel}
|
||||
</Button>
|
||||
|
||||
+1
@@ -249,6 +249,7 @@ const ColumnEditor: FC<Props> = ({
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<ColumnType
|
||||
showRecommendation
|
||||
value={columnFields?.format ?? ''}
|
||||
layout="vertical"
|
||||
enumTypes={enumTypes}
|
||||
|
||||
+3
-1
@@ -31,6 +31,7 @@ interface Props {
|
||||
disabled?: boolean
|
||||
showLabel?: boolean
|
||||
description?: ReactNode
|
||||
showRecommendation?: boolean
|
||||
onOptionSelect: (value: string) => void
|
||||
}
|
||||
|
||||
@@ -44,6 +45,7 @@ const ColumnType: FC<Props> = ({
|
||||
disabled = false,
|
||||
showLabel = true,
|
||||
description,
|
||||
showRecommendation = false,
|
||||
onOptionSelect = () => {},
|
||||
}) => {
|
||||
// @ts-ignore
|
||||
@@ -162,7 +164,7 @@ const ColumnType: FC<Props> = ({
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
{recommendation !== undefined && (
|
||||
{showRecommendation && recommendation !== undefined && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
|
||||
+25
-6
@@ -12,21 +12,33 @@ type JsonEditProps = {
|
||||
column: string
|
||||
jsonString: string
|
||||
visible: boolean
|
||||
backButtonLabel?: string
|
||||
applyButtonLabel?: string
|
||||
closePanel: () => void
|
||||
onSaveJSON: (value: string | number) => void
|
||||
}
|
||||
|
||||
const JsonEdit: FC<JsonEditProps> = ({ column, jsonString, visible, closePanel, onSaveJSON }) => {
|
||||
const JsonEdit: FC<JsonEditProps> = ({
|
||||
column,
|
||||
jsonString,
|
||||
visible,
|
||||
backButtonLabel,
|
||||
applyButtonLabel,
|
||||
closePanel,
|
||||
onSaveJSON,
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const [view, setView] = useState<'edit' | 'view'>('edit')
|
||||
const [jsonStr, setJsonStr] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const temp = prettifyJSON(jsonString)
|
||||
setJsonStr(temp)
|
||||
}, [jsonString])
|
||||
if (visible) {
|
||||
const temp = prettifyJSON(jsonString)
|
||||
setJsonStr(temp)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
function validateJSON(resolve: () => void) {
|
||||
const validateJSON = async (resolve: () => void) => {
|
||||
try {
|
||||
const minifiedJSON = minifyJSON(jsonStr)
|
||||
if (onSaveJSON) onSaveJSON(minifiedJSON)
|
||||
@@ -72,7 +84,14 @@ const JsonEdit: FC<JsonEditProps> = ({ column, jsonString, visible, closePanel,
|
||||
}
|
||||
visible={visible}
|
||||
onCancel={closePanel}
|
||||
customFooter={<ActionBar closePanel={closePanel} applyFunction={validateJSON} />}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
closePanel={closePanel}
|
||||
backButtonLabel={backButtonLabel}
|
||||
applyButtonLabel={applyButtonLabel}
|
||||
applyFunction={validateJSON}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="py-4">
|
||||
<SidePanel.Content>
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
import type { PostgresRelationship } from '@supabase/postgres-meta'
|
||||
|
||||
export interface JsonEditValue {
|
||||
row?: any
|
||||
column: string
|
||||
jsonString: string
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { RowEditor, ColumnEditor, TableEditor } from '.'
|
||||
import { ImportContent } from './TableEditor/TableEditor.types'
|
||||
import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from './SidePanelEditor.types'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import JsonEdit from './RowEditor/JsonEditor/JsonEditor'
|
||||
import { JsonEditValue } from './RowEditor/RowEditor.types'
|
||||
|
||||
interface Props {
|
||||
selectedSchema: string
|
||||
@@ -16,7 +18,8 @@ interface Props {
|
||||
selectedRowToEdit?: Dictionary<any>
|
||||
selectedColumnToEdit?: PostgresColumn
|
||||
selectedTableToEdit?: PostgresTable
|
||||
sidePanelKey?: 'row' | 'column' | 'table'
|
||||
selectedValueForJsonEdit?: JsonEditValue
|
||||
sidePanelKey?: 'row' | 'column' | 'table' | 'json'
|
||||
isDuplicating?: boolean
|
||||
closePanel: () => void
|
||||
onRowCreated?: (row: Dictionary<any>) => void
|
||||
@@ -34,6 +37,7 @@ const SidePanelEditor: FC<Props> = ({
|
||||
selectedRowToEdit,
|
||||
selectedColumnToEdit,
|
||||
selectedTableToEdit,
|
||||
selectedValueForJsonEdit,
|
||||
sidePanelKey,
|
||||
isDuplicating = false,
|
||||
closePanel,
|
||||
@@ -113,6 +117,22 @@ const SidePanelEditor: FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const onSaveJSON = async (value: string | number) => {
|
||||
if (selectedTable === undefined || selectedValueForJsonEdit === undefined) return
|
||||
|
||||
try {
|
||||
const { row, column } = selectedValueForJsonEdit
|
||||
const payload = { [column]: JSON.parse(value as any) }
|
||||
const identifiers = {} as Dictionary<any>
|
||||
selectedTable.primary_keys.forEach((column) => (identifiers[column.name] = row![column.name]))
|
||||
|
||||
const isNewRecord = false
|
||||
const configuration = { identifiers, rowIdx: row.idx }
|
||||
|
||||
saveRow(payload, isNewRecord, configuration, () => {})
|
||||
} catch (error: any) {}
|
||||
}
|
||||
|
||||
const saveColumn = async (
|
||||
payload: CreateColumnPayload | UpdateColumnPayload,
|
||||
foreignKey: Partial<PostgresRelationship> | undefined,
|
||||
@@ -287,6 +307,15 @@ const SidePanelEditor: FC<Props> = ({
|
||||
saveChanges={saveTable}
|
||||
updateEditorDirty={() => setIsEdited(true)}
|
||||
/>
|
||||
<JsonEdit
|
||||
visible={sidePanelKey === 'json'}
|
||||
column={selectedValueForJsonEdit?.column ?? ''}
|
||||
jsonString={selectedValueForJsonEdit?.jsonString ?? ''}
|
||||
backButtonLabel="Cancel"
|
||||
applyButtonLabel="Save changes"
|
||||
closePanel={onClosePanel}
|
||||
onSaveJSON={onSaveJSON}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
header="Confirm to close"
|
||||
|
||||
@@ -11,6 +11,7 @@ import GridHeaderActions from './GridHeaderActions'
|
||||
import NotFoundState from './NotFoundState'
|
||||
import SidePanelEditor from './SidePanelEditor'
|
||||
import { Dictionary, parseSupaTable, SupabaseGrid, SupabaseGridRef } from 'components/grid'
|
||||
import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types'
|
||||
|
||||
interface Props {
|
||||
/** Theme for the editor */
|
||||
@@ -20,19 +21,21 @@ interface Props {
|
||||
selectedTable: any // PostgresTable | SchemaView
|
||||
|
||||
/** Determines what side panel editor to show */
|
||||
sidePanelKey?: 'row' | 'column' | 'table'
|
||||
sidePanelKey?: 'row' | 'column' | 'table' | 'json'
|
||||
/** Toggles if we're duplicating a table */
|
||||
isDuplicating: boolean
|
||||
/** Selected entities if we're editing a row, column or table */
|
||||
selectedRowToEdit?: Dictionary<any>
|
||||
selectedColumnToEdit?: PostgresColumn
|
||||
selectedTableToEdit?: PostgresTable
|
||||
selectedValueForJsonEdit?: JsonEditValue
|
||||
|
||||
onAddRow: () => void
|
||||
onEditRow: (row: Dictionary<any>) => void
|
||||
onAddColumn: () => void
|
||||
onEditColumn: (column: PostgresColumn) => void
|
||||
onDeleteColumn: (column: PostgresColumn) => void
|
||||
onExpandJSONEditor: (column: string, row: any) => void
|
||||
onClosePanel: () => void
|
||||
}
|
||||
|
||||
@@ -46,12 +49,14 @@ const TableGridEditor: FC<Props> = ({
|
||||
selectedRowToEdit,
|
||||
selectedColumnToEdit,
|
||||
selectedTableToEdit,
|
||||
selectedValueForJsonEdit,
|
||||
|
||||
onAddRow = () => {},
|
||||
onEditRow = () => {},
|
||||
onAddColumn = () => {},
|
||||
onEditColumn = () => {},
|
||||
onDeleteColumn = () => {},
|
||||
onExpandJSONEditor = () => {},
|
||||
onClosePanel = () => {},
|
||||
}) => {
|
||||
const { meta, ui, vault } = useStore()
|
||||
@@ -59,8 +64,8 @@ const TableGridEditor: FC<Props> = ({
|
||||
const gridRef = useRef<SupabaseGridRef>(null)
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
const [encryptedColumns, setEncryptedColumns] = useState([])
|
||||
const isVaultEnabled = useFlag('vaultExtension')
|
||||
const [encryptedColumns, setEncryptedColumns] = useState([])
|
||||
|
||||
const getEncryptedColumns = async (table: any) => {
|
||||
const columns = await vault.listEncryptedColumns(table.schema, table.name)
|
||||
@@ -178,6 +183,7 @@ const TableGridEditor: FC<Props> = ({
|
||||
onEditRow={onEditRow}
|
||||
onError={onError}
|
||||
onSqlQuery={onSqlQuery}
|
||||
onExpandJSONEditor={onExpandJSONEditor}
|
||||
/>
|
||||
{!isUndefined(selectedSchema) && (
|
||||
<SidePanelEditor
|
||||
@@ -187,6 +193,7 @@ const TableGridEditor: FC<Props> = ({
|
||||
selectedRowToEdit={selectedRowToEdit}
|
||||
selectedColumnToEdit={selectedColumnToEdit}
|
||||
selectedTableToEdit={selectedTableToEdit}
|
||||
selectedValueForJsonEdit={selectedValueForJsonEdit}
|
||||
sidePanelKey={sidePanelKey}
|
||||
onRowCreated={onRowCreated}
|
||||
onRowUpdated={onRowUpdated}
|
||||
|
||||
@@ -5,9 +5,10 @@ import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { auth, STORAGE_KEY } from 'lib/gotrue'
|
||||
import { useStore, withAuth, useFlag } from 'hooks'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import WithSidebar from './WithSidebar'
|
||||
import { SidebarSection } from './AccountLayout.types'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
@@ -21,6 +22,7 @@ interface Props {
|
||||
const AccountLayout: FC<Props> = ({ children, title, breadcrumbs }) => {
|
||||
const router = useRouter()
|
||||
const { app, ui } = useStore()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
@@ -28,7 +30,8 @@ const AccountLayout: FC<Props> = ({ children, title, breadcrumbs }) => {
|
||||
const onClickLogout = async () => {
|
||||
await auth.signOut()
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
window.location.href = '/sign-in'
|
||||
await router.push('/sign-in')
|
||||
await queryClient.resetQueries()
|
||||
}
|
||||
|
||||
const organizationsLinks = app.organizations
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import Link from 'next/link'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { IS_PLATFORM, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { useStore, useProjectUsage } from 'hooks'
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import BreadcrumbsView from './BreadcrumbsView'
|
||||
import OrgDropdown from './OrgDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
@@ -11,15 +10,15 @@ import FeedbackDropdown from './FeedbackDropdown'
|
||||
import HelpPopover from './HelpPopover'
|
||||
import NotificationsPopover from './NotificationsPopover'
|
||||
import { getResourcesExceededLimits } from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
|
||||
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
|
||||
const { ui } = useStore()
|
||||
const { selectedOrganization, selectedProject } = ui
|
||||
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const { usage } = useProjectUsage(ref as string)
|
||||
const { data: usage } = useProjectUsageQuery({ projectRef })
|
||||
const resourcesExceededLimits = getResourcesExceededLimits(usage)
|
||||
const projectHasNoLimits =
|
||||
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.PAYG ||
|
||||
@@ -67,7 +66,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
{/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */}
|
||||
{/* {showOverUsageBadge && (
|
||||
<div className="ml-2">
|
||||
<Link href={`/project/${ref}/settings/billing/subscription`}>
|
||||
<Link href={`/project/${projectRef}/settings/billing/subscription`}>
|
||||
<a>
|
||||
<Badge color="red">Project has exceeded usage limits </Badge>
|
||||
</a>
|
||||
|
||||
+12
-10
@@ -1,10 +1,11 @@
|
||||
import dayjs from 'dayjs'
|
||||
import useSWR from 'swr'
|
||||
import { FC, useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Notification, NotificationStatus } from '@supabase/shared-types/out/notifications'
|
||||
|
||||
import { useStore, useNotifications } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { notificationKeys } from 'data/notifications/keys'
|
||||
import { Project } from 'types'
|
||||
import { formatNotificationCTAText, formatNotificationText } from './NotificationRows.utils'
|
||||
import NotificationActions from './NotificationActions'
|
||||
@@ -28,7 +29,7 @@ const NotificationRow: FC<Props> = ({
|
||||
onSelectFinalizeMigration,
|
||||
}) => {
|
||||
const { app, ui } = useStore()
|
||||
const { refresh } = useNotifications()
|
||||
const queryClient = useQueryClient()
|
||||
const [dismissing, setDismissing] = useState(false)
|
||||
const [project] = app.projects.list((project: Project) => project.id === notification.project_id)
|
||||
|
||||
@@ -36,14 +37,15 @@ const NotificationRow: FC<Props> = ({
|
||||
const changelogLink = (notification.data as any).changelog_link
|
||||
const availableActions = notification.meta?.actions_available ?? []
|
||||
|
||||
// [Joshen TODO] This should be removed after 5th November when the migration notifications
|
||||
// [Joshen TODO] This should be removed after the env of Feb when the migration notifications
|
||||
// have been removed, double check with Qiao before removing.
|
||||
// Relevant PR: https://github.com/supabase/supabase/pull/9229
|
||||
const { data: ownerReassignStatus } = useSWR(
|
||||
(notification.data as any).upgrade_type === 'schema-migration'
|
||||
? `${API_URL}/database/${project.ref}/owner-reassign`
|
||||
: null,
|
||||
get
|
||||
const { data: ownerReassignStatus } = useQuery(
|
||||
['projects', project.ref, 'owner-reassign'],
|
||||
({ signal }) => get(`${API_URL}/database/${project.ref}/owner-reassign`, { signal }),
|
||||
{
|
||||
enabled: (notification.data as any).upgrade_type === 'schema-migration',
|
||||
}
|
||||
)
|
||||
|
||||
const dismissNotification = async (notificationId: string) => {
|
||||
@@ -58,7 +60,7 @@ const NotificationRow: FC<Props> = ({
|
||||
duration: 4000,
|
||||
})
|
||||
} else {
|
||||
refresh()
|
||||
await queryClient.invalidateQueries(notificationKeys.list())
|
||||
}
|
||||
setDismissing(false)
|
||||
}
|
||||
|
||||
@@ -9,18 +9,19 @@ import {
|
||||
} from '@supabase/shared-types/out/notifications'
|
||||
|
||||
import { Project } from 'types'
|
||||
import { useNotifications, useStore } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { delete_, patch, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import NotificationRow from './NotificationRow'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import { useNotificationsQuery } from 'data/notifications/notifications-query'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const NotificationsPopover: FC<Props> = () => {
|
||||
const router = useRouter()
|
||||
const { app, meta, ui } = useStore()
|
||||
const { notifications, refresh } = useNotifications()
|
||||
const { data: notifications, refetch } = useNotificationsQuery()
|
||||
|
||||
const [projectToRestart, setProjectToRestart] = useState<Project>()
|
||||
const [projectToApplyMigration, setProjectToApplyMigration] = useState<Project>()
|
||||
@@ -36,6 +37,7 @@ const NotificationsPopover: FC<Props> = () => {
|
||||
)
|
||||
|
||||
const onOpenChange = async (open: boolean) => {
|
||||
// TODO(alaister): move this to a mutation
|
||||
if (!open) {
|
||||
// Mark notifications as seen
|
||||
const notificationIds = notifications
|
||||
@@ -44,7 +46,7 @@ const NotificationsPopover: FC<Props> = () => {
|
||||
if (notificationIds.length > 0) {
|
||||
const { error } = await patch(`${API_URL}/notifications`, { ids: notificationIds })
|
||||
if (error) console.error('Failed to update notifications', error)
|
||||
refresh()
|
||||
refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ const ProjectLayout = ({
|
||||
|
||||
export const ProjectLayoutWithAuth = withAuth(observer(ProjectLayout))
|
||||
|
||||
export default ProjectLayout
|
||||
export default observer(ProjectLayout)
|
||||
|
||||
interface MenuBarWrapperProps {
|
||||
isLoading: boolean
|
||||
|
||||
@@ -4,11 +4,12 @@ import { Modal, Button, IconPauseCircle } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { Project } from 'types'
|
||||
import { checkPermissions, useStore, useFlag, useFreeProjectLimitCheck } from 'hooks'
|
||||
import { checkPermissions, useStore, useFlag } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { DeleteProjectButton } from 'components/interfaces/Settings/General'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
@@ -19,7 +20,7 @@ const ProjectPausedState: FC<Props> = ({ project }) => {
|
||||
const orgSlug = ui.selectedOrganization?.slug
|
||||
|
||||
const kpsEnabled = useFlag('initWithKps')
|
||||
const { membersExceededLimit } = useFreeProjectLimitCheck(orgSlug)
|
||||
const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug: orgSlug })
|
||||
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
|
||||
|
||||
const [showConfirmRestore, setShowConfirmRestore] = useState(false)
|
||||
@@ -49,7 +50,7 @@ const ProjectPausedState: FC<Props> = ({ project }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="mx-auto mb-16 w-full max-w-7xl">
|
||||
<div className="w-full mx-auto mb-16 max-w-7xl">
|
||||
<div className="mx-6 flex h-[500px] items-center justify-center rounded border border-scale-400 bg-scale-300 p-8">
|
||||
<div className="grid w-[420px] gap-4">
|
||||
<div className="mx-auto flex max-w-[300px] items-center justify-center space-x-4 lg:space-x-8">
|
||||
@@ -112,14 +113,14 @@ const ProjectPausedState: FC<Props> = ({ project }) => {
|
||||
header="Your organization has members who have exceeded their free project limits"
|
||||
onCancel={() => setShowFreeProjectLimitWarning(false)}
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="py-4 space-y-4">
|
||||
<Modal.Content>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-scale-1100">
|
||||
The following members have reached their maximum limits for the number of active
|
||||
free tier projects within organizations where they are an administrator or owner:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 text-sm text-scale-1100">
|
||||
<ul className="pl-5 text-sm list-disc text-scale-1100">
|
||||
{(membersExceededLimit || []).map((member, idx: number) => (
|
||||
<li key={`member-${idx}`}>
|
||||
{member.username || member.primary_email} (Limit: {member.free_project_limit}{' '}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PropsWithChildren } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import BaseLayout from '..'
|
||||
import ProjectLayout from '../ProjectLayout/ProjectLayout'
|
||||
import ProductMenu from 'components/ui/ProductMenu'
|
||||
import { generateReportsMenu } from './ReportsMenu.utils'
|
||||
|
||||
@@ -18,7 +18,7 @@ const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps
|
||||
const page = router.pathname.split('/')[4] || ''
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
<ProjectLayout
|
||||
title={title}
|
||||
product="Reports"
|
||||
productMenu={<ProductMenu page={page} menu={generateReportsMenu(project)} />}
|
||||
@@ -26,7 +26,7 @@ const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</BaseLayout>
|
||||
</ProjectLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { usePushNext } from 'hooks/misc/useAutoAuthRedirect'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
@@ -8,7 +9,6 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { PropsWithChildren, useEffect, useState } from 'react'
|
||||
import { tweets } from 'shared-data'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { Button, IconFileText } from 'ui'
|
||||
|
||||
type SignInLayoutProps = {
|
||||
@@ -27,7 +27,7 @@ const SignInLayout = ({
|
||||
}: PropsWithChildren<SignInLayoutProps>) => {
|
||||
const pushNext = usePushNext()
|
||||
const { ui } = useStore()
|
||||
const { cache } = useSWRConfig()
|
||||
const queryClient = useQueryClient()
|
||||
const { theme } = ui
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,9 +54,7 @@ const SignInLayout = ({
|
||||
} = await auth.getSession()
|
||||
|
||||
if (session) {
|
||||
// .clear() does actually exist on the cache object, but it's not in the types 🤦🏻
|
||||
// @ts-ignore
|
||||
cache.clear()
|
||||
await queryClient.resetQueries()
|
||||
|
||||
await pushNext()
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useRouter } from 'next/router'
|
||||
import { find, filter, get as _get } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { AutoApiService, useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { useParams, useStore, withAuth } from 'hooks'
|
||||
import { AutoApiService, useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import BaseLayout from 'components/layouts'
|
||||
import ProjectLayout from '../ProjectLayout/ProjectLayout'
|
||||
import StorageMenu from './StorageMenu'
|
||||
@@ -23,7 +23,7 @@ interface Props {
|
||||
const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
const { ui, meta } = useStore()
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
@@ -40,7 +40,7 @@ const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
buckets,
|
||||
} = storageExplorerStore || {}
|
||||
|
||||
const { data: settings, isLoading } = useProjectSettingsQuery({ projectRef: ref as string })
|
||||
const { data: settings, isLoading } = useProjectApiQuery({ projectRef })
|
||||
const apiService = settings?.autoApiService
|
||||
const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role')
|
||||
const canAccessStorage = !isLoading && apiService && serviceKey
|
||||
@@ -53,7 +53,7 @@ const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
if (apiService.endpoint) {
|
||||
if (serviceKey) {
|
||||
storageExplorerStore.initStore(
|
||||
ref,
|
||||
projectRef,
|
||||
apiService.endpoint,
|
||||
apiService.serviceApiKey,
|
||||
apiService.protocol
|
||||
@@ -72,7 +72,7 @@ const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
|
||||
const onSelectCreateBucket = async (bucketName: string, isPublic: boolean) => {
|
||||
const bucket = await createBucket(bucketName, isPublic)
|
||||
if (bucket !== undefined) router.push(`/project/${ref}/storage/buckets/${bucket.name}`)
|
||||
if (bucket !== undefined) router.push(`/project/${projectRef}/storage/buckets/${bucket.name}`)
|
||||
}
|
||||
|
||||
const onSelectDeleteBucket = async (bucket: any) => {
|
||||
|
||||
@@ -234,14 +234,20 @@ const ResultsDropdown = observer(() => {
|
||||
|
||||
function onDownloadCSV() {
|
||||
csvRef.current?.link.click()
|
||||
Telemetry.sendEvent('sql_editor', 'sql_download_csv', '')
|
||||
Telemetry.sendEvent(
|
||||
{ category: 'sql_editor', action: 'sql_download_csv', label: '' },
|
||||
ui.googleAnalyticsProps
|
||||
)
|
||||
}
|
||||
|
||||
function onCopyAsMarkdown() {
|
||||
if (navigator) {
|
||||
copyToClipboard(sqlEditorStore.activeTab.markdownData, () => {
|
||||
ui.setNotification({ category: 'success', message: 'Copied results to clipboard' })
|
||||
Telemetry.sendEvent('sql_editor', 'sql_copy_as_markdown', '')
|
||||
Telemetry.sendEvent(
|
||||
{ category: 'sql_editor', action: 'sql_copy_as_markdown', label: '' },
|
||||
ui.googleAnalyticsProps
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,14 @@ const TabWelcome = observer(() => {
|
||||
sql={x.sql}
|
||||
onClick={(sql, title) => {
|
||||
handleNewQuery({ sql, name: title })
|
||||
Telemetry.sendEvent('scripts', 'script_clicked', x.title)
|
||||
Telemetry.sendEvent(
|
||||
{
|
||||
category: 'scripts',
|
||||
action: 'script_clicked',
|
||||
label: x.title,
|
||||
},
|
||||
ui.googleAnalyticsProps
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -64,7 +71,14 @@ const TabWelcome = observer(() => {
|
||||
sql={x.sql}
|
||||
onClick={(sql, title) => {
|
||||
handleNewQuery({ sql, name: title })
|
||||
Telemetry.sendEvent('quickstart', 'quickstart_clicked', x.title)
|
||||
Telemetry.sendEvent(
|
||||
{
|
||||
category: 'quickstart',
|
||||
action: 'quickstart_clicked',
|
||||
label: x.title,
|
||||
},
|
||||
ui.googleAnalyticsProps
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import useSWR from 'swr'
|
||||
import { FC, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Button, Form, IconClock, Input, Listbox } from 'ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { patch, get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
|
||||
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { convertFromBytes, convertToBytes } from './StorageSettings.utils'
|
||||
import { StorageSizeUnits, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from './StorageSettings.constants'
|
||||
|
||||
const StorageSettings: FC<any> = ({ projectRef }) => {
|
||||
const { data, error } = useSWR(`${API_URL}/projects/${projectRef}/config/storage`, get)
|
||||
export type StorageSettingsProps = {
|
||||
projectRef: string | undefined
|
||||
}
|
||||
|
||||
const StorageSettings = ({ projectRef }: StorageSettingsProps) => {
|
||||
const { data, error } = useProjectStorageConfigQuery({ projectRef })
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<p className="text-sm">Error loading storage settings</p>
|
||||
</div>
|
||||
)
|
||||
@@ -22,7 +25,7 @@ const StorageSettings: FC<any> = ({ projectRef }) => {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<p className="text-sm">Loading...</p>
|
||||
</div>
|
||||
)
|
||||
@@ -58,6 +61,8 @@ const StorageConfig = ({ config, projectRef }: any) => {
|
||||
return errors
|
||||
}
|
||||
|
||||
const { mutateAsync: updateStorageConfig } = useProjectStorageConfigUpdateUpdateMutation()
|
||||
|
||||
const onSubmit = async (values: any) => {
|
||||
const errors = onValidate(values)
|
||||
|
||||
@@ -67,20 +72,23 @@ const StorageConfig = ({ config, projectRef }: any) => {
|
||||
message: `Upload file size limit must be up to 5GB (${formattedMaxSizeBytes})`,
|
||||
})
|
||||
} else {
|
||||
const payload = { fileSizeLimit: convertToBytes(values.fileSizeLimit, selectedUnit) }
|
||||
const res = await patch(`${API_URL}/projects/${projectRef}/config/storage`, payload)
|
||||
if (res?.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update storage settings: ${res.error.message}`,
|
||||
try {
|
||||
const res = await updateStorageConfig({
|
||||
projectRef,
|
||||
fileSizeLimit: convertToBytes(values.fileSizeLimit, selectedUnit),
|
||||
})
|
||||
} else {
|
||||
|
||||
const updatedValue = convertFromBytes(res.fileSizeLimit)
|
||||
initialValues = {
|
||||
fileSizeLimit: updatedValue.value,
|
||||
unformattedFileSizeLimit: res.fileSizeLimit,
|
||||
}
|
||||
ui.setNotification({ category: 'success', message: 'Successfully updated settings' })
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update storage settings: ${error?.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,12 +127,12 @@ const StorageConfig = ({ config, projectRef }: any) => {
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex flex-col gap-0 divide-y divide-scale-400">
|
||||
<div className="block grid grid-cols-12 gap-6 px-8 py-8 lg:gap-12">
|
||||
<div className="relative col-span-12 flex flex-col gap-6 lg:col-span-4">
|
||||
<div className="grid grid-cols-12 gap-6 px-8 py-8 lg:gap-12">
|
||||
<div className="relative flex flex-col col-span-12 gap-6 lg:col-span-4">
|
||||
<p className="text-sm">Upload file size limit</p>
|
||||
</div>
|
||||
<div className="relative col-span-12 flex flex-col gap-x-6 gap-y-2 lg:col-span-8">
|
||||
<div className="col-span-12 grid grid-cols-12 gap-2">
|
||||
<div className="relative flex flex-col col-span-12 gap-x-6 gap-y-2 lg:col-span-8">
|
||||
<div className="grid grid-cols-12 col-span-12 gap-2">
|
||||
<div className="col-span-8">
|
||||
<Input
|
||||
id="fileSizeLimit"
|
||||
@@ -181,8 +189,8 @@ const StorageConfig = ({ config, projectRef }: any) => {
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-scale-400" />
|
||||
<div className="flex justify-between py-4 px-8">
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<div className="flex justify-between px-8 py-4">
|
||||
<div className="flex items-center justify-end w-full gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="default"
|
||||
|
||||
@@ -2,9 +2,10 @@ import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import { Alert, Button } from 'ui'
|
||||
|
||||
import { useStore, useProjectUsage } from 'hooks'
|
||||
import { useParams } from 'hooks'
|
||||
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { getResourcesApproachingLimits, getResourcesExceededLimits } from './OveragesBanner.utils'
|
||||
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
|
||||
interface Props {
|
||||
tier: string
|
||||
@@ -14,10 +15,8 @@ interface Props {
|
||||
// Banner will not be shown for PAYG or Enterprise projects
|
||||
|
||||
const OveragesBanner: FC<Props> = ({ tier, minimal }) => {
|
||||
const { ui } = useStore()
|
||||
const ref = ui.selectedProject?.ref
|
||||
|
||||
const { usage, error, isLoading } = useProjectUsage(ref as string)
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef })
|
||||
|
||||
const resourcesApproachingLimits = getResourcesApproachingLimits(usage)
|
||||
const isApproachingUsageLimits = resourcesApproachingLimits.length > 0
|
||||
@@ -86,7 +85,7 @@ const OveragesBanner: FC<Props> = ({ tier, minimal }) => {
|
||||
actions={
|
||||
minimal ? (
|
||||
<div className="flex h-full items-center">
|
||||
<Link href={`/project/${ref}/settings/billing/subscription`}>
|
||||
<Link href={`/project/${projectRef}/settings/billing/subscription`}>
|
||||
<a>
|
||||
<Button type="default">Explore usage details</Button>
|
||||
</a>
|
||||
|
||||
@@ -4,35 +4,34 @@ import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { User } from 'types'
|
||||
|
||||
const PageTelemetry: FC = ({ children }) => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
const { profile } = ui
|
||||
|
||||
useEffect(() => {
|
||||
function handleRouteChange() {
|
||||
handlePageTelemetry(profile)
|
||||
function handleRouteChange(url: string) {
|
||||
handlePageTelemetry(url)
|
||||
}
|
||||
|
||||
// Listen for page changes after a navigation or when the query changes
|
||||
router.events.on('routeChangeComplete', handleRouteChange)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', handleRouteChange)
|
||||
}
|
||||
}, [router.events, profile])
|
||||
}, [router.events])
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Send page telemetry on first page load
|
||||
* if there asPath is defined, then this isn't needed
|
||||
* if the route is not ready. Don't need to send it will be picked up by router.event above
|
||||
*/
|
||||
if (router.route === '/' && router.asPath === '/') {
|
||||
handlePageTelemetry(profile)
|
||||
if (router.isReady) {
|
||||
handlePageTelemetry(router.route)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handlePageTelemetry = (profile?: User) => {
|
||||
const handlePageTelemetry = async (route?: string) => {
|
||||
if (IS_PLATFORM) {
|
||||
/**
|
||||
* Get referrer from browser
|
||||
@@ -48,6 +47,11 @@ const PageTelemetry: FC = ({ children }) => {
|
||||
post(`${API_URL}/telemetry/page`, {
|
||||
referrer: referrer,
|
||||
title: document.title,
|
||||
route,
|
||||
ga: {
|
||||
screen_resolution: ui.googleAnalyticsProps?.screenResolution,
|
||||
language: ui.googleAnalyticsProps?.language,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { IconAlertCircle, IconLoader, Input } from 'ui'
|
||||
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
|
||||
import { IconAlertCircle, IconLoader, Input } from 'ui'
|
||||
|
||||
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
|
||||
import { checkPermissions, useJwtSecretUpdateStatus, useProjectSettings } from 'hooks'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
|
||||
import { checkPermissions, useParams } from 'hooks'
|
||||
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
|
||||
|
||||
const DisplayApiSettings = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const {
|
||||
services,
|
||||
data: settings,
|
||||
isError: isProjectSettingsError,
|
||||
isLoading: isProjectSettingsLoading,
|
||||
} = useProjectSettings(ref as string | undefined)
|
||||
} = useProjectSettingsQuery({ projectRef })
|
||||
const {
|
||||
jwtSecretUpdateStatus,
|
||||
data,
|
||||
isError: isJwtSecretUpdateStatusError,
|
||||
isLoading: isJwtSecretUpdateStatusLoading,
|
||||
}: any = useJwtSecretUpdateStatus(ref)
|
||||
} = useJwtSecretUpdatingStatusQuery({ projectRef })
|
||||
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
|
||||
|
||||
const canReadAPIKeys = checkPermissions(PermissionAction.READ, 'service_api_keys')
|
||||
|
||||
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 apiService = (settings?.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
|
||||
@@ -46,14 +49,14 @@ const DisplayApiSettings = () => {
|
||||
}
|
||||
>
|
||||
{isProjectSettingsError || isJwtSecretUpdateStatusError ? (
|
||||
<div className="flex items-center justify-center space-x-2 py-8">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">
|
||||
{isProjectSettingsError ? 'Failed to retrieve API keys' : 'Failed to update JWT secret'}
|
||||
</p>
|
||||
</div>
|
||||
) : isApiKeysEmpty || isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2 py-8">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">
|
||||
{isProjectSettingsLoading || isApiKeysEmpty
|
||||
|
||||
@@ -1,38 +1,43 @@
|
||||
import { FC } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Input, IconAlertCircle, IconLoader } from 'ui'
|
||||
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
|
||||
import { useJwtSecretUpdateStatus, useProjectSettings } from 'hooks'
|
||||
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useParams } from 'hooks'
|
||||
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
|
||||
import { FC } from 'react'
|
||||
import { IconAlertCircle, IconLoader, Input } from 'ui'
|
||||
|
||||
const DisplayConfigSettings = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref: projectRef } = useParams()
|
||||
const {
|
||||
project,
|
||||
services,
|
||||
data: settings,
|
||||
isLoading: isProjectSettingsLoading,
|
||||
isError: isProjectSettingsError,
|
||||
} = useProjectSettings(ref as string | undefined)
|
||||
} = useProjectSettingsQuery({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
const {
|
||||
data,
|
||||
isError: isJwtSecretUpdateStatusError,
|
||||
isLoading: isJwtSecretUpdateStatusLoading,
|
||||
jwtSecretUpdateStatus,
|
||||
}: any = useJwtSecretUpdateStatus(ref)
|
||||
} = useJwtSecretUpdatingStatusQuery({ projectRef })
|
||||
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
|
||||
|
||||
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 ?? {}
|
||||
const apiUrl = `${apiConfig.protocol ?? 'https'}://${apiConfig.endpoint ?? '-'}`
|
||||
const jwtSecret = settings?.project?.jwt_secret ?? ''
|
||||
const apiService = (settings?.services ?? []).find(
|
||||
(x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID
|
||||
)
|
||||
const apiConfig = apiService?.app_config
|
||||
const apiUrl = `${apiConfig?.protocol ?? 'https'}://${apiConfig?.endpoint ?? '-'}`
|
||||
|
||||
return (
|
||||
<ConfigContentWrapper>
|
||||
{isProjectSettingsError || isJwtSecretUpdateStatusError ? (
|
||||
<div className="flex items-center justify-center space-x-2 py-8">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">
|
||||
{isProjectSettingsError
|
||||
@@ -41,7 +46,7 @@ const DisplayConfigSettings = () => {
|
||||
</p>
|
||||
</div>
|
||||
) : isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2 py-8">
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">
|
||||
{isProjectSettingsLoading ? 'Retrieving API keys' : 'JWT secret is being updated'}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
const ShimmeringLoader = ({ className = '' }) => {
|
||||
return <div className={`shimmering-loader rounded py-3 mx-1 ${className}`} />
|
||||
const ANIMATION_DELAY = 150
|
||||
|
||||
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = 150 }) => {
|
||||
return (
|
||||
<div
|
||||
className={`shimmering-loader rounded py-3 mx-1 ${className}`}
|
||||
style={{
|
||||
animationFillMode: 'backwards',
|
||||
animationDelay: `${delayIndex * animationDelay}ms`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShimmeringLoader
|
||||
@@ -56,5 +56,5 @@ export const useResourcePrefetch = ({ projectRef, id }: ResourceVariables) => {
|
||||
getResource({ projectRef, id }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}, [projectRef, id])
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { AccessToken } from './access-tokens-query'
|
||||
import { accessTokenKeys } from './keys'
|
||||
|
||||
export type AccessTokenCreateVariables = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type NewAccessToken = AccessToken & { token: string }
|
||||
|
||||
export async function createAccessToken({ name }: AccessTokenCreateVariables) {
|
||||
const response = await post(`${API_URL}/profile/access-tokens`, { name })
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as NewAccessToken
|
||||
}
|
||||
|
||||
type AccessTokenCreateData = Awaited<ReturnType<typeof createAccessToken>>
|
||||
|
||||
export const useAccessTokenCreateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<AccessTokenCreateData, unknown, AccessTokenCreateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<AccessTokenCreateData, unknown, AccessTokenCreateVariables>(
|
||||
(vars) => createAccessToken(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
await queryClient.invalidateQueries(accessTokenKeys.list())
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { delete_ } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { accessTokenKeys } from './keys'
|
||||
|
||||
export type AccessTokenDeleteVariables = {
|
||||
id: number
|
||||
}
|
||||
|
||||
export async function deleteAccessToken({ id }: AccessTokenDeleteVariables) {
|
||||
const response = await delete_(`${API_URL}/profile/access-tokens/${id}`)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type AccessTokenDeleteData = Awaited<ReturnType<typeof deleteAccessToken>>
|
||||
|
||||
export const useAccessTokenDeleteMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<AccessTokenDeleteData, unknown, AccessTokenDeleteVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<AccessTokenDeleteData, unknown, AccessTokenDeleteVariables>(
|
||||
(vars) => deleteAccessToken(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
await queryClient.invalidateQueries(accessTokenKeys.list())
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { accessTokenKeys } from './keys'
|
||||
|
||||
export type AccessToken = {
|
||||
id: number
|
||||
token_alias: string
|
||||
name: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export type AccessTokensResponse = AccessToken[]
|
||||
|
||||
export async function getAccessTokens(signal?: AbortSignal) {
|
||||
const response = await get(`${API_URL}/profile/access-tokens`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as AccessTokensResponse
|
||||
}
|
||||
|
||||
export type AccessTokensData = Awaited<ReturnType<typeof getAccessTokens>>
|
||||
export type AccessTokensError = unknown
|
||||
|
||||
export const useAccessTokensQuery = <TData = AccessTokensData>({
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<AccessTokensData, AccessTokensError, TData> = {}) =>
|
||||
useQuery<AccessTokensData, AccessTokensError, TData>(
|
||||
accessTokenKeys.list(),
|
||||
({ signal }) => getAccessTokens(signal),
|
||||
options
|
||||
)
|
||||
|
||||
export const useAccessTokensPrefetch = () => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
client.prefetchQuery(accessTokenKeys.list(), ({ signal }) => getAccessTokens(signal))
|
||||
}, [])
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const accessTokenKeys = {
|
||||
list: () => ['access-tokens'] as const,
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
export type FunctionsInvStatsVariables = {
|
||||
projectRef?: string
|
||||
functionId?: string
|
||||
interval?: string
|
||||
}
|
||||
|
||||
export type FunctionsInvStatsResponse = any
|
||||
|
||||
export async function getFunctionsInvStats(
|
||||
{ projectRef, functionId, interval }: FunctionsInvStatsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!functionId) {
|
||||
throw new Error('functionId is required')
|
||||
}
|
||||
if (!interval) {
|
||||
throw new Error('interval is required')
|
||||
}
|
||||
|
||||
const response = await get(
|
||||
`${API_URL}/projects/${projectRef}/analytics/endpoints/functions.inv-stats?interval=${interval}&function_id=${functionId}`,
|
||||
{
|
||||
signal,
|
||||
}
|
||||
)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as FunctionsInvStatsResponse
|
||||
}
|
||||
|
||||
export type FunctionsInvStatsData = Awaited<ReturnType<typeof getFunctionsInvStats>>
|
||||
export type FunctionsInvStatsError = unknown
|
||||
|
||||
export const useFunctionsInvStatsQuery = <TData = FunctionsInvStatsData>(
|
||||
{ projectRef, functionId, interval }: FunctionsInvStatsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<FunctionsInvStatsData, FunctionsInvStatsError, TData> = {}
|
||||
) =>
|
||||
useQuery<FunctionsInvStatsData, FunctionsInvStatsError, TData>(
|
||||
analyticsKeys.functionsInvStats(projectRef, { functionId, interval }),
|
||||
({ signal }) => getFunctionsInvStats({ projectRef, functionId, interval }, signal),
|
||||
{
|
||||
enabled:
|
||||
enabled &&
|
||||
typeof projectRef !== 'undefined' &&
|
||||
typeof functionId !== 'undefined' &&
|
||||
typeof interval !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useFunctionsInvStatsPrefetch = ({
|
||||
projectRef,
|
||||
functionId,
|
||||
interval,
|
||||
}: FunctionsInvStatsVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef && functionId && interval) {
|
||||
client.prefetchQuery(
|
||||
analyticsKeys.functionsInvStats(projectRef, { functionId, interval }),
|
||||
({ signal }) => getFunctionsInvStats({ projectRef, functionId, interval }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef, functionId, interval])
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { LogsEndpointParams } from 'components/interfaces/Settings/Logs'
|
||||
|
||||
export const analyticsKeys = {
|
||||
functionsInvStats: (
|
||||
projectRef: string | undefined,
|
||||
{ interval, functionId }: { functionId: string | undefined; interval: string | undefined }
|
||||
) => ['projects', projectRef, 'functions-inv-stats', { interval, functionId }] as const,
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
export type JwtSecretUpdateVariables = {
|
||||
projectRef: string
|
||||
jwtSecret: string
|
||||
changeTrackingId: string
|
||||
}
|
||||
|
||||
export async function updateJwtSecret({
|
||||
projectRef,
|
||||
jwtSecret,
|
||||
changeTrackingId,
|
||||
}: JwtSecretUpdateVariables) {
|
||||
const response = await patch(`${API_URL}/projects/${projectRef}/config/secrets`, {
|
||||
jwt_secret: jwtSecret,
|
||||
change_tracking_id: changeTrackingId,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type JwtSecretUpdateData = Awaited<ReturnType<typeof updateJwtSecret>>
|
||||
|
||||
export const useJwtSecretUpdateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<JwtSecretUpdateData, unknown, JwtSecretUpdateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<JwtSecretUpdateData, unknown, JwtSecretUpdateVariables>(
|
||||
(vars) => updateJwtSecret(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(configKeys.jwtSecretUpdatingStatus(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
export type JwtSecretUpdatingStatusVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export type JwtSecretUpdatingStatusResponse = {
|
||||
changeTrackingId: string | undefined
|
||||
jwtSecretUpdateError: number | null | undefined
|
||||
jwtSecretUpdateProgress: number | null | undefined
|
||||
jwtSecretUpdateStatus: JwtSecretUpdateStatus | undefined
|
||||
}
|
||||
|
||||
export async function getJwtSecretUpdatingStatus(
|
||||
{ projectRef }: JwtSecretUpdatingStatusVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
const response = await get(`${API_URL}/props/project/${projectRef}/jwt-secret-update-status`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
const meta = response?.jwtSecretUpdateStatus?.meta
|
||||
|
||||
return {
|
||||
changeTrackingId: meta?.change_tracking_id,
|
||||
jwtSecretUpdateError: meta?.error,
|
||||
jwtSecretUpdateProgress: meta?.progress,
|
||||
jwtSecretUpdateStatus: meta?.status,
|
||||
} as JwtSecretUpdatingStatusResponse
|
||||
}
|
||||
|
||||
export type JwtSecretUpdatingStatusData = Awaited<ReturnType<typeof getJwtSecretUpdatingStatus>>
|
||||
export type JwtSecretUpdatingStatusError = unknown
|
||||
|
||||
export const useJwtSecretUpdatingStatusQuery = <TData = JwtSecretUpdatingStatusData>(
|
||||
{ projectRef }: JwtSecretUpdatingStatusVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<JwtSecretUpdatingStatusData, JwtSecretUpdatingStatusError, TData> = {}
|
||||
) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useQuery<JwtSecretUpdatingStatusData, JwtSecretUpdatingStatusError, TData>(
|
||||
configKeys.jwtSecretUpdatingStatus(projectRef),
|
||||
({ signal }) => getJwtSecretUpdatingStatus({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
refetchInterval(data) {
|
||||
if (!data) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { jwtSecretUpdateStatus } = data as unknown as JwtSecretUpdatingStatusResponse
|
||||
|
||||
const interval = jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating ? 1000 : false
|
||||
|
||||
return interval
|
||||
},
|
||||
onSuccess() {
|
||||
client.invalidateQueries(configKeys.postgrest(projectRef))
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const useJwtSecretUpdatingStatusPrefetch = ({
|
||||
projectRef,
|
||||
}: JwtSecretUpdatingStatusVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(configKeys.jwtSecretUpdatingStatus(projectRef), ({ signal }) =>
|
||||
getJwtSecretUpdatingStatus({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
export const configKeys = {
|
||||
settings: (projectRef: string | undefined) => ['projects', projectRef, 'settings'] as const,
|
||||
api: (projectRef: string | undefined) => ['projects', projectRef, 'settings', 'api'] as const,
|
||||
postgrest: (projectRef: string | undefined) => ['projects', projectRef, 'postgrest'] as const,
|
||||
jwtSecretUpdatingStatus: (projectRef: string | undefined) =>
|
||||
['projects', projectRef, 'jwt-secret-updating-status'] as const,
|
||||
storage: (projectRef: string | undefined) => ['projects', projectRef, 'storage'] as const,
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
export type ProjectApiVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export type Project = {
|
||||
id: number
|
||||
name: string
|
||||
ref: string
|
||||
status: string
|
||||
db_host: string
|
||||
db_name: string
|
||||
db_user: string
|
||||
db_schema: string
|
||||
db_port: number
|
||||
db_ssl: boolean
|
||||
services: Service[]
|
||||
}
|
||||
|
||||
export type App = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type AppConfig = {
|
||||
endpoint: string
|
||||
db_schema: string
|
||||
realtime_multitenant_enabled: boolean
|
||||
}
|
||||
|
||||
export type Service = {
|
||||
id: number
|
||||
name: string
|
||||
app_config: AppConfig
|
||||
app: App
|
||||
service_api_keys: ServiceApiKey[]
|
||||
}
|
||||
|
||||
export type ServiceApiKey = {
|
||||
api_key_encrypted: string
|
||||
tags: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type AutoApiService = Service & {
|
||||
protocol: 'https' | 'http'
|
||||
endpoint: string
|
||||
restUrl: string
|
||||
project: Project
|
||||
defaultApiKey: string
|
||||
serviceApiKey: string
|
||||
}
|
||||
|
||||
export type ProjectApiResponse = {
|
||||
project: Project
|
||||
autoApiService: AutoApiService
|
||||
}
|
||||
|
||||
export async function getProjectApi({ projectRef }: ProjectApiVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
const response = await get(`${API_URL}/props/project/${projectRef}/api`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as ProjectApiResponse
|
||||
}
|
||||
|
||||
export type ProjectApiData = Awaited<ReturnType<typeof getProjectApi>>
|
||||
export type ProjectApiError = unknown
|
||||
|
||||
export const useProjectApiQuery = <TData = ProjectApiData>(
|
||||
{ projectRef }: ProjectApiVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<ProjectApiData, ProjectApiError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectApiData, ProjectApiError, TData>(
|
||||
configKeys.api(projectRef),
|
||||
({ signal }) => getProjectApi({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
refetchInterval(data, query) {
|
||||
if (!data) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { autoApiService } = data as unknown as ProjectApiData
|
||||
|
||||
const apiKeys = autoApiService?.service_api_keys ?? []
|
||||
const interval = apiKeys.length === 0 ? 2000 : 0
|
||||
|
||||
return interval
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useProjectApiPrefetch = ({ projectRef }: ProjectApiVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(configKeys.api(projectRef), ({ signal }) =>
|
||||
getProjectApi({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
export type ProjectPostgrestConfigVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export type ProjectPostgrestConfigResponse = {
|
||||
max_rows: number
|
||||
role_claim_key: string
|
||||
db_schema: string
|
||||
db_anon_role: string
|
||||
db_extra_search_path: string
|
||||
jwt_secret: string
|
||||
}
|
||||
|
||||
export async function getProjectPostgrestConfig(
|
||||
{ projectRef }: ProjectPostgrestConfigVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
const response = await get(`${API_URL}/projects/${projectRef}/config/postgrest`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as ProjectPostgrestConfigResponse
|
||||
}
|
||||
|
||||
export type ProjectPostgrestConfigData = Awaited<ReturnType<typeof getProjectPostgrestConfig>>
|
||||
export type ProjectPostgrestConfigError = unknown
|
||||
|
||||
export const useProjectPostgrestConfigQuery = <TData = ProjectPostgrestConfigData>(
|
||||
{ projectRef }: ProjectPostgrestConfigVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectPostgrestConfigData, ProjectPostgrestConfigError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectPostgrestConfigData, ProjectPostgrestConfigError, TData>(
|
||||
configKeys.postgrest(projectRef),
|
||||
({ signal }) => getProjectPostgrestConfig({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useProjectPostgrestConfigPrefetch = ({
|
||||
projectRef,
|
||||
}: ProjectPostgrestConfigVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(configKeys.postgrest(projectRef), ({ signal }) =>
|
||||
getProjectPostgrestConfig({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
export type ProjectPostgrestConfigUpdateVariables = {
|
||||
projectRef: string
|
||||
dbSchema: string
|
||||
maxRows: string
|
||||
dbExtraSearchPath: string
|
||||
}
|
||||
|
||||
export async function updateProjectPostgrestConfig({
|
||||
projectRef,
|
||||
dbSchema,
|
||||
maxRows,
|
||||
dbExtraSearchPath,
|
||||
}: ProjectPostgrestConfigUpdateVariables) {
|
||||
const response = await patch(`${API_URL}/projects/${projectRef}/config/postgrest`, {
|
||||
db_schema: dbSchema,
|
||||
max_rows: maxRows,
|
||||
db_extra_search_path: dbExtraSearchPath,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type ProjectPostgrestConfigUpdateData = Awaited<ReturnType<typeof updateProjectPostgrestConfig>>
|
||||
|
||||
export const useProjectPostgrestConfigUpdateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<
|
||||
ProjectPostgrestConfigUpdateData,
|
||||
unknown,
|
||||
ProjectPostgrestConfigUpdateVariables
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
ProjectPostgrestConfigUpdateData,
|
||||
unknown,
|
||||
ProjectPostgrestConfigUpdateVariables
|
||||
>((vars) => updateProjectPostgrestConfig(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
queryClient.invalidateQueries(configKeys.postgrest(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { API_URL, DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
@@ -13,24 +13,17 @@ export type Project = {
|
||||
name: string
|
||||
ref: string
|
||||
status: string
|
||||
db_host: string
|
||||
db_name: string
|
||||
db_user: string
|
||||
db_schema: string
|
||||
inserted_at: string
|
||||
db_dns_name: string
|
||||
db_port: number
|
||||
db_name: string
|
||||
db_ssl: boolean
|
||||
db_host: string
|
||||
db_user: string
|
||||
cloud_provider: string
|
||||
region: string
|
||||
services: Service[]
|
||||
}
|
||||
|
||||
export type App = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type AppConfig = {
|
||||
endpoint: string
|
||||
db_schema: string
|
||||
realtime_multitenant_enabled: boolean
|
||||
jwt_secret: string
|
||||
}
|
||||
|
||||
export type Service = {
|
||||
@@ -41,24 +34,28 @@ export type Service = {
|
||||
service_api_keys: ServiceApiKey[]
|
||||
}
|
||||
|
||||
export type AppConfig = {
|
||||
protocol: 'https' | 'http'
|
||||
endpoint: string
|
||||
db_schema: string
|
||||
realtime_multitenant_enabled: boolean
|
||||
}
|
||||
|
||||
export type App = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ServiceApiKey = {
|
||||
api_key_encrypted: string
|
||||
tags: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type AutoApiService = Service & {
|
||||
protocol: 'https' | 'http'
|
||||
endpoint: string
|
||||
restUrl: string
|
||||
project: Project
|
||||
defaultApiKey: string
|
||||
serviceApiKey: string
|
||||
api_key: string
|
||||
}
|
||||
|
||||
export type ProjectSettingsResponse = {
|
||||
project: Project
|
||||
autoApiService: AutoApiService
|
||||
services: Service[]
|
||||
}
|
||||
|
||||
export async function getProjectSettings(
|
||||
@@ -69,7 +66,7 @@ export async function getProjectSettings(
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
const response = await get(`${API_URL}/props/project/${projectRef}/api`, {
|
||||
const response = await get(`${API_URL}/props/project/${projectRef}/settings`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
@@ -95,13 +92,10 @@ export const useProjectSettingsQuery = <TData = ProjectSettingsData>(
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
refetchInterval(data, query) {
|
||||
if (!data) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { autoApiService } = data as unknown as ProjectSettingsData
|
||||
|
||||
const apiKeys = autoApiService?.service_api_keys ?? []
|
||||
const apiService = (
|
||||
(data as unknown as ProjectSettingsData | undefined)?.services ?? []
|
||||
).find((x) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID)
|
||||
const apiKeys = apiService?.service_api_keys ?? []
|
||||
const interval = apiKeys.length === 0 ? 2000 : 0
|
||||
|
||||
return interval
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
export type ProjectStorageConfigVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
// TODO: Add proper type
|
||||
export type ProjectStorageConfigResponse = any
|
||||
|
||||
export async function getProjectStorageConfig(
|
||||
{ projectRef }: ProjectStorageConfigVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
const response = await get(`${API_URL}/projects/${projectRef}/config/storage`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as ProjectStorageConfigResponse
|
||||
}
|
||||
|
||||
export type ProjectStorageConfigData = Awaited<ReturnType<typeof getProjectStorageConfig>>
|
||||
export type ProjectStorageConfigError = unknown
|
||||
|
||||
export const useProjectStorageConfigQuery = <TData = ProjectStorageConfigData>(
|
||||
{ projectRef }: ProjectStorageConfigVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectStorageConfigData, ProjectStorageConfigError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectStorageConfigData, ProjectStorageConfigError, TData>(
|
||||
configKeys.storage(projectRef),
|
||||
({ signal }) => getProjectStorageConfig({ projectRef }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
|
||||
export const useProjectStorageConfigPrefetch = ({ projectRef }: ProjectStorageConfigVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(configKeys.storage(projectRef), ({ signal }) =>
|
||||
getProjectStorageConfig({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { configKeys } from './keys'
|
||||
|
||||
export type ProjectStorageConfigUpdateUpdateVariables = {
|
||||
projectRef: string
|
||||
fileSizeLimit: number
|
||||
}
|
||||
|
||||
export async function updateProjectStorageConfigUpdate({
|
||||
projectRef,
|
||||
fileSizeLimit,
|
||||
}: ProjectStorageConfigUpdateUpdateVariables) {
|
||||
const response = await patch(`${API_URL}/projects/${projectRef}/config/storage`, {
|
||||
fileSizeLimit,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type ProjectStorageConfigUpdateUpdateData = Awaited<
|
||||
ReturnType<typeof updateProjectStorageConfigUpdate>
|
||||
>
|
||||
|
||||
export const useProjectStorageConfigUpdateUpdateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<
|
||||
ProjectStorageConfigUpdateUpdateData,
|
||||
unknown,
|
||||
ProjectStorageConfigUpdateUpdateVariables
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
ProjectStorageConfigUpdateUpdateData,
|
||||
unknown,
|
||||
ProjectStorageConfigUpdateUpdateVariables
|
||||
>((vars) => updateProjectStorageConfigUpdate(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(configKeys.storage(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const docsKeys = {
|
||||
jsonSchema: (
|
||||
projectRef: string | undefined,
|
||||
swaggerUrl: string | undefined,
|
||||
apiKey: string | undefined
|
||||
) => ['projects', projectRef, 'docs', { swaggerUrl, apiKey }] as const,
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { useCallback } from 'react'
|
||||
import { docsKeys } from './keys'
|
||||
|
||||
export type ProjectJsonSchemaVariables = {
|
||||
projectRef?: string
|
||||
swaggerUrl?: string
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
export type ProjectJsonSchemaResponse = any
|
||||
|
||||
export async function getProjectJsonSchema(
|
||||
{ projectRef, swaggerUrl, apiKey }: ProjectJsonSchemaVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!swaggerUrl) {
|
||||
throw new Error('id is required')
|
||||
}
|
||||
|
||||
let headers: {
|
||||
[key: string]: string | undefined
|
||||
} = { apikey: apiKey }
|
||||
if ((apiKey?.length ?? 0) > 40) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
|
||||
const response = await get(swaggerUrl, {
|
||||
signal,
|
||||
headers,
|
||||
credentials: 'omit',
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as ProjectJsonSchemaResponse
|
||||
}
|
||||
|
||||
export type ProjectJsonSchemaData = Awaited<ReturnType<typeof getProjectJsonSchema>>
|
||||
export type ProjectJsonSchemaError = unknown
|
||||
|
||||
export const useProjectJsonSchemaQuery = <TData = ProjectJsonSchemaData>(
|
||||
{ projectRef, swaggerUrl, apiKey }: ProjectJsonSchemaVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectJsonSchemaData, ProjectJsonSchemaError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectJsonSchemaData, ProjectJsonSchemaError, TData>(
|
||||
docsKeys.jsonSchema(projectRef, swaggerUrl, apiKey),
|
||||
({ signal }) => getProjectJsonSchema({ projectRef, swaggerUrl, apiKey }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof swaggerUrl !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useProjectJsonSchemaPrefetch = ({
|
||||
projectRef,
|
||||
swaggerUrl,
|
||||
apiKey,
|
||||
}: ProjectJsonSchemaVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef && swaggerUrl) {
|
||||
client.prefetchQuery(docsKeys.jsonSchema(projectRef, swaggerUrl, apiKey), ({ signal }) =>
|
||||
getProjectJsonSchema({ projectRef, swaggerUrl, apiKey }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef, swaggerUrl])
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const logKeys = {
|
||||
logStats: (projectRef: string | undefined, interval: string | undefined) =>
|
||||
['projects', projectRef, 'log-stats', interval] as const,
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { logKeys } from './keys'
|
||||
|
||||
export type ProjectLogStatsVariables = {
|
||||
projectRef?: string
|
||||
interval?: string
|
||||
}
|
||||
|
||||
export type ProjectLogStatsResponse = any
|
||||
|
||||
export async function getProjectLogStats(
|
||||
{ projectRef, interval }: ProjectLogStatsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!interval) {
|
||||
throw new Error('interval is required')
|
||||
}
|
||||
|
||||
const response = await get(`${API_URL}/projects/${projectRef}/log-stats?interval=${interval}`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as ProjectLogStatsResponse
|
||||
}
|
||||
|
||||
export type ProjectLogStatsData = Awaited<ReturnType<typeof getProjectLogStats>>
|
||||
export type ProjectLogStatsError = unknown
|
||||
|
||||
export const useProjectLogStatsQuery = <TData = ProjectLogStatsData>(
|
||||
{ projectRef, interval }: ProjectLogStatsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectLogStatsData, ProjectLogStatsError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectLogStatsData, ProjectLogStatsError, TData>(
|
||||
logKeys.logStats(projectRef, interval),
|
||||
({ signal }) => getProjectLogStats({ projectRef, interval }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof interval !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useProjectLogStatsPrefetch = ({ projectRef, interval }: ProjectLogStatsVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef && interval) {
|
||||
client.prefetchQuery(logKeys.logStats(projectRef, interval), ({ signal }) =>
|
||||
getProjectLogStats({ projectRef, interval }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef, interval])
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const notificationKeys = {
|
||||
list: () => ['notifications'] as const,
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Notification } from '@supabase/shared-types/out/notifications'
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { notificationKeys } from './keys'
|
||||
|
||||
export type NotificationsResponse = Notification[]
|
||||
|
||||
export async function getNotifications(signal?: AbortSignal) {
|
||||
const response = await get(`${API_URL}/notifications`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as NotificationsResponse
|
||||
}
|
||||
|
||||
export type NotificationsData = Awaited<ReturnType<typeof getNotifications>>
|
||||
export type NotificationsError = unknown
|
||||
|
||||
export const useNotificationsQuery = <TData = NotificationsData>(
|
||||
options: UseQueryOptions<NotificationsData, NotificationsError, TData> = {}
|
||||
) =>
|
||||
useQuery<NotificationsData, NotificationsError, TData>(
|
||||
notificationKeys.list(),
|
||||
({ signal }) => getNotifications(signal),
|
||||
options
|
||||
)
|
||||
|
||||
export const useNotificationsPrefetch = () => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
client.prefetchQuery(notificationKeys.list(), ({ signal }) => getNotifications(signal))
|
||||
}, [])
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { organizationKeys } from './keys'
|
||||
|
||||
export type MemberWithFreeProjectLimit = {
|
||||
free_project_limit: number
|
||||
primary_email: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export type FreeProjectLimitCheckVariables = {
|
||||
slug?: string
|
||||
}
|
||||
|
||||
export type FreeProjectLimitCheckResponse = MemberWithFreeProjectLimit[]
|
||||
|
||||
export async function getFreeProjectLimitCheck(
|
||||
{ slug }: FreeProjectLimitCheckVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!slug) {
|
||||
throw new Error('slug is required')
|
||||
}
|
||||
|
||||
const response = await get(
|
||||
`${API_URL}/organizations/${slug}/members/reached-free-project-limit`,
|
||||
{
|
||||
signal,
|
||||
}
|
||||
)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as FreeProjectLimitCheckResponse
|
||||
}
|
||||
|
||||
export type FreeProjectLimitCheckData = Awaited<ReturnType<typeof getFreeProjectLimitCheck>>
|
||||
export type FreeProjectLimitCheckError = unknown
|
||||
|
||||
export const useFreeProjectLimitCheckQuery = <TData = FreeProjectLimitCheckData>(
|
||||
{ slug }: FreeProjectLimitCheckVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<FreeProjectLimitCheckData, FreeProjectLimitCheckError, TData> = {}
|
||||
) =>
|
||||
useQuery<FreeProjectLimitCheckData, FreeProjectLimitCheckError, TData>(
|
||||
organizationKeys.freeProjectLimitCheck(slug),
|
||||
({ signal }) => getFreeProjectLimitCheck({ slug }, signal),
|
||||
{
|
||||
enabled: enabled && typeof slug !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useFreeProjectLimitCheckPrefetch = ({ slug }: FreeProjectLimitCheckVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (slug) {
|
||||
client.prefetchQuery(organizationKeys.freeProjectLimitCheck(slug), ({ signal }) =>
|
||||
getFreeProjectLimitCheck({ slug }, signal)
|
||||
)
|
||||
}
|
||||
}, [slug])
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const organizationKeys = {
|
||||
detail: (slug: string | undefined) => ['organizations', slug, 'detail'] as const,
|
||||
roles: (slug: string | undefined) => ['organizations', slug, 'roles'] as const,
|
||||
freeProjectLimitCheck: (slug: string | undefined) =>
|
||||
['organizations', slug, 'free-project-limit-check'] as const,
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { Member } from 'types'
|
||||
import { organizationKeys } from './keys'
|
||||
|
||||
export type OrganizationDetailVariables = {
|
||||
slug?: string
|
||||
}
|
||||
|
||||
export type OrganizationDetailResponse = {
|
||||
members: Member[]
|
||||
}
|
||||
|
||||
export async function getOrganizationDetail(
|
||||
{ slug }: OrganizationDetailVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!slug) {
|
||||
throw new Error('slug is required')
|
||||
}
|
||||
|
||||
let [data, inviteData] = await Promise.all([
|
||||
get(`${API_URL}/organizations/${slug}/members?member_roles`, {
|
||||
signal,
|
||||
}),
|
||||
get(`${API_URL}/organizations/${slug}/members/invite`, {
|
||||
signal,
|
||||
}),
|
||||
])
|
||||
if (data.error) {
|
||||
throw data.error
|
||||
}
|
||||
if (inviteData.error) {
|
||||
throw inviteData.error
|
||||
}
|
||||
|
||||
if (data && inviteData && inviteData.length > 0) {
|
||||
// Remap invite data to look like existing members data
|
||||
const invitedMembers = inviteData.map((x: any) => {
|
||||
const member = {
|
||||
is_owner: false,
|
||||
invited_at: x.invited_at,
|
||||
invited_id: x.invited_id,
|
||||
username: x.invited_email.slice(0, 1),
|
||||
primary_email: x.invited_email,
|
||||
}
|
||||
return { ...member, role_ids: [x.role_id] }
|
||||
})
|
||||
|
||||
data = [...data, ...invitedMembers]
|
||||
}
|
||||
|
||||
return { members: data } as OrganizationDetailResponse
|
||||
}
|
||||
|
||||
export type OrganizationDetailData = Awaited<ReturnType<typeof getOrganizationDetail>>
|
||||
export type OrganizationDetailError = unknown
|
||||
|
||||
export const useOrganizationDetailQuery = <TData = OrganizationDetailData>(
|
||||
{ slug }: OrganizationDetailVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<OrganizationDetailData, OrganizationDetailError, TData> = {}
|
||||
) =>
|
||||
useQuery<OrganizationDetailData, OrganizationDetailError, TData>(
|
||||
organizationKeys.detail(slug),
|
||||
({ signal }) => getOrganizationDetail({ slug }, signal),
|
||||
{
|
||||
enabled: enabled && typeof slug !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useOrganizationDetailPrefetch = ({ slug }: OrganizationDetailVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (slug) {
|
||||
client.prefetchQuery(organizationKeys.detail(slug), ({ signal }) =>
|
||||
getOrganizationDetail({ slug }, signal)
|
||||
)
|
||||
}
|
||||
}, [slug])
|
||||
}
|
||||
Loaded 100 of 184 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user