Merge pull request #12219 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-02-08 13:10:09 +08:00
commit 2b8d81bd12
164 files changed
+3694 -2009

No files matched your search

+2 -2
View File
@@ -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">
@@ -237,7 +237,7 @@ const ClippyModal: FC<Props> = ({ onClose }) => {
<div className="flex justify-between items-center py-2 text-xs">
<div className="flex items-centerp gap-1 pt-3 pb-1">
<span>Powered by OpenAI.</span>
<a href="/blog/clippy" className="underline">
<a href="/blog/chatgpt-supabase-docs" className="underline">
Read the blog post
</a>
</div>
@@ -850,14 +850,14 @@ export const reference = {
export const reference_javascript_v1 = {
icon: 'reference-javascript',
title: 'javascript',
title: 'JavaScript',
url: '/guides/reference/javascript',
parent: '/reference',
}
export const reference_javascript_v2 = {
icon: 'reference-javascript',
title: 'javascript',
title: 'JavaScript',
url: '/guides/reference/javascript',
parent: '/reference',
}
@@ -878,7 +878,7 @@ export const reference_dart_v1 = {
export const reference_csharp_v0 = {
icon: 'reference-csharp',
title: 'c#',
title: 'C#',
url: 'guides/reference/csharp',
parent: '/reference',
}
@@ -89,16 +89,7 @@ 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>
<SearchButton className="md:w-full lg:w-96">
<SearchButton className="md:w-full lg:w-96 order-2 lg:order-1">
<div
className="
flex
@@ -126,6 +117,16 @@ const TopNavBarRef: FC = () => {
</div>
</div>
</SearchButton>
<div className="flex cursor-pointer order-1 lg:order-2">
<Image
onClick={openClippy}
width={26}
height={29}
src={isDarkMode ? clippyImageDark : clippyImage}
alt="Clippy"
/>
</div>
</div>
<div className="hidden lg:flex grow items-center justify-end gap-3">
<Button
@@ -109,6 +109,8 @@ This will be used any time we need to create a Supabase client _server-side_ - i
Next, we need a middleware file to refresh the user's session on navigation.
> If you were using Middleware prior to 12.2, see the [upgrade guide](https://nextjs.org/docs/messages/middleware-upgrade-guide).
<Tabs
scrollable
size="small"
@@ -117,7 +119,7 @@ Next, we need a middleware file to refresh the user's session on navigation.
>
<TabPanel id="js" label="JavaScript">
Create a new `/app/middleware.js` file and populate with the following:
Create a new `middleware.js` file at the same level as your `app` (in the root or `src` directory) and populate with the following:
```jsx title="middleware.js"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
@@ -140,7 +142,7 @@ export async function middleware(req) {
<TabPanel id="ts" label="TypeScript">
Create a new `/app/middleware.ts` file and populate with the following:
Create a new `middleware.ts` file at the same level as your `app` (in the root or `src` directory) and populate with the following:
```tsx title="middleware.ts"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
@@ -67,7 +67,7 @@ export const meta = {
<StepHikeCompact.Code>
```bash Terminal
cd my-app && npm install @supabase/supabase-js
cd myapp && npm install @supabase/supabase-js
```
</StepHikeCompact.Code>
@@ -66,7 +66,27 @@ export default defineNuxtConfig({
Let's set up a Vue component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
```html title=/components/Auth.vue
```vue title=/components/Auth.vue
<script setup>
const supabase = useSupabaseClient()
const loading = ref(false)
const email = ref('')
const handleLogin = async () => {
try {
loading.value = true
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
alert(error.error_description || error.message)
} finally {
loading.value = false
}
}
</script>
<template>
<form class="row flex-center flex" @submit.prevent="handleLogin">
<div class="col-6 form-widget">
@@ -86,25 +106,6 @@ Let's set up a Vue component to manage logins and sign ups. We'll use Magic Link
</div>
</form>
</template>
<script setup>
const supabase = useSupabaseClient()
const loading = ref(false)
const email = ref('')
const handleLogin = async () => {
try {
loading.value = true
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
alert(error.error_description || error.message)
} finally {
loading.value = false
}
}
</script>
```
### User state
@@ -116,7 +117,70 @@ To access the user information, use the composable [useSupabaseUser](https://sup
After a user is signed in we can allow them to edit their profile details and manage their account.
Let's create a new component for that called `Account.vue`.
```html title=components/Account.vue
```vue title=components/Account.vue
<script setup>
const supabase = useSupabaseClient()
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_path = ref('')
loading.value = true
const user = useSupabaseUser()
let { data } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.value.id)
.single()
if (data) {
username.value = data.username
website.value = data.website
avatar_path.value = data.avatar_url
}
loading.value = false
async function updateProfile() {
try {
loading.value = true
const user = useSupabaseUser()
const updates = {
id: user.value.id,
username: username.value,
website: website.value,
avatar_url: avatar_path.value,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
})
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
user.value = null
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
</script>
<template>
<form class="form-widget" @submit.prevent="updateProfile">
<div>
@@ -146,82 +210,23 @@ Let's create a new component for that called `Account.vue`.
</div>
</form>
</template>
<script setup>
const supabase = useSupabaseClient()
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_path = ref('')
loading.value = true
const user = useSupabaseUser();
let { data } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.value.id)
.single()
if (data) {
username.value = data.username
website.value = data.website
avatar_path.value = data.avatar_url
}
loading.value = false
async function updateProfile() {
try {
loading.value = true
const user = useSupabaseUser();
const updates = {
id: user.value.id,
username: username.value,
website: website.value,
avatar_url: avatar_path.value,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
})
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
user.value = null
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
</script>
```
### Launch!
Now that we have all the components in place, let's update `app.vue`:
```html title=app.vue
```vue title=app.vue
<script setup>
const user = useSupabaseUser()
</script>
<template>
<div class="container" style="padding: 50px 0 100px 0">
<Account v-if="user" />
<Auth v-else />
</div>
</template>
<script setup>
const user = useSupabaseUser()
</script>
```
Once that's done, run this in a terminal window:
@@ -242,7 +247,65 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
```html title=components/Avatar.vue
```vue title=components/Avatar.vue
<script setup>
const props = defineProps(['path'])
const { path } = toRefs(props)
const emit = defineEmits(['update:path', 'upload'])
const supabase = useSupabaseClient()
const uploading = ref(false)
const src = ref('')
const files = ref()
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path.value)
if (error) throw error
src.value = URL.createObjectURL(data)
} catch (error) {
console.error('Error downloading image: ', error.message)
}
}
const uploadAvatar = async (evt) => {
files.value = evt.target.files
try {
uploading.value = true
if (!files.value || files.value.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = files.value[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
if (uploadError) throw uploadError
emit('update:path', filePath)
emit('upload')
} catch (error) {
alert(error.message)
} finally {
uploading.value = false
}
}
downloadImage()
watch(path, () => {
if (path.value) {
downloadImage()
}
})
</script>
<template>
<div>
<img
@@ -256,7 +319,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
<div style="width: 10em; position: relative;">
<label class="button primary block" for="single">
{{ uploading ? "Uploading ..." : "Upload" }}
{{ uploading ? 'Uploading ...' : 'Upload' }}
</label>
<input
style="position: absolute; visibility: hidden;"
@@ -269,65 +332,76 @@ Let's create an avatar for the user so that they can upload a profile photo. We
</div>
</div>
</template>
<script setup>
const props = defineProps(['path'])
const { path } = toRefs(props)
const emit = defineEmits(['update:path', 'upload'])
const supabase = useSupabaseClient()
const uploading = ref(false)
const src = ref('')
const files = ref()
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path.value)
if (error) throw error
src.value = URL.createObjectURL(data)
} catch (error) {
console.error('Error downloading image: ', error.message)
}
}
const uploadAvatar = async (evt) => {
files.value = evt.target.files
try {
uploading.value = true
if (!files.value || files.value.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = files.value[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
if (uploadError) throw uploadError
emit('update:path', filePath)
emit('upload')
} catch (error) {
alert(error.message)
} finally {
uploading.value = false
}
}
downloadImage()
watch(path, () => {
if (path.value) {
downloadImage()
}
})
</script>
```
### Add the new widget
And then we can add the widget to the Account page:
```html title=components/Account.vue
```vue title=components/Account.vue
<script setup>
const supabase = useSupabaseClient()
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_path = ref('')
loading.value = true
const user = useSupabaseUser()
let { data } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.value.id)
.single()
if (data) {
username.value = data.username
website.value = data.website
avatar_path.value = data.avatar_url
}
loading.value = false
async function updateProfile() {
try {
loading.value = true
const user = useSupabaseUser()
const updates = {
id: user.value.id,
username: username.value,
website: website.value,
avatar_url: avatar_path.value,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
})
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
</script>
<template>
<form class="form-widget" @submit.prevent="updateProfile">
<Avatar v-model:path="avatar_path" @upload="updateProfile" />
@@ -358,64 +432,6 @@ And then we can add the widget to the Account page:
</div>
</form>
</template>
<script setup>
const supabase = useSupabaseClient()
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_path = ref('')
loading.value = true
const user = useSupabaseUser();
let { data } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.value.id)
.single()
if (data) {
username.value = data.username
website.value = data.website
avatar_path.value = data.avatar_url
}
loading.value = false
async function updateProfile() {
try {
loading.value = true
const user = useSupabaseUser();
const updates = {
id: user.value.id,
username: username.value,
website: website.value,
avatar_url: avatar_path.value,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
})
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
</script>
```
That is it! You should now be able to upload a profile photo to Supabase Storage.
@@ -68,30 +68,30 @@ Optionally, update [src/style.css](https://raw.githubusercontent.com/supabase/su
Set up an `src/components/Auth.vue` component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
```html title=/src/components/Auth.vue
```vue title=/src/components/Auth.vue
<script setup>
import { ref } from 'vue'
import { supabase } from '../supabase'
import { ref } from 'vue'
import { supabase } from '../supabase'
const loading = ref(false)
const email = ref('')
const loading = ref(false)
const email = ref('')
const handleLogin = async () => {
try {
loading.value = true
const { error } = await supabase.auth.signInWithOtp({
email: email.value,
})
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
loading.value = false
const handleLogin = async () => {
try {
loading.value = true
const { error } = await supabase.auth.signInWithOtp({
email: email.value,
})
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
loading.value = false
}
}
</script>
<template>
@@ -120,82 +120,82 @@ Set up an `src/components/Auth.vue` component to manage logins and sign ups. We'
After a user is signed in we can allow them to edit their profile details and manage their account.
Create a new `src/components/Account.vue` component to handle this.
```html title=src/components/Account.vue
```vue title=src/components/Account.vue
<script setup>
import { supabase } from '../supabase'
import { onMounted, ref, toRefs } from 'vue'
import { supabase } from '../supabase'
import { onMounted, ref, toRefs } from 'vue'
const props = defineProps(['session'])
const { session } = toRefs(props)
const props = defineProps(['session'])
const { session } = toRefs(props)
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_url = ref('')
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_url = ref('')
onMounted(() => {
getProfile()
})
onMounted(() => {
getProfile()
})
async function getProfile() {
try {
loading.value = true
const { user } = session.value
async function getProfile() {
try {
loading.value = true
const { user } = session.value
let { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
let { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (error && status !== 406) throw error
if (error && status !== 406) throw error
if (data) {
username.value = data.username
website.value = data.website
avatar_url.value = data.avatar_url
}
} catch (error) {
alert(error.message)
} finally {
loading.value = false
if (data) {
username.value = data.username
website.value = data.website
avatar_url.value = data.avatar_url
}
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function updateProfile() {
try {
loading.value = true
const { user } = session.value
async function updateProfile() {
try {
loading.value = true
const { user } = session.value
const updates = {
id: user.id,
username: username.value,
website: website.value,
avatar_url: avatar_url.value,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
const updates = {
id: user.id,
username: username.value,
website: website.value,
avatar_url: avatar_url.value,
updated_at: new Date(),
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
</script>
<template>
@@ -233,24 +233,24 @@ Create a new `src/components/Account.vue` component to handle this.
Now that we have all the components in place, let's update `App.vue`:
```html title=src/App.vue
```vue title=src/App.vue
<script setup>
import { onMounted, ref } from 'vue'
import Account from './components/Account.vue'
import Auth from './components/Auth.vue'
import { supabase } from './supabase'
import { onMounted, ref } from 'vue'
import Account from './components/Account.vue'
import Auth from './components/Auth.vue'
import { supabase } from './supabase'
const session = ref()
const session = ref()
onMounted(() => {
supabase.auth.getSession().then(({ data }) => {
session.value = data.session
})
supabase.auth.onAuthStateChange((_, _session) => {
session.value = _session
})
onMounted(() => {
supabase.auth.getSession().then(({ data }) => {
session.value = data.session
})
supabase.auth.onAuthStateChange((_, _session) => {
session.value = _session
})
})
</script>
<template>
@@ -279,56 +279,56 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
Create a new `src/components/Avatar.vue` component that allows users to upload profile photos:
```html title=src/components/Avatar.vue
```vue title=src/components/Avatar.vue
<script setup>
import { ref, toRefs, watch } from 'vue'
import { supabase } from '../supabase'
import { ref, toRefs, watch } from 'vue'
import { supabase } from '../supabase'
const prop = defineProps(['path', 'size'])
const { path, size } = toRefs(prop)
const prop = defineProps(['path', 'size'])
const { path, size } = toRefs(prop)
const emit = defineEmits(['upload', 'update:path'])
const uploading = ref(false)
const src = ref('')
const files = ref()
const emit = defineEmits(['upload', 'update:path'])
const uploading = ref(false)
const src = ref('')
const files = ref()
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path.value)
if (error) throw error
src.value = URL.createObjectURL(data)
} catch (error) {
console.error('Error downloading image: ', error.message)
}
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path.value)
if (error) throw error
src.value = URL.createObjectURL(data)
} catch (error) {
console.error('Error downloading image: ', error.message)
}
}
const uploadAvatar = async (evt) => {
files.value = evt.target.files
try {
uploading.value = true
if (!files.value || files.value.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = files.value[0]
const fileExt = file.name.split('.').pop()
const filePath = `${Math.random()}.${fileExt}`
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
if (uploadError) throw uploadError
emit('update:path', filePath)
emit('upload')
} catch (error) {
alert(error.message)
} finally {
uploading.value = false
const uploadAvatar = async (evt) => {
files.value = evt.target.files
try {
uploading.value = true
if (!files.value || files.value.length === 0) {
throw new Error('You must select an image to upload.')
}
}
watch(path, () => {
if (path.value) downloadImage()
})
const file = files.value[0]
const fileExt = file.name.split('.').pop()
const filePath = `${Math.random()}.${fileExt}`
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
if (uploadError) throw uploadError
emit('update:path', filePath)
emit('upload')
} catch (error) {
alert(error.message)
} finally {
uploading.value = false
}
}
watch(path, () => {
if (path.value) downloadImage()
})
</script>
<template>
@@ -344,7 +344,7 @@ Create a new `src/components/Avatar.vue` component that allows users to upload p
<div :style="{ width: size + 'em' }">
<label class="button primary block" for="single">
{{ uploading ? "Uploading ..." : "Upload" }}
{{ uploading ? 'Uploading ...' : 'Upload' }}
</label>
<input
style="visibility: hidden; position: absolute"
@@ -363,10 +363,10 @@ Create a new `src/components/Avatar.vue` component that allows users to upload p
And then we can add the widget to the Account page in `src/components/Account.vue`:
```html title=src/components/Account.vue
```vue title=src/components/Account.vue
<script>
// Import the new component
import Avatar from './Avatar.vue'
// Import the new component
import Avatar from './Avatar.vue'
</script>
<template>
-1
View File
@@ -18,7 +18,6 @@ Dave Wilson
Div Arora
Egor Romanov
Fabrizio Fenoglio
Francisco Mazzoni
Haydn Maley
Hieu Pham
Inian P
@@ -7,7 +7,7 @@ thumb: embeddings/og_pgvector.png
tags:
- postgres
- ai
date: '2023-01-06'
date: '2023-02-06'
toc_depth: 3
---
@@ -0,0 +1,116 @@
---
title: 'Supabase Clippy: ChatGPT for Supabase Docs'
description: Creating a ChatGPT interface for the Supabase documentation.
author: paul_copplestone
image: docsgpt/og_supabase_clippy.png
thumb: docsgpt/og_supabase_clippy.png
tags:
- ai
- docs
date: '2023-02-07'
toc_depth: 3
---
We all know that Microsoft's real agenda for pouring billions into OpenAI to revive their favorite friend Clippy.
Today, we're doing our part to support the momentum by releasing “Supabase Clippy” for our docs (and we don't expect this name to last long before the lawyers catch on).
![Clippy](/images/blog/docsgpt/clippy.png)
Want to try it out? It's a hidden feature while in MVP - visit [supabase.com/docs](/docs) and hit `cmd` + `/` to pull up the search box (sorry mobile users).
# ChatGPT for Supabase Docs
Last launch week we [released](/blog/new-supabase-docs-built-with-nextjs) our new documentation site to improve the developer experience, especially for new users.
When we first started Supabase our docs were very concise. We deliberately withheld "non-critical information" so that developers could consume everything in one sitting.
Our product suite has grown in the past 2 years and our docs have grown as a result. There's a lot to read. We're also attracting developers who've never used Postgres before, and it's becoming incumbent on us to train these developers on best-practices.
![Missing indexes](/images/blog/docsgpt/indexes.png)
### The “ask” interface
Developers have recently gained an the ability to trust a bot. Where Clippy failed, ChatGPT succeeded.
This is convenient timing for us, since our documentation content is more than the average developer wants to consume in one go. Today we're providing a similar interface to ChatGPT which is trained on our own docs.
<video width="100%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<source src="/images/blog/embeddings/embeddings.mp4" type="video/mp4" />
</video>
To keep your expectations low, this implementation is a 1-week MVP. We're sharing this iteration to gather feedback and to teach you how to build something similar.
## How it's built
It's built with Supabase/Postgres, and consists of several key parts:
1. Parsing the Supabase docs into sections.
2. Creating embeddings for each section using OpenAI's embeddings API.
3. Storing the embeddings in Postgres using the pgvector extension.
4. Getting a user's question.
5. Query the Postgres database for the most relevant documents related to the question.
6. Inject these documents as context for GPT-3 to reference in its answer.
7. Streaming the results back to the user in realtime.
All the changes we made are in [this](https://github.com/supabase/supabase/pull/12056) Pull Request. Greg is here to explain every step in full detail:
<div class="video-container">
<iframe
title="ClippyGPT"
src="https://www.youtube-nocookie.com/embed/Yhtjd7yGGGA"
frameborder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
You can find a full write up in our previous blog post: [Storing OpenAI embeddings in Postgres with pgvector](https://supabase.com/blog/openai-embeddings-postgres-vector).
## What's next
This current version is very basic, which makes it a good starting point if you want to build something similar.
- **Expand the knowledge base:** Create embeddings for our reference docs, GitHub Discussions, Discord discussions, etc. The current iteration is only using the Guides.
- **Caching:** Cache queries so that we can provide answers instantly to similar/matching questions.
- **User-assisted improvements:** Ability to thumbs-up responses for relevance.
- **Beyond search:** perhaps we can make this interface interactive, guiding users through instructions.
## Bonus: The power of open source
One of the most interesting things about this feature was _how_ it happened - almost completely from open source contributions. A short series of events:
### A friendly email
A few weeks ago I received an email, drawing our attention to a [Pull Request](https://github.com/supabase/postgres/pull/472) in our [`postgres`](https://github.com/supabase/postgres) GitHub repo:
![Greg](/images/blog/docsgpt/greg.png)
### A friendly chat
After we merged the PR, I reached out to see if Greg was interested in helping with some documentation or a blog post to help the community to use pgvector. On the call, Greg surprised me with an initial prototype where he had ingested the Supabase docs for a ChatGPT-like experience. I was suitably impressed and suggested that we could explore ways to work together to build it into the Supabase docs. He wasted no time:
<div class="video-container">
<iframe
title="ClippyGPT"
src="https://www.youtube-nocookie.com/embed/yAsR7oe5vng"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
### A friendly collaboration
We sponsored Greg to work on an MVP with us, along with some guides to help developers do the same. He [pushed all his changes](https://github.com/supabase/supabase/pull/12056) to the Supabase Docs, and we wrote a [blog post](/blog/openai-embeddings-postgres-vector) together to help developers build something similar.
If you re-read the events above and swap “Supabase” for any other big-tech company, you'll grasp how impossible this series of events is for any company that's not open source. All the code that was contributed is licensed liberally, so we didn't need to jump through IP checklists. Our docs are publicly available on GitHub, so Greg could build his prototype without being granted access.
We're incredibly grateful for Greg's work here, so if you like what he's done then consider supporting him by checking out [prmpts.ai](https://prmpts.ai/) and following him on twitter: [@ggrdson](https://twitter.com/ggrdson).
While you're at it, consider supporting [@ankane](https://github.com/ankane)'s great work on [pgvector](https://github.com/pgvector/pgvector) by starring them on GitHub, and sending your thanks.
## Try it out
Want to try Supabase Clippy? It's a hidden feature while in MVP - visit [supabase.com/docs](/docs) and hit `cmd` + `/` to pull up the search box (sorry mobile users).
If you have any comments we'd love to hear them in our [ShowHN](https://news.ycombinator.com/item?id=34695306).
+5
View File
@@ -1842,4 +1842,9 @@ module.exports = [
source: '/docs/guides/examples',
destination: '/docs/guides/resources/examples',
},
{
permanent: true,
source: '/docs/reference/javascript/v0/rpc',
destination: '/docs/reference/javascript/rpc',
},
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

+162 -155
View File
File diff suppressed because it is too large. Load diff
-16
View File
@@ -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",
+3 -3
View File
@@ -907,7 +907,7 @@ functions:
```dart
supabase.from('countries')
.stream(primaryKey: ['id'])
.order('name', ascending: true)
.order('name', { ascending: true })
.listen((List<Map<String, dynamic>> data) {
// Do something awesome with the data
});
@@ -918,7 +918,7 @@ functions:
```dart
supabase.from('countries')
.stream(primaryKey: ['id'])
.order('name', ascending: true)
.order('name', { ascending: true })
.limit(10)
.listen((List<Map<String, dynamic>> data) {
// Do something awesome with the data
@@ -1334,7 +1334,7 @@ functions:
final data = await supabase
.from('cities')
.select('name, country_id')
.order('id', ascending: false );
.order('id', { ascending: false });
```
- id: with-embedded-resources
name: With embedded resources
+113 -6
View File
@@ -972,6 +972,40 @@ functions:
.from('countries')
.select()
```
data:
sql: |
```sql
create table
countries (id int8 primary key, name text);
insert into
countries (id, name)
values
(1, 'Afghanistan'),
(2, 'Albania'),
(3, 'Algeria');
```
response: |
```json
{
"data": [
{
"id": 1,
"name": "Afghanistan"
},
{
"id": 2,
"name": "Albania"
},
{
"id": 3,
"name": "Algeria"
}
],
"status": 200,
"statusText": "OK"
}
```
- id: selecting-specific-columns
name: Selecting specific columns
code: |
@@ -980,6 +1014,37 @@ functions:
.from('countries')
.select('name')
```
data:
sql: |
```sql
create table
countries (id int8 primary key, name text);
insert into
countries (id, name)
values
(1, 'Afghanistan'),
(2, 'Albania'),
(3, 'Algeria');
```
response: |
```json
{
"data": [
{
"name": "Afghanistan"
},
{
"name": "Albania"
},
{
"name": "Algeria"
}
],
"status": 200,
"statusText": "OK"
}
```
- id: query-foreign-tables
name: Query foreign tables
description: |
@@ -995,6 +1060,54 @@ functions:
)
`)
```
data:
sql: |
```sql
create table
countries (id int8 primary key, name text);
create table
cities (
id int8 primary key,
country_id int8 not null references countries,
name text
);
insert into
countries (id, name)
values
(1, 'Germany'),
(2, 'Indonesia');
insert into
cities (id, country_id, name)
values
(1, 2, 'Bali'),
(2, 1, 'Munich');
```
response: |
```json
{
"data": [
{
"name": "Germany",
"cities": [
{
"name": "Munich"
}
]
},
{
"name": "Indonesia",
"cities": [
{
"name": "Bali"
}
]
}
],
"status": 200,
"statusText": "OK"
}
```
- id: query-foreign-tables-through-a-join-table
name: Query foreign tables through a join table
code: |
@@ -1327,12 +1440,6 @@ functions:
- id: insert
title: 'Create data: insert()'
$ref: '@supabase/postgrest-js.PostgrestQueryBuilder.insert'
params:
- name: values
type: object | object []
isOptional: false
description: |
Bobby Bobby Bobby Bobby Bobby Bobby
examples:
- id: create-a-record
name: Create a record
@@ -0,0 +1,7 @@
export const useProjectSubscriptionQuery = jest.fn().mockReturnValue({
data: {
tier: {
supabase_prod_id: 'tier_free',
},
},
})
+3 -8
View File
@@ -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()
@@ -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
@@ -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">
@@ -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,
})
@@ -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) => {
@@ -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
@@ -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>
@@ -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) => {
@@ -383,7 +383,7 @@ const Results = ({ results }) => {
<div className="group sb-grid-select-cell__formatter overflow-hidden">
<span className="font-mono text-xs truncate">{JSON.stringify(row[column])}</span>
{row[column] && (
{row[column] != undefined && (
<Button
type="outline"
icon={isCopied ? <IconCheck size="tiny" /> : <IconClipboard size="tiny" />}
@@ -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>
@@ -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'}
+12 -2
View File
@@ -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
+1 -1
View File
@@ -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))
}, [])
}
+3
View File
@@ -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])
}
+8
View File
@@ -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])
}
+5
View File
@@ -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,
}
+117
View File
@@ -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,
})
}
+28 -34
View File
@@ -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,
})
}
+7
View File
@@ -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])
}
+4
View File
@@ -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])
}
+3
View File
@@ -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])
}
+6
View File
@@ -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])
}
@@ -0,0 +1,50 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { delete_ } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { organizationKeys } from './keys'
export type OrganizationMemberDeleteVariables = {
slug: string
gotrueId: string
}
export async function deleteOrganizationMember({
slug,
gotrueId,
}: OrganizationMemberDeleteVariables) {
const response = await delete_(`${API_URL}/organizations/${slug}/members/${gotrueId}`)
if (response.error) {
throw response.error
}
return response
}
type OrganizationMemberDeleteData = Awaited<ReturnType<typeof deleteOrganizationMember>>
export const useOrganizationMemberDeleteMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<OrganizationMemberDeleteData, unknown, OrganizationMemberDeleteVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<OrganizationMemberDeleteData, unknown, OrganizationMemberDeleteVariables>(
(vars) => deleteOrganizationMember(vars),
{
async onSuccess(data, variables, context) {
const { slug } = variables
await Promise.all([
queryClient.invalidateQueries(organizationKeys.detail(slug)),
queryClient.invalidateQueries(organizationKeys.roles(slug)),
])
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -0,0 +1,63 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { organizationKeys } from './keys'
export type OrganizationMemberInviteCreateVariables = {
slug: string
invitedEmail: string
ownerId: number
roleId: number
}
export async function createOrganizationMemberInvite({
slug,
invitedEmail,
ownerId,
roleId,
}: OrganizationMemberInviteCreateVariables) {
const response = await post(`${API_URL}/organizations/${slug}/members/invite`, {
invited_email: invitedEmail,
owner_id: ownerId,
role_id: roleId,
})
if (response.error) {
throw response.error
}
return response
}
type OrganizationMemberInviteCreateData = Awaited<ReturnType<typeof createOrganizationMemberInvite>>
export const useOrganizationMemberInviteCreateMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<
OrganizationMemberInviteCreateData,
unknown,
OrganizationMemberInviteCreateVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<
OrganizationMemberInviteCreateData,
unknown,
OrganizationMemberInviteCreateVariables
>((vars) => createOrganizationMemberInvite(vars), {
async onSuccess(data, variables, context) {
const { slug } = variables
await Promise.all([
queryClient.invalidateQueries(organizationKeys.detail(slug)),
queryClient.invalidateQueries(organizationKeys.roles(slug)),
])
await onSuccess?.(data, variables, context)
},
...options,
})
}
@@ -0,0 +1,58 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { delete_ } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { organizationKeys } from './keys'
export type OrganizationMemberInviteDeleteVariables = {
slug: string
invitedId: number
}
export async function deleteOrganizationMemberInvite({
slug,
invitedId,
}: OrganizationMemberInviteDeleteVariables) {
const response = await delete_(
`${API_URL}/organizations/${slug}/members/invite?invited_id=${invitedId}`,
{}
)
if (response.error) {
throw response.error
}
return response
}
type OrganizationMemberInviteDeleteData = Awaited<ReturnType<typeof deleteOrganizationMemberInvite>>
export const useOrganizationMemberInviteDeleteMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<
OrganizationMemberInviteDeleteData,
unknown,
OrganizationMemberInviteDeleteVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<
OrganizationMemberInviteDeleteData,
unknown,
OrganizationMemberInviteDeleteVariables
>((vars) => deleteOrganizationMemberInvite(vars), {
async onSuccess(data, variables, context) {
const { slug } = variables
await Promise.all([
queryClient.invalidateQueries(organizationKeys.detail(slug)),
queryClient.invalidateQueries(organizationKeys.roles(slug)),
])
await onSuccess?.(data, variables, context)
},
...options,
})
}
@@ -0,0 +1,54 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { patch } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { organizationKeys } from './keys'
export type OrganizationMemberUpdateVariables = {
slug: string
gotrueId: string
roleId: number
}
export async function updateOrganizationMember({
slug,
gotrueId,
roleId,
}: OrganizationMemberUpdateVariables) {
const response = await patch(`${API_URL}/organizations/${slug}/members/${gotrueId}`, {
role_id: roleId,
})
if (response.error) {
throw response.error
}
return response
}
type OrganizationMemberUpdateData = Awaited<ReturnType<typeof updateOrganizationMember>>
export const useOrganizationMemberUpdateMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<OrganizationMemberUpdateData, unknown, OrganizationMemberUpdateVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<OrganizationMemberUpdateData, unknown, OrganizationMemberUpdateVariables>(
(vars) => updateOrganizationMember(vars),
{
async onSuccess(data, variables, context) {
const { slug } = variables
await Promise.all([
queryClient.invalidateQueries(organizationKeys.detail(slug)),
queryClient.invalidateQueries(organizationKeys.roles(slug)),
])
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -0,0 +1,63 @@
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 { Role } from 'types'
import { organizationKeys } from './keys'
export type OrganizationRolesVariables = {
slug?: string
}
export type OrganizationRolesResponse = {
roles: Role[]
}
export async function getOrganizationRoles(
{ slug }: OrganizationRolesVariables,
signal?: AbortSignal
) {
if (!slug) {
throw new Error('slug is required')
}
const data = await get(`${API_URL}/organizations/${slug}/roles`, {
signal,
})
if (data.error) {
throw data.error
}
return { roles: data } as OrganizationRolesResponse
}
export type OrganizationRolesData = Awaited<ReturnType<typeof getOrganizationRoles>>
export type OrganizationRolesError = unknown
export const useOrganizationRolesQuery = <TData = OrganizationRolesData>(
{ slug }: OrganizationRolesVariables,
{
enabled = true,
...options
}: UseQueryOptions<OrganizationRolesData, OrganizationRolesError, TData> = {}
) =>
useQuery<OrganizationRolesData, OrganizationRolesError, TData>(
organizationKeys.roles(slug),
({ signal }) => getOrganizationRoles({ slug }, signal),
{
enabled: enabled && typeof slug !== 'undefined',
...options,
}
)
export const useOrganizationRolesPrefetch = ({ slug }: OrganizationRolesVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (slug) {
client.prefetchQuery(organizationKeys.roles(slug), ({ signal }) =>
getOrganizationRoles({ slug }, signal)
)
}
}, [slug])
}
+3
View File
@@ -0,0 +1,3 @@
export const permissionKeys = {
list: () => ['permissions'] as const,
}
@@ -0,0 +1,40 @@
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 { Permission } from 'types'
import { permissionKeys } from './keys'
export type PermissionsResponse = Permission[]
export async function getPermissions(signal?: AbortSignal) {
const response = await get(`${API_URL}/profile/permissions`, {
signal,
})
if (response.error) {
throw response.error
}
return response as PermissionsResponse
}
export type PermissionsData = Awaited<ReturnType<typeof getPermissions>>
export type PermissionsError = unknown
export const usePermissionsQuery = <TData = PermissionsData>({
enabled = true,
...options
}: UseQueryOptions<PermissionsData, PermissionsError, TData> = {}) =>
useQuery<PermissionsData, PermissionsError, TData>(
permissionKeys.list(),
({ signal }) => getPermissions(signal),
options
)
export const usePermissionsPrefetch = () => {
const client = useQueryClient()
return useCallback(() => {
client.prefetchQuery(permissionKeys.list(), ({ signal }) => getPermissions(signal))
}, [])
}
+3
View File
@@ -0,0 +1,3 @@
export const platformKeys = {
status: () => ['platform', 'status'] as const,
}
@@ -0,0 +1,40 @@
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 { platformKeys } from './keys'
export type PlatformStatusResponse = {
isHealthy: boolean
}
export async function getPlatformStatus(signal?: AbortSignal) {
const response = await get(`${API_URL}/status`, {
signal,
})
if (response.error) {
throw response.error
}
return { isHealthy: response.is_healthy } as PlatformStatusResponse
}
export type PlatformStatusData = Awaited<ReturnType<typeof getPlatformStatus>>
export type PlatformStatusError = unknown
export const usePlatformStatusQuery = <TData = PlatformStatusData>(
options: UseQueryOptions<PlatformStatusData, PlatformStatusError, TData> = {}
) =>
useQuery<PlatformStatusData, PlatformStatusError, TData>(
platformKeys.status(),
({ signal }) => getPlatformStatus(signal),
options
)
export const usePlatformStatusPrefetch = () => {
const client = useQueryClient()
return useCallback(() => {
client.prefetchQuery(platformKeys.status(), ({ signal }) => getPlatformStatus(signal))
}, [])
}
+3
View File
@@ -0,0 +1,3 @@
export const profileKeys = {
profile: () => ['profile'] as const,
}
+52
View File
@@ -0,0 +1,52 @@
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 { profileKeys } from './keys'
export type Profile = {
id: number
auth0_id: string
primary_email: string
username: string
first_name: string
last_name: string
mobile: string | null
is_alpha_user: boolean
gotrue_id: string
free_project_limit: number
}
export type ProfileResponse = Profile
export async function getProfile(signal?: AbortSignal) {
const response = await get(`${API_URL}/profile`, {
signal,
})
if (response.error) {
throw response.error
}
return response as ProfileResponse
}
export type ProfileData = Awaited<ReturnType<typeof getProfile>>
export type ProfileError = unknown
export const useProfileQuery = <TData = ProfileData>({
enabled = true,
...options
}: UseQueryOptions<ProfileData, ProfileError, TData> = {}) =>
useQuery<ProfileData, ProfileError, TData>(
profileKeys.profile(),
({ signal }) => getProfile(signal),
options
)
export const useProfilePrefetch = () => {
const client = useQueryClient()
return useCallback(() => {
client.prefetchQuery(profileKeys.profile(), ({ signal }) => getProfile(signal))
}, [])
}
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more